Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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];
};
Comment thread
jsjgdh marked this conversation as resolved.
Outdated

$: 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;
Comment thread
jsjgdh marked this conversation as resolved.
Outdated
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 };
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
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) {
Comment thread
jsjgdh marked this conversation as resolved.
Outdated
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) {
Comment thread
jsjgdh marked this conversation as resolved.
Outdated
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)";
Comment thread
jsjgdh marked this conversation as resolved.
Outdated

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
Comment thread
jsjgdh marked this conversation as resolved.
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