Skip to content

Commit 3e425d0

Browse files
committed
Refactor types to be based off of branded numbers
1 parent 43ce01c commit 3e425d0

4 files changed

Lines changed: 92 additions & 53 deletions

File tree

src/App.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
getRelativePatternPosition,
1010
} from './functions'
1111
import { useStableFunction } from './hooks'
12-
import { AbsolutePoint, State, asAbsolutePoint } from './types'
12+
import { AbsolutePattern, AbsolutePoint, RelativePattern, State, asAbsolutePoint } from './types'
1313

1414
const StyledCanvas = styled.canvas`
1515
/* border: 1px solid #faf; */
@@ -22,7 +22,7 @@ const getDraftState = (state: State, draftClick: DraftClick, mousePosition: Abso
2222
const draftPattern = {
2323
anchor: draftClick.anchor,
2424
target: mousePosition,
25-
}
25+
}
2626

2727
if (draftClick.clickedPath === undefined) {
2828
// create top-level screen
@@ -32,7 +32,11 @@ const getDraftState = (state: State, draftClick: DraftClick, mousePosition: Abso
3232
// if draft origin is inside existing screen, add a pattern instead
3333
const { screenIndex, nestedPath } = draftClick.clickedPath
3434

35-
let newDraft = getRelativePatternPosition(draftPattern, state.screens[screenIndex])
35+
36+
// Re-interpret as relative pattern.
37+
const draftPatternRelative = draftPattern satisfies AbsolutePattern as unknown as RelativePattern
38+
39+
let newDraft = getRelativePatternPosition(draftPatternRelative, state.screens[screenIndex])
3640
for (const k of nestedPath) {
3741
newDraft = getRelativePatternPosition(newDraft, state.patterns[k])
3842
}

src/draw.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
mapPointToViewportSpace,
66
pointIsInBoundaries,
77
} from './functions'
8-
import { AbsolutePattern, Boundaries, Pattern, Point, Size, State } from './types'
8+
import { AbsoluteNumber, AbsolutePattern, Boundaries, Pattern, PatternNumber, Point, Size, State, ViewportPattern } from './types'
99

1010
// -- constants
1111

@@ -32,30 +32,31 @@ type GlobalMutableState = {
3232

3333
// -- helper functions
3434

35-
const mapPatternToViewportSpace = (pattern: AbsolutePattern, screenSize: Size): Pattern => ({
35+
const mapPatternToViewportSpace = (pattern: AbsolutePattern, screenSize: Size): ViewportPattern => ({
3636
anchor: mapPointToViewportSpace(pattern.anchor, screenSize),
3737
target: mapPointToViewportSpace(pattern.target, screenSize),
3838
})
3939

40-
const getPatternPoints = (pattern: Pattern): Point[] => {
40+
const getPatternPoints = <N extends PatternNumber>(pattern: Pattern<N>): Point<N>[] => {
4141
const { xMin, xMax, yMin, yMax } = getBoundariesFromPattern(pattern)
4242

4343
return [
4444
[xMin, yMin], // top left
4545
[xMax, yMin], // top right
4646
[xMax, yMax], // bottom right
4747
[xMin, yMax], // bottom left
48-
]
48+
]
4949
}
5050

5151
// We'll ignore everything that's a bit outside the viewport.
5252
// This is not really accurate, but should be OK for our purposes.
53-
const VALID_BOUNDARIES: Boundaries = {
54-
xMin: -0.1,
55-
xMax: 1.1,
56-
yMin: -0.1,
57-
yMax: 1.1,
58-
}
53+
const VALID_BOUNDARIES: Boundaries<AbsoluteNumber> = {
54+
xMin: -0.1 as AbsoluteNumber,
55+
xMax: 1.1 as AbsoluteNumber,
56+
yMin: -0.1 as AbsoluteNumber,
57+
yMax: 1.1 as AbsoluteNumber,
58+
}
59+
5960
const validatePatternPosition = (pattern: AbsolutePattern): boolean => {
6061
return getPatternPoints(pattern).some(p => pointIsInBoundaries(p, VALID_BOUNDARIES))
6162
}

src/functions.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
11
import {
2+
AbsoluteNumber,
23
AbsolutePattern,
34
AbsolutePoint,
45
Boundaries,
6+
NumberPair,
57
Pattern,
8+
PatternNumber,
69
Point,
10+
RelativeNumber,
11+
RelativePattern,
12+
RelativePoint,
713
Size,
814
State,
9-
asAbsolutePoint,
15+
ViewportPoint
1016
} from './types'
1117

12-
const getBoundariesFromTwoPoints = ([x1, y1]: Point, [x2, y2]: Point): Boundaries => {
18+
function min<N extends PatternNumber>(a: N, b: N): N {
19+
return a < b ? a : b
20+
}
21+
22+
function max<N extends PatternNumber>(a: N, b: N): N {
23+
return a > b ? a : b
24+
}
25+
26+
const getBoundariesFromTwoPoints = <N extends PatternNumber>([x1, y1]: Point<N>, [x2, y2]: Point<N>): Boundaries<N> => {
1327
return {
14-
xMin: Math.min(x1, x2),
15-
xMax: Math.max(x1, x2),
16-
yMin: Math.min(y1, y2),
17-
yMax: Math.max(y1, y2),
28+
xMin: min(x1, x2),
29+
xMax: max(x1, x2),
30+
yMin: min(y1, y2),
31+
yMax: max(y1, y2),
1832
}
1933
}
2034

21-
export const normalizePattern = (pattern: Pattern): Pattern => {
35+
export const normalizePattern = <N extends PatternNumber>(pattern: Pattern<N>): Pattern<N> => {
2236
const { xMin, xMax, yMin, yMax } = getBoundariesFromPattern(pattern)
2337

2438
return {
@@ -27,11 +41,11 @@ export const normalizePattern = (pattern: Pattern): Pattern => {
2741
}
2842
}
2943

30-
export const getBoundariesFromPattern = (pattern: Pattern): Boundaries => {
44+
export const getBoundariesFromPattern = <N extends PatternNumber>(pattern: Pattern<N>): Boundaries<N>=> {
3145
return getBoundariesFromTwoPoints(pattern.anchor, pattern.target)
3246
}
3347

34-
export const pointIsInBoundaries = (point: Point, boundaries: Boundaries): boolean => {
48+
export const pointIsInBoundaries = <N extends PatternNumber>(point: Point<N>, boundaries: Boundaries<N>): boolean => {
3549
const { xMin, xMax, yMin, yMax } = boundaries
3650
const [pointX, pointY] = point
3751

@@ -45,45 +59,45 @@ const pointIsInPattern = (point: AbsolutePoint, pattern: AbsolutePattern): boole
4559
export const mapPointToViewportSpace = (
4660
[x, y]: AbsolutePoint,
4761
[viewportWidth, viewportHeight]: Size
48-
): Point => {
49-
return [x * viewportWidth, y * viewportHeight]
62+
): ViewportPoint => {
63+
return [x * viewportWidth, y * viewportHeight] satisfies NumberPair as ViewportPoint
5064
}
5165

5266
// ts-unused-exports:disable-next-line
5367
export const mapPointFromViewportSpace = (
54-
[x, y]: Point,
68+
[x, y]: ViewportPoint,
5569
[viewportWidth, viewportHeight]: Size
5670
): AbsolutePoint => {
57-
return asAbsolutePoint([x / viewportWidth, y / viewportHeight])
71+
return [x / viewportWidth, y / viewportHeight] satisfies NumberPair as AbsolutePoint
5872
}
5973

60-
const getRelativePointPosition = (point: Point, pattern: Pattern): Point => {
74+
function getRelativePointPosition(point: RelativePoint, pattern: AbsolutePattern | RelativePattern): RelativePoint {
6175
const [x1, y1] = pattern.anchor
6276
const [x2, y2] = pattern.target
6377
const [x, y] = point
6478

6579
const relativeX = (x - x1) / (x2 - x1)
6680
const relativeY = (y - y1) / (y2 - y1)
6781

68-
return [relativeX, relativeY]
82+
return [relativeX, relativeY] satisfies NumberPair as RelativePoint
6983
}
7084

71-
export const getRelativePatternPosition = (pattern: Pattern, basePattern: Pattern): Pattern => {
85+
export function getRelativePatternPosition(pattern: RelativePattern, basePattern: AbsolutePattern | RelativePattern): RelativePattern {
7286
return {
7387
anchor: getRelativePointPosition(pattern.anchor, basePattern),
7488
target: getRelativePointPosition(pattern.target, basePattern),
7589
}
7690
}
7791

78-
const resolveRelativePointPosition = (relativePoint: Point, pattern: Pattern): Point => {
92+
const resolveRelativePointPosition = <N extends RelativeNumber | AbsoluteNumber>(relativePoint: RelativePoint, pattern: Pattern<N>): Point<N> => {
7993
const [x1, y1] = pattern.anchor
8094
const [x2, y2] = pattern.target
8195
const [x, y] = relativePoint
8296

8397
const resolvedX = x1 + x * (x2 - x1)
8498
const resolvedY = y1 + y * (y2 - y1)
8599

86-
return [resolvedX, resolvedY]
100+
return [resolvedX, resolvedY] satisfies NumberPair as Point<N>
87101
}
88102

89103
export const getScreenSize = (ctx: CanvasRenderingContext2D): Size => [ctx.canvas.width, ctx.canvas.height]
@@ -92,16 +106,16 @@ export const getMousePoint = (
92106
ctx: CanvasRenderingContext2D,
93107
mouseEvent: React.MouseEvent<HTMLCanvasElement, MouseEvent>
94108
): AbsolutePoint => {
95-
return mapPointFromViewportSpace([mouseEvent.clientX, mouseEvent.clientY], getScreenSize(ctx))
109+
const mousePosition = [mouseEvent.clientX, mouseEvent.clientY] satisfies NumberPair as ViewportPoint
110+
111+
return mapPointFromViewportSpace(mousePosition, getScreenSize(ctx))
96112
}
97113

98-
export function combinePatterns(parent: AbsolutePattern, child: Pattern): AbsolutePattern
99-
export function combinePatterns(parent: Pattern, child: Pattern): Pattern
100-
export function combinePatterns(parent: Pattern, child: Pattern): Pattern {
114+
export function combinePatterns<ParentNumber extends RelativeNumber | AbsoluteNumber>(parent: Pattern<ParentNumber>, child: RelativePattern): Pattern<ParentNumber> {
101115
return {
102116
anchor: resolveRelativePointPosition(child.anchor, parent),
103117
target: resolveRelativePointPosition(child.target, parent),
104-
}
118+
}
105119
}
106120

107121
type NestedPath = number[]
@@ -115,7 +129,7 @@ const MAX_DEPTH = 4
115129

116130
const findClickedPattern = (
117131
previousBasePattern: AbsolutePattern,
118-
patterns: Pattern[],
132+
patterns: RelativePattern[],
119133
point: AbsolutePoint,
120134
path: number[] = []
121135
): NestedPath | undefined => {

src/types.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,54 @@
1-
type Brand<T> = T & { readonly __brand: unique symbol }
1+
type Brand<T, B extends string> = T & { readonly __brand: B }
22

3-
export type Point = [x: number, y: number]
43

5-
export type AbsolutePoint = Brand<Point>
4+
export type RelativeNumber = Brand<number, 'RelativeNumber'>
5+
export type AbsoluteNumber = Brand<number, 'AbsoluteNumber'>
6+
export type ViewportNumber = Brand<number, 'ViewportNumber'>
67

7-
export const asAbsolutePoint = (point: Point): AbsolutePoint => point as AbsolutePoint
8+
9+
export type PatternNumber = RelativeNumber | AbsoluteNumber | ViewportNumber
10+
11+
// Used as intermediate type when casting after doing calculations on coordinates.
12+
export type NumberPair = [x: number, y: number]
13+
14+
export type Point<N extends PatternNumber> = [x: N, y: N]
15+
16+
// offset within pattern
17+
export type RelativePoint = Point<RelativeNumber>
18+
19+
// absolute point in screen space
20+
export type AbsolutePoint = Point<AbsoluteNumber>
21+
22+
// absolute point in viewport space
23+
export type ViewportPoint = Point<ViewportNumber>
24+
25+
export const asAbsolutePoint = (point: NumberPair): AbsolutePoint => point as AbsolutePoint
826

927
export type Size = [width: number, height: number]
1028

11-
export type Pattern<P extends Point = Point> = {
12-
anchor: P // relative point representing base of new screen.
13-
target: P // relative point representing other corner of new screen. can have negative coordinates.
29+
export type Pattern<N extends PatternNumber> = {
30+
anchor: Point<N> // relative point representing base of new screen.
31+
target: Point<N> // relative point representing other corner of new screen. can have negative coordinates.
1432
}
1533

1634
// represents a pattern in relative coordinates.
17-
export type RelativePattern = Brand<Pattern>
35+
export type RelativePattern = Pattern<RelativeNumber>
1836

1937
// represents a pattern in absolute coordinates.
20-
export type AbsolutePattern = Pattern<AbsolutePoint>
38+
export type AbsolutePattern = Pattern<AbsoluteNumber>
39+
40+
// represents a pattern in viewport coordinates.
41+
export type ViewportPattern = Pattern<ViewportNumber>
2142

22-
export const asAbsolutePattern = (pattern: Pattern): AbsolutePattern => pattern as AbsolutePattern
2343

24-
export type Boundaries = {
25-
xMin: number
26-
xMax: number
27-
yMin: number
28-
yMax: number
44+
export type Boundaries<N extends PatternNumber> = {
45+
xMin: N
46+
xMax: N
47+
yMin: N
48+
yMax: N
2949
}
3050

3151
export type State = {
3252
screens: AbsolutePattern[]
33-
patterns: Pattern[]
53+
patterns: RelativePattern[]
3454
}

0 commit comments

Comments
 (0)