|
13 | 13 | export let originX: number; |
14 | 14 | export let originY: number; |
15 | 15 | export let tilt: number; |
| 16 | + export let flip: boolean = false; |
16 | 17 | export let numberInterval: number; |
17 | 18 | export let majorMarkSpacing: number; |
18 | 19 | export let minorDivisions = 5; |
19 | 20 | export let microDivisions = 2; |
| 21 | + export let cursorPosition: { x: number; y: number } | undefined = undefined; |
20 | 22 |
|
21 | 23 | let rulerInput: HTMLDivElement | undefined; |
22 | 24 | let rulerLength = 0; |
|
28 | 30 | $: isHorizontal = direction === "Horizontal"; |
29 | 31 | $: trackedAxis = isHorizontal ? axes.horiz : axes.vert; |
30 | 32 | $: otherAxis = isHorizontal ? axes.vert : axes.horiz; |
| 33 | + $: crossAxisDirection = flipVector(otherAxis.vec, flip); |
31 | 34 | $: stretchFactor = 1 / Math.max(Math.abs(isHorizontal ? trackedAxis.vec[0] : trackedAxis.vec[1]), 1e-10); |
32 | 35 | $: stretchedSpacing = majorMarkSpacing * stretchFactor; |
33 | | - $: effectiveOrigin = computeEffectiveOrigin(direction, originX, originY, otherAxis); |
34 | | - $: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, stretchFactor, minorDivisions, microDivisions, rulerLength, otherAxis); |
35 | | - $: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, otherAxis, tilt); |
| 36 | + $: effectiveOrigin = projectOntoRuler(direction, originX, originY, crossAxisDirection); |
| 37 | + $: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, stretchFactor, minorDivisions, microDivisions, rulerLength, crossAxisDirection); |
| 38 | + $: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, crossAxisDirection); |
| 39 | + $: cursorIndicatorPath = computeCursorIndicator(direction, cursorPosition, crossAxisDirection); |
36 | 40 |
|
37 | 41 | function computeAxes(tilt: number): { horiz: Axis; vert: Axis } { |
38 | 42 | const normTilt = ((tilt % TAU) + TAU) % TAU; |
|
50 | 54 | return { horiz: posY, vert: negX }; |
51 | 55 | } |
52 | 56 |
|
53 | | - function computeEffectiveOrigin(direction: RulerDirection, ox: number, oy: number, otherAxis: Axis): number { |
54 | | - const [vx, vy] = otherAxis.vec; |
55 | | - if (direction === "Horizontal") { |
56 | | - return Math.abs(vy) < 1e-10 ? ox : ox - oy * (vx / vy); |
57 | | - } else { |
58 | | - return Math.abs(vx) < 1e-10 ? oy : oy - ox * (vy / vx); |
59 | | - } |
| 57 | + function flipVector(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 | + }; |
60 | 75 | } |
61 | 76 |
|
62 | 77 | function computeSvgPath( |
|
67 | 82 | minorDivisions: number, |
68 | 83 | microDivisions: number, |
69 | 84 | rulerLength: number, |
70 | | - otherAxis: Axis, |
| 85 | + crossAxisDirection: [number, number], |
71 | 86 | ): string { |
72 | 87 | const adaptive = stretchFactor > 1.3 ? { minor: minorDivisions, micro: 1 } : { minor: minorDivisions, micro: microDivisions }; |
73 | 88 | const divisions = stretchedSpacing / adaptive.minor / adaptive.micro; |
74 | 89 | const majorMarksFrequency = adaptive.minor * adaptive.micro; |
75 | 90 | const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing; |
76 | 91 |
|
77 | | - const [vx, vy] = otherAxis.vec; |
78 | | - const flip = direction === "Horizontal" ? (vy > 0 ? -1 : 1) : vx > 0 ? -1 : 1; |
79 | | - const [dx, dy] = [vx * flip, vy * flip]; |
80 | | - const [sxBase, syBase] = direction === "Horizontal" ? [0, RULER_THICKNESS] : [RULER_THICKNESS, 0]; |
| 92 | + const { dx, dy, sxBase, syBase } = tickMarkGeometry(direction, crossAxisDirection[0], crossAxisDirection[1]); |
81 | 93 |
|
82 | 94 | let path = ""; |
83 | 95 | let i = 0; |
|
103 | 115 | numberInterval: number, |
104 | 116 | rulerLength: number, |
105 | 117 | trackedAxis: Axis, |
106 | | - otherAxis: Axis, |
107 | | - tilt: number, |
| 118 | + crossAxisDirection: [number, number], |
108 | 119 | ): { transform: string; text: string }[] { |
109 | 120 | const isVertical = direction === "Vertical"; |
110 | 121 |
|
111 | | - const [vx, vy] = otherAxis.vec; |
112 | | - const flip = isVertical ? (vx > 0 ? -1 : 1) : vy > 0 ? -1 : 1; |
113 | | - const tiltScale = tilt >= 0 ? 1 : 0.5; |
114 | | - const tipOffsetX = vx * flip * MAJOR_MARK_THICKNESS * tiltScale; |
115 | | - const tipOffsetY = vy * flip * MAJOR_MARK_THICKNESS * tiltScale; |
| 122 | + const { dx: tipDx, dy: tipDy } = tickMarkGeometry(direction, crossAxisDirection[0], crossAxisDirection[1]); |
| 123 | + const forwardTip = isVertical ? -tipDy : tipDx; |
| 124 | + const tiltScale = forwardTip >= 0 ? 1 : 0.5; |
| 125 | + const tipOffsetX = tipDx * MAJOR_MARK_THICKNESS * tiltScale; |
| 126 | + const tipOffsetY = tipDy * MAJOR_MARK_THICKNESS * tiltScale; |
116 | 127 |
|
117 | 128 | const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing; |
118 | 129 | const increments = Math.round((shiftedOffsetStart - effectiveOrigin) / stretchedSpacing); |
|
139 | 150 | return results; |
140 | 151 | } |
141 | 152 |
|
| 153 | + function computeCursorIndicator(direction: RulerDirection, cursor: { x: number; y: number } | undefined, crossAxisDirection: [number, number]): string { |
| 154 | + if (cursor === undefined) return ""; |
| 155 | +
|
| 156 | + const projected = projectOntoRuler(direction, cursor.x, cursor.y, crossAxisDirection); |
| 157 | + const { dx, dy, sxBase, syBase } = tickMarkGeometry(direction, crossAxisDirection[0], crossAxisDirection[1]); |
| 158 | +
|
| 159 | + // Scale the line so it spans the full ruler bar thickness |
| 160 | + const thicknessComponent = Math.abs(direction === "Horizontal" ? dy : dx); |
| 161 | + const length = thicknessComponent < 1e-10 ? RULER_THICKNESS : RULER_THICKNESS / thicknessComponent; |
| 162 | +
|
| 163 | + const destination = Math.round(projected) + 0.5; |
| 164 | + const [sx, sy] = direction === "Horizontal" ? [destination, syBase] : [sxBase, destination]; |
| 165 | + return `M${sx},${sy}l${dx * length},${dy * length}`; |
| 166 | + } |
| 167 | +
|
142 | 168 | export function resize() { |
143 | 169 | if (!rulerInput) return; |
144 | 170 |
|
|
170 | 196 | {#each svgTexts as svgText} |
171 | 197 | <text transform={svgText.transform}>{svgText.text}</text> |
172 | 198 | {/each} |
| 199 | + {#if cursorIndicatorPath} |
| 200 | + <path class="cursor-indicator" d={cursorIndicatorPath} /> |
| 201 | + {/if} |
173 | 202 | </svg> |
174 | 203 | </div> |
175 | 204 |
|
|
201 | 230 | path { |
202 | 231 | stroke-width: 1px; |
203 | 232 | stroke: var(--color-5-dullgray); |
| 233 | +
|
| 234 | + &.cursor-indicator { |
| 235 | + stroke: var(--color-8-uppergray); |
| 236 | + } |
204 | 237 | } |
205 | 238 |
|
206 | 239 | text { |
|
0 commit comments