Skip to content
Draft
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 @@ -248,6 +248,7 @@ pub enum FrontendMessage {
spacing: f64,
interval: f64,
visible: bool,
tilt: f64,
},
UpdateDocumentScrollbars {
position: (f64, f64),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
spacing: ruler_spacing,
interval: ruler_interval,
visible: self.rulers_visible,
tilt: if self.graph_view_overlay_open { 0. } else { current_ptz.tilt() },
});
}
DocumentMessage::RenderScrollbars => {
Expand Down
28 changes: 23 additions & 5 deletions frontend/src/components/panels/Document.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
let rulerSpacing = 100;
let rulerInterval = 100;
let rulersVisible = true;
let rulerTilt = 0;

// Rendered SVG viewport data
let artworkSvg = "";
Expand Down Expand Up @@ -296,11 +297,12 @@
scrollbarMultiplier = multiplier;
}

export function updateDocumentRulers(origin: XY, spacing: number, interval: number, visible: boolean) {
export function updateDocumentRulers(origin: XY, spacing: number, interval: number, visible: boolean, tilt: number) {
rulerOrigin = origin;
rulerSpacing = spacing;
rulerInterval = interval;
rulersVisible = visible;
rulerTilt = tilt;
}

// Update mouse cursor icon
Expand Down Expand Up @@ -479,8 +481,8 @@
editor.subscriptions.subscribeJsMessage(UpdateDocumentRulers, async (data) => {
await tick();

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

// Update mouse cursor icon
Expand Down Expand Up @@ -572,13 +574,29 @@
{#if rulersVisible}
<LayoutRow class="ruler-or-scrollbar top-ruler">
<LayoutCol class="ruler-corner"></LayoutCol>
<RulerInput origin={rulerOrigin.x} majorMarkSpacing={rulerSpacing} numberInterval={rulerInterval} direction="Horizontal" bind:this={rulerHorizontal} />
<RulerInput
originX={rulerOrigin.x}
originY={rulerOrigin.y}
majorMarkSpacing={rulerSpacing}
numberInterval={rulerInterval}
direction="Horizontal"
tilt={rulerTilt}
bind:this={rulerHorizontal}
/>
</LayoutRow>
{/if}
<LayoutRow class="viewport-container-inner-1">
{#if rulersVisible}
<LayoutCol class="ruler-or-scrollbar">
<RulerInput origin={rulerOrigin.y} majorMarkSpacing={rulerSpacing} numberInterval={rulerInterval} direction="Vertical" bind:this={rulerVertical} />
<RulerInput
originX={rulerOrigin.x}
originY={rulerOrigin.y}
majorMarkSpacing={rulerSpacing}
numberInterval={rulerInterval}
direction="Vertical"
tilt={rulerTilt}
bind:this={rulerVertical}
/>
</LayoutCol>
{/if}
<LayoutCol class="viewport-container-inner-2" styles={{ cursor: canvasCursor }} data-viewport-container>
Expand Down
134 changes: 94 additions & 40 deletions frontend/src/components/widgets/inputs/RulerInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,81 +11,136 @@
const MICRO_MARK_THICKNESS = 3;

export let direction: RulerDirection = "Vertical";
export let origin: number;
export let originX: number;
export let originY: number;
export let numberInterval: number;
export let majorMarkSpacing: number;
export let minorDivisions = 5;
export let microDivisions = 2;
export let tilt: number = 0;

let rulerInput: HTMLDivElement | undefined;
let rulerLength = 0;
let svgBounds = { width: "0px", height: "0px" };

$: svgPath = computeSvgPath(direction, origin, majorMarkSpacing, minorDivisions, microDivisions, rulerLength);
$: svgTexts = computeSvgTexts(direction, origin, majorMarkSpacing, numberInterval, rulerLength);

function computeSvgPath(direction: RulerDirection, origin: number, majorMarkSpacing: number, minorDivisions: number, microDivisions: number, rulerLength: number): string {
const isVertical = direction === "Vertical";
const lineDirection = isVertical ? "H" : "V";

const offsetStart = mod(origin, majorMarkSpacing);
const shiftedOffsetStart = offsetStart - majorMarkSpacing;
type Axis = {
sign: number;
vec: [number, number];
};

$: axes = computeAxes(tilt);
$: isHorizontal = direction === "Horizontal";
$: trackedAxis = isHorizontal ? axes.horiz : axes.vert;
$: otherAxis = isHorizontal ? axes.vert : axes.horiz;
$: stretchFactor = 1 / (isHorizontal ? trackedAxis.vec[0] : trackedAxis.vec[1]);
$: stretchedSpacing = majorMarkSpacing * stretchFactor;
$: effectiveOrigin = computeEffectiveOrigin(direction, originX, originY, otherAxis);
$: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, minorDivisions, microDivisions, rulerLength, otherAxis);
$: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, otherAxis);

function computeAxes(tilt: number): { horiz: Axis; vert: Axis } {
const HALF_PI = Math.PI / 2;
const normTilt = ((tilt % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
const octant = Math.floor((normTilt + Math.PI / 4) / HALF_PI) % 4;

const [c, s] = [Math.cos(tilt), Math.sin(tilt)];
const posX: Axis = { sign: 1, vec: [c, s] };
const posY: Axis = { sign: 1, vec: [-s, c] };
const negX: Axis = { sign: -1, vec: [-c, -s] };
const negY: Axis = { sign: -1, vec: [s, -c] };

if (octant === 0) return { horiz: posX, vert: posY };
if (octant === 1) return { horiz: negY, vert: posX };
if (octant === 2) return { horiz: negX, vert: negY };
return { horiz: posY, vert: negX };
}

const divisions = majorMarkSpacing / minorDivisions / microDivisions;
const majorMarksFrequency = minorDivisions * microDivisions;
function computeEffectiveOrigin(direction: RulerDirection, ox: number, oy: number, otherAxis: Axis): number {
const [vx, vy] = otherAxis.vec;
return direction === "Horizontal" ? ox - oy * (vx / vy) : oy - ox * (vy / vx);
}

let dPathAttribute = "";
function computeSvgPath(
direction: RulerDirection,
effectiveOrigin: number,
stretchedSpacing: number,
minorDivisions: number,
microDivisions: number,
rulerLength: number,
otherAxis: Axis,
): 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;
// Tick direction: project outward from viewport edge into the ruler strip
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 i = 0;
for (let location = shiftedOffsetStart; location < rulerLength; location += divisions) {
let length;
if (i % majorMarksFrequency === 0) length = MAJOR_MARK_THICKNESS;
else if (i % microDivisions === 0) length = MINOR_MARK_THICKNESS;
else length = MICRO_MARK_THICKNESS;
for (let loc = shiftedOffsetStart; loc < rulerLength + RULER_THICKNESS; loc += divisions) {
const length = i % majorMarksFrequency === 0 ? MAJOR_MARK_THICKNESS : i % adaptive.micro === 0 ? MINOR_MARK_THICKNESS : MICRO_MARK_THICKNESS;
i += 1;

const destination = Math.round(location) + 0.5;
const startPoint = isVertical ? `${RULER_THICKNESS - length},${destination}` : `${destination},${RULER_THICKNESS - length}`;
dPathAttribute += `M${startPoint}${lineDirection}${RULER_THICKNESS} `;
const pos = Math.round(loc) + 0.5;
const [sx, sy] = direction === "Horizontal" ? [pos, syBase] : [sxBase, pos];
path += `M${sx},${sy}l${dx * length},${dy * length} `;
}

return dPathAttribute;
return path;
}

function computeSvgTexts(direction: RulerDirection, origin: number, majorMarkSpacing: number, numberInterval: number, rulerLength: number): { transform: string; text: string }[] {
function computeSvgTexts(
direction: RulerDirection,
effectiveOrigin: number,
stretchedSpacing: number,
numberInterval: number,
rulerLength: number,
trackedAxis: Axis,
otherAxis: Axis,
): { transform: string; text: string }[] {
const isVertical = direction === "Vertical";

const offsetStart = mod(origin, majorMarkSpacing);
const shiftedOffsetStart = offsetStart - majorMarkSpacing;
// Compute the tick tip offset so labels align with the top of the slanted tick
const [vx, vy] = otherAxis.vec;
const flip = isVertical ? (vx > 0 ? -1 : 1) : vy > 0 ? -1 : 1;
const tipOffsetX = vx * flip * MAJOR_MARK_THICKNESS;
const tipOffsetY = vy * flip * MAJOR_MARK_THICKNESS;

const svgTextCoordinates = [];
const shiftedOffsetStart = mod(effectiveOrigin, stretchedSpacing) - stretchedSpacing;
const increments = Math.round((shiftedOffsetStart - effectiveOrigin) / stretchedSpacing);
let labelNumber = increments * numberInterval * trackedAxis.sign;

let labelNumber = (Math.ceil(-origin / majorMarkSpacing) - 1) * numberInterval;
const results: { transform: string; text: string }[] = [];

for (let location = shiftedOffsetStart; location < rulerLength; location += majorMarkSpacing) {
const destination = Math.round(location);
const x = isVertical ? 9 : destination + 2;
const y = isVertical ? destination + 1 : 9;
for (let loc = shiftedOffsetStart; loc < rulerLength; loc += stretchedSpacing) {
const destination = Math.round(loc);
const x = isVertical ? 9 : destination + 2 + tipOffsetX;
const y = isVertical ? destination + 1 + tipOffsetY : 9;

let transform = `translate(${x} ${y})`;
if (isVertical) transform += " rotate(270)";

const text = numberInterval >= 1 ? `${labelNumber}` : labelNumber.toFixed(Math.abs(Math.log10(numberInterval))).replace(/\.0+$/, "");
if (isVertical) transform += " rotate(-90)";

svgTextCoordinates.push({ transform, text });
const num = Math.abs(labelNumber) < 1e-9 ? 0 : labelNumber;
const text = numberInterval >= 1 ? `${num}` : num.toFixed(Math.abs(Math.log10(numberInterval))).replace(/\.0+$/, "");

labelNumber += numberInterval;
results.push({ transform, text });
labelNumber += numberInterval * trackedAxis.sign;
}

return svgTextCoordinates;
return results;
}

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

const isVertical = direction === "Vertical";

const newLength = isVertical ? rulerInput.clientHeight : rulerInput.clientWidth;
const roundedUp = (Math.floor(newLength / majorMarkSpacing) + 1) * majorMarkSpacing;
const roundedUp = (Math.floor(newLength / stretchedSpacing) + 2) * stretchedSpacing;

if (roundedUp !== rulerLength) {
rulerLength = roundedUp;
Expand All @@ -95,7 +150,6 @@
}
}

// Modulo function that works for negative numbers, unlike the JS `%` operator
function mod(n: number, m: number): number {
const remainder = n % m;
return Math.floor(remainder >= 0 ? remainder : remainder + m);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,8 @@ export class UpdateDocumentRulers extends JsMessage {
readonly interval!: number;

readonly visible!: boolean;

readonly tilt!: number;
}

export class EyedropperPreviewImage {
Expand Down
Loading