Skip to content

Commit c368f19

Browse files
committed
Implement ruler bounds visualization for the AABB of selected layers
1 parent eddd742 commit c368f19

5 files changed

Lines changed: 128 additions & 37 deletions

File tree

editor/src/messages/frontend/frontend_message.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ pub enum FrontendMessage {
246246
visible: bool,
247247
tilt: f64,
248248
flip: bool,
249+
#[serde(rename = "selectionQuad")]
250+
selection_quad: Option<[(f64, f64); 4]>,
249251
},
250252
UpdateDocumentScrollbars {
251253
position: (f64, f64),

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,13 +838,31 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
838838

839839
let ruler_spacing = ruler_interval * ruler_scale;
840840

841+
// Compute the selection bounding box as 4 viewport-space corners preserving orientation
842+
let selection_quad = if !self.graph_view_overlay_open {
843+
self.network_interface
844+
.selected_nodes()
845+
.0
846+
.iter()
847+
.filter(|node| self.network_interface.is_layer(node, &[]))
848+
.filter_map(|layer| self.metadata().bounding_box_document(LayerNodeIdentifier::new(*layer, &self.network_interface)))
849+
.reduce(Quad::combine_bounds)
850+
.map(|[min, max]| {
851+
let corners = [DVec2::new(min.x, min.y), DVec2::new(max.x, min.y), DVec2::new(max.x, max.y), DVec2::new(min.x, max.y)];
852+
corners.map(|c| document_to_viewport.transform_point2(c).into())
853+
})
854+
} else {
855+
None
856+
};
857+
841858
responses.add(FrontendMessage::UpdateDocumentRulers {
842859
origin: ruler_origin.into(),
843860
spacing: ruler_spacing,
844861
interval: ruler_interval,
845862
visible: self.rulers_visible,
846863
tilt: if self.graph_view_overlay_open { 0. } else { current_ptz.tilt() },
847864
flip: !self.graph_view_overlay_open && current_ptz.flip,
865+
selection_quad,
848866
});
849867
}
850868
DocumentMessage::RenderScrollbars => {

frontend/src/components/Editor.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@
144144
--color-data-invalid: #d6536e; // Same as --color-error-red
145145
--color-data-invalid-dim: #a7324a;
146146
147+
--color-overlay-blue: #00a8ff;
148+
--color-overlay-blue: rgb(0, 168, 255);
149+
147150
--color-none: white;
148151
--color-none-repeat: no-repeat;
149152
--color-none-position: center center;

frontend/src/components/panels/Document.svelte

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
let rulerTilt = 0;
4848
let rulerFlip = false;
4949
let rulerCursorPosition: { x: number; y: number } | undefined;
50+
let rulerSelectionQuad: [number, number][] | undefined;
5051
let viewportBounds: DOMRect | undefined;
5152
5253
// Rendered SVG viewport data
@@ -291,13 +292,14 @@
291292
scrollbarMultiplier = { x: multiplier[0], y: multiplier[1] };
292293
}
293294
294-
export function updateDocumentRulers(origin: [number, number], spacing: number, interval: number, visible: boolean, tilt: number, flip: boolean) {
295+
export function updateDocumentRulers(origin: [number, number], spacing: number, interval: number, visible: boolean, tilt: number, flip: boolean, selectionQuad: [number, number][] | undefined) {
295296
rulerOrigin = { x: origin[0], y: origin[1] };
296297
rulerSpacing = spacing;
297298
rulerInterval = interval;
298299
rulersVisible = visible;
299300
rulerTilt = tilt;
300301
rulerFlip = flip;
302+
rulerSelectionQuad = selectionQuad;
301303
}
302304
303305
function updateRulerCursorPosition(e: PointerEvent) {
@@ -498,8 +500,8 @@
498500
subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
499501
await tick();
500502
501-
const { origin, spacing, interval, visible, tilt, flip } = data;
502-
updateDocumentRulers(origin, spacing, interval, visible, tilt, flip);
503+
const { origin, spacing, interval, visible, tilt, flip, selectionQuad } = data;
504+
updateDocumentRulers(origin, spacing, interval, visible, tilt, flip, selectionQuad || undefined);
503505
});
504506
505507
// Update mouse cursor icon
@@ -615,6 +617,7 @@
615617
numberInterval={rulerInterval}
616618
direction="Horizontal"
617619
cursorPosition={rulerCursorPosition}
620+
selectionQuad={rulerSelectionQuad}
618621
bind:this={rulerHorizontal}
619622
/>
620623
</LayoutRow>
@@ -631,6 +634,7 @@
631634
numberInterval={rulerInterval}
632635
direction="Vertical"
633636
cursorPosition={rulerCursorPosition}
637+
selectionQuad={rulerSelectionQuad}
634638
bind:this={rulerVertical}
635639
/>
636640
</LayoutCol>
@@ -888,7 +892,7 @@
888892
}
889893
}
890894
891-
.top-ruler .ruler-input {
895+
.top-ruler .ruler-wrapper {
892896
margin-right: 16px;
893897
}
894898

frontend/src/components/widgets/inputs/RulerInput.svelte

Lines changed: 97 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { onMount } from "svelte";
33
4+
const SELECTION_ENDPOINT_SIZE = 5;
45
const RULER_THICKNESS = 16;
56
const MAJOR_MARK_THICKNESS = 16;
67
const MINOR_MARK_THICKNESS = 6;
@@ -19,6 +20,7 @@
1920
export let minorDivisions = 5;
2021
export let microDivisions = 2;
2122
export let cursorPosition: { x: number; y: number } | undefined = undefined;
23+
export let selectionQuad: [number, number][] | undefined = undefined;
2224
2325
let rulerInput: HTMLDivElement | undefined;
2426
let rulerLength = 0;
@@ -37,6 +39,7 @@
3739
$: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, stretchFactor, minorDivisions, microDivisions, rulerLength, crossAxisDirection);
3840
$: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, crossAxisDirection);
3941
$: cursorIndicatorPath = computeCursorIndicator(direction, cursorPosition, crossAxisDirection);
42+
$: selectionExtent = computeSelectionExtent(direction, selectionQuad, crossAxisDirection);
4043
4144
function computeAxes(tilt: number): { horiz: Axis; vert: Axis } {
4245
const normTilt = ((tilt % TAU) + TAU) % TAU;
@@ -165,6 +168,14 @@
165168
return `M${sx},${sy}l${dx * length},${dy * length}`;
166169
}
167170
171+
function computeSelectionExtent(direction: RulerDirection, quad: [number, number][] | undefined, crossAxisDirection: [number, number]): { min: number; max: number } | undefined {
172+
if (!quad || quad.length === 0) return undefined;
173+
174+
const projected = quad.map(([x, y]) => projectOntoRuler(direction, x, y, crossAxisDirection));
175+
176+
return { min: Math.min(...projected), max: Math.max(...projected) };
177+
}
178+
168179
export function resize() {
169180
if (!rulerInput) return;
170181
@@ -190,56 +201,109 @@
190201
onMount(resize);
191202
</script>
192203

193-
<div class={`ruler-input ${direction.toLowerCase()}`} bind:this={rulerInput}>
194-
<svg style:width={svgBounds.width} style:height={svgBounds.height}>
195-
<path d={svgPath} />
196-
{#each svgTexts as svgText}
197-
<text transform={svgText.transform}>{svgText.text}</text>
198-
{/each}
199-
{#if cursorIndicatorPath}
200-
<path class="cursor-indicator" d={cursorIndicatorPath} />
201-
{/if}
202-
</svg>
204+
<div class="ruler-input">
205+
<div class={`ruler-area ${direction === "Horizontal" ? "horizontal" : "vertical"}`} bind:this={rulerInput}>
206+
<svg style:width={svgBounds.width} style:height={svgBounds.height}>
207+
<path d={svgPath} />
208+
{#each svgTexts as svgText}
209+
<text transform={svgText.transform}>{svgText.text}</text>
210+
{/each}
211+
{#if cursorIndicatorPath}
212+
<path class="cursor-indicator" d={cursorIndicatorPath} />
213+
{/if}
214+
</svg>
215+
</div>
216+
{#if selectionExtent}
217+
{@const isVertical = direction === "Vertical"}
218+
{@const minPos = Math.round(selectionExtent.min)}
219+
{@const maxPos = Math.round(selectionExtent.max)}
220+
{@const half = Math.floor(SELECTION_ENDPOINT_SIZE / 2)}
221+
{@const overlap = Math.ceil(SELECTION_ENDPOINT_SIZE / 2)}
222+
<div class="selection-overlay-container" style:width={isVertical ? `${RULER_THICKNESS + overlap}px` : "100%"} style:height={isVertical ? "100%" : `${RULER_THICKNESS + overlap}px`}>
223+
<div
224+
class="selection-line"
225+
style:left={isVertical ? `${RULER_THICKNESS}px` : `${minPos}px`}
226+
style:top={isVertical ? `${minPos}px` : `${RULER_THICKNESS}px`}
227+
style:width={isVertical ? "1px" : `${maxPos - minPos}px`}
228+
style:height={isVertical ? `${maxPos - minPos}px` : "1px"}
229+
></div>
230+
{#each [minPos, maxPos] as pos}
231+
<div
232+
class="selection-endpoint"
233+
style:left={isVertical ? `${RULER_THICKNESS - half}px` : `${pos - half}px`}
234+
style:top={isVertical ? `${pos - half}px` : `${RULER_THICKNESS - half}px`}
235+
style:width={`${SELECTION_ENDPOINT_SIZE}px`}
236+
style:height={`${SELECTION_ENDPOINT_SIZE}px`}
237+
></div>
238+
{/each}
239+
</div>
240+
{/if}
203241
</div>
204242

205243
<style lang="scss">
206244
.ruler-input {
207245
flex: 1 1 100%;
208-
background: var(--color-2-mildblack);
209-
overflow: hidden;
210246
position: relative;
211247
box-sizing: border-box;
212248
213-
&.horizontal {
214-
height: 16px;
215-
border-bottom: 1px solid var(--color-5-dullgray);
216-
}
249+
.ruler-area {
250+
background: var(--color-2-mildblack);
251+
width: 100%;
252+
height: 100%;
253+
position: relative;
254+
overflow: hidden;
217255
218-
&.vertical {
219-
width: 16px;
220-
border-right: 1px solid var(--color-5-dullgray);
256+
&.horizontal {
257+
height: 16px;
258+
border-bottom: 1px solid var(--color-5-dullgray);
259+
}
221260
222-
svg text {
223-
text-anchor: end;
261+
&.vertical {
262+
width: 16px;
263+
border-right: 1px solid var(--color-5-dullgray);
264+
265+
svg text {
266+
text-anchor: end;
267+
}
224268
}
225-
}
226269
227-
svg {
228-
position: absolute;
270+
svg {
271+
position: absolute;
229272
230-
path {
231-
stroke-width: 1px;
232-
stroke: var(--color-5-dullgray);
273+
path {
274+
stroke-width: 1px;
275+
stroke: var(--color-5-dullgray);
233276
234-
&.cursor-indicator {
235-
stroke: var(--color-8-uppergray);
277+
&.cursor-indicator {
278+
stroke: var(--color-8-uppergray);
279+
}
236280
}
237-
}
238281
239-
text {
240-
font-size: 12px;
241-
fill: var(--color-8-uppergray);
282+
text {
283+
font-size: 12px;
284+
fill: var(--color-8-uppergray);
285+
}
242286
}
243287
}
288+
289+
.selection-overlay-container {
290+
overflow: hidden;
291+
position: absolute;
292+
z-index: 1;
293+
top: 0;
294+
left: 0;
295+
}
296+
297+
.selection-line {
298+
position: absolute;
299+
background: var(--color-8-uppergray);
300+
}
301+
302+
.selection-endpoint {
303+
position: absolute;
304+
background: var(--color-2-mildblack);
305+
border: 1px solid var(--color-overlay-blue);
306+
box-sizing: border-box;
307+
}
244308
}
245309
</style>

0 commit comments

Comments
 (0)