@weasel-js/guides 1.7.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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/chunk-5BCCIJY3.js +33 -0
- package/dist/chunk-5BCCIJY3.js.map +1 -0
- package/dist/chunk-B25KFI3I.js +18 -0
- package/dist/chunk-B25KFI3I.js.map +1 -0
- package/dist/chunk-GEDAPTOM.js +48 -0
- package/dist/chunk-GEDAPTOM.js.map +1 -0
- package/dist/index.d.ts +463 -0
- package/dist/index.js +756 -0
- package/dist/index.js.map +1 -0
- package/dist/insert.d.ts +30 -0
- package/dist/insert.js +77 -0
- package/dist/insert.js.map +1 -0
- package/dist/move.d.ts +42 -0
- package/dist/move.js +18 -0
- package/dist/move.js.map +1 -0
- package/dist/pointSnapToGrid-BaVEjV23.d.ts +13 -0
- package/dist/resize.d.ts +38 -0
- package/dist/resize.js +111 -0
- package/dist/resize.js.map +1 -0
- package/dist/types-D2tTKEU0.d.ts +18 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { pointToGridCell } from './chunk-5BCCIJY3.js';
|
|
2
|
+
export { gridSnapStrategy, pointToGridCell } from './chunk-5BCCIJY3.js';
|
|
3
|
+
export { pointSnapToGrid } from './chunk-B25KFI3I.js';
|
|
4
|
+
export { DEFAULT_GUIDE_TOLERANCE_PX, guideSnapStrategy } from './chunk-GEDAPTOM.js';
|
|
5
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
6
|
+
import { useLatest, resolveUnit, pxExtent, AUTO_POSE_DESCRIPTOR, visualBoundsViaDescriptor, translatePoseViaDescriptor, unionBounds, fixedCornerOf, createTransformOp, resolveStrokeWidth, screenToWorld, screenTolerance } from '@weasel-js/core';
|
|
7
|
+
import { PATH_M, PATH_L } from '@weasel-js/geom';
|
|
8
|
+
|
|
9
|
+
// src/grid/roundToCell.ts
|
|
10
|
+
function roundToCell(value, cellSize) {
|
|
11
|
+
return Math.round(value / cellSize) * cellSize || 0;
|
|
12
|
+
}
|
|
13
|
+
function cellsEqual(a, b) {
|
|
14
|
+
if (a === b) return true;
|
|
15
|
+
if (a === null || b === null) return false;
|
|
16
|
+
return a.col === b.col && a.row === b.row;
|
|
17
|
+
}
|
|
18
|
+
function useGridCellHover(opts) {
|
|
19
|
+
const [cell, setCell] = useState(null);
|
|
20
|
+
const cellRef = useRef(null);
|
|
21
|
+
const optsRef = useLatest(opts);
|
|
22
|
+
const getCell = useCallback(() => cellRef.current, []);
|
|
23
|
+
const enabled = opts.enabled ?? true;
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!enabled) return;
|
|
26
|
+
const el = opts.ref.current;
|
|
27
|
+
if (!el) return;
|
|
28
|
+
const update = (next) => {
|
|
29
|
+
if (cellsEqual(cellRef.current, next)) return;
|
|
30
|
+
cellRef.current = next;
|
|
31
|
+
setCell(next);
|
|
32
|
+
optsRef.current.onChange?.(next);
|
|
33
|
+
};
|
|
34
|
+
const onMove = (e) => {
|
|
35
|
+
const o = optsRef.current;
|
|
36
|
+
const rect = el.getBoundingClientRect();
|
|
37
|
+
const sx = e.clientX - rect.left;
|
|
38
|
+
const sy = e.clientY - rect.top;
|
|
39
|
+
const [wx, wy] = screenToWorld(sx, sy, o.view());
|
|
40
|
+
update(pointToGridCell({ x: wx, y: wy }, o.spacing, o.unitSystem, o.origin));
|
|
41
|
+
};
|
|
42
|
+
const onLeave = () => update(null);
|
|
43
|
+
el.addEventListener("pointermove", onMove);
|
|
44
|
+
el.addEventListener("pointerleave", onLeave);
|
|
45
|
+
el.addEventListener("pointercancel", onLeave);
|
|
46
|
+
return () => {
|
|
47
|
+
el.removeEventListener("pointermove", onMove);
|
|
48
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
49
|
+
el.removeEventListener("pointercancel", onLeave);
|
|
50
|
+
};
|
|
51
|
+
}, [enabled, opts.ref, optsRef]);
|
|
52
|
+
return { cell, getCell };
|
|
53
|
+
}
|
|
54
|
+
var DEFAULT_LINE = {
|
|
55
|
+
paint: { fill: "solid", color: "rgba(255, 255, 255, 0.08)" },
|
|
56
|
+
width: 1
|
|
57
|
+
};
|
|
58
|
+
var DEFAULT_ACCENT = {
|
|
59
|
+
paint: { fill: "solid", color: "rgba(255, 255, 255, 0.16)" },
|
|
60
|
+
width: 1
|
|
61
|
+
};
|
|
62
|
+
var DEFAULT_SUB = {
|
|
63
|
+
paint: { fill: "solid", color: "rgba(255, 255, 255, 0.04)" },
|
|
64
|
+
width: 1
|
|
65
|
+
};
|
|
66
|
+
function createGridLayer(opts) {
|
|
67
|
+
const line = opts.style?.line ?? DEFAULT_LINE;
|
|
68
|
+
const accent = opts.style?.accent ?? DEFAULT_ACCENT;
|
|
69
|
+
const sub = opts.style?.sub ?? DEFAULT_SUB;
|
|
70
|
+
return {
|
|
71
|
+
id: "grid",
|
|
72
|
+
label: "Grid",
|
|
73
|
+
draw: (_data, view) => {
|
|
74
|
+
const b = opts.bounds();
|
|
75
|
+
if (b.width <= 0 || b.height <= 0) return [];
|
|
76
|
+
const { accentEvery, subdivisions } = opts;
|
|
77
|
+
const spacing = resolveUnit(opts.spacing, opts.unitSystem);
|
|
78
|
+
const x0 = b.x;
|
|
79
|
+
const y0 = b.y;
|
|
80
|
+
const x1 = b.x + b.width;
|
|
81
|
+
const y1 = b.y + b.height;
|
|
82
|
+
const px = pxExtent(1, view.scale);
|
|
83
|
+
const children = [];
|
|
84
|
+
const pushSegment = (sx, sy, ex, ey, s) => {
|
|
85
|
+
const path = {
|
|
86
|
+
kind: "polygon",
|
|
87
|
+
commands: new Uint8Array([PATH_M, PATH_L]),
|
|
88
|
+
coords: new Float32Array([sx, sy, ex, ey]),
|
|
89
|
+
fillRule: "nonzero"
|
|
90
|
+
};
|
|
91
|
+
const across = sx === ex ? px.x : px.y;
|
|
92
|
+
children.push({
|
|
93
|
+
kind: "path",
|
|
94
|
+
path,
|
|
95
|
+
stroke: { ...s, width: resolveStrokeWidth(s.width ?? 1, 1) * across }
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
if (subdivisions && subdivisions > 1) {
|
|
99
|
+
const step = spacing / subdivisions;
|
|
100
|
+
for (let x = x0; x <= x1 + 1e-9; x += step) {
|
|
101
|
+
const k = Math.round((x - x0) / step);
|
|
102
|
+
if (k % subdivisions === 0) continue;
|
|
103
|
+
pushSegment(x, y0, x, y1, sub);
|
|
104
|
+
}
|
|
105
|
+
for (let y = y0; y <= y1 + 1e-9; y += step) {
|
|
106
|
+
const k = Math.round((y - y0) / step);
|
|
107
|
+
if (k % subdivisions === 0) continue;
|
|
108
|
+
pushSegment(x0, y, x1, y, sub);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let idx = 0;
|
|
112
|
+
for (let x = x0; x <= x1 + 1e-9; x += spacing) {
|
|
113
|
+
const isAccent = !!accentEvery && accentEvery > 0 && idx % accentEvery === 0;
|
|
114
|
+
if (!isAccent) pushSegment(x, y0, x, y1, line);
|
|
115
|
+
idx++;
|
|
116
|
+
}
|
|
117
|
+
idx = 0;
|
|
118
|
+
for (let y = y0; y <= y1 + 1e-9; y += spacing) {
|
|
119
|
+
const isAccent = !!accentEvery && accentEvery > 0 && idx % accentEvery === 0;
|
|
120
|
+
if (!isAccent) pushSegment(x0, y, x1, y, line);
|
|
121
|
+
idx++;
|
|
122
|
+
}
|
|
123
|
+
if (accentEvery && accentEvery > 0) {
|
|
124
|
+
idx = 0;
|
|
125
|
+
for (let x = x0; x <= x1 + 1e-9; x += spacing) {
|
|
126
|
+
if (idx % accentEvery === 0) pushSegment(x, y0, x, y1, accent);
|
|
127
|
+
idx++;
|
|
128
|
+
}
|
|
129
|
+
idx = 0;
|
|
130
|
+
for (let y = y0; y <= y1 + 1e-9; y += spacing) {
|
|
131
|
+
if (idx % accentEvery === 0) pushSegment(x0, y, x1, y, accent);
|
|
132
|
+
idx++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return children;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
var DEFAULT_FILL = { fill: "solid", color: "rgba(127,176,105,0.15)" };
|
|
140
|
+
function createCellHighlightLayer(opts) {
|
|
141
|
+
const fill = opts.fill ?? DEFAULT_FILL;
|
|
142
|
+
return {
|
|
143
|
+
id: "cell-highlight",
|
|
144
|
+
label: "Cell highlight",
|
|
145
|
+
draw: () => {
|
|
146
|
+
const cell = opts.getCell();
|
|
147
|
+
if (!cell) return [];
|
|
148
|
+
const spacing = resolveUnit(opts.spacing, opts.unitSystem);
|
|
149
|
+
const o = opts.origin ? opts.origin() : { x: 0, y: 0 };
|
|
150
|
+
const x = o.x + cell.col * spacing;
|
|
151
|
+
const y = o.y + cell.row * spacing;
|
|
152
|
+
const children = [
|
|
153
|
+
{
|
|
154
|
+
kind: "path",
|
|
155
|
+
path: { kind: "rect", x, y, width: spacing, height: spacing },
|
|
156
|
+
fill
|
|
157
|
+
}
|
|
158
|
+
];
|
|
159
|
+
return children;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function useGuides(initial = []) {
|
|
164
|
+
const [guides, setGuides] = useState(initial);
|
|
165
|
+
const ref = useLatest(guides);
|
|
166
|
+
const getGuides = useCallback(() => ref.current, [ref]);
|
|
167
|
+
const addGuide = useCallback((guide) => {
|
|
168
|
+
setGuides((prev) => {
|
|
169
|
+
const idx = prev.findIndex((g) => g.id === guide.id);
|
|
170
|
+
if (idx === -1) return [...prev, guide];
|
|
171
|
+
const next = prev.slice();
|
|
172
|
+
next[idx] = guide;
|
|
173
|
+
return next;
|
|
174
|
+
});
|
|
175
|
+
}, []);
|
|
176
|
+
const removeGuide = useCallback((id) => {
|
|
177
|
+
setGuides((prev) => {
|
|
178
|
+
const idx = prev.findIndex((g) => g.id === id);
|
|
179
|
+
if (idx === -1) return prev;
|
|
180
|
+
const next = prev.slice();
|
|
181
|
+
next.splice(idx, 1);
|
|
182
|
+
return next;
|
|
183
|
+
});
|
|
184
|
+
}, []);
|
|
185
|
+
const clearGuides = useCallback(() => {
|
|
186
|
+
setGuides((prev) => prev.length === 0 ? prev : []);
|
|
187
|
+
}, []);
|
|
188
|
+
return { guides, getGuides, addGuide, removeGuide, clearGuides };
|
|
189
|
+
}
|
|
190
|
+
var DEFAULT_COLOR = "#3aa0ff";
|
|
191
|
+
function createGuidesLayer(opts) {
|
|
192
|
+
const lineWidth = opts.lineWidth ?? 1;
|
|
193
|
+
const color = opts.color ?? DEFAULT_COLOR;
|
|
194
|
+
const stroke = {
|
|
195
|
+
paint: { fill: "solid", color },
|
|
196
|
+
width: lineWidth
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
id: opts.id ?? "guides",
|
|
200
|
+
label: opts.label ?? "Guides",
|
|
201
|
+
space: "screen",
|
|
202
|
+
draw: (_data, view, dims) => {
|
|
203
|
+
const guides = opts.getGuides();
|
|
204
|
+
if (guides.length === 0) return [];
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const g of guides) {
|
|
207
|
+
if (g.axis === "x") {
|
|
208
|
+
const sx = (g.offset - view.x) * view.scale.x;
|
|
209
|
+
if (sx < -lineWidth || sx > dims.width + lineWidth) continue;
|
|
210
|
+
const path = {
|
|
211
|
+
kind: "polygon",
|
|
212
|
+
commands: new Uint8Array([PATH_M, PATH_L]),
|
|
213
|
+
coords: new Float32Array([sx, 0, sx, dims.height]),
|
|
214
|
+
fillRule: "nonzero"
|
|
215
|
+
};
|
|
216
|
+
out.push({ kind: "path", path, stroke });
|
|
217
|
+
} else {
|
|
218
|
+
const sy = (g.offset - view.y) * view.scale.y;
|
|
219
|
+
if (sy < -lineWidth || sy > dims.height + lineWidth) continue;
|
|
220
|
+
const path = {
|
|
221
|
+
kind: "polygon",
|
|
222
|
+
commands: new Uint8Array([PATH_M, PATH_L]),
|
|
223
|
+
coords: new Float32Array([0, sy, dims.width, sy]),
|
|
224
|
+
fillRule: "nonzero"
|
|
225
|
+
};
|
|
226
|
+
out.push({ kind: "path", path, stroke });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
var EPS = 1e-3;
|
|
234
|
+
function deriveAlignmentGuides(targets, opts = {}) {
|
|
235
|
+
const d = opts.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
|
|
236
|
+
const edges = opts.edges ?? true;
|
|
237
|
+
const centers = opts.centers ?? true;
|
|
238
|
+
const seenX = /* @__PURE__ */ new Map();
|
|
239
|
+
const seenY = /* @__PURE__ */ new Map();
|
|
240
|
+
const add = (axis, offset) => {
|
|
241
|
+
const seen = axis === "x" ? seenX : seenY;
|
|
242
|
+
const key = Math.round(offset / EPS);
|
|
243
|
+
if (seen.has(key)) return;
|
|
244
|
+
seen.set(key, { id: `align:${axis}:${offset.toFixed(3)}`, axis, offset });
|
|
245
|
+
};
|
|
246
|
+
const emit = (b) => {
|
|
247
|
+
if (edges) {
|
|
248
|
+
add("x", b.x);
|
|
249
|
+
add("x", b.x + b.width);
|
|
250
|
+
add("y", b.y);
|
|
251
|
+
add("y", b.y + b.height);
|
|
252
|
+
}
|
|
253
|
+
if (centers) {
|
|
254
|
+
add("x", b.x + b.width / 2);
|
|
255
|
+
add("y", b.y + b.height / 2);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
for (const t of targets) emit(visualBoundsViaDescriptor(t, d));
|
|
259
|
+
if (opts.page) emit(opts.page);
|
|
260
|
+
return [...seenX.values(), ...seenY.values()];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/guides/alignment/match.ts
|
|
264
|
+
var MOVE_ANCHORS = {
|
|
265
|
+
x: ["min", "center", "max"],
|
|
266
|
+
y: ["min", "center", "max"]
|
|
267
|
+
};
|
|
268
|
+
function featureOffset(b, axis, anchor) {
|
|
269
|
+
if (axis === "x") {
|
|
270
|
+
if (anchor === "min") return b.x;
|
|
271
|
+
if (anchor === "center") return b.x + b.width / 2;
|
|
272
|
+
return b.x + b.width;
|
|
273
|
+
}
|
|
274
|
+
if (anchor === "min") return b.y;
|
|
275
|
+
if (anchor === "center") return b.y + b.height / 2;
|
|
276
|
+
return b.y + b.height;
|
|
277
|
+
}
|
|
278
|
+
function bestAxis(b, axis, anchors, candidates, worldTolerance) {
|
|
279
|
+
let bestAbs = Infinity;
|
|
280
|
+
let bestDelta = 0;
|
|
281
|
+
let bestGuide = null;
|
|
282
|
+
for (const anchor of anchors) {
|
|
283
|
+
const o = featureOffset(b, axis, anchor);
|
|
284
|
+
for (const g of candidates) {
|
|
285
|
+
if (g.axis !== axis) continue;
|
|
286
|
+
const d = g.offset - o;
|
|
287
|
+
const ad = Math.abs(d);
|
|
288
|
+
if (ad <= worldTolerance && ad < bestAbs) {
|
|
289
|
+
bestAbs = ad;
|
|
290
|
+
bestDelta = d;
|
|
291
|
+
bestGuide = g;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { delta: bestGuide ? bestDelta : 0, guide: bestGuide };
|
|
296
|
+
}
|
|
297
|
+
function matchAlignment(bounds, candidates, worldTolerance, anchors) {
|
|
298
|
+
const x = bestAxis(bounds, "x", anchors.x, candidates, worldTolerance.x);
|
|
299
|
+
const y = bestAxis(bounds, "y", anchors.y, candidates, worldTolerance.y);
|
|
300
|
+
return { dx: x.delta, dy: y.delta, activeX: x.guide, activeY: y.guide };
|
|
301
|
+
}
|
|
302
|
+
var activeList = (m) => [m.activeX, m.activeY].filter((g) => g !== null);
|
|
303
|
+
function worldTol(base, ctx) {
|
|
304
|
+
return screenTolerance(base.tolerance ?? 6, ctx);
|
|
305
|
+
}
|
|
306
|
+
function alignMoveBehavior(args) {
|
|
307
|
+
const d = args.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
|
|
308
|
+
return {
|
|
309
|
+
onMove(ctx, transform) {
|
|
310
|
+
if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
|
|
311
|
+
args.setActiveGuides([]);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (transform.kind !== "translate") return;
|
|
315
|
+
const boxes = [];
|
|
316
|
+
for (const id of ctx.draggedIds) {
|
|
317
|
+
const originPose = ctx.origin.get(id);
|
|
318
|
+
if (originPose === void 0) continue;
|
|
319
|
+
boxes.push(visualBoundsViaDescriptor(
|
|
320
|
+
translatePoseViaDescriptor(originPose, transform.dx, transform.dy, d),
|
|
321
|
+
d
|
|
322
|
+
));
|
|
323
|
+
}
|
|
324
|
+
const union = unionBounds(boxes);
|
|
325
|
+
if (union === null) return;
|
|
326
|
+
const m = matchAlignment(union, args.getCandidates(), worldTol(args, ctx), MOVE_ANCHORS);
|
|
327
|
+
if (m.activeX === null && m.activeY === null) {
|
|
328
|
+
args.setActiveGuides([]);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
args.setActiveGuides(activeList(m));
|
|
332
|
+
return { transform: { kind: "translate", dx: transform.dx + m.dx, dy: transform.dy + m.dy } };
|
|
333
|
+
},
|
|
334
|
+
onEnd() {
|
|
335
|
+
args.setActiveGuides([]);
|
|
336
|
+
},
|
|
337
|
+
onCancel() {
|
|
338
|
+
args.setActiveGuides([]);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function alignInsertBehavior(args) {
|
|
343
|
+
const pointAnchors = { x: ["min"], y: ["min"] };
|
|
344
|
+
return {
|
|
345
|
+
onMove(ctx, { current }) {
|
|
346
|
+
if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
|
|
347
|
+
args.setActiveGuides([]);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const box = { x: current.x, y: current.y, width: 0, height: 0 };
|
|
351
|
+
const m = matchAlignment(box, args.getCandidates(), worldTol(args, ctx), pointAnchors);
|
|
352
|
+
if (m.activeX === null && m.activeY === null) {
|
|
353
|
+
args.setActiveGuides([]);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
args.setActiveGuides(activeList(m));
|
|
357
|
+
return { current: { x: current.x + m.dx, y: current.y + m.dy } };
|
|
358
|
+
},
|
|
359
|
+
onEnd() {
|
|
360
|
+
args.setActiveGuides([]);
|
|
361
|
+
},
|
|
362
|
+
onCancel() {
|
|
363
|
+
args.setActiveGuides([]);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function alignResizeBehavior(args) {
|
|
368
|
+
const d = args.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
|
|
369
|
+
return {
|
|
370
|
+
onMove(ctx, { pose, anchor }) {
|
|
371
|
+
if (args.bypassKey && ctx.modifiers[args.bypassKey]) {
|
|
372
|
+
args.setActiveGuides([]);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const origin = ctx.origin.get(ctx.draggedIds[0]);
|
|
376
|
+
const rotation = origin === void 0 ? 0 : d.getRotation?.(origin) ?? 0;
|
|
377
|
+
const m = rotation === 0 ? matchMovingEdges(pose, anchor, args.getCandidates(), worldTol(args, ctx)) : matchRotatedCorners(pose, anchor, d.getBounds(origin), rotation, args.getCandidates(), worldTol(args, ctx));
|
|
378
|
+
if (m === null) {
|
|
379
|
+
args.setActiveGuides([]);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
let { x, y, width, height } = pose;
|
|
383
|
+
if (anchor.x === "min") width += m.lx;
|
|
384
|
+
else if (anchor.x === "max") {
|
|
385
|
+
x += m.lx;
|
|
386
|
+
width -= m.lx;
|
|
387
|
+
}
|
|
388
|
+
if (anchor.y === "min") height += m.ly;
|
|
389
|
+
else if (anchor.y === "max") {
|
|
390
|
+
y += m.ly;
|
|
391
|
+
height -= m.ly;
|
|
392
|
+
}
|
|
393
|
+
args.setActiveGuides(m.guides);
|
|
394
|
+
return { pose: { ...pose, x, y, width, height } };
|
|
395
|
+
},
|
|
396
|
+
onEnd() {
|
|
397
|
+
args.setActiveGuides([]);
|
|
398
|
+
},
|
|
399
|
+
onCancel() {
|
|
400
|
+
args.setActiveGuides([]);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function matchMovingEdges(pose, anchor, candidates, tol) {
|
|
405
|
+
const movingX = anchor.x === "min" ? ["max"] : anchor.x === "max" ? ["min"] : [];
|
|
406
|
+
const movingY = anchor.y === "min" ? ["max"] : anchor.y === "max" ? ["min"] : [];
|
|
407
|
+
const m = matchAlignment(pose, candidates, tol, { x: movingX, y: movingY });
|
|
408
|
+
if (m.activeX === null && m.activeY === null) return null;
|
|
409
|
+
return { lx: m.dx, ly: m.dy, guides: activeList(m) };
|
|
410
|
+
}
|
|
411
|
+
function matchRotatedCorners(pose, anchor, originBox, rotation, candidates, tol) {
|
|
412
|
+
const cos = Math.cos(rotation);
|
|
413
|
+
const sin = Math.sin(rotation);
|
|
414
|
+
const toWorld = (lx, ly) => ({ x: lx * cos - ly * sin, y: lx * sin + ly * cos });
|
|
415
|
+
const fixedLocal = fixedCornerOf(pose, anchor);
|
|
416
|
+
const f0 = fixedCornerOf(originBox, anchor);
|
|
417
|
+
const pivot = toWorld(f0.x - originBox.x - originBox.width / 2, f0.y - originBox.y - originBox.height / 2);
|
|
418
|
+
const fixedWorld = { x: originBox.x + originBox.width / 2 + pivot.x, y: originBox.y + originBox.height / 2 + pivot.y };
|
|
419
|
+
const drawn = (lx, ly) => {
|
|
420
|
+
const o = toWorld(lx - fixedLocal.x, ly - fixedLocal.y);
|
|
421
|
+
return { x: fixedWorld.x + o.x, y: fixedWorld.y + o.y };
|
|
422
|
+
};
|
|
423
|
+
const movingEdge = (axis) => {
|
|
424
|
+
const lo = axis === "x" ? pose.x : pose.y;
|
|
425
|
+
const hi = lo + (axis === "x" ? pose.width : pose.height);
|
|
426
|
+
const a = anchor[axis];
|
|
427
|
+
return a === "min" ? [hi] : a === "max" ? [lo] : [lo, hi];
|
|
428
|
+
};
|
|
429
|
+
if (anchor.x !== "free" && anchor.y !== "free") {
|
|
430
|
+
const c = drawn(movingEdge("x")[0], movingEdge("y")[0]);
|
|
431
|
+
const m = matchAlignment({ x: c.x, y: c.y, width: 0, height: 0 }, candidates, tol, { x: ["min"], y: ["min"] });
|
|
432
|
+
if (m.activeX === null && m.activeY === null) return null;
|
|
433
|
+
const lx = m.dx * cos + m.dy * sin;
|
|
434
|
+
const ly = -m.dx * sin + m.dy * cos;
|
|
435
|
+
return { lx, ly, guides: activeList(m) };
|
|
436
|
+
}
|
|
437
|
+
if (anchor.x === "free" && anchor.y === "free") return null;
|
|
438
|
+
const along = anchor.x === "free" ? "y" : "x";
|
|
439
|
+
const dir = along === "x" ? toWorld(1, 0) : toWorld(0, 1);
|
|
440
|
+
const edge = movingEdge(along)[0];
|
|
441
|
+
let best = null;
|
|
442
|
+
for (const across of movingEdge(along === "x" ? "y" : "x")) {
|
|
443
|
+
const c = along === "x" ? drawn(edge, across) : drawn(across, edge);
|
|
444
|
+
for (const g of candidates) {
|
|
445
|
+
const reach = g.axis === "x" ? dir.x : dir.y;
|
|
446
|
+
if (Math.abs(reach) < 1e-9) continue;
|
|
447
|
+
const t = (g.offset - (g.axis === "x" ? c.x : c.y)) / reach;
|
|
448
|
+
if (Math.abs(t * dir.x) > tol.x || Math.abs(t * dir.y) > tol.y) continue;
|
|
449
|
+
if (best === null || Math.abs(t) < Math.abs(best.t)) best = { t, guide: g };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (best === null) return null;
|
|
453
|
+
return along === "x" ? { lx: best.t, ly: 0, guides: [best.guide] } : { lx: 0, ly: best.t, guides: [best.guide] };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/layout/snaps.ts
|
|
457
|
+
function dist2(a, b) {
|
|
458
|
+
const dx = a.x - b.x;
|
|
459
|
+
const dy = a.y - b.y;
|
|
460
|
+
return dx * dx + dy * dy;
|
|
461
|
+
}
|
|
462
|
+
function nearestOf(targets, pointer) {
|
|
463
|
+
if (targets.length === 0) return null;
|
|
464
|
+
let best = targets[0];
|
|
465
|
+
let bestD = dist2(best.origin, pointer);
|
|
466
|
+
for (let i = 1; i < targets.length; i++) {
|
|
467
|
+
const d = dist2(targets[i].origin, pointer);
|
|
468
|
+
if (d < bestD) {
|
|
469
|
+
bestD = d;
|
|
470
|
+
best = targets[i];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return best;
|
|
474
|
+
}
|
|
475
|
+
function none() {
|
|
476
|
+
return { pickTarget: () => null };
|
|
477
|
+
}
|
|
478
|
+
function nearest() {
|
|
479
|
+
return { pickTarget: (targets, pointer) => nearestOf(targets, pointer) };
|
|
480
|
+
}
|
|
481
|
+
function nearestWithin(opts) {
|
|
482
|
+
const tol2 = opts.tolerance * opts.tolerance;
|
|
483
|
+
return {
|
|
484
|
+
pickTarget(targets, pointer) {
|
|
485
|
+
const got = nearestOf(targets, pointer);
|
|
486
|
+
if (got === null) return null;
|
|
487
|
+
return dist2(got.origin, pointer) <= tol2 ? got : null;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function rectContains(r, p) {
|
|
492
|
+
return p.x >= r.x && p.x < r.x + r.width && p.y >= r.y && p.y < r.y + r.height;
|
|
493
|
+
}
|
|
494
|
+
function containedThenNearest() {
|
|
495
|
+
return {
|
|
496
|
+
pickTarget(targets, pointer) {
|
|
497
|
+
for (const t of targets) {
|
|
498
|
+
if (t.hitBounds && rectContains(t.hitBounds, pointer)) return t;
|
|
499
|
+
}
|
|
500
|
+
return nearestOf(targets, pointer);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function isCellMeta(m) {
|
|
505
|
+
return typeof m === "object" && m !== null && "cellRect" in m && typeof m.cellRect === "object";
|
|
506
|
+
}
|
|
507
|
+
function cellAt() {
|
|
508
|
+
return {
|
|
509
|
+
pickTarget(targets, pointer) {
|
|
510
|
+
for (const t of targets) {
|
|
511
|
+
if (t.hitBounds && rectContains(t.hitBounds, pointer)) return t;
|
|
512
|
+
if (isCellMeta(t.meta) && rectContains(t.meta.cellRect, pointer)) return t;
|
|
513
|
+
}
|
|
514
|
+
return nearestOf(targets, pointer);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function freeform(opts = {}) {
|
|
519
|
+
const snap = opts.snap ?? none();
|
|
520
|
+
return {
|
|
521
|
+
snap,
|
|
522
|
+
childPoses(_container, children) {
|
|
523
|
+
const out = /* @__PURE__ */ new Map();
|
|
524
|
+
for (const c of children) out.set(c.id, c.pose);
|
|
525
|
+
return out;
|
|
526
|
+
},
|
|
527
|
+
getDropTargets() {
|
|
528
|
+
return [];
|
|
529
|
+
},
|
|
530
|
+
reflowPoses() {
|
|
531
|
+
return /* @__PURE__ */ new Map();
|
|
532
|
+
},
|
|
533
|
+
commitDrop(_container, _children, dragged, target) {
|
|
534
|
+
return [
|
|
535
|
+
createTransformOp({
|
|
536
|
+
id: dragged.id,
|
|
537
|
+
from: dragged.originPose,
|
|
538
|
+
to: target?.pose ?? dragged.pose,
|
|
539
|
+
label: "Drop"
|
|
540
|
+
})
|
|
541
|
+
];
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function defaultCenterOf(pose) {
|
|
546
|
+
const p = pose;
|
|
547
|
+
return { x: (p.x ?? 0) + (p.width ?? 0) / 2, y: (p.y ?? 0) + (p.height ?? 0) / 2 };
|
|
548
|
+
}
|
|
549
|
+
function cellRectAt(bounds, cols, rows, gap, col, row) {
|
|
550
|
+
const cw = (bounds.width - gap * (cols - 1)) / cols;
|
|
551
|
+
const ch = (bounds.height - gap * (rows - 1)) / rows;
|
|
552
|
+
return {
|
|
553
|
+
x: bounds.x + col * (cw + gap),
|
|
554
|
+
y: bounds.y + row * (ch + gap),
|
|
555
|
+
width: cw,
|
|
556
|
+
height: ch
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function tileGrid(opts) {
|
|
560
|
+
const { cols, rows } = opts;
|
|
561
|
+
const gap = opts.gap ?? 0;
|
|
562
|
+
const snap = opts.snap ?? cellAt();
|
|
563
|
+
const capacity = cols * rows;
|
|
564
|
+
const cellToPose = opts.cellToPose ?? ((cell, dragged) => {
|
|
565
|
+
return { ...dragged, ...cell };
|
|
566
|
+
});
|
|
567
|
+
const centerOf = opts.centerOf ?? defaultCenterOf;
|
|
568
|
+
function cellPose(bounds, col, row, basis) {
|
|
569
|
+
return cellToPose(cellRectAt(bounds, cols, rows, gap, col, row), basis);
|
|
570
|
+
}
|
|
571
|
+
function cellIndexOf(bounds, pose) {
|
|
572
|
+
const p = centerOf(pose);
|
|
573
|
+
for (let row = 0; row < rows; row++) {
|
|
574
|
+
for (let col = 0; col < cols; col++) {
|
|
575
|
+
const r = cellRectAt(bounds, cols, rows, gap, col, row);
|
|
576
|
+
if (p.x >= r.x && p.x < r.x + r.width && p.y >= r.y && p.y < r.y + r.height) {
|
|
577
|
+
return row * cols + col;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
function computeSwap(container, children, dragged, target) {
|
|
584
|
+
if (target === null) return null;
|
|
585
|
+
if (dragged.sourceContainerId !== container.id) return null;
|
|
586
|
+
const meta = target.meta;
|
|
587
|
+
const idx = meta.row * cols + meta.col;
|
|
588
|
+
const occupant = children.find(
|
|
589
|
+
(c) => c.id !== dragged.id && cellIndexOf(container.bounds, c.pose) === idx
|
|
590
|
+
);
|
|
591
|
+
if (occupant === void 0) return null;
|
|
592
|
+
const self = children.find((c) => c.id === dragged.id);
|
|
593
|
+
const fromIdx = cellIndexOf(container.bounds, self ? self.pose : dragged.originPose);
|
|
594
|
+
if (fromIdx === null || fromIdx === idx) return null;
|
|
595
|
+
const oldCellRect = cellRectAt(
|
|
596
|
+
container.bounds,
|
|
597
|
+
cols,
|
|
598
|
+
rows,
|
|
599
|
+
gap,
|
|
600
|
+
fromIdx % cols,
|
|
601
|
+
Math.floor(fromIdx / cols)
|
|
602
|
+
);
|
|
603
|
+
return { occupant: occupant.id, from: occupant.pose, newPose: cellToPose(oldCellRect, occupant.pose) };
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
snap,
|
|
607
|
+
childPoses(container, children) {
|
|
608
|
+
const out = /* @__PURE__ */ new Map();
|
|
609
|
+
const ordered = children.map((c) => ({ c, cell: cellIndexOf(container.bounds, c.pose) })).sort((a, b) => {
|
|
610
|
+
if (a.cell !== b.cell) {
|
|
611
|
+
if (a.cell === null) return 1;
|
|
612
|
+
if (b.cell === null) return -1;
|
|
613
|
+
return a.cell - b.cell;
|
|
614
|
+
}
|
|
615
|
+
return a.c.id < b.c.id ? -1 : a.c.id > b.c.id ? 1 : 0;
|
|
616
|
+
});
|
|
617
|
+
for (let i = 0; i < ordered.length && i < capacity; i++) {
|
|
618
|
+
const col = i % cols;
|
|
619
|
+
const row = Math.floor(i / cols);
|
|
620
|
+
out.set(ordered[i].c.id, cellPose(container.bounds, col, row, ordered[i].c.pose));
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
},
|
|
624
|
+
getDropTargets(container, children, dragged) {
|
|
625
|
+
const occupied = /* @__PURE__ */ new Set();
|
|
626
|
+
if (dragged.sourceContainerId !== container.id) {
|
|
627
|
+
for (const c of children) {
|
|
628
|
+
if (c.id === dragged.id) continue;
|
|
629
|
+
const idx = cellIndexOf(container.bounds, c.pose);
|
|
630
|
+
if (idx !== null) occupied.add(idx);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const out = [];
|
|
634
|
+
for (let row = 0; row < rows; row++) {
|
|
635
|
+
for (let col = 0; col < cols; col++) {
|
|
636
|
+
if (occupied.has(row * cols + col)) continue;
|
|
637
|
+
const cellRect = cellRectAt(container.bounds, cols, rows, gap, col, row);
|
|
638
|
+
out.push({
|
|
639
|
+
pose: cellToPose(cellRect, dragged.pose),
|
|
640
|
+
origin: { x: cellRect.x + cellRect.width / 2, y: cellRect.y + cellRect.height / 2 },
|
|
641
|
+
meta: { col, row, cellRect }
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
},
|
|
647
|
+
reflowPoses(container, children, dragged, target) {
|
|
648
|
+
const out = /* @__PURE__ */ new Map();
|
|
649
|
+
const swap = computeSwap(container, children, dragged, target);
|
|
650
|
+
if (swap !== null) {
|
|
651
|
+
out.set(swap.occupant, swap.newPose);
|
|
652
|
+
}
|
|
653
|
+
return out;
|
|
654
|
+
},
|
|
655
|
+
commitDrop(container, children, dragged, target) {
|
|
656
|
+
const ops = [];
|
|
657
|
+
let droppedPose;
|
|
658
|
+
if (target === null) {
|
|
659
|
+
droppedPose = dragged.pose;
|
|
660
|
+
} else {
|
|
661
|
+
const meta = target.meta;
|
|
662
|
+
droppedPose = cellToPose(meta.cellRect, dragged.pose);
|
|
663
|
+
const swap = computeSwap(container, children, dragged, target);
|
|
664
|
+
if (swap !== null) {
|
|
665
|
+
ops.push(
|
|
666
|
+
createTransformOp({
|
|
667
|
+
id: swap.occupant,
|
|
668
|
+
from: swap.from,
|
|
669
|
+
to: swap.newPose,
|
|
670
|
+
label: "Tile swap"
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
ops.push(
|
|
676
|
+
createTransformOp({
|
|
677
|
+
id: dragged.id,
|
|
678
|
+
from: dragged.originPose,
|
|
679
|
+
to: droppedPose,
|
|
680
|
+
label: "Tile drop"
|
|
681
|
+
})
|
|
682
|
+
);
|
|
683
|
+
return ops;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function buildPoints(bounds, pattern, gridSpacing) {
|
|
688
|
+
switch (pattern) {
|
|
689
|
+
case "corners":
|
|
690
|
+
return [
|
|
691
|
+
{ x: bounds.x, y: bounds.y },
|
|
692
|
+
{ x: bounds.x + bounds.width, y: bounds.y },
|
|
693
|
+
{ x: bounds.x, y: bounds.y + bounds.height },
|
|
694
|
+
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height }
|
|
695
|
+
];
|
|
696
|
+
case "edges":
|
|
697
|
+
return [
|
|
698
|
+
{ x: bounds.x + bounds.width / 2, y: bounds.y },
|
|
699
|
+
{ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height },
|
|
700
|
+
{ x: bounds.x, y: bounds.y + bounds.height / 2 },
|
|
701
|
+
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 }
|
|
702
|
+
];
|
|
703
|
+
case "center":
|
|
704
|
+
return [{ x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 }];
|
|
705
|
+
case "grid": {
|
|
706
|
+
const out = [];
|
|
707
|
+
const cols = Math.floor(bounds.width / gridSpacing);
|
|
708
|
+
const rows = Math.floor(bounds.height / gridSpacing);
|
|
709
|
+
for (let j = 0; j <= rows; j++) {
|
|
710
|
+
for (let i = 0; i <= cols; i++) {
|
|
711
|
+
out.push({ x: bounds.x + i * gridSpacing, y: bounds.y + j * gridSpacing });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return out;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function snapPoint(opts) {
|
|
719
|
+
const gridSpacing = opts.gridSpacing ?? 50;
|
|
720
|
+
const tolerance = opts.tolerance ?? Infinity;
|
|
721
|
+
const snap = opts.snap ?? nearestWithin({ tolerance });
|
|
722
|
+
return {
|
|
723
|
+
snap,
|
|
724
|
+
childPoses(_container, children) {
|
|
725
|
+
const out = /* @__PURE__ */ new Map();
|
|
726
|
+
for (const c of children) out.set(c.id, c.pose);
|
|
727
|
+
return out;
|
|
728
|
+
},
|
|
729
|
+
getDropTargets(container, _children, dragged) {
|
|
730
|
+
const points = buildPoints(container.bounds, opts.pattern, gridSpacing);
|
|
731
|
+
return points.map((p) => ({
|
|
732
|
+
// Anchor the dragged child's top-left at the snap point.
|
|
733
|
+
pose: { ...dragged.pose, x: p.x, y: p.y },
|
|
734
|
+
origin: p
|
|
735
|
+
}));
|
|
736
|
+
},
|
|
737
|
+
reflowPoses() {
|
|
738
|
+
return /* @__PURE__ */ new Map();
|
|
739
|
+
},
|
|
740
|
+
commitDrop(_container, _children, dragged, target) {
|
|
741
|
+
const to = target === null ? dragged.pose : target.pose;
|
|
742
|
+
return [
|
|
743
|
+
createTransformOp({
|
|
744
|
+
id: dragged.id,
|
|
745
|
+
from: dragged.originPose,
|
|
746
|
+
to,
|
|
747
|
+
label: "Snap drop"
|
|
748
|
+
})
|
|
749
|
+
];
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export { MOVE_ANCHORS, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, cellAt, containedThenNearest, createCellHighlightLayer, createGridLayer, createGuidesLayer, deriveAlignmentGuides, freeform, matchAlignment, nearest, nearestWithin, none, roundToCell, snapPoint, tileGrid, useGridCellHover, useGuides };
|
|
755
|
+
//# sourceMappingURL=index.js.map
|
|
756
|
+
//# sourceMappingURL=index.js.map
|