mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-03 01:48:11 +08:00
Improve viewport rulers by tilting tick marks to align with tilted documents (#3844)
* Add support for tilted rulers * Fix Ruler Text * Address PR review * Fix per review * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Timon
co-authored by
Keavon Chambers
parent
4474de4662
commit
4c1974c200
@@ -244,6 +244,7 @@ pub enum FrontendMessage {
|
|||||||
spacing: f64,
|
spacing: f64,
|
||||||
interval: f64,
|
interval: f64,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
|
tilt: f64,
|
||||||
},
|
},
|
||||||
UpdateDocumentScrollbars {
|
UpdateDocumentScrollbars {
|
||||||
position: (f64, f64),
|
position: (f64, f64),
|
||||||
|
|||||||
@@ -843,6 +843,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
spacing: ruler_spacing,
|
spacing: ruler_spacing,
|
||||||
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() },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
DocumentMessage::RenderScrollbars => {
|
DocumentMessage::RenderScrollbars => {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
let rulerSpacing = 100;
|
let rulerSpacing = 100;
|
||||||
let rulerInterval = 100;
|
let rulerInterval = 100;
|
||||||
let rulersVisible = true;
|
let rulersVisible = true;
|
||||||
|
let rulerTilt = 0;
|
||||||
|
|
||||||
// Rendered SVG viewport data
|
// Rendered SVG viewport data
|
||||||
let artworkSvg = "";
|
let artworkSvg = "";
|
||||||
@@ -287,11 +288,12 @@
|
|||||||
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) {
|
export function updateDocumentRulers(origin: [number, number], spacing: number, interval: number, visible: boolean, tilt: number) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update mouse cursor icon
|
// Update mouse cursor icon
|
||||||
@@ -487,8 +489,8 @@
|
|||||||
subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
|
subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
const { origin, spacing, interval, visible } = data;
|
const { origin, spacing, interval, visible, tilt } = data;
|
||||||
updateDocumentRulers(origin, spacing, interval, visible);
|
updateDocumentRulers(origin, spacing, interval, visible, tilt);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update mouse cursor icon
|
// Update mouse cursor icon
|
||||||
@@ -595,13 +597,29 @@
|
|||||||
{#if rulersVisible}
|
{#if rulersVisible}
|
||||||
<LayoutRow class="ruler-or-scrollbar top-ruler">
|
<LayoutRow class="ruler-or-scrollbar top-ruler">
|
||||||
<LayoutCol class="ruler-corner"></LayoutCol>
|
<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}
|
||||||
|
tilt={rulerTilt}
|
||||||
|
majorMarkSpacing={rulerSpacing}
|
||||||
|
numberInterval={rulerInterval}
|
||||||
|
direction="Horizontal"
|
||||||
|
bind:this={rulerHorizontal}
|
||||||
|
/>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
{/if}
|
{/if}
|
||||||
<LayoutRow class="viewport-container-inner-1">
|
<LayoutRow class="viewport-container-inner-1">
|
||||||
{#if rulersVisible}
|
{#if rulersVisible}
|
||||||
<LayoutCol class="ruler-or-scrollbar">
|
<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}
|
||||||
|
tilt={rulerTilt}
|
||||||
|
majorMarkSpacing={rulerSpacing}
|
||||||
|
numberInterval={rulerInterval}
|
||||||
|
direction="Vertical"
|
||||||
|
bind:this={rulerVertical}
|
||||||
|
/>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
{/if}
|
{/if}
|
||||||
<LayoutCol class="viewport-container-inner-2" styles={{ cursor: canvasCursor }} data-viewport-container>
|
<LayoutCol class="viewport-container-inner-2" styles={{ cursor: canvasCursor }} data-viewport-container>
|
||||||
|
|||||||
@@ -5,11 +5,14 @@
|
|||||||
const MAJOR_MARK_THICKNESS = 16;
|
const MAJOR_MARK_THICKNESS = 16;
|
||||||
const MINOR_MARK_THICKNESS = 6;
|
const MINOR_MARK_THICKNESS = 6;
|
||||||
const MICRO_MARK_THICKNESS = 3;
|
const MICRO_MARK_THICKNESS = 3;
|
||||||
|
const TAU = 2 * Math.PI;
|
||||||
|
|
||||||
type RulerDirection = "Horizontal" | "Vertical";
|
type RulerDirection = "Horizontal" | "Vertical";
|
||||||
|
|
||||||
export let direction: RulerDirection = "Vertical";
|
export let direction: RulerDirection = "Vertical";
|
||||||
export let origin: number;
|
export let originX: number;
|
||||||
|
export let originY: number;
|
||||||
|
export let tilt: number;
|
||||||
export let numberInterval: number;
|
export let numberInterval: number;
|
||||||
export let majorMarkSpacing: number;
|
export let majorMarkSpacing: number;
|
||||||
export let minorDivisions = 5;
|
export let minorDivisions = 5;
|
||||||
@@ -19,62 +22,121 @@
|
|||||||
let rulerLength = 0;
|
let rulerLength = 0;
|
||||||
let svgBounds = { width: "0px", height: "0px" };
|
let svgBounds = { width: "0px", height: "0px" };
|
||||||
|
|
||||||
$: svgPath = computeSvgPath(direction, origin, majorMarkSpacing, minorDivisions, microDivisions, rulerLength);
|
type Axis = { sign: number; vec: [number, number] };
|
||||||
$: svgTexts = computeSvgTexts(direction, origin, majorMarkSpacing, numberInterval, rulerLength);
|
|
||||||
|
|
||||||
function computeSvgPath(direction: RulerDirection, origin: number, majorMarkSpacing: number, minorDivisions: number, microDivisions: number, rulerLength: number): string {
|
$: axes = computeAxes(tilt);
|
||||||
const isVertical = direction === "Vertical";
|
$: isHorizontal = direction === "Horizontal";
|
||||||
const lineDirection = isVertical ? "H" : "V";
|
$: trackedAxis = isHorizontal ? axes.horiz : axes.vert;
|
||||||
|
$: otherAxis = isHorizontal ? axes.vert : axes.horiz;
|
||||||
|
$: 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);
|
||||||
|
|
||||||
const offsetStart = mod(origin, majorMarkSpacing);
|
function computeAxes(tilt: number): { horiz: Axis; vert: Axis } {
|
||||||
const shiftedOffsetStart = offsetStart - majorMarkSpacing;
|
const normTilt = ((tilt % TAU) + TAU) % TAU;
|
||||||
|
const octant = Math.floor((normTilt + Math.PI / 4) / (Math.PI / 2)) % 4;
|
||||||
|
|
||||||
const divisions = majorMarkSpacing / minorDivisions / microDivisions;
|
const [c, s] = [Math.cos(tilt), Math.sin(tilt)];
|
||||||
const majorMarksFrequency = minorDivisions * microDivisions;
|
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] };
|
||||||
|
|
||||||
let dPathAttribute = "";
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 computeSvgPath(
|
||||||
|
direction: RulerDirection,
|
||||||
|
effectiveOrigin: number,
|
||||||
|
stretchedSpacing: number,
|
||||||
|
stretchFactor: 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;
|
||||||
|
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;
|
let i = 0;
|
||||||
for (let location = shiftedOffsetStart; location < rulerLength; location += divisions) {
|
for (let location = shiftedOffsetStart; location < rulerLength + RULER_THICKNESS; location += divisions) {
|
||||||
let length;
|
let length;
|
||||||
if (i % majorMarksFrequency === 0) length = MAJOR_MARK_THICKNESS;
|
if (i % majorMarksFrequency === 0) length = MAJOR_MARK_THICKNESS;
|
||||||
else if (i % microDivisions === 0) length = MINOR_MARK_THICKNESS;
|
else if (i % adaptive.micro === 0) length = MINOR_MARK_THICKNESS;
|
||||||
else length = MICRO_MARK_THICKNESS;
|
else length = MICRO_MARK_THICKNESS;
|
||||||
i += 1;
|
i += 1;
|
||||||
|
|
||||||
const destination = Math.round(location) + 0.5;
|
const destination = Math.round(location) + 0.5;
|
||||||
const startPoint = isVertical ? `${RULER_THICKNESS - length},${destination}` : `${destination},${RULER_THICKNESS - length}`;
|
const [sx, sy] = direction === "Horizontal" ? [destination, syBase] : [sxBase, destination];
|
||||||
dPathAttribute += `M${startPoint}${lineDirection}${RULER_THICKNESS} `;
|
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,
|
||||||
|
tilt: number,
|
||||||
|
): { transform: string; text: string }[] {
|
||||||
const isVertical = direction === "Vertical";
|
const isVertical = direction === "Vertical";
|
||||||
|
|
||||||
const offsetStart = mod(origin, majorMarkSpacing);
|
const [vx, vy] = otherAxis.vec;
|
||||||
const shiftedOffsetStart = offsetStart - majorMarkSpacing;
|
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 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) {
|
for (let location = shiftedOffsetStart; location < rulerLength; location += stretchedSpacing) {
|
||||||
const destination = Math.round(location);
|
const destination = Math.round(location);
|
||||||
const x = isVertical ? 9 : destination + 2;
|
const x = isVertical ? 9 : destination + 2 + tipOffsetX;
|
||||||
const y = isVertical ? destination + 1 : 9;
|
const y = isVertical ? destination + 1 + tipOffsetY : 9;
|
||||||
|
|
||||||
let transform = `translate(${x} ${y})`;
|
let transform = `translate(${x} ${y})`;
|
||||||
if (isVertical) transform += " rotate(270)";
|
if (isVertical) transform += " rotate(270)";
|
||||||
|
|
||||||
const text = numberInterval >= 1 ? `${labelNumber}` : labelNumber.toFixed(Math.abs(Math.log10(numberInterval))).replace(/\.0+$/, "");
|
const num = Math.abs(labelNumber) < 1e-9 ? 0 : labelNumber;
|
||||||
|
const text = numberInterval >= 1 ? `${num}` : num.toFixed(Math.abs(Math.log10(numberInterval))).replace(/\.0+$/, "");
|
||||||
|
|
||||||
svgTextCoordinates.push({ transform, text });
|
results.push({ transform, text });
|
||||||
|
|
||||||
labelNumber += numberInterval;
|
labelNumber += numberInterval * trackedAxis.sign;
|
||||||
}
|
}
|
||||||
|
|
||||||
return svgTextCoordinates;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resize() {
|
export function resize() {
|
||||||
@@ -83,7 +145,7 @@
|
|||||||
const isVertical = direction === "Vertical";
|
const isVertical = direction === "Vertical";
|
||||||
|
|
||||||
const newLength = isVertical ? rulerInput.clientHeight : rulerInput.clientWidth;
|
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) {
|
if (roundedUp !== rulerLength) {
|
||||||
rulerLength = roundedUp;
|
rulerLength = roundedUp;
|
||||||
|
|||||||
Reference in New Issue
Block a user