Skip to content

Commit 0acfd3e

Browse files
authored
Add a pointer hover marker line to the rulers (#4088)
* Add a pointer hover marker line to the rulers * Fix rulers and pointer marker when document is flipped * Reduce duplicate code * Fix ruler label placement * Performance
1 parent 29f6e68 commit 0acfd3e

4 files changed

Lines changed: 75 additions & 25 deletions

File tree

editor/src/messages/frontend/frontend_message.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ pub enum FrontendMessage {
245245
interval: f64,
246246
visible: bool,
247247
tilt: f64,
248+
flip: bool,
248249
},
249250
UpdateDocumentScrollbars {
250251
position: (f64, f64),

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
844844
interval: ruler_interval,
845845
visible: self.rulers_visible,
846846
tilt: if self.graph_view_overlay_open { 0. } else { current_ptz.tilt() },
847+
flip: !self.graph_view_overlay_open && current_ptz.flip,
847848
});
848849
}
849850
DocumentMessage::RenderScrollbars => {

frontend/src/components/panels/Document.svelte

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
let rulerInterval = 100;
4646
let rulersVisible = true;
4747
let rulerTilt = 0;
48+
let rulerFlip = false;
49+
let rulerCursorPosition: { x: number; y: number } | undefined;
50+
let viewportBounds: DOMRect | undefined;
4851
4952
// Rendered SVG viewport data
5053
let artworkSvg = "";
@@ -288,12 +291,17 @@
288291
scrollbarMultiplier = { x: multiplier[0], y: multiplier[1] };
289292
}
290293
291-
export function updateDocumentRulers(origin: [number, number], spacing: number, interval: number, visible: boolean, tilt: number) {
294+
export function updateDocumentRulers(origin: [number, number], spacing: number, interval: number, visible: boolean, tilt: number, flip: boolean) {
292295
rulerOrigin = { x: origin[0], y: origin[1] };
293296
rulerSpacing = spacing;
294297
rulerInterval = interval;
295298
rulersVisible = visible;
296299
rulerTilt = tilt;
300+
rulerFlip = flip;
301+
}
302+
303+
function updateRulerCursorPosition(e: PointerEvent) {
304+
if (viewportBounds) rulerCursorPosition = { x: e.clientX - viewportBounds.left, y: e.clientY - viewportBounds.top };
297305
}
298306
299307
// Update mouse cursor icon
@@ -416,6 +424,7 @@
416424
canvasHeight = Math.ceil(parseFloat(getComputedStyle(viewport).height));
417425
418426
devicePixelRatio = window.devicePixelRatio || 1;
427+
viewportBounds = viewport.getBoundingClientRect();
419428
420429
// Resize the rulers
421430
rulerHorizontal?.resize();
@@ -489,8 +498,8 @@
489498
subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
490499
await tick();
491500
492-
const { origin, spacing, interval, visible, tilt } = data;
493-
updateDocumentRulers(origin, spacing, interval, visible, tilt);
501+
const { origin, spacing, interval, visible, tilt, flip } = data;
502+
updateDocumentRulers(origin, spacing, interval, visible, tilt, flip);
494503
});
495504
496505
// Update mouse cursor icon
@@ -601,9 +610,11 @@
601610
originX={rulerOrigin.x}
602611
originY={rulerOrigin.y}
603612
tilt={rulerTilt}
613+
flip={rulerFlip}
604614
majorMarkSpacing={rulerSpacing}
605615
numberInterval={rulerInterval}
606616
direction="Horizontal"
617+
cursorPosition={rulerCursorPosition}
607618
bind:this={rulerHorizontal}
608619
/>
609620
</LayoutRow>
@@ -615,9 +626,11 @@
615626
originX={rulerOrigin.x}
616627
originY={rulerOrigin.y}
617628
tilt={rulerTilt}
629+
flip={rulerFlip}
618630
majorMarkSpacing={rulerSpacing}
619631
numberInterval={rulerInterval}
620632
direction="Vertical"
633+
cursorPosition={rulerCursorPosition}
621634
bind:this={rulerVertical}
622635
/>
623636
</LayoutCol>
@@ -664,6 +677,8 @@
664677
class:viewport={!$appWindow.viewportHolePunch}
665678
class:viewport-transparent={$appWindow.viewportHolePunch}
666679
on:pointerdown={(e) => canvasPointerDown(e)}
680+
on:pointermove={updateRulerCursorPosition}
681+
on:pointerleave={() => (rulerCursorPosition = undefined)}
667682
bind:this={viewport}
668683
data-viewport
669684
>

frontend/src/components/widgets/inputs/RulerInput.svelte

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
export let originX: number;
1414
export let originY: number;
1515
export let tilt: number;
16+
export let flip: boolean = false;
1617
export let numberInterval: number;
1718
export let majorMarkSpacing: number;
1819
export let minorDivisions = 5;
1920
export let microDivisions = 2;
21+
export let cursorPosition: { x: number; y: number } | undefined = undefined;
2022
2123
let rulerInput: HTMLDivElement | undefined;
2224
let rulerLength = 0;
@@ -28,11 +30,13 @@
2830
$: isHorizontal = direction === "Horizontal";
2931
$: trackedAxis = isHorizontal ? axes.horiz : axes.vert;
3032
$: otherAxis = isHorizontal ? axes.vert : axes.horiz;
33+
$: crossAxisDirection = flipVector(otherAxis.vec, flip);
3134
$: stretchFactor = 1 / Math.max(Math.abs(isHorizontal ? trackedAxis.vec[0] : trackedAxis.vec[1]), 1e-10);
3235
$: 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);
3640
3741
function computeAxes(tilt: number): { horiz: Axis; vert: Axis } {
3842
const normTilt = ((tilt % TAU) + TAU) % TAU;
@@ -50,13 +54,24 @@
5054
return { horiz: posY, vert: negX };
5155
}
5256
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+
};
6075
}
6176
6277
function computeSvgPath(
@@ -67,17 +82,14 @@
6782
minorDivisions: number,
6883
microDivisions: number,
6984
rulerLength: number,
70-
otherAxis: Axis,
85+
crossAxisDirection: [number, number],
7186
): string {
7287
const adaptive = stretchFactor > 1.3 ? { minor: minorDivisions, micro: 1 } : { minor: minorDivisions, micro: microDivisions };
7388
const divisions = stretchedSpacing / adaptive.minor / adaptive.micro;
7489
const majorMarksFrequency = adaptive.minor * adaptive.micro;
7590
const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing;
7691
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]);
8193
8294
let path = "";
8395
let i = 0;
@@ -103,16 +115,15 @@
103115
numberInterval: number,
104116
rulerLength: number,
105117
trackedAxis: Axis,
106-
otherAxis: Axis,
107-
tilt: number,
118+
crossAxisDirection: [number, number],
108119
): { transform: string; text: string }[] {
109120
const isVertical = direction === "Vertical";
110121
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;
116127
117128
const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing;
118129
const increments = Math.round((shiftedOffsetStart - effectiveOrigin) / stretchedSpacing);
@@ -139,6 +150,21 @@
139150
return results;
140151
}
141152
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+
142168
export function resize() {
143169
if (!rulerInput) return;
144170
@@ -170,6 +196,9 @@
170196
{#each svgTexts as svgText}
171197
<text transform={svgText.transform}>{svgText.text}</text>
172198
{/each}
199+
{#if cursorIndicatorPath}
200+
<path class="cursor-indicator" d={cursorIndicatorPath} />
201+
{/if}
173202
</svg>
174203
</div>
175204

@@ -201,6 +230,10 @@
201230
path {
202231
stroke-width: 1px;
203232
stroke: var(--color-5-dullgray);
233+
234+
&.cursor-indicator {
235+
stroke: var(--color-8-uppergray);
236+
}
204237
}
205238
206239
text {

0 commit comments

Comments
 (0)