@canvus/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/matrix.js ADDED
@@ -0,0 +1,264 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // canvus/src/matrix.ts
3
+ // Pure mathematical viewport transform utilities.
4
+ // Every function is stateless, side-effect free, and operates
5
+ // exclusively on the primitive types defined in types.ts.
6
+ // ─────────────────────────────────────────────────────────────
7
+ import { ZOOM_MAX, ZOOM_MIN } from "./types.js";
8
+ // ── Coordinate Space Conversions ────────────────────────────
9
+ /**
10
+ * Projects a raw screen-space pointer coordinate (e.g. `MouseEvent.clientX/Y`)
11
+ * into the infinite canvas coordinate space, accounting for:
12
+ * 1. The canvas element's own position within the page (`canvasRect`).
13
+ * 2. The current pan offset (`viewport.offsetX/Y`).
14
+ * 3. The current zoom scale (`viewport.scale`).
15
+ *
16
+ * Derivation:
17
+ * screenX = canvasX * scale + offsetX + canvasRect.x
18
+ * ⟹ canvasX = (clientX - canvasRect.x - offsetX) / scale
19
+ *
20
+ * @param clientX - `MouseEvent.clientX` (viewport-relative screen pixel).
21
+ * @param clientY - `MouseEvent.clientY` (viewport-relative screen pixel).
22
+ * @param viewport - Current affine viewport transform state.
23
+ * @param canvasRect - Bounding rect of the `<canvas>` element on screen.
24
+ * @returns The corresponding point in canvas (world) space.
25
+ */
26
+ export function screenToCanvas(clientX, clientY, viewport, canvasRect) {
27
+ return {
28
+ x: (clientX - canvasRect.x - viewport.offsetX) / viewport.scale,
29
+ y: (clientY - canvasRect.y - viewport.offsetY) / viewport.scale,
30
+ };
31
+ }
32
+ /**
33
+ * Projects a canvas-space coordinate back to absolute screen pixels,
34
+ * the algebraic inverse of `screenToCanvas`.
35
+ *
36
+ * Derivation:
37
+ * screenX = canvasX * scale + offsetX + canvasRect.x
38
+ *
39
+ * @param canvasX - X position in canvas (world) space.
40
+ * @param canvasY - Y position in canvas (world) space.
41
+ * @param viewport - Current affine viewport transform state.
42
+ * @param canvasRect - Bounding rect of the `<canvas>` element on screen.
43
+ * @returns The corresponding point in screen (client) space.
44
+ */
45
+ export function canvasToScreen(canvasX, canvasY, viewport, canvasRect) {
46
+ return {
47
+ x: canvasX * viewport.scale + viewport.offsetX + canvasRect.x,
48
+ y: canvasY * viewport.scale + viewport.offsetY + canvasRect.y,
49
+ };
50
+ }
51
+ // ── Zoom Anchoring ──────────────────────────────────────────
52
+ /**
53
+ * Computes a new `ViewportMatrix` after applying a zoom delta
54
+ * anchored precisely at the cursor's current screen position.
55
+ *
56
+ * The key invariant: the canvas-space point directly beneath the
57
+ * cursor must map to the *exact same screen pixel* before and
58
+ * after the zoom. This prevents the jarring "zoom drift" that
59
+ * occurs with naïve center-point scaling.
60
+ *
61
+ * ### Mathematical derivation
62
+ *
63
+ * Let `(cx, cy)` be the cursor in screen-space relative to the
64
+ * canvas element origin:
65
+ * cx = clientX - canvasRect.x
66
+ * cy = clientY - canvasRect.y
67
+ *
68
+ * Before zoom, the canvas-space point under the cursor is:
69
+ * worldX = (cx - offsetX) / oldScale
70
+ * worldY = (cy - offsetY) / oldScale
71
+ *
72
+ * After zoom with `newScale`, we require the same world point to
73
+ * project back to `(cx, cy)`:
74
+ * cx = worldX * newScale + newOffsetX
75
+ * cy = worldY * newScale + newOffsetY
76
+ *
77
+ * Solving for the new offsets:
78
+ * newOffsetX = cx - worldX * newScale
79
+ * = cx - ((cx - offsetX) / oldScale) * newScale
80
+ * = cx * (1 - newScale / oldScale) + offsetX * (newScale / oldScale)
81
+ *
82
+ * Equivalently (and more numerically stable):
83
+ * newOffsetX = cx - (cx - offsetX) * (newScale / oldScale)
84
+ *
85
+ * @param clientX - `MouseEvent.clientX` at the zoom gesture origin.
86
+ * @param clientY - `MouseEvent.clientY` at the zoom gesture origin.
87
+ * @param scaleDelta - Multiplicative scale factor to apply (e.g. 1.1 for zoom-in).
88
+ * @param viewport - The current viewport transform state before zooming.
89
+ * @param canvasRect - Bounding rect of the `<canvas>` element on screen.
90
+ * @returns A new `ViewportMatrix` with the anchored zoom applied.
91
+ */
92
+ export function calculateZoomAnchor(clientX, clientY, scaleDelta, viewport, canvasRect) {
93
+ const newScale = clampScale(viewport.scale * scaleDelta);
94
+ // Actual ratio may differ from `scaleDelta` due to clamping.
95
+ const effectiveRatio = newScale / viewport.scale;
96
+ // Cursor position relative to the canvas element's top-left corner.
97
+ const cx = clientX - canvasRect.x;
98
+ const cy = clientY - canvasRect.y;
99
+ // Re-derive offsets so the world-point under (cx, cy) is invariant.
100
+ const newOffsetX = cx - (cx - viewport.offsetX) * effectiveRatio;
101
+ const newOffsetY = cy - (cy - viewport.offsetY) * effectiveRatio;
102
+ return {
103
+ scale: newScale,
104
+ offsetX: newOffsetX,
105
+ offsetY: newOffsetY,
106
+ };
107
+ }
108
+ /**
109
+ * Convenience wrapper that converts a `WheelEvent.deltaY` value
110
+ * into a multiplicative scale factor and delegates to
111
+ * `calculateZoomAnchor`.
112
+ *
113
+ * Scroll-up (negative deltaY) zooms in, scroll-down zooms out.
114
+ * The sensitivity constant (0.001) yields a smooth, non-jarring
115
+ * zoom curve across trackpad and discrete-notch scroll wheels.
116
+ *
117
+ * @param clientX - `MouseEvent.clientX` at the wheel event.
118
+ * @param clientY - `MouseEvent.clientY` at the wheel event.
119
+ * @param deltaY - `WheelEvent.deltaY` (positive = scroll down = zoom out).
120
+ * @param viewport - Current viewport state.
121
+ * @param canvasRect - Canvas element bounding rect.
122
+ * @returns A new `ViewportMatrix` with the wheel-zoom applied.
123
+ */
124
+ export function applyWheelZoom(clientX, clientY, deltaY, viewport, canvasRect, isPinch = false) {
125
+ // Trackpad pinch events have small deltas. We use a higher sensitivity (0.01)
126
+ // for pinch to make it feel fluid, and a standard sensitivity (0.001) for regular scroll.
127
+ const sensitivity = isPinch ? 0.01 : 0.001;
128
+ let scaleDelta = Math.exp(-deltaY * sensitivity);
129
+ // Clamp the scaleDelta to a safe range per event [0.85, 1.15] (15% max change)
130
+ // to prevent standard mouse wheel zooms (e.g. Ctrl + Mouse Wheel) from zooming too fast.
131
+ scaleDelta = Math.min(1.15, Math.max(0.85, scaleDelta));
132
+ return calculateZoomAnchor(clientX, clientY, scaleDelta, viewport, canvasRect);
133
+ }
134
+ // ── Pan (Translate) ─────────────────────────────────────────
135
+ /**
136
+ * Applies a screen-space pan delta to the viewport offset.
137
+ * Typically driven by pointer-move events while spacebar is held
138
+ * or middle-mouse is pressed.
139
+ *
140
+ * @param dx - Horizontal screen-pixel delta (`e.movementX`).
141
+ * @param dy - Vertical screen-pixel delta (`e.movementY`).
142
+ * @param viewport - Current viewport state.
143
+ * @returns A new `ViewportMatrix` with the pan offset applied.
144
+ */
145
+ export function applyPan(dx, dy, viewport) {
146
+ return {
147
+ scale: viewport.scale,
148
+ offsetX: viewport.offsetX + dx,
149
+ offsetY: viewport.offsetY + dy,
150
+ };
151
+ }
152
+ // ── Hit Testing ─────────────────────────────────────────────
153
+ /**
154
+ * Axis-Aligned Bounding Box (AABB) point-inclusion test.
155
+ *
156
+ * Returns `true` if the point `(x, y)` lies within (or exactly on
157
+ * the boundary of) the rectangle described by `bounds`.
158
+ *
159
+ * Both coordinates and bounds should be in the same space
160
+ * (typically canvas-space after `screenToCanvas` conversion).
161
+ *
162
+ * @param x - Point X coordinate.
163
+ * @param y - Point Y coordinate.
164
+ * @param bounds - The axis-aligned bounding rectangle to test against.
165
+ * @returns Whether the point is inside or on the edge of `bounds`.
166
+ */
167
+ export function isPointInElement(x, y, bounds) {
168
+ return (x >= bounds.x &&
169
+ x <= bounds.x + bounds.width &&
170
+ y >= bounds.y &&
171
+ y <= bounds.y + bounds.height);
172
+ }
173
+ /**
174
+ * Determines which `WebHTMLNode` (if any) is hit by a canvas-space
175
+ * point, respecting z-order (last in the array = topmost).
176
+ *
177
+ * @param x - Canvas-space X coordinate.
178
+ * @param y - Canvas-space Y coordinate.
179
+ * @param elements - Array of elements with `id` and bounding `Rect`.
180
+ * @returns The `id` of the topmost hit element, or `null` if none.
181
+ */
182
+ export function hitTestElements(x, y, elements) {
183
+ // Walk backwards for top-most-first z-order.
184
+ for (let i = elements.length - 1; i >= 0; i--) {
185
+ const el = elements[i];
186
+ if (el.currentRect && isPointInElement(x, y, el.currentRect)) {
187
+ return el.id;
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+ // ── Resize Anchor Positions ─────────────────────────────────
193
+ /**
194
+ * Computes the screen-space positions of all 8 resize anchor
195
+ * handles for a given element bounding box.
196
+ *
197
+ * Returns an object keyed by anchor direction with the
198
+ * corresponding screen-space `Vec2` position of each handle center.
199
+ *
200
+ * @param bounds - The element's bounding rect in canvas-space.
201
+ * @param viewport - Current viewport transform.
202
+ * @param canvasRect - Canvas element bounding rect on screen.
203
+ * @returns Record mapping each anchor to its screen-space center point.
204
+ */
205
+ export function getAnchorPositions(bounds, viewport, canvasRect) {
206
+ const { x, y, width, height } = bounds;
207
+ // Canvas-space anchor centers (mid-edge and corners).
208
+ const midX = x + width / 2;
209
+ const midY = y + height / 2;
210
+ const right = x + width;
211
+ const bottom = y + height;
212
+ const toScreen = (cx, cy) => canvasToScreen(cx, cy, viewport, canvasRect);
213
+ return {
214
+ nw: toScreen(x, y),
215
+ n: toScreen(midX, y),
216
+ ne: toScreen(right, y),
217
+ e: toScreen(right, midY),
218
+ se: toScreen(right, bottom),
219
+ s: toScreen(midX, bottom),
220
+ sw: toScreen(x, bottom),
221
+ w: toScreen(x, midY),
222
+ };
223
+ }
224
+ // ── Utilities ───────────────────────────────────────────────
225
+ /** Clamps a scale value to the allowed zoom range. */
226
+ export function clampScale(s) {
227
+ return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, s));
228
+ }
229
+ /**
230
+ * Linearly interpolates between two values.
231
+ * Useful for animated viewport transitions.
232
+ *
233
+ * @param a - Start value.
234
+ * @param b - End value.
235
+ * @param t - Interpolation factor in [0, 1].
236
+ */
237
+ export function lerp(a, b, t) {
238
+ return a + (b - a) * t;
239
+ }
240
+ /**
241
+ * Linearly interpolates between two `ViewportMatrix` states.
242
+ * Useful for smooth animated zoom/pan transitions.
243
+ *
244
+ * @param from - Starting viewport.
245
+ * @param to - Target viewport.
246
+ * @param t - Interpolation factor in [0, 1].
247
+ */
248
+ export function lerpViewport(from, to, t) {
249
+ return {
250
+ scale: lerp(from.scale, to.scale, t),
251
+ offsetX: lerp(from.offsetX, to.offsetX, t),
252
+ offsetY: lerp(from.offsetY, to.offsetY, t),
253
+ };
254
+ }
255
+ /**
256
+ * Checks if two axis-aligned bounding rectangles intersect.
257
+ */
258
+ export function rectsIntersect(r1, r2) {
259
+ return !(r2.x > r1.x + r1.width ||
260
+ r2.x + r2.width < r1.x ||
261
+ r2.y > r1.y + r1.height ||
262
+ r2.y + r2.height < r1.y);
263
+ }
264
+ //# sourceMappingURL=matrix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matrix.js","sourceRoot":"","sources":["../src/matrix.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,286 @@
1
+ import type { Rect, ResizeAnchor, Vec2, ViewportMatrix, ResolvedNode } from "./types.js";
2
+ import type { GridTrack } from "./layout.js";
3
+ import type { DropTarget } from "./drop-zone.js";
4
+ /** Visual styling tokens for the overlay renderer. */
5
+ export interface OverlayStyle {
6
+ /** Stroke color for selected element outlines. */
7
+ selectionStroke: string;
8
+ /** Stroke width (screen pixels) for selection outlines. */
9
+ selectionWidth: number;
10
+ /** Shadow color for the selection glow effect. */
11
+ selectionGlow: string;
12
+ /** Blur radius (px) for the selection glow. */
13
+ selectionGlowRadius: number;
14
+ /** Stroke color for hovered (non-selected) element outlines. */
15
+ hoverStroke: string;
16
+ /** Stroke width for hover outlines. */
17
+ hoverWidth: number;
18
+ /** Dash pattern for hover outlines (`[]` = solid). */
19
+ hoverDash: number[];
20
+ /** Side length of each square resize handle (screen px). */
21
+ handleSize: number;
22
+ /** Fill color for resize handles. */
23
+ handleFill: string;
24
+ /** Stroke color for resize handles. */
25
+ handleStroke: string;
26
+ /** Stroke width for resize handles. */
27
+ handleStrokeWidth: number;
28
+ /** Corner radius of resize handles (`0` = square). */
29
+ handleRadius: number;
30
+ /**
31
+ * Hit-test radius around each handle center (screen px).
32
+ * Should be ≥ handleSize/2 for comfortable targeting (Fitts's law).
33
+ */
34
+ handleHitRadius: number;
35
+ /** Fill color when a handle is actively being dragged. */
36
+ handleActiveFill: string;
37
+ /** Stroke color for alignment guide lines. */
38
+ guideStroke: string;
39
+ /** Stroke width for guides. */
40
+ guideWidth: number;
41
+ /** Dash pattern for guides. */
42
+ guideDash: number[];
43
+ /** Stroke color for the origin crosshair. */
44
+ originStroke: string;
45
+ /** Dash pattern for the origin crosshair. */
46
+ originDash: number[];
47
+ /** Stroke color for the aggregate bounding box. */
48
+ multiSelectStroke: string;
49
+ /** Dash pattern for the aggregate bounding box. */
50
+ multiSelectDash: number[];
51
+ /** Background color for layout mode badge pills. */
52
+ layoutBadgeBg: string;
53
+ /** Text color for layout badge labels. */
54
+ layoutBadgeText: string;
55
+ /** Font for layout badge labels. */
56
+ layoutBadgeFont: string;
57
+ /** Stroke color for grid track overlay lines. */
58
+ gridTrackStroke: string;
59
+ /** Dash pattern for grid track lines. */
60
+ gridTrackDash: number[];
61
+ /** Stroke color for parent highlight outline when child is selected. */
62
+ parentHighlightStroke: string;
63
+ /** Stroke color for available children outlines when container is selected. */
64
+ childOutlineStroke: string;
65
+ /** Stroke color for the active drop container border. */
66
+ dropZoneStroke: string;
67
+ /** Stroke width for drop zones. */
68
+ dropZoneWidth: number;
69
+ /** Stroke color for the insertion line. */
70
+ insertionLineStroke: string;
71
+ /** Stroke width for the insertion line. */
72
+ insertionLineWidth: number;
73
+ }
74
+ /** Alignment guide line in canvas-space. */
75
+ export interface Guide {
76
+ /** Which axis the guide runs along. `"x"` = vertical line, `"y"` = horizontal line. */
77
+ axis: "x" | "y";
78
+ /** Canvas-space coordinate on the perpendicular axis. */
79
+ position: number;
80
+ }
81
+ /**
82
+ * Complete description of a single overlay frame.
83
+ * Passed to `OverlayRenderer.render()` each time the
84
+ * workspace state changes.
85
+ */
86
+ export interface OverlayFrame {
87
+ /** Current viewport transform. */
88
+ viewport: ViewportMatrix;
89
+ /** All mounted nodes with their canvas-space rects and hierarchy data. */
90
+ nodes: ReadonlyArray<ResolvedNode>;
91
+ /** Set of currently selected node IDs. */
92
+ selectedIds: ReadonlySet<string>;
93
+ /** ID of the node under the cursor (hover), or `null`. */
94
+ hoveredId: string | null;
95
+ /** The resize anchor currently being dragged, or `null`. */
96
+ activeAnchor: ResizeAnchor | null;
97
+ /** Alignment guides to render (typically computed by `computeAlignmentGuides`). */
98
+ guides: ReadonlyArray<Guide>;
99
+ /** ID of the node currently being dragged, or `null`. */
100
+ draggedNodeId?: string | null;
101
+ /** ID of the node currently being resized, or `null`. */
102
+ resizedNodeId?: string | null;
103
+ /** Layout mode badges to render on selected containers. */
104
+ layoutBadges?: ReadonlyArray<LayoutBadgeInfo>;
105
+ /** Grid track overlays to render on selected grid containers. */
106
+ gridOverlays?: ReadonlyArray<GridOverlayInfo>;
107
+ /** Active drop zone target container and index. */
108
+ activeDropTarget?: DropTarget | null;
109
+ /** Active marquee selection rect in canvas-space. */
110
+ marqueeRect?: Rect | null;
111
+ /** Active spacing adjusters to render on screen. */
112
+ spacingAdjusters?: ReadonlyArray<SpacingAdjusterInfo>;
113
+ /** Active drawing element bounds in canvas-space. */
114
+ drawingRect?: Rect | null;
115
+ /** Active drawing element HTML tag name. */
116
+ drawingTag?: string | null;
117
+ /** Active or hovered corner radius handle name (tl, tr, bl, br). */
118
+ activeRadiusCorner?: string | null;
119
+ }
120
+ export type SpacingAdjusterType = "padding-top" | "padding-right" | "padding-bottom" | "padding-left" | "margin-top" | "margin-right" | "margin-bottom" | "margin-left";
121
+ export interface SpacingAdjusterInfo {
122
+ type: SpacingAdjusterType;
123
+ /** Bounding box of the handle bar in canvas-space. */
124
+ rect: Rect;
125
+ /** Visual bounding box representing the actual spacing dimensions exactly. */
126
+ visualRect: Rect;
127
+ /** Spacing value in pixels. */
128
+ value: number;
129
+ /** Hover state. */
130
+ isHovered: boolean;
131
+ /** Active dragging state. */
132
+ isActive: boolean;
133
+ }
134
+ /** Data for rendering a layout mode badge on a container. */
135
+ export interface LayoutBadgeInfo {
136
+ /** Canvas-space rect of the container. */
137
+ rect: Rect;
138
+ /** Short label like "FLEX →", "GRID", "BLOCK". */
139
+ label: string;
140
+ /** True if this is a script/JS badge. */
141
+ isJS?: boolean;
142
+ }
143
+ /** Data for rendering grid track lines on a container. */
144
+ export interface GridOverlayInfo {
145
+ /** Canvas-space rect of the container. */
146
+ rect: Rect;
147
+ /** Column tracks (offsets relative to container padding edge). */
148
+ columns: ReadonlyArray<GridTrack>;
149
+ /** Row tracks. */
150
+ rows: ReadonlyArray<GridTrack>;
151
+ }
152
+ /**
153
+ * Returns the appropriate CSS cursor string for a given
154
+ * resize anchor direction.
155
+ *
156
+ * @param anchor - The anchor being hovered, or `null`.
157
+ * @returns CSS cursor value (e.g. `"nwse-resize"`), or
158
+ * `"default"` if no anchor is active.
159
+ */
160
+ export declare function anchorCursor(anchor: ResizeAnchor | null): string;
161
+ /**
162
+ * Hardware-accelerated canvas overlay renderer.
163
+ *
164
+ * Handles all visual affordances drawn on top of the Shadow DOM
165
+ * projection layer: selection outlines with glow, 8-point resize
166
+ * handles, hover highlights, alignment guides, and origin markers.
167
+ *
168
+ * ### Usage
169
+ * ```ts
170
+ * const renderer = new OverlayRenderer(canvas);
171
+ * renderer.resize(width, height);
172
+ * renderer.render(frame);
173
+ * ```
174
+ *
175
+ * ### Performance Notes
176
+ * - Single `render()` call per frame — no internal rAF loop.
177
+ * - DPR-aware: physical pixels are scaled so lines stay crisp
178
+ * on retina displays.
179
+ * - Canvas state changes are minimized by batching similar
180
+ * operations (hover pass → selection pass → handle pass).
181
+ */
182
+ export declare function isContainerNode(node: ResolvedNode): boolean;
183
+ export declare class OverlayRenderer {
184
+ private readonly canvas;
185
+ private readonly ctx;
186
+ private readonly style;
187
+ private dpr;
188
+ private width;
189
+ private height;
190
+ /**
191
+ * @param canvas - The `<canvas>` element to draw on.
192
+ * @param style - Optional partial style overrides.
193
+ */
194
+ constructor(canvas: HTMLCanvasElement, style?: Partial<OverlayStyle>);
195
+ /**
196
+ * Resizes the canvas buffer to match the given CSS dimensions,
197
+ * scaling by the device pixel ratio for crisp rendering.
198
+ *
199
+ * Call this on window resize and initial setup.
200
+ *
201
+ * @param cssWidth - Desired CSS width in pixels.
202
+ * @param cssHeight - Desired CSS height in pixels.
203
+ */
204
+ resize(cssWidth: number, cssHeight: number): void;
205
+ /**
206
+ * Draws a complete overlay frame.
207
+ *
208
+ * Rendering order (painter's algorithm, back to front):
209
+ * 1. Clear
210
+ * 2. Origin crosshair
211
+ * 3. Alignment guides
212
+ * 4. Hover outlines (non-selected, hovered node)
213
+ * 5. Selection outlines + glow
214
+ * 6. Multi-select bounding box (if > 1 selected)
215
+ * 7. Resize handles (selected nodes only)
216
+ */
217
+ render(frame: OverlayFrame): void;
218
+ /**
219
+ * Tests if a screen-space point is within the hit radius of
220
+ * any of the 8 resize handles for a given element.
221
+ *
222
+ * The `canvasRect` parameter is not needed here because
223
+ * `bounds` is in canvas-space and we project it using the
224
+ * viewport ourselves.
225
+ *
226
+ * @param screenX - Pointer X in screen-space (relative to
227
+ * the canvas element, NOT clientX).
228
+ * @param screenY - Pointer Y relative to canvas element.
229
+ * @param bounds - The element's bounding rect in canvas-space.
230
+ * @param viewport - Current viewport transform.
231
+ * @returns The anchor being hovered, or `null`.
232
+ */
233
+ hitTestHandle(screenX: number, screenY: number, bounds: Readonly<Rect>, viewport: Readonly<ViewportMatrix>): ResizeAnchor | null;
234
+ /** Draws the 8 resize handles around a projected rect. */
235
+ private drawHandles;
236
+ /** Draws the origin crosshair spanning the full viewport. */
237
+ private drawOrigin;
238
+ /** Draws a single alignment guide line. */
239
+ private drawGuide;
240
+ /** Draws the aggregate bounding box around all selected nodes. */
241
+ private drawMultiSelectBounds;
242
+ /**
243
+ * Draws a layout mode badge pill (e.g. "FLEX →", "GRID")
244
+ * anchored to the top-left corner of a container's projected rect.
245
+ */
246
+ private drawLayoutBadge;
247
+ /**
248
+ * Draws dotted grid track lines overlaying a grid container.
249
+ */
250
+ private drawGridOverlay;
251
+ }
252
+ /**
253
+ * Computes alignment guide lines between a moving element and
254
+ * all other elements. Detects edge-to-edge and center-to-center
255
+ * alignment on both axes.
256
+ *
257
+ * ### Checked alignments (per axis)
258
+ * - **Left/Top edge** of the moving element ↔ left/top, center,
259
+ * right/bottom of each other element.
260
+ * - **Center** of the moving element ↔ left/top, center,
261
+ * right/bottom of each other element.
262
+ * - **Right/Bottom edge** of the moving element ↔ left/top,
263
+ * center, right/bottom of each other element.
264
+ *
265
+ * @param movingRect - The bounding rect of the element being
266
+ * dragged or resized (canvas-space).
267
+ * @param otherRects - Array of bounding rects of all other
268
+ * elements to snap against (canvas-space).
269
+ * @param threshold - Snap distance in canvas-space pixels.
270
+ * @returns Array of `Guide` objects to render.
271
+ */
272
+ export declare function computeAlignmentGuides(movingRect: Readonly<Rect>, otherRects: ReadonlyArray<Readonly<Rect>>, threshold?: number): Guide[];
273
+ /**
274
+ * Computes snap-corrected position for a moving rect.
275
+ *
276
+ * If any edge or center of the moving rect is within `threshold`
277
+ * of an alignment target, the returned position is adjusted to
278
+ * snap exactly onto that target.
279
+ *
280
+ * @param movingRect - The element's current canvas-space rect.
281
+ * @param otherRects - All other element rects to snap against.
282
+ * @param threshold - Snap distance in canvas-space pixels.
283
+ * @returns A new `{ x, y }` position with snapping applied.
284
+ */
285
+ export declare function computeSnappedPosition(movingRect: Readonly<Rect>, otherRects: ReadonlyArray<Readonly<Rect>>, threshold?: number): Vec2;
286
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"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"}