@zoneflow/editor-dom 0.0.5 → 0.0.6

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.
@@ -26,11 +26,23 @@ export type PathMoveOriginSnapshot = {
26
26
  componentId: "body" | "label" | null;
27
27
  coordinateSpace: PathMoveCoordinateSpace;
28
28
  };
29
+ export type ObjectSnapGuides = {
30
+ x: number[];
31
+ y: number[];
32
+ };
33
+ export type ObjectSnapAxisMatch = {
34
+ guide: number;
35
+ align: "start" | "center" | "end";
36
+ snappedStart: number;
37
+ };
29
38
  export type MoveEditorDragOrigin = {
30
39
  kind: "zone";
31
40
  zoneId: ZoneId;
32
41
  originX: number;
33
42
  originY: number;
43
+ width: number;
44
+ height: number;
45
+ objectSnapGuides?: ObjectSnapGuides;
34
46
  } | {
35
47
  kind: "zone-group";
36
48
  primaryZoneId: ZoneId;
@@ -39,6 +51,7 @@ export type MoveEditorDragOrigin = {
39
51
  kind: "path";
40
52
  pathId: PathId;
41
53
  origin: PathMoveOriginSnapshot;
54
+ objectSnapGuides?: ObjectSnapGuides;
42
55
  } | {
43
56
  kind: "path-group";
44
57
  primaryPathId: PathId;
@@ -61,6 +74,10 @@ export type GridSnapOptions = {
61
74
  enabled?: boolean;
62
75
  size?: number;
63
76
  };
77
+ export type ObjectSnapOptions = {
78
+ enabled?: boolean;
79
+ threshold?: number;
80
+ };
64
81
  export type ZoneAlignMode = "left" | "right" | "top" | "bottom" | "center-horizontal" | "center-vertical";
65
82
  export type ZoneDistributeMode = "horizontal" | "vertical";
66
83
  export type PathAlignMode = ZoneAlignMode;
@@ -83,5 +100,20 @@ export declare function resolveSnappedMove(params: {
83
100
  effectiveDeltaX: number;
84
101
  effectiveDeltaY: number;
85
102
  };
103
+ export declare function collectRectObjectSnapGuides(rects: Rect[]): ObjectSnapGuides;
104
+ export declare function resolveObjectSnappedRectPosition(params: {
105
+ x: number;
106
+ y: number;
107
+ width: number;
108
+ height: number;
109
+ camera: CameraState;
110
+ guides?: ObjectSnapGuides;
111
+ objectSnap?: ObjectSnapOptions;
112
+ }): {
113
+ x: number;
114
+ y: number;
115
+ guideX: number | undefined;
116
+ guideY: number | undefined;
117
+ };
86
118
  export declare function containsPoint(rect: Rect, point: Point): boolean;
87
119
  export declare function getRectArea(rect: Rect): number;
@@ -37,6 +37,85 @@ export function resolveSnappedMove(params) {
37
37
  effectiveDeltaY: nextY - originY,
38
38
  };
39
39
  }
40
+ export function collectRectObjectSnapGuides(rects) {
41
+ const x = new Set();
42
+ const y = new Set();
43
+ for (const rect of rects) {
44
+ x.add(roundCoordinate(rect.x));
45
+ x.add(roundCoordinate(rect.x + rect.width / 2));
46
+ x.add(roundCoordinate(rect.x + rect.width));
47
+ y.add(roundCoordinate(rect.y));
48
+ y.add(roundCoordinate(rect.y + rect.height / 2));
49
+ y.add(roundCoordinate(rect.y + rect.height));
50
+ }
51
+ return {
52
+ x: Array.from(x).sort((a, b) => a - b),
53
+ y: Array.from(y).sort((a, b) => a - b),
54
+ };
55
+ }
56
+ function resolveAxisObjectSnap(params) {
57
+ const { start, size, guides, threshold } = params;
58
+ const candidates = [
59
+ { coordinate: start, align: "start" },
60
+ { coordinate: start + size / 2, align: "center" },
61
+ { coordinate: start + size, align: "end" },
62
+ ];
63
+ let best;
64
+ for (const guide of guides) {
65
+ for (const candidate of candidates) {
66
+ const distance = Math.abs(guide - candidate.coordinate);
67
+ if (distance > threshold)
68
+ continue;
69
+ const snappedStart = candidate.align === "start"
70
+ ? guide
71
+ : candidate.align === "center"
72
+ ? guide - size / 2
73
+ : guide - size;
74
+ if (!best || distance < best.distance) {
75
+ best = {
76
+ distance,
77
+ match: {
78
+ guide: roundCoordinate(guide),
79
+ align: candidate.align,
80
+ snappedStart: roundCoordinate(snappedStart),
81
+ },
82
+ };
83
+ }
84
+ }
85
+ }
86
+ return best?.match;
87
+ }
88
+ export function resolveObjectSnappedRectPosition(params) {
89
+ const { x, y, width, height, camera, guides, objectSnap } = params;
90
+ if (!guides || !objectSnap?.enabled) {
91
+ return {
92
+ x: roundCoordinate(x),
93
+ y: roundCoordinate(y),
94
+ guideX: undefined,
95
+ guideY: undefined,
96
+ };
97
+ }
98
+ const threshold = objectSnap.threshold ?? 8;
99
+ const worldThreshold = threshold / camera.zoom;
100
+ const xMatch = resolveAxisObjectSnap({
101
+ start: x,
102
+ size: width,
103
+ guides: guides.x,
104
+ threshold: worldThreshold,
105
+ });
106
+ const yMatch = resolveAxisObjectSnap({
107
+ start: y,
108
+ size: height,
109
+ guides: guides.y,
110
+ threshold: worldThreshold,
111
+ });
112
+ return {
113
+ x: xMatch?.snappedStart ?? roundCoordinate(x),
114
+ y: yMatch?.snappedStart ?? roundCoordinate(y),
115
+ guideX: xMatch?.guide,
116
+ guideY: yMatch?.guide,
117
+ };
118
+ }
40
119
  export function containsPoint(rect, point) {
41
120
  return (point.x >= rect.x &&
42
121
  point.x <= rect.x + rect.width &&
@@ -1,7 +1,7 @@
1
1
  import { type UniverseLayoutModel, type UniverseModel, type ZoneId } from "@zoneflow/core";
2
2
  import type { CameraState, RendererFrame } from "@zoneflow/renderer-dom";
3
- import { type GridSnapOptions, type MoveEditorDragOrigin, type MoveEditorTarget, type MoveEditorTargetOptions, type ZoneAlignMode, type ZoneDistributeMode, type ZoneResizeOrigin } from "./moveEditorShared";
4
- export type { GridSnapOptions, MoveEditorDragOrigin, MoveEditorTarget, MoveEditorTargetOptions, PathAlignMode, PathDistributeMode, PathResizeOrigin, ZoneAlignMode, ZoneDistributeMode, ZoneResizeOrigin, } from "./moveEditorShared";
3
+ import { type GridSnapOptions, type ObjectSnapOptions, type MoveEditorDragOrigin, type MoveEditorTarget, type MoveEditorTargetOptions, type ZoneAlignMode, type ZoneDistributeMode, type ZoneResizeOrigin } from "./moveEditorShared";
4
+ export type { GridSnapOptions, MoveEditorDragOrigin, MoveEditorTarget, MoveEditorTargetOptions, ObjectSnapOptions, PathAlignMode, PathDistributeMode, PathResizeOrigin, ZoneAlignMode, ZoneDistributeMode, ZoneResizeOrigin, } from "./moveEditorShared";
5
5
  export { alignPathsByMode, distributePathsByMode, resolveGroupPathDragOrigin, resolvePathResizeOrigin, resizePathNodeByScreenDelta, } from "./pathMoveEditor";
6
6
  export { commitZoneGroupReparentAtCurrentPosition, commitZoneReparentAtCurrentPosition, reparentZoneAtCurrentPosition, resolveZonePlacementAtWorldRect, resolveZoneReparentCandidate, } from "./zoneReparent";
7
7
  export declare function getMoveEditorTargets(params: {
@@ -10,7 +10,12 @@ export declare function getMoveEditorTargets(params: {
10
10
  camera: CameraState;
11
11
  options?: MoveEditorTargetOptions;
12
12
  }): MoveEditorTarget[];
13
- export declare function resolveMoveEditorDragOrigin(layoutModel: UniverseLayoutModel, target: MoveEditorTarget, frame?: RendererFrame): MoveEditorDragOrigin | undefined;
13
+ export declare function resolveMoveEditorDragOrigin(params: {
14
+ model: UniverseModel;
15
+ layoutModel: UniverseLayoutModel;
16
+ target: MoveEditorTarget;
17
+ frame?: RendererFrame;
18
+ }): MoveEditorDragOrigin | undefined;
14
19
  export declare function resolveGroupZoneDragOrigin(params: {
15
20
  model: UniverseModel;
16
21
  layoutModel: UniverseLayoutModel;
@@ -24,7 +29,19 @@ export declare function moveEditorTargetByScreenDelta(params: {
24
29
  deltaX: number;
25
30
  deltaY: number;
26
31
  gridSnap?: GridSnapOptions;
32
+ objectSnap?: ObjectSnapOptions;
27
33
  }): UniverseLayoutModel;
34
+ export declare function resolveMoveEditorObjectSnapGuides(params: {
35
+ camera: CameraState;
36
+ origin: MoveEditorDragOrigin;
37
+ deltaX: number;
38
+ deltaY: number;
39
+ gridSnap?: GridSnapOptions;
40
+ objectSnap?: ObjectSnapOptions;
41
+ }): {
42
+ guideX: number | undefined;
43
+ guideY: number | undefined;
44
+ };
28
45
  export declare function resolveZoneResizeOrigin(layoutModel: UniverseLayoutModel, zoneId: ZoneId): ZoneResizeOrigin | undefined;
29
46
  export declare function resizeZoneByScreenDelta(params: {
30
47
  layoutModel: UniverseLayoutModel;
@@ -1,5 +1,5 @@
1
1
  import { getZoneDepth, getZoneLayout, updateZoneLayout, } from "@zoneflow/core";
2
- import { projectWorldRectToScreenRect, resolveSnappedMove, roundCoordinate, snapCoordinate, typedValues, } from "./moveEditorShared";
2
+ import { collectRectObjectSnapGuides, projectWorldRectToScreenRect, resolveObjectSnappedRectPosition, resolveSnappedMove, roundCoordinate, snapCoordinate, typedValues, } from "./moveEditorShared";
3
3
  import { applyPathMovePosition, resolvePathMoveOriginSnapshot, } from "./pathMoveEditor";
4
4
  import { applyZoneOriginsDelta, resolveZoneGroupOrigins, } from "./zoneGeometry";
5
5
  export { alignPathsByMode, distributePathsByMode, resolveGroupPathDragOrigin, resolvePathResizeOrigin, resizePathNodeByScreenDelta, } from "./pathMoveEditor";
@@ -7,6 +7,48 @@ export { commitZoneGroupReparentAtCurrentPosition, commitZoneReparentAtCurrentPo
7
7
  const DEFAULT_MIN_VISIBLE_SIZE = 18;
8
8
  const DEFAULT_MIN_ZONE_WIDTH = 140;
9
9
  const DEFAULT_MIN_ZONE_HEIGHT = 96;
10
+ function collectDescendantZoneIds(model, zoneId) {
11
+ const descendants = new Set();
12
+ const queue = [...(model.zonesById[zoneId]?.childZoneIds ?? [])];
13
+ while (queue.length > 0) {
14
+ const current = queue.shift();
15
+ if (!current || descendants.has(current))
16
+ continue;
17
+ descendants.add(current);
18
+ queue.push(...(model.zonesById[current]?.childZoneIds ?? []));
19
+ }
20
+ return descendants;
21
+ }
22
+ function resolveObjectSnapGuides(params) {
23
+ const { model, frame, target } = params;
24
+ if (!frame)
25
+ return undefined;
26
+ const excludedZoneIds = target.kind === "zone"
27
+ ? new Set([target.zoneId, ...collectDescendantZoneIds(model, target.zoneId)])
28
+ : new Set();
29
+ const candidateRects = [];
30
+ for (const zoneVisual of typedValues(frame.pipeline.graphLayout.zonesById)) {
31
+ const visibility = frame.pipeline.visibility.zoneVisibilityById[zoneVisual.zoneId];
32
+ if (!visibility?.isVisible)
33
+ continue;
34
+ if (excludedZoneIds.has(zoneVisual.zoneId))
35
+ continue;
36
+ candidateRects.push(zoneVisual.rect);
37
+ }
38
+ for (const pathVisual of typedValues(frame.pipeline.graphLayout.pathsById)) {
39
+ const visibility = frame.pipeline.visibility.pathVisibilityById[pathVisual.pathId];
40
+ if (!visibility?.shouldRenderNode || !pathVisual.rect)
41
+ continue;
42
+ if (target.kind === "path" && pathVisual.pathId === target.pathId)
43
+ continue;
44
+ if (target.kind === "zone" && excludedZoneIds.has(pathVisual.sourceZoneId))
45
+ continue;
46
+ candidateRects.push(pathVisual.rect);
47
+ }
48
+ return candidateRects.length > 0
49
+ ? collectRectObjectSnapGuides(candidateRects)
50
+ : undefined;
51
+ }
10
52
  function resolveResizedAnchor(params) {
11
53
  const { kind, width, height, current } = params;
12
54
  const rectWidth = current?.rect?.width;
@@ -82,16 +124,28 @@ export function getMoveEditorTargets(params) {
82
124
  ...pathTargets,
83
125
  ];
84
126
  }
85
- export function resolveMoveEditorDragOrigin(layoutModel, target, frame) {
127
+ export function resolveMoveEditorDragOrigin(params) {
128
+ const { model, layoutModel, target, frame } = params;
86
129
  if (target.kind === "zone") {
87
130
  const zoneLayout = getZoneLayout(layoutModel, target.zoneId);
88
131
  if (!zoneLayout)
89
132
  return undefined;
133
+ const zoneRect = frame?.pipeline.graphLayout.zonesById[target.zoneId]?.rect ??
134
+ getZoneLayout(layoutModel, target.zoneId);
135
+ const width = zoneRect?.width ?? 0;
136
+ const height = zoneRect?.height ?? 0;
90
137
  return {
91
138
  kind: "zone",
92
139
  zoneId: target.zoneId,
93
140
  originX: zoneLayout.x,
94
141
  originY: zoneLayout.y,
142
+ width,
143
+ height,
144
+ objectSnapGuides: resolveObjectSnapGuides({
145
+ model,
146
+ frame,
147
+ target,
148
+ }),
95
149
  };
96
150
  }
97
151
  return {
@@ -102,6 +156,11 @@ export function resolveMoveEditorDragOrigin(layoutModel, target, frame) {
102
156
  layoutModel,
103
157
  pathId: target.pathId,
104
158
  }),
159
+ objectSnapGuides: resolveObjectSnapGuides({
160
+ model,
161
+ frame,
162
+ target,
163
+ }),
105
164
  };
106
165
  }
107
166
  export function resolveGroupZoneDragOrigin(params) {
@@ -115,7 +174,7 @@ export function resolveGroupZoneDragOrigin(params) {
115
174
  };
116
175
  }
117
176
  export function moveEditorTargetByScreenDelta(params) {
118
- const { layoutModel, camera, origin, deltaX, deltaY, gridSnap, } = params;
177
+ const { layoutModel, camera, origin, deltaX, deltaY, gridSnap, objectSnap, } = params;
119
178
  if (origin.kind === "zone") {
120
179
  const { nextX, nextY } = resolveSnappedMove({
121
180
  originX: origin.originX,
@@ -125,9 +184,18 @@ export function moveEditorTargetByScreenDelta(params) {
125
184
  camera,
126
185
  gridSnap,
127
186
  });
128
- return updateZoneLayout(layoutModel, origin.zoneId, {
187
+ const snapped = resolveObjectSnappedRectPosition({
129
188
  x: nextX,
130
189
  y: nextY,
190
+ width: origin.width,
191
+ height: origin.height,
192
+ camera,
193
+ guides: origin.objectSnapGuides,
194
+ objectSnap,
195
+ });
196
+ return updateZoneLayout(layoutModel, origin.zoneId, {
197
+ x: snapped.x,
198
+ y: snapped.y,
131
199
  });
132
200
  }
133
201
  if (origin.kind === "zone-group") {
@@ -181,14 +249,76 @@ export function moveEditorTargetByScreenDelta(params) {
181
249
  camera,
182
250
  gridSnap,
183
251
  });
252
+ const snapped = resolveObjectSnappedRectPosition({
253
+ x: nextX,
254
+ y: nextY,
255
+ width: origin.origin.width,
256
+ height: origin.origin.height,
257
+ camera,
258
+ guides: origin.objectSnapGuides,
259
+ objectSnap,
260
+ });
184
261
  return applyPathMovePosition({
185
262
  layoutModel,
186
263
  pathId: origin.pathId,
187
264
  origin: origin.origin,
188
- x: nextX,
189
- y: nextY,
265
+ x: snapped.x,
266
+ y: snapped.y,
190
267
  });
191
268
  }
269
+ export function resolveMoveEditorObjectSnapGuides(params) {
270
+ const { camera, origin, deltaX, deltaY, gridSnap, objectSnap } = params;
271
+ if (origin.kind === "zone") {
272
+ const { nextX, nextY } = resolveSnappedMove({
273
+ originX: origin.originX,
274
+ originY: origin.originY,
275
+ deltaX,
276
+ deltaY,
277
+ camera,
278
+ gridSnap,
279
+ });
280
+ const snapped = resolveObjectSnappedRectPosition({
281
+ x: nextX,
282
+ y: nextY,
283
+ width: origin.width,
284
+ height: origin.height,
285
+ camera,
286
+ guides: origin.objectSnapGuides,
287
+ objectSnap,
288
+ });
289
+ return {
290
+ guideX: snapped.guideX,
291
+ guideY: snapped.guideY,
292
+ };
293
+ }
294
+ if (origin.kind === "path") {
295
+ const { nextX, nextY } = resolveSnappedMove({
296
+ originX: origin.origin.x,
297
+ originY: origin.origin.y,
298
+ deltaX,
299
+ deltaY,
300
+ camera,
301
+ gridSnap,
302
+ });
303
+ const snapped = resolveObjectSnappedRectPosition({
304
+ x: nextX,
305
+ y: nextY,
306
+ width: origin.origin.width,
307
+ height: origin.origin.height,
308
+ camera,
309
+ guides: origin.objectSnapGuides,
310
+ objectSnap,
311
+ });
312
+ return {
313
+ guideX: snapped.guideX,
314
+ guideY: snapped.guideY,
315
+ };
316
+ }
317
+ return {
318
+ guideX: undefined,
319
+ guideY: undefined,
320
+ };
321
+ }
192
322
  export function resolveZoneResizeOrigin(layoutModel, zoneId) {
193
323
  const zoneLayout = getZoneLayout(layoutModel, zoneId);
194
324
  if (!zoneLayout)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoneflow/editor-dom",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "license": "MIT",
5
5
  "description": "Low-level editor geometry and interaction helpers for Zoneflow.",
6
6
  "type": "module",
@@ -19,8 +19,8 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
- "@zoneflow/core": "0.0.5",
23
- "@zoneflow/renderer-dom": "0.0.5"
22
+ "@zoneflow/core": "0.0.6",
23
+ "@zoneflow/renderer-dom": "0.0.6"
24
24
  },
25
25
  "scripts": {
26
26
  "build": "tsc -p tsconfig.json",