diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 743d6ed2c2..49960ba3d4 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -240,6 +240,7 @@ pub enum FrontendMessage { spacing: f64, interval: f64, visible: bool, + tilt: f64, }, UpdateDocumentScrollbars { position: (f64, f64), diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 8cdb18e959..ea483d2308 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -807,6 +807,7 @@ impl MessageHandler> 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 => { diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 5f00696c2b..dbc9c2d182 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1057,7 +1057,7 @@ impl MessageHandler> for Portfolio } PortfolioMessage::RequestStatusBarInfoLayout => { #[cfg(not(target_family = "wasm"))] - let widgets = vec![TextLabel::new("Graphite 1.0.0-RC3").disabled(true).widget_instance()]; // TODO: After the RCs, call this "Graphite (beta) x.y.z" + let widgets = vec![TextLabel::new("Graphite 1.0.0-RC4").disabled(true).widget_instance()]; // TODO: After the RCs, call this "Graphite (beta) x.y.z" #[cfg(target_family = "wasm")] let widgets = vec![]; diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 1767c18e6c..c9210b7ff4 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -45,6 +45,7 @@ let rulerSpacing = 100; let rulerInterval = 100; let rulersVisible = true; + let rulerTilt = 0; // Rendered SVG viewport data let artworkSvg = ""; @@ -284,11 +285,12 @@ 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] }; rulerSpacing = spacing; rulerInterval = interval; rulersVisible = visible; + rulerTilt = tilt; } // Update mouse cursor icon @@ -483,8 +485,8 @@ editor.subscriptions.subscribeFrontendMessage("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 @@ -575,13 +577,29 @@ {#if rulersVisible} - + {/if} {#if rulersVisible} - + {/if} diff --git a/frontend/src/components/panels/Welcome.svelte b/frontend/src/components/panels/Welcome.svelte index 1c908286a3..49101abc37 100644 --- a/frontend/src/components/panels/Welcome.svelte +++ b/frontend/src/components/panels/Welcome.svelte @@ -52,7 +52,7 @@ {#if isPlatformNative()} - You are testing Release Candidate 3 of the 1.0 desktop release. Please regularly check Discord for the next testing build and report issues you encounter. + You are testing Release Candidate 4 of the 1.0 desktop release. Please regularly check Discord for the next testing build and report issues you encounter. {/if} diff --git a/frontend/src/components/widgets/inputs/RulerInput.svelte b/frontend/src/components/widgets/inputs/RulerInput.svelte index b1f5495943..e3bd23d170 100644 --- a/frontend/src/components/widgets/inputs/RulerInput.svelte +++ b/frontend/src/components/widgets/inputs/RulerInput.svelte @@ -5,85 +5,139 @@ const MAJOR_MARK_THICKNESS = 16; const MINOR_MARK_THICKNESS = 6; const MICRO_MARK_THICKNESS = 3; + const TAU = 2 * Math.PI; type RulerDirection = "Horizontal" | "Vertical"; 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, stretchFactor, minorDivisions, microDivisions, rulerLength, otherAxis); + $: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, otherAxis); + + function computeAxes(tilt: number): { horiz: Axis; vert: Axis } { + const normTilt = ((tilt % TAU) + TAU) % TAU; + const octant = Math.floor((normTilt + Math.PI / 4) / (Math.PI / 2)) % 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, + 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; - for (let location = shiftedOffsetStart; location < rulerLength; location += divisions) { + for (let location = shiftedOffsetStart; location < rulerLength + RULER_THICKNESS; location += divisions) { let length; 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; 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 [sx, sy] = direction === "Horizontal" ? [destination, syBase] : [sxBase, destination]; + 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; + 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) { + for (let location = shiftedOffsetStart; location < rulerLength; location += stretchedSpacing) { const destination = Math.round(location); - const x = isVertical ? 9 : destination + 2; - const y = isVertical ? destination + 1 : 9; + 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+$/, ""); - - 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; diff --git a/frontend/src/components/window/MainWindow.svelte b/frontend/src/components/window/MainWindow.svelte index 252c417425..de83f3ab18 100644 --- a/frontend/src/components/window/MainWindow.svelte +++ b/frontend/src/components/window/MainWindow.svelte @@ -31,7 +31,7 @@ {#if $tooltip.visible} {/if} - {#if isPlatformNative() && new Date() > new Date("2026-03-15")} + {#if isPlatformNative() && new Date() > new Date("2026-04-30")}