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
This commit is contained in:
parent
29f6e686ee
commit
0acfd3e178
|
|
@ -245,6 +245,7 @@ pub enum FrontendMessage {
|
|||
interval: f64,
|
||||
visible: bool,
|
||||
tilt: f64,
|
||||
flip: bool,
|
||||
},
|
||||
UpdateDocumentScrollbars {
|
||||
position: (f64, f64),
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
// Update mouse cursor icon
|
||||
|
|
@ -416,6 +424,7 @@
|
|||
canvasHeight = Math.ceil(parseFloat(getComputedStyle(viewport).height));
|
||||
|
||||
devicePixelRatio = window.devicePixelRatio || 1;
|
||||
viewportBounds = viewport.getBoundingClientRect();
|
||||
|
||||
// Resize the rulers
|
||||
rulerHorizontal?.resize();
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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(
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -201,6 +230,10 @@
|
|||
path {
|
||||
stroke-width: 1px;
|
||||
stroke: var(--color-5-dullgray);
|
||||
|
||||
&.cursor-indicator {
|
||||
stroke: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
|
||||
text {
|
||||
|
|
|
|||
Loading…
Reference in New Issue