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,
|
interval: f64,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
tilt: f64,
|
tilt: f64,
|
||||||
|
flip: bool,
|
||||||
},
|
},
|
||||||
UpdateDocumentScrollbars {
|
UpdateDocumentScrollbars {
|
||||||
position: (f64, f64),
|
position: (f64, f64),
|
||||||
|
|
|
||||||
|
|
@ -844,6 +844,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||||
interval: ruler_interval,
|
interval: ruler_interval,
|
||||||
visible: self.rulers_visible,
|
visible: self.rulers_visible,
|
||||||
tilt: if self.graph_view_overlay_open { 0. } else { current_ptz.tilt() },
|
tilt: if self.graph_view_overlay_open { 0. } else { current_ptz.tilt() },
|
||||||
|
flip: !self.graph_view_overlay_open && current_ptz.flip,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
DocumentMessage::RenderScrollbars => {
|
DocumentMessage::RenderScrollbars => {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@
|
||||||
let rulerInterval = 100;
|
let rulerInterval = 100;
|
||||||
let rulersVisible = true;
|
let rulersVisible = true;
|
||||||
let rulerTilt = 0;
|
let rulerTilt = 0;
|
||||||
|
let rulerFlip = false;
|
||||||
|
let rulerCursorPosition: { x: number; y: number } | undefined;
|
||||||
|
let viewportBounds: DOMRect | undefined;
|
||||||
|
|
||||||
// Rendered SVG viewport data
|
// Rendered SVG viewport data
|
||||||
let artworkSvg = "";
|
let artworkSvg = "";
|
||||||
|
|
@ -288,12 +291,17 @@
|
||||||
scrollbarMultiplier = { x: multiplier[0], y: multiplier[1] };
|
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] };
|
rulerOrigin = { x: origin[0], y: origin[1] };
|
||||||
rulerSpacing = spacing;
|
rulerSpacing = spacing;
|
||||||
rulerInterval = interval;
|
rulerInterval = interval;
|
||||||
rulersVisible = visible;
|
rulersVisible = visible;
|
||||||
rulerTilt = tilt;
|
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
|
// Update mouse cursor icon
|
||||||
|
|
@ -416,6 +424,7 @@
|
||||||
canvasHeight = Math.ceil(parseFloat(getComputedStyle(viewport).height));
|
canvasHeight = Math.ceil(parseFloat(getComputedStyle(viewport).height));
|
||||||
|
|
||||||
devicePixelRatio = window.devicePixelRatio || 1;
|
devicePixelRatio = window.devicePixelRatio || 1;
|
||||||
|
viewportBounds = viewport.getBoundingClientRect();
|
||||||
|
|
||||||
// Resize the rulers
|
// Resize the rulers
|
||||||
rulerHorizontal?.resize();
|
rulerHorizontal?.resize();
|
||||||
|
|
@ -489,8 +498,8 @@
|
||||||
subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
|
subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
const { origin, spacing, interval, visible, tilt } = data;
|
const { origin, spacing, interval, visible, tilt, flip } = data;
|
||||||
updateDocumentRulers(origin, spacing, interval, visible, tilt);
|
updateDocumentRulers(origin, spacing, interval, visible, tilt, flip);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update mouse cursor icon
|
// Update mouse cursor icon
|
||||||
|
|
@ -601,9 +610,11 @@
|
||||||
originX={rulerOrigin.x}
|
originX={rulerOrigin.x}
|
||||||
originY={rulerOrigin.y}
|
originY={rulerOrigin.y}
|
||||||
tilt={rulerTilt}
|
tilt={rulerTilt}
|
||||||
|
flip={rulerFlip}
|
||||||
majorMarkSpacing={rulerSpacing}
|
majorMarkSpacing={rulerSpacing}
|
||||||
numberInterval={rulerInterval}
|
numberInterval={rulerInterval}
|
||||||
direction="Horizontal"
|
direction="Horizontal"
|
||||||
|
cursorPosition={rulerCursorPosition}
|
||||||
bind:this={rulerHorizontal}
|
bind:this={rulerHorizontal}
|
||||||
/>
|
/>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
|
|
@ -615,9 +626,11 @@
|
||||||
originX={rulerOrigin.x}
|
originX={rulerOrigin.x}
|
||||||
originY={rulerOrigin.y}
|
originY={rulerOrigin.y}
|
||||||
tilt={rulerTilt}
|
tilt={rulerTilt}
|
||||||
|
flip={rulerFlip}
|
||||||
majorMarkSpacing={rulerSpacing}
|
majorMarkSpacing={rulerSpacing}
|
||||||
numberInterval={rulerInterval}
|
numberInterval={rulerInterval}
|
||||||
direction="Vertical"
|
direction="Vertical"
|
||||||
|
cursorPosition={rulerCursorPosition}
|
||||||
bind:this={rulerVertical}
|
bind:this={rulerVertical}
|
||||||
/>
|
/>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
|
|
@ -664,6 +677,8 @@
|
||||||
class:viewport={!$appWindow.viewportHolePunch}
|
class:viewport={!$appWindow.viewportHolePunch}
|
||||||
class:viewport-transparent={$appWindow.viewportHolePunch}
|
class:viewport-transparent={$appWindow.viewportHolePunch}
|
||||||
on:pointerdown={(e) => canvasPointerDown(e)}
|
on:pointerdown={(e) => canvasPointerDown(e)}
|
||||||
|
on:pointermove={updateRulerCursorPosition}
|
||||||
|
on:pointerleave={() => (rulerCursorPosition = undefined)}
|
||||||
bind:this={viewport}
|
bind:this={viewport}
|
||||||
data-viewport
|
data-viewport
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,12 @@
|
||||||
export let originX: number;
|
export let originX: number;
|
||||||
export let originY: number;
|
export let originY: number;
|
||||||
export let tilt: number;
|
export let tilt: number;
|
||||||
|
export let flip: boolean = false;
|
||||||
export let numberInterval: number;
|
export let numberInterval: number;
|
||||||
export let majorMarkSpacing: number;
|
export let majorMarkSpacing: number;
|
||||||
export let minorDivisions = 5;
|
export let minorDivisions = 5;
|
||||||
export let microDivisions = 2;
|
export let microDivisions = 2;
|
||||||
|
export let cursorPosition: { x: number; y: number } | undefined = undefined;
|
||||||
|
|
||||||
let rulerInput: HTMLDivElement | undefined;
|
let rulerInput: HTMLDivElement | undefined;
|
||||||
let rulerLength = 0;
|
let rulerLength = 0;
|
||||||
|
|
@ -28,11 +30,13 @@
|
||||||
$: isHorizontal = direction === "Horizontal";
|
$: isHorizontal = direction === "Horizontal";
|
||||||
$: trackedAxis = isHorizontal ? axes.horiz : axes.vert;
|
$: trackedAxis = isHorizontal ? axes.horiz : axes.vert;
|
||||||
$: otherAxis = isHorizontal ? axes.vert : axes.horiz;
|
$: 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);
|
$: stretchFactor = 1 / Math.max(Math.abs(isHorizontal ? trackedAxis.vec[0] : trackedAxis.vec[1]), 1e-10);
|
||||||
$: stretchedSpacing = majorMarkSpacing * stretchFactor;
|
$: stretchedSpacing = majorMarkSpacing * stretchFactor;
|
||||||
$: effectiveOrigin = computeEffectiveOrigin(direction, originX, originY, otherAxis);
|
$: effectiveOrigin = projectOntoRuler(direction, originX, originY, crossAxisDirection);
|
||||||
$: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, stretchFactor, minorDivisions, microDivisions, rulerLength, otherAxis);
|
$: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, stretchFactor, minorDivisions, microDivisions, rulerLength, crossAxisDirection);
|
||||||
$: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, otherAxis, tilt);
|
$: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, crossAxisDirection);
|
||||||
|
$: cursorIndicatorPath = computeCursorIndicator(direction, cursorPosition, crossAxisDirection);
|
||||||
|
|
||||||
function computeAxes(tilt: number): { horiz: Axis; vert: Axis } {
|
function computeAxes(tilt: number): { horiz: Axis; vert: Axis } {
|
||||||
const normTilt = ((tilt % TAU) + TAU) % TAU;
|
const normTilt = ((tilt % TAU) + TAU) % TAU;
|
||||||
|
|
@ -50,13 +54,24 @@
|
||||||
return { horiz: posY, vert: negX };
|
return { horiz: posY, vert: negX };
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeEffectiveOrigin(direction: RulerDirection, ox: number, oy: number, otherAxis: Axis): number {
|
function flipVector(vec: [number, number], flipped: boolean): [number, number] {
|
||||||
const [vx, vy] = otherAxis.vec;
|
return flipped ? [-vec[0], vec[1]] : vec;
|
||||||
if (direction === "Horizontal") {
|
}
|
||||||
return Math.abs(vy) < 1e-10 ? ox : ox - oy * (vx / vy);
|
|
||||||
} else {
|
function projectOntoRuler(direction: RulerDirection, x: number, y: number, vec: [number, number]): number {
|
||||||
return Math.abs(vx) < 1e-10 ? oy : oy - ox * (vy / vx);
|
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(
|
function computeSvgPath(
|
||||||
|
|
@ -67,17 +82,14 @@
|
||||||
minorDivisions: number,
|
minorDivisions: number,
|
||||||
microDivisions: number,
|
microDivisions: number,
|
||||||
rulerLength: number,
|
rulerLength: number,
|
||||||
otherAxis: Axis,
|
crossAxisDirection: [number, number],
|
||||||
): string {
|
): string {
|
||||||
const adaptive = stretchFactor > 1.3 ? { minor: minorDivisions, micro: 1 } : { minor: minorDivisions, micro: microDivisions };
|
const adaptive = stretchFactor > 1.3 ? { minor: minorDivisions, micro: 1 } : { minor: minorDivisions, micro: microDivisions };
|
||||||
const divisions = stretchedSpacing / adaptive.minor / adaptive.micro;
|
const divisions = stretchedSpacing / adaptive.minor / adaptive.micro;
|
||||||
const majorMarksFrequency = adaptive.minor * adaptive.micro;
|
const majorMarksFrequency = adaptive.minor * adaptive.micro;
|
||||||
const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing;
|
const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing;
|
||||||
|
|
||||||
const [vx, vy] = otherAxis.vec;
|
const { dx, dy, sxBase, syBase } = tickMarkGeometry(direction, crossAxisDirection[0], crossAxisDirection[1]);
|
||||||
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];
|
|
||||||
|
|
||||||
let path = "";
|
let path = "";
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
@ -103,16 +115,15 @@
|
||||||
numberInterval: number,
|
numberInterval: number,
|
||||||
rulerLength: number,
|
rulerLength: number,
|
||||||
trackedAxis: Axis,
|
trackedAxis: Axis,
|
||||||
otherAxis: Axis,
|
crossAxisDirection: [number, number],
|
||||||
tilt: number,
|
|
||||||
): { transform: string; text: string }[] {
|
): { transform: string; text: string }[] {
|
||||||
const isVertical = direction === "Vertical";
|
const isVertical = direction === "Vertical";
|
||||||
|
|
||||||
const [vx, vy] = otherAxis.vec;
|
const { dx: tipDx, dy: tipDy } = tickMarkGeometry(direction, crossAxisDirection[0], crossAxisDirection[1]);
|
||||||
const flip = isVertical ? (vx > 0 ? -1 : 1) : vy > 0 ? -1 : 1;
|
const forwardTip = isVertical ? -tipDy : tipDx;
|
||||||
const tiltScale = tilt >= 0 ? 1 : 0.5;
|
const tiltScale = forwardTip >= 0 ? 1 : 0.5;
|
||||||
const tipOffsetX = vx * flip * MAJOR_MARK_THICKNESS * tiltScale;
|
const tipOffsetX = tipDx * MAJOR_MARK_THICKNESS * tiltScale;
|
||||||
const tipOffsetY = vy * flip * MAJOR_MARK_THICKNESS * tiltScale;
|
const tipOffsetY = tipDy * MAJOR_MARK_THICKNESS * tiltScale;
|
||||||
|
|
||||||
const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing;
|
const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing;
|
||||||
const increments = Math.round((shiftedOffsetStart - effectiveOrigin) / stretchedSpacing);
|
const increments = Math.round((shiftedOffsetStart - effectiveOrigin) / stretchedSpacing);
|
||||||
|
|
@ -139,6 +150,21 @@
|
||||||
return results;
|
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() {
|
export function resize() {
|
||||||
if (!rulerInput) return;
|
if (!rulerInput) return;
|
||||||
|
|
||||||
|
|
@ -170,6 +196,9 @@
|
||||||
{#each svgTexts as svgText}
|
{#each svgTexts as svgText}
|
||||||
<text transform={svgText.transform}>{svgText.text}</text>
|
<text transform={svgText.transform}>{svgText.text}</text>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if cursorIndicatorPath}
|
||||||
|
<path class="cursor-indicator" d={cursorIndicatorPath} />
|
||||||
|
{/if}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -201,6 +230,10 @@
|
||||||
path {
|
path {
|
||||||
stroke-width: 1px;
|
stroke-width: 1px;
|
||||||
stroke: var(--color-5-dullgray);
|
stroke: var(--color-5-dullgray);
|
||||||
|
|
||||||
|
&.cursor-indicator {
|
||||||
|
stroke: var(--color-8-uppergray);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text {
|
text {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue