Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions editor/src/messages/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ pub enum FrontendMessage {
interval: f64,
visible: bool,
tilt: f64,
flip: bool,
},
UpdateDocumentScrollbars {
position: (f64, f64),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
interval: ruler_interval,
visible: self.rulers_visible,
tilt: if self.graph_view_overlay_open { 0. } else { current_ptz.tilt() },
flip: !self.graph_view_overlay_open && current_ptz.flip,
});
}
DocumentMessage::RenderScrollbars => {
Expand Down
21 changes: 18 additions & 3 deletions frontend/src/components/panels/Document.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
let rulerInterval = 100;
let rulersVisible = true;
let rulerTilt = 0;
let rulerFlip = false;
let rulerCursorPosition: { x: number; y: number } | undefined;
let viewportBounds: DOMRect | undefined;

// Rendered SVG viewport data
let artworkSvg = "";
Expand Down Expand Up @@ -288,12 +291,17 @@
scrollbarMultiplier = { x: multiplier[0], y: multiplier[1] };
}

export function updateDocumentRulers(origin: [number, number], spacing: number, interval: number, visible: boolean, tilt: number) {
export function updateDocumentRulers(origin: [number, number], spacing: number, interval: number, visible: boolean, tilt: number, flip: boolean) {
rulerOrigin = { x: origin[0], y: origin[1] };
rulerSpacing = spacing;
rulerInterval = interval;
rulersVisible = visible;
rulerTilt = tilt;
rulerFlip = flip;
}

function updateRulerCursorPosition(e: PointerEvent) {
if (viewportBounds) rulerCursorPosition = { x: e.clientX - viewportBounds.left, y: e.clientY - viewportBounds.top };
Comment thread
Keavon marked this conversation as resolved.
}
Comment thread
Keavon marked this conversation as resolved.

// Update mouse cursor icon
Expand Down Expand Up @@ -416,6 +424,7 @@
canvasHeight = Math.ceil(parseFloat(getComputedStyle(viewport).height));

devicePixelRatio = window.devicePixelRatio || 1;
viewportBounds = viewport.getBoundingClientRect();

// Resize the rulers
rulerHorizontal?.resize();
Expand Down Expand Up @@ -489,8 +498,8 @@
subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
await tick();

const { origin, spacing, interval, visible, tilt } = data;
updateDocumentRulers(origin, spacing, interval, visible, tilt);
const { origin, spacing, interval, visible, tilt, flip } = data;
updateDocumentRulers(origin, spacing, interval, visible, tilt, flip);
});

// Update mouse cursor icon
Expand Down Expand Up @@ -601,9 +610,11 @@
originX={rulerOrigin.x}
originY={rulerOrigin.y}
tilt={rulerTilt}
flip={rulerFlip}
majorMarkSpacing={rulerSpacing}
numberInterval={rulerInterval}
direction="Horizontal"
cursorPosition={rulerCursorPosition}
bind:this={rulerHorizontal}
/>
</LayoutRow>
Expand All @@ -615,9 +626,11 @@
originX={rulerOrigin.x}
originY={rulerOrigin.y}
tilt={rulerTilt}
flip={rulerFlip}
majorMarkSpacing={rulerSpacing}
numberInterval={rulerInterval}
direction="Vertical"
cursorPosition={rulerCursorPosition}
bind:this={rulerVertical}
/>
</LayoutCol>
Expand Down Expand Up @@ -664,6 +677,8 @@
class:viewport={!$appWindow.viewportHolePunch}
class:viewport-transparent={$appWindow.viewportHolePunch}
on:pointerdown={(e) => canvasPointerDown(e)}
on:pointermove={updateRulerCursorPosition}
on:pointerleave={() => (rulerCursorPosition = undefined)}
bind:this={viewport}
data-viewport
>
Expand Down
77 changes: 55 additions & 22 deletions frontend/src/components/widgets/inputs/RulerInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
export let originX: number;
export let originY: number;
export let tilt: number;
export let flip: boolean = false;
export let numberInterval: number;
export let majorMarkSpacing: number;
export let minorDivisions = 5;
export let microDivisions = 2;
export let cursorPosition: { x: number; y: number } | undefined = undefined;

let rulerInput: HTMLDivElement | undefined;
let rulerLength = 0;
Expand All @@ -28,11 +30,13 @@
$: isHorizontal = direction === "Horizontal";
$: trackedAxis = isHorizontal ? axes.horiz : axes.vert;
$: otherAxis = isHorizontal ? axes.vert : axes.horiz;
$: crossAxisDirection = flipVector(otherAxis.vec, flip);
$: stretchFactor = 1 / Math.max(Math.abs(isHorizontal ? trackedAxis.vec[0] : trackedAxis.vec[1]), 1e-10);
$: stretchedSpacing = majorMarkSpacing * stretchFactor;
$: effectiveOrigin = computeEffectiveOrigin(direction, originX, originY, otherAxis);
$: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, stretchFactor, minorDivisions, microDivisions, rulerLength, otherAxis);
$: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, otherAxis, tilt);
$: effectiveOrigin = projectOntoRuler(direction, originX, originY, crossAxisDirection);
$: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, stretchFactor, minorDivisions, microDivisions, rulerLength, crossAxisDirection);
$: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, crossAxisDirection);
$: cursorIndicatorPath = computeCursorIndicator(direction, cursorPosition, crossAxisDirection);

function computeAxes(tilt: number): { horiz: Axis; vert: Axis } {
const normTilt = ((tilt % TAU) + TAU) % TAU;
Expand All @@ -50,13 +54,24 @@
return { horiz: posY, vert: negX };
}

function computeEffectiveOrigin(direction: RulerDirection, ox: number, oy: number, otherAxis: Axis): number {
const [vx, vy] = otherAxis.vec;
if (direction === "Horizontal") {
return Math.abs(vy) < 1e-10 ? ox : ox - oy * (vx / vy);
} else {
return Math.abs(vx) < 1e-10 ? oy : oy - ox * (vy / vx);
}
function flipVector(vec: [number, number], flipped: boolean): [number, number] {
return flipped ? [-vec[0], vec[1]] : vec;
}

function projectOntoRuler(direction: RulerDirection, x: number, y: number, vec: [number, number]): number {
const [vx, vy] = vec;
if (direction === "Horizontal") return Math.abs(vy) < 1e-10 ? x : x - y * (vx / vy);
return Math.abs(vx) < 1e-10 ? y : y - x * (vy / vx);
}

function tickMarkGeometry(direction: RulerDirection, vx: number, vy: number): { dx: number; dy: number; sxBase: number; syBase: number } {
const reversal = direction === "Horizontal" ? (vy > 0 ? -1 : 1) : vx > 0 ? -1 : 1;
return {
dx: vx * reversal,
dy: vy * reversal,
sxBase: direction === "Horizontal" ? 0 : RULER_THICKNESS,
syBase: direction === "Horizontal" ? RULER_THICKNESS : 0,
};
}

function computeSvgPath(
Expand All @@ -67,17 +82,14 @@
minorDivisions: number,
microDivisions: number,
rulerLength: number,
otherAxis: Axis,
crossAxisDirection: [number, number],
): string {
const adaptive = stretchFactor > 1.3 ? { minor: minorDivisions, micro: 1 } : { minor: minorDivisions, micro: microDivisions };
const divisions = stretchedSpacing / adaptive.minor / adaptive.micro;
const majorMarksFrequency = adaptive.minor * adaptive.micro;
const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing;

const [vx, vy] = otherAxis.vec;
const flip = direction === "Horizontal" ? (vy > 0 ? -1 : 1) : vx > 0 ? -1 : 1;
const [dx, dy] = [vx * flip, vy * flip];
const [sxBase, syBase] = direction === "Horizontal" ? [0, RULER_THICKNESS] : [RULER_THICKNESS, 0];
const { dx, dy, sxBase, syBase } = tickMarkGeometry(direction, crossAxisDirection[0], crossAxisDirection[1]);

let path = "";
let i = 0;
Expand All @@ -103,16 +115,15 @@
numberInterval: number,
rulerLength: number,
trackedAxis: Axis,
otherAxis: Axis,
tilt: number,
crossAxisDirection: [number, number],
): { transform: string; text: string }[] {
const isVertical = direction === "Vertical";

const [vx, vy] = otherAxis.vec;
const flip = isVertical ? (vx > 0 ? -1 : 1) : vy > 0 ? -1 : 1;
const tiltScale = tilt >= 0 ? 1 : 0.5;
const tipOffsetX = vx * flip * MAJOR_MARK_THICKNESS * tiltScale;
const tipOffsetY = vy * flip * MAJOR_MARK_THICKNESS * tiltScale;
const { dx: tipDx, dy: tipDy } = tickMarkGeometry(direction, crossAxisDirection[0], crossAxisDirection[1]);
const forwardTip = isVertical ? -tipDy : tipDx;
const tiltScale = forwardTip >= 0 ? 1 : 0.5;
const tipOffsetX = tipDx * MAJOR_MARK_THICKNESS * tiltScale;
const tipOffsetY = tipDy * MAJOR_MARK_THICKNESS * tiltScale;

const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing;
const increments = Math.round((shiftedOffsetStart - effectiveOrigin) / stretchedSpacing);
Expand All @@ -139,6 +150,21 @@
return results;
}

function computeCursorIndicator(direction: RulerDirection, cursor: { x: number; y: number } | undefined, crossAxisDirection: [number, number]): string {
if (cursor === undefined) return "";

const projected = projectOntoRuler(direction, cursor.x, cursor.y, crossAxisDirection);
const { dx, dy, sxBase, syBase } = tickMarkGeometry(direction, crossAxisDirection[0], crossAxisDirection[1]);

// Scale the line so it spans the full ruler bar thickness
const thicknessComponent = Math.abs(direction === "Horizontal" ? dy : dx);
const length = thicknessComponent < 1e-10 ? RULER_THICKNESS : RULER_THICKNESS / thicknessComponent;

const destination = Math.round(projected) + 0.5;
const [sx, sy] = direction === "Horizontal" ? [destination, syBase] : [sxBase, destination];
return `M${sx},${sy}l${dx * length},${dy * length}`;
}

export function resize() {
if (!rulerInput) return;

Expand Down Expand Up @@ -170,6 +196,9 @@
{#each svgTexts as svgText}
<text transform={svgText.transform}>{svgText.text}</text>
{/each}
{#if cursorIndicatorPath}
<path class="cursor-indicator" d={cursorIndicatorPath} />
{/if}
</svg>
</div>

Expand Down Expand Up @@ -201,6 +230,10 @@
path {
stroke-width: 1px;
stroke: var(--color-5-dullgray);

&.cursor-indicator {
stroke: var(--color-8-uppergray);
}
}

text {
Expand Down
Loading