@zoneflow/editor-dom 0.0.15 → 0.0.17
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/floatingLayout.d.ts +59 -0
- package/dist/floatingLayout.js +226 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pathCreateEditor.d.ts +3 -1
- package/dist/pathCreateEditor.js +23 -3
- package/package.json +3 -3
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Point, type UniverseLayoutModel, type UniverseModel, type Zone, type ZoneId } from "@zoneflow/core";
|
|
2
|
+
/**
|
|
3
|
+
* Per-zone velocity vectors carried between simulation steps.
|
|
4
|
+
*/
|
|
5
|
+
export type FloatingVelocities = Record<ZoneId, Point>;
|
|
6
|
+
export type FloatingLayoutOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* Flow direction. Zoneflow flows left→right by default ("x"); "y" lays the
|
|
9
|
+
* flow out top→bottom. Connected zones are spaced along this axis and aligned
|
|
10
|
+
* along the perpendicular one.
|
|
11
|
+
*/
|
|
12
|
+
flowAxis?: "x" | "y";
|
|
13
|
+
/** Target spacing (px) between a source and its target along the flow axis. */
|
|
14
|
+
layerGap?: number;
|
|
15
|
+
/** Stiffness of the flow-axis spacing force (the dominant ordering force). */
|
|
16
|
+
flowStrength?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Stiffness of the cross-axis force that aligns a target with its source,
|
|
19
|
+
* keeping each chain reading as a straight flow line.
|
|
20
|
+
*/
|
|
21
|
+
alignStrength?: number;
|
|
22
|
+
/** Strength of the inverse-square push that keeps mobile zones apart. */
|
|
23
|
+
repulsionStrength?: number;
|
|
24
|
+
/** Gentle pull toward the mobile cluster's centroid (prevents drift). */
|
|
25
|
+
centeringStrength?: number;
|
|
26
|
+
/** Velocity retention per step (0..1). Lower = settles faster. */
|
|
27
|
+
damping?: number;
|
|
28
|
+
/** Overall multiplier applied to velocity when moving (lower = slower). */
|
|
29
|
+
speed?: number;
|
|
30
|
+
/** Max distance (px) a zone may move in a single step. Keeps motion gentle. */
|
|
31
|
+
maxStep?: number;
|
|
32
|
+
/** Inner padding (px) kept between a child zone and its container's edges. */
|
|
33
|
+
containerPadding?: number;
|
|
34
|
+
/** Decides which zones drift. Defaults to action zones. */
|
|
35
|
+
isMobile?: (zone: Zone) => boolean;
|
|
36
|
+
};
|
|
37
|
+
export type FloatingStepResult = {
|
|
38
|
+
layoutModel: UniverseLayoutModel;
|
|
39
|
+
velocities: FloatingVelocities;
|
|
40
|
+
/** Total kinetic energy (sum of |v|²). Near zero once the layout settles. */
|
|
41
|
+
energy: number;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Advances a force-directed layout by one frame. Pure: given the same inputs
|
|
45
|
+
* it always returns the same output, so the caller (a RAF loop) owns all
|
|
46
|
+
* mutable state via the returned `velocities`.
|
|
47
|
+
*
|
|
48
|
+
* Forces are computed in world space; deltas are written back to each zone's
|
|
49
|
+
* local layout coordinates. Because only action zones are mobile and their
|
|
50
|
+
* container parents stay fixed, a world-space delta equals the local-space
|
|
51
|
+
* delta, so no coordinate conversion is needed on write-back.
|
|
52
|
+
*/
|
|
53
|
+
export declare function stepFloatingLayout(params: {
|
|
54
|
+
model: UniverseModel;
|
|
55
|
+
layoutModel: UniverseLayoutModel;
|
|
56
|
+
velocities?: FloatingVelocities;
|
|
57
|
+
pinnedZoneIds?: ReadonlySet<ZoneId>;
|
|
58
|
+
options?: FloatingLayoutOptions;
|
|
59
|
+
}): FloatingStepResult;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { getPaths, resolvePathTarget, updateZoneLayout, } from "@zoneflow/core";
|
|
2
|
+
import { resolveWorldZoneOrigin } from "./zoneGeometry";
|
|
3
|
+
import { roundCoordinate } from "./moveEditorShared";
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
flowAxis: "x",
|
|
6
|
+
layerGap: 200,
|
|
7
|
+
flowStrength: 0.03,
|
|
8
|
+
alignStrength: 0.014,
|
|
9
|
+
repulsionStrength: 130000,
|
|
10
|
+
// Very weak — just enough to stop unbounded drift. Stronger values compact
|
|
11
|
+
// the whole graph toward the centroid and fight the left→right spread.
|
|
12
|
+
centeringStrength: 0.0015,
|
|
13
|
+
damping: 0.8,
|
|
14
|
+
speed: 0.9,
|
|
15
|
+
maxStep: 6,
|
|
16
|
+
containerPadding: 12,
|
|
17
|
+
};
|
|
18
|
+
const MIN_DISTANCE = 1;
|
|
19
|
+
function isActionZone(zone) {
|
|
20
|
+
return zone.zoneType === "action";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Advances a force-directed layout by one frame. Pure: given the same inputs
|
|
24
|
+
* it always returns the same output, so the caller (a RAF loop) owns all
|
|
25
|
+
* mutable state via the returned `velocities`.
|
|
26
|
+
*
|
|
27
|
+
* Forces are computed in world space; deltas are written back to each zone's
|
|
28
|
+
* local layout coordinates. Because only action zones are mobile and their
|
|
29
|
+
* container parents stay fixed, a world-space delta equals the local-space
|
|
30
|
+
* delta, so no coordinate conversion is needed on write-back.
|
|
31
|
+
*/
|
|
32
|
+
export function stepFloatingLayout(params) {
|
|
33
|
+
const { model, layoutModel, velocities = {}, pinnedZoneIds, options } = params;
|
|
34
|
+
const flowAxis = options?.flowAxis ?? DEFAULTS.flowAxis;
|
|
35
|
+
const layerGap = options?.layerGap ?? DEFAULTS.layerGap;
|
|
36
|
+
const flowStrength = options?.flowStrength ?? DEFAULTS.flowStrength;
|
|
37
|
+
const alignStrength = options?.alignStrength ?? DEFAULTS.alignStrength;
|
|
38
|
+
const repulsionStrength = options?.repulsionStrength ?? DEFAULTS.repulsionStrength;
|
|
39
|
+
const centeringStrength = options?.centeringStrength ?? DEFAULTS.centeringStrength;
|
|
40
|
+
const damping = options?.damping ?? DEFAULTS.damping;
|
|
41
|
+
const speed = options?.speed ?? DEFAULTS.speed;
|
|
42
|
+
const maxStep = options?.maxStep ?? DEFAULTS.maxStep;
|
|
43
|
+
const containerPadding = options?.containerPadding ?? DEFAULTS.containerPadding;
|
|
44
|
+
const isMobile = options?.isMobile ?? isActionZone;
|
|
45
|
+
// 1. Sample world-space centers for every laid-out zone.
|
|
46
|
+
const originCache = new Map();
|
|
47
|
+
const samples = [];
|
|
48
|
+
const sampleById = new Map();
|
|
49
|
+
for (const zone of Object.values(model.zonesById)) {
|
|
50
|
+
const layout = layoutModel.zoneLayoutsById[zone.id];
|
|
51
|
+
if (!layout)
|
|
52
|
+
continue;
|
|
53
|
+
const origin = resolveWorldZoneOrigin({
|
|
54
|
+
model,
|
|
55
|
+
layoutModel,
|
|
56
|
+
zoneId: zone.id,
|
|
57
|
+
cache: originCache,
|
|
58
|
+
});
|
|
59
|
+
const center = {
|
|
60
|
+
x: origin.x + (layout.width ?? 0) / 2,
|
|
61
|
+
y: origin.y + (layout.height ?? 0) / 2,
|
|
62
|
+
};
|
|
63
|
+
const sample = {
|
|
64
|
+
zoneId: zone.id,
|
|
65
|
+
center,
|
|
66
|
+
mobile: isMobile(zone) && !pinnedZoneIds?.has(zone.id),
|
|
67
|
+
};
|
|
68
|
+
samples.push(sample);
|
|
69
|
+
sampleById.set(zone.id, sample);
|
|
70
|
+
}
|
|
71
|
+
const mobileSamples = samples.filter((sample) => sample.mobile);
|
|
72
|
+
if (mobileSamples.length === 0) {
|
|
73
|
+
return { layoutModel, velocities, energy: 0 };
|
|
74
|
+
}
|
|
75
|
+
// Accumulated force per mobile zone.
|
|
76
|
+
const forces = new Map();
|
|
77
|
+
for (const sample of mobileSamples) {
|
|
78
|
+
forces.set(sample.zoneId, { x: 0, y: 0 });
|
|
79
|
+
}
|
|
80
|
+
const addForce = (zoneId, fx, fy) => {
|
|
81
|
+
const force = forces.get(zoneId);
|
|
82
|
+
if (!force)
|
|
83
|
+
return; // not mobile — ignore
|
|
84
|
+
force.x += fx;
|
|
85
|
+
force.y += fy;
|
|
86
|
+
};
|
|
87
|
+
// 2. Flow forces along every path edge (source → target). The flow axis
|
|
88
|
+
// carries a directional spacing force (target sits one `layerGap`
|
|
89
|
+
// downstream of its source); the cross axis carries a gentle alignment
|
|
90
|
+
// force so each chain reads as a straight flow line.
|
|
91
|
+
for (const zone of Object.values(model.zonesById)) {
|
|
92
|
+
const source = sampleById.get(zone.id);
|
|
93
|
+
if (!source)
|
|
94
|
+
continue;
|
|
95
|
+
for (const path of getPaths(zone)) {
|
|
96
|
+
const targetZone = resolvePathTarget(model, path);
|
|
97
|
+
if (!targetZone || targetZone.id === zone.id)
|
|
98
|
+
continue;
|
|
99
|
+
const target = sampleById.get(targetZone.id);
|
|
100
|
+
if (!target)
|
|
101
|
+
continue;
|
|
102
|
+
if (!source.mobile && !target.mobile)
|
|
103
|
+
continue;
|
|
104
|
+
// Flow axis: drive the gap toward +layerGap (target downstream).
|
|
105
|
+
const flowGap = flowAxis === "x"
|
|
106
|
+
? target.center.x - source.center.x
|
|
107
|
+
: target.center.y - source.center.y;
|
|
108
|
+
const flowForce = (layerGap - flowGap) * flowStrength;
|
|
109
|
+
// Cross axis: pull the target onto the source's line.
|
|
110
|
+
const crossDelta = flowAxis === "x"
|
|
111
|
+
? target.center.y - source.center.y
|
|
112
|
+
: target.center.x - source.center.x;
|
|
113
|
+
const alignForce = -crossDelta * alignStrength;
|
|
114
|
+
if (flowAxis === "x") {
|
|
115
|
+
addForce(targetZone.id, flowForce, alignForce);
|
|
116
|
+
addForce(zone.id, -flowForce, -alignForce);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
addForce(targetZone.id, alignForce, flowForce);
|
|
120
|
+
addForce(zone.id, -alignForce, -flowForce);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// 3. Inverse-square repulsion between every pair of mobile zones.
|
|
125
|
+
for (let i = 0; i < mobileSamples.length; i += 1) {
|
|
126
|
+
for (let j = i + 1; j < mobileSamples.length; j += 1) {
|
|
127
|
+
const a = mobileSamples[i];
|
|
128
|
+
const b = mobileSamples[j];
|
|
129
|
+
let dx = a.center.x - b.center.x;
|
|
130
|
+
let dy = a.center.y - b.center.y;
|
|
131
|
+
let distanceSq = dx * dx + dy * dy;
|
|
132
|
+
if (distanceSq < MIN_DISTANCE) {
|
|
133
|
+
// Perfectly overlapping — nudge deterministically by index so the
|
|
134
|
+
// pair separates without relying on randomness.
|
|
135
|
+
dx = (i - j) || 1;
|
|
136
|
+
dy = 1;
|
|
137
|
+
distanceSq = dx * dx + dy * dy;
|
|
138
|
+
}
|
|
139
|
+
const distance = Math.sqrt(distanceSq);
|
|
140
|
+
const magnitude = repulsionStrength / distanceSq;
|
|
141
|
+
const ux = dx / distance;
|
|
142
|
+
const uy = dy / distance;
|
|
143
|
+
addForce(a.zoneId, ux * magnitude, uy * magnitude);
|
|
144
|
+
addForce(b.zoneId, -ux * magnitude, -uy * magnitude);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 4. Centering pull toward the mobile cluster's centroid.
|
|
148
|
+
let centroidX = 0;
|
|
149
|
+
let centroidY = 0;
|
|
150
|
+
for (const sample of mobileSamples) {
|
|
151
|
+
centroidX += sample.center.x;
|
|
152
|
+
centroidY += sample.center.y;
|
|
153
|
+
}
|
|
154
|
+
centroidX /= mobileSamples.length;
|
|
155
|
+
centroidY /= mobileSamples.length;
|
|
156
|
+
for (const sample of mobileSamples) {
|
|
157
|
+
addForce(sample.zoneId, (centroidX - sample.center.x) * centeringStrength, (centroidY - sample.center.y) * centeringStrength);
|
|
158
|
+
}
|
|
159
|
+
// 5. Integrate velocities and write displaced positions back to the layout,
|
|
160
|
+
// clamping each child zone inside its parent container's bounds.
|
|
161
|
+
const nextVelocities = {};
|
|
162
|
+
let nextLayoutModel = layoutModel;
|
|
163
|
+
let energy = 0;
|
|
164
|
+
for (const sample of mobileSamples) {
|
|
165
|
+
const force = forces.get(sample.zoneId) ?? { x: 0, y: 0 };
|
|
166
|
+
const prev = velocities[sample.zoneId] ?? { x: 0, y: 0 };
|
|
167
|
+
let vx = (prev.x + force.x) * damping;
|
|
168
|
+
let vy = (prev.y + force.y) * damping;
|
|
169
|
+
// Clamp the per-step displacement so motion stays slow and gentle.
|
|
170
|
+
const stepDistance = Math.hypot(vx * speed, vy * speed);
|
|
171
|
+
if (stepDistance > maxStep) {
|
|
172
|
+
const scale = maxStep / stepDistance;
|
|
173
|
+
vx *= scale;
|
|
174
|
+
vy *= scale;
|
|
175
|
+
}
|
|
176
|
+
const layout = layoutModel.zoneLayoutsById[sample.zoneId];
|
|
177
|
+
if (!layout) {
|
|
178
|
+
nextVelocities[sample.zoneId] = { x: vx, y: vy };
|
|
179
|
+
energy += vx * vx + vy * vy;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
let nextX = layout.x + vx * speed;
|
|
183
|
+
let nextY = layout.y + vy * speed;
|
|
184
|
+
// Containment: keep a child fully within its parent container. Local
|
|
185
|
+
// coordinates are relative to the parent origin, so the valid box is
|
|
186
|
+
// [pad, parentSize - childSize - pad] on each axis. Velocity on a clamped
|
|
187
|
+
// axis is zeroed so the zone rests against the wall without jitter.
|
|
188
|
+
const parentId = model.zonesById[sample.zoneId]?.parentZoneId;
|
|
189
|
+
const parentLayout = parentId
|
|
190
|
+
? layoutModel.zoneLayoutsById[parentId]
|
|
191
|
+
: undefined;
|
|
192
|
+
if (parentLayout) {
|
|
193
|
+
const childW = layout.width ?? 0;
|
|
194
|
+
const childH = layout.height ?? 0;
|
|
195
|
+
const parentW = parentLayout.width;
|
|
196
|
+
const parentH = parentLayout.height;
|
|
197
|
+
if (parentW !== undefined) {
|
|
198
|
+
const maxX = Math.max(containerPadding, parentW - childW - containerPadding);
|
|
199
|
+
const clampedX = Math.min(Math.max(nextX, containerPadding), maxX);
|
|
200
|
+
if (clampedX !== nextX) {
|
|
201
|
+
nextX = clampedX;
|
|
202
|
+
vx = 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (parentH !== undefined) {
|
|
206
|
+
const maxY = Math.max(containerPadding, parentH - childH - containerPadding);
|
|
207
|
+
const clampedY = Math.min(Math.max(nextY, containerPadding), maxY);
|
|
208
|
+
if (clampedY !== nextY) {
|
|
209
|
+
nextY = clampedY;
|
|
210
|
+
vy = 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
nextVelocities[sample.zoneId] = { x: vx, y: vy };
|
|
215
|
+
energy += vx * vx + vy * vy;
|
|
216
|
+
nextLayoutModel = updateZoneLayout(nextLayoutModel, sample.zoneId, {
|
|
217
|
+
x: roundCoordinate(nextX),
|
|
218
|
+
y: roundCoordinate(nextY),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
layoutModel: nextLayoutModel,
|
|
223
|
+
velocities: nextVelocities,
|
|
224
|
+
energy,
|
|
225
|
+
};
|
|
226
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./zoneMoveEditor";
|
|
2
2
|
export * from "./pathCreateEditor";
|
|
3
3
|
export * from "./zOrderEditor";
|
|
4
|
+
export * from "./floatingLayout";
|
|
4
5
|
export { alignPathsByMode, alignZonesByMode, commitZoneGroupReparentAtCurrentPosition, commitZoneReparentAtCurrentPosition, distributePathsByMode, distributeZonesByMode, resolveGroupPathDragOrigin, resolveGroupZoneDragOrigin, resolveZonePlacementAtWorldRect, resolvePathResizeOrigin, resizePathNodeByScreenDelta, } from "./zoneMoveEditor";
|
|
5
6
|
export type { PathResizeOrigin } from "./zoneMoveEditor";
|
|
6
7
|
export { resolvePathOutputAnchorScreenRect, retargetPathFromOutputAnchorDrag, } from "./pathCreateEditor";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./zoneMoveEditor";
|
|
2
2
|
export * from "./pathCreateEditor";
|
|
3
3
|
export * from "./zOrderEditor";
|
|
4
|
+
export * from "./floatingLayout";
|
|
4
5
|
export { alignPathsByMode, alignZonesByMode, commitZoneGroupReparentAtCurrentPosition, commitZoneReparentAtCurrentPosition, distributePathsByMode, distributeZonesByMode, resolveGroupPathDragOrigin, resolveGroupZoneDragOrigin, resolveZonePlacementAtWorldRect, resolvePathResizeOrigin, resizePathNodeByScreenDelta, } from "./zoneMoveEditor";
|
|
5
6
|
export { resolvePathOutputAnchorScreenRect, retargetPathFromOutputAnchorDrag, } from "./pathCreateEditor";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Path, type PathId, type Point, type UniverseLayoutModel, type UniverseModel, type Zone, type ZoneId } from "@zoneflow/core";
|
|
2
|
-
import { type CameraState, type Rect, type RendererFrame } from "@zoneflow/renderer-dom";
|
|
2
|
+
import { type CameraState, type Rect, type RendererFrame, type ResolveZoneShape } from "@zoneflow/renderer-dom";
|
|
3
3
|
import type { GridSnapOptions } from "./zoneMoveEditor";
|
|
4
4
|
export type CanConnectPathParams = {
|
|
5
5
|
mode: "create" | "retarget";
|
|
@@ -18,6 +18,7 @@ export declare function resolveZoneAnchorScreenRect(params: {
|
|
|
18
18
|
camera: CameraState;
|
|
19
19
|
zoneId: ZoneId;
|
|
20
20
|
kind: "inlet" | "outlet";
|
|
21
|
+
resolveZoneShape?: ResolveZoneShape;
|
|
21
22
|
}): Rect | undefined;
|
|
22
23
|
export declare function resolveInputAnchorTargetZoneId(params: {
|
|
23
24
|
model: UniverseModel;
|
|
@@ -26,6 +27,7 @@ export declare function resolveInputAnchorTargetZoneId(params: {
|
|
|
26
27
|
point: Point;
|
|
27
28
|
excludeZoneIds?: ZoneId[];
|
|
28
29
|
canConnect?: (targetZoneId: ZoneId) => boolean;
|
|
30
|
+
resolveZoneShape?: ResolveZoneShape;
|
|
29
31
|
}): ZoneId | null;
|
|
30
32
|
export declare function resolvePathOutputAnchorScreenRect(params: {
|
|
31
33
|
frame: RendererFrame;
|
package/dist/pathCreateEditor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { addPath, createPathId, isZoneInputEnabled, isZoneOutputEnabled, setPathTarget, updatePathLayout, } from "@zoneflow/core";
|
|
2
|
+
import { normalizeZoneShape, } from "@zoneflow/renderer-dom";
|
|
2
3
|
function typedValues(record) {
|
|
3
4
|
return Object.values(record);
|
|
4
5
|
}
|
|
@@ -8,6 +9,9 @@ const DEFAULT_PATH_NODE_OFFSET_X = 32;
|
|
|
8
9
|
const DEFAULT_PATH_NODE_GAP_Y = 40;
|
|
9
10
|
const DEFAULT_ANCHOR_WIDTH = 24;
|
|
10
11
|
const DEFAULT_ANCHOR_ATTACH_DEPTH = 10;
|
|
12
|
+
// Square hit area for vertex-mode anchors (circle/diamond/…), centered on the
|
|
13
|
+
// shape's left/right vertex to match the vertex dot drawn by the renderer.
|
|
14
|
+
const VERTEX_ANCHOR_HIT_SIZE = 28;
|
|
11
15
|
const DEFAULT_PATH_OUTPUT_HANDLE_WIDTH = 18;
|
|
12
16
|
const DEFAULT_PATH_OUTPUT_HANDLE_MIN_HEIGHT = 22;
|
|
13
17
|
const DEFAULT_PATH_OUTPUT_HANDLE_MAX_HEIGHT = 40;
|
|
@@ -57,7 +61,20 @@ function projectWorldRectToScreenRect(rect, camera) {
|
|
|
57
61
|
};
|
|
58
62
|
}
|
|
59
63
|
function resolveZoneAnchorRect(params) {
|
|
60
|
-
const { zoneRect, anchor, kind } = params;
|
|
64
|
+
const { zoneRect, anchor, kind, mode = "edge" } = params;
|
|
65
|
+
if (mode === "vertex") {
|
|
66
|
+
// Non-rectangular zones expose their anchor only at the shape vertex, so
|
|
67
|
+
// the grab/drop hit area is a compact square centered on the anchor point
|
|
68
|
+
// instead of the full-height edge band used for rectangular zones.
|
|
69
|
+
// `anchor.point` is already in world coordinates (zone position + offset),
|
|
70
|
+
// so it is used directly — do NOT add zoneRect again.
|
|
71
|
+
return {
|
|
72
|
+
x: anchor.point.x - VERTEX_ANCHOR_HIT_SIZE / 2,
|
|
73
|
+
y: anchor.point.y - VERTEX_ANCHOR_HIT_SIZE / 2,
|
|
74
|
+
width: VERTEX_ANCHOR_HIT_SIZE,
|
|
75
|
+
height: VERTEX_ANCHOR_HIT_SIZE,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
61
78
|
if (anchor.rect) {
|
|
62
79
|
return {
|
|
63
80
|
x: anchor.rect.x,
|
|
@@ -82,7 +99,7 @@ export function screenPointToWorldPoint(point, camera) {
|
|
|
82
99
|
};
|
|
83
100
|
}
|
|
84
101
|
export function resolveZoneAnchorScreenRect(params) {
|
|
85
|
-
const { frame, camera, zoneId, kind } = params;
|
|
102
|
+
const { frame, camera, zoneId, kind, resolveZoneShape } = params;
|
|
86
103
|
const zoneVisual = frame.pipeline.graphLayout.zonesById[zoneId];
|
|
87
104
|
if (!zoneVisual)
|
|
88
105
|
return undefined;
|
|
@@ -90,15 +107,17 @@ export function resolveZoneAnchorScreenRect(params) {
|
|
|
90
107
|
return undefined;
|
|
91
108
|
if (kind === "outlet" && !isZoneOutputEnabled(zoneVisual.zone))
|
|
92
109
|
return undefined;
|
|
110
|
+
const mode = normalizeZoneShape(resolveZoneShape?.(zoneVisual.zone)).anchors;
|
|
93
111
|
const anchorRect = resolveZoneAnchorRect({
|
|
94
112
|
zoneRect: zoneVisual.rect,
|
|
95
113
|
anchor: zoneVisual.anchors[kind],
|
|
96
114
|
kind,
|
|
115
|
+
mode,
|
|
97
116
|
});
|
|
98
117
|
return projectWorldRectToScreenRect(anchorRect, camera);
|
|
99
118
|
}
|
|
100
119
|
export function resolveInputAnchorTargetZoneId(params) {
|
|
101
|
-
const { model, frame, camera, point, excludeZoneIds, canConnect } = params;
|
|
120
|
+
const { model, frame, camera, point, excludeZoneIds, canConnect, resolveZoneShape, } = params;
|
|
102
121
|
const excluded = new Set(excludeZoneIds ?? []);
|
|
103
122
|
let bestZoneId = null;
|
|
104
123
|
let bestArea = Number.POSITIVE_INFINITY;
|
|
@@ -117,6 +136,7 @@ export function resolveInputAnchorTargetZoneId(params) {
|
|
|
117
136
|
camera,
|
|
118
137
|
zoneId: zoneVisual.zoneId,
|
|
119
138
|
kind: "inlet",
|
|
139
|
+
resolveZoneShape,
|
|
120
140
|
});
|
|
121
141
|
if (!rect || !containsPoint(rect, point))
|
|
122
142
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoneflow/editor-dom",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
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.
|
|
23
|
-
"@zoneflow/renderer-dom": "0.0.
|
|
22
|
+
"@zoneflow/core": "0.0.17",
|
|
23
|
+
"@zoneflow/renderer-dom": "0.0.17"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsc -p tsconfig.json",
|