|
30 | 30 | $: isHorizontal = direction === "Horizontal"; |
31 | 31 | $: trackedAxis = isHorizontal ? axes.horiz : axes.vert; |
32 | 32 | $: otherAxis = isHorizontal ? axes.vert : axes.horiz; |
| 33 | + $: otherVec = flipVec(otherAxis.vec, flip); |
33 | 34 | $: stretchFactor = 1 / Math.max(Math.abs(isHorizontal ? trackedAxis.vec[0] : trackedAxis.vec[1]), 1e-10); |
34 | 35 | $: stretchedSpacing = majorMarkSpacing * stretchFactor; |
35 | | - $: effectiveOrigin = computeEffectiveOrigin(direction, originX, originY, otherAxis, flip); |
36 | | - $: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, stretchFactor, minorDivisions, microDivisions, rulerLength, otherAxis, flip); |
37 | | - $: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, otherAxis, tilt); |
38 | | - $: cursorIndicatorPath = computeCursorIndicator(direction, cursorPosition, otherAxis, flip); |
| 36 | + $: effectiveOrigin = projectOntoRuler(direction, originX, originY, otherVec); |
| 37 | + $: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, stretchFactor, minorDivisions, microDivisions, rulerLength, otherVec); |
| 38 | + $: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, otherAxis.vec, tilt); |
| 39 | + $: cursorIndicatorPath = computeCursorIndicator(direction, cursorPosition, otherVec); |
39 | 40 |
|
40 | 41 | function computeAxes(tilt: number): { horiz: Axis; vert: Axis } { |
41 | 42 | const normTilt = ((tilt % TAU) + TAU) % TAU; |
|
53 | 54 | return { horiz: posY, vert: negX }; |
54 | 55 | } |
55 | 56 |
|
56 | | - function computeEffectiveOrigin(direction: RulerDirection, ox: number, oy: number, otherAxis: Axis, flip: boolean): number { |
57 | | - const [rawVx, vy] = otherAxis.vec; |
58 | | - const vx = flip ? -rawVx : rawVx; |
59 | | - if (direction === "Horizontal") { |
60 | | - return Math.abs(vy) < 1e-10 ? ox : ox - oy * (vx / vy); |
61 | | - } else { |
62 | | - return Math.abs(vx) < 1e-10 ? oy : oy - ox * (vy / vx); |
63 | | - } |
| 57 | + function flipVec(vec: [number, number], flipped: boolean): [number, number] { |
| 58 | + return flipped ? [-vec[0], vec[1]] : vec; |
| 59 | + } |
| 60 | +
|
| 61 | + function projectOntoRuler(direction: RulerDirection, x: number, y: number, vec: [number, number]): number { |
| 62 | + const [vx, vy] = vec; |
| 63 | + if (direction === "Horizontal") return Math.abs(vy) < 1e-10 ? x : x - y * (vx / vy); |
| 64 | + return Math.abs(vx) < 1e-10 ? y : y - x * (vy / vx); |
| 65 | + } |
| 66 | +
|
| 67 | + function tickMarkGeometry(direction: RulerDirection, vx: number, vy: number): { dx: number; dy: number; sxBase: number; syBase: number } { |
| 68 | + const reversal = direction === "Horizontal" ? (vy > 0 ? -1 : 1) : vx > 0 ? -1 : 1; |
| 69 | + return { |
| 70 | + dx: vx * reversal, |
| 71 | + dy: vy * reversal, |
| 72 | + sxBase: direction === "Horizontal" ? 0 : RULER_THICKNESS, |
| 73 | + syBase: direction === "Horizontal" ? RULER_THICKNESS : 0, |
| 74 | + }; |
64 | 75 | } |
65 | 76 |
|
66 | 77 | function computeSvgPath( |
|
71 | 82 | minorDivisions: number, |
72 | 83 | microDivisions: number, |
73 | 84 | rulerLength: number, |
74 | | - otherAxis: Axis, |
75 | | - flip: boolean, |
| 85 | + otherVec: [number, number], |
76 | 86 | ): string { |
77 | 87 | const adaptive = stretchFactor > 1.3 ? { minor: minorDivisions, micro: 1 } : { minor: minorDivisions, micro: microDivisions }; |
78 | 88 | const divisions = stretchedSpacing / adaptive.minor / adaptive.micro; |
79 | 89 | const majorMarksFrequency = adaptive.minor * adaptive.micro; |
80 | 90 | const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing; |
81 | 91 |
|
82 | | - const [rawVx, vy] = otherAxis.vec; |
83 | | - const vx = flip ? -rawVx : rawVx; |
84 | | - const reversal = direction === "Horizontal" ? (vy > 0 ? -1 : 1) : vx > 0 ? -1 : 1; |
85 | | - const [dx, dy] = [vx * reversal, vy * reversal]; |
86 | | - const [sxBase, syBase] = direction === "Horizontal" ? [0, RULER_THICKNESS] : [RULER_THICKNESS, 0]; |
| 92 | + const { dx, dy, sxBase, syBase } = tickMarkGeometry(direction, otherVec[0], otherVec[1]); |
87 | 93 |
|
88 | 94 | let path = ""; |
89 | 95 | let i = 0; |
|
109 | 115 | numberInterval: number, |
110 | 116 | rulerLength: number, |
111 | 117 | trackedAxis: Axis, |
112 | | - otherAxis: Axis, |
| 118 | + unflippedOtherVec: [number, number], |
113 | 119 | tilt: number, |
114 | 120 | ): { transform: string; text: string }[] { |
115 | 121 | const isVertical = direction === "Vertical"; |
116 | 122 |
|
117 | | - const [rawVx, vy] = otherAxis.vec; |
118 | | -
|
119 | 123 | // Tip offset uses the un-flipped axis so text stays on the correct side of tick marks |
120 | | - const tipReversal = isVertical ? (rawVx > 0 ? -1 : 1) : vy > 0 ? -1 : 1; |
| 124 | + const { dx: tipDx, dy: tipDy } = tickMarkGeometry(direction, unflippedOtherVec[0], unflippedOtherVec[1]); |
121 | 125 | const tiltScale = tilt >= 0 ? 1 : 0.5; |
122 | | - const tipOffsetX = rawVx * tipReversal * MAJOR_MARK_THICKNESS * tiltScale; |
123 | | - const tipOffsetY = vy * tipReversal * MAJOR_MARK_THICKNESS * tiltScale; |
| 126 | + const tipOffsetX = tipDx * MAJOR_MARK_THICKNESS * tiltScale; |
| 127 | + const tipOffsetY = tipDy * MAJOR_MARK_THICKNESS * tiltScale; |
124 | 128 |
|
125 | 129 | const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing; |
126 | 130 | const increments = Math.round((shiftedOffsetStart - effectiveOrigin) / stretchedSpacing); |
|
147 | 151 | return results; |
148 | 152 | } |
149 | 153 |
|
150 | | - function computeCursorIndicator(direction: RulerDirection, cursor: { x: number; y: number } | undefined, otherAxis: Axis, flip: boolean): string { |
| 154 | + function computeCursorIndicator(direction: RulerDirection, cursor: { x: number; y: number } | undefined, otherVec: [number, number]): string { |
151 | 155 | if (cursor === undefined) return ""; |
152 | 156 |
|
153 | | - // Project cursor position along the other axis onto the ruler strip |
154 | | - const [rawVx, vy] = otherAxis.vec; |
155 | | - const vx = flip ? -rawVx : rawVx; |
156 | | - let projected: number; |
157 | | - if (direction === "Horizontal") { |
158 | | - projected = Math.abs(vy) < 1e-10 ? cursor.x : cursor.x - cursor.y * (vx / vy); |
159 | | - } else { |
160 | | - projected = Math.abs(vx) < 1e-10 ? cursor.y : cursor.y - cursor.x * (vy / vx); |
161 | | - } |
162 | | -
|
163 | | - const reversal = direction === "Horizontal" ? (vy > 0 ? -1 : 1) : vx > 0 ? -1 : 1; |
164 | | - const [dx, dy] = [vx * reversal, vy * reversal]; |
165 | | - const [sxBase, syBase] = direction === "Horizontal" ? [0, RULER_THICKNESS] : [RULER_THICKNESS, 0]; |
| 157 | + const projected = projectOntoRuler(direction, cursor.x, cursor.y, otherVec); |
| 158 | + const { dx, dy, sxBase, syBase } = tickMarkGeometry(direction, otherVec[0], otherVec[1]); |
166 | 159 |
|
167 | 160 | // Scale the line so it spans the full ruler bar thickness |
168 | 161 | const thicknessComponent = Math.abs(direction === "Horizontal" ? dy : dx); |
|
0 commit comments