@zoneflow/renderer-dom 0.0.1

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.
@@ -0,0 +1,13 @@
1
+ import type { AnchorRect, Point } from "@zoneflow/core";
2
+ import type { Rect } from "./types";
3
+ export type ZoneAnchorKind = "inlet" | "outlet";
4
+ type AnchorGeometry = {
5
+ point: Point;
6
+ rect?: AnchorRect;
7
+ };
8
+ export declare function resolveZoneAnchorRect(params: {
9
+ zoneRect: Rect;
10
+ anchor: AnchorGeometry;
11
+ kind: ZoneAnchorKind;
12
+ }): Rect;
13
+ export {};
@@ -0,0 +1,22 @@
1
+ const DEFAULT_ANCHOR_WIDTH = 24;
2
+ const DEFAULT_ANCHOR_ATTACH_DEPTH = 10;
3
+ export function resolveZoneAnchorRect(params) {
4
+ const { zoneRect, anchor, kind } = params;
5
+ if (anchor.rect) {
6
+ return {
7
+ x: anchor.rect.x,
8
+ y: anchor.rect.y,
9
+ width: anchor.rect.width ?? DEFAULT_ANCHOR_WIDTH,
10
+ height: anchor.rect.height ?? zoneRect.height,
11
+ };
12
+ }
13
+ const x = kind === "inlet"
14
+ ? zoneRect.x - (DEFAULT_ANCHOR_WIDTH - DEFAULT_ANCHOR_ATTACH_DEPTH)
15
+ : zoneRect.x + zoneRect.width - DEFAULT_ANCHOR_ATTACH_DEPTH;
16
+ return {
17
+ x,
18
+ y: zoneRect.y,
19
+ width: DEFAULT_ANCHOR_WIDTH,
20
+ height: zoneRect.height,
21
+ };
22
+ }
@@ -0,0 +1,2 @@
1
+ import type { ComponentLayoutEngine } from "../types";
2
+ export declare const defaultComponentLayoutEngine: ComponentLayoutEngine;
@@ -0,0 +1,200 @@
1
+ const ZONE_PADDING_X = 12;
2
+ const ZONE_PADDING_Y = 10;
3
+ const ZONE_GAP_Y = 6;
4
+ const ZONE_TITLE_HEIGHT = 24;
5
+ const ZONE_TYPE_HEIGHT = 18;
6
+ const ZONE_BADGE_HEIGHT = 20;
7
+ const ZONE_BODY_MIN_HEIGHT = 28;
8
+ const ZONE_FOOTER_HEIGHT = 18;
9
+ const PATH_PADDING_X = 8;
10
+ const PATH_PADDING_Y = 6;
11
+ const PATH_GAP_Y = 4;
12
+ const PATH_LABEL_HEIGHT = 18;
13
+ const PATH_RULE_HEIGHT = 16;
14
+ const PATH_TARGET_HEIGHT = 16;
15
+ const PATH_BODY_MIN_HEIGHT = 20;
16
+ function insetRect(rect, insetX, insetY) {
17
+ return {
18
+ x: rect.x + insetX,
19
+ y: rect.y + insetY,
20
+ width: Math.max(0, rect.width - insetX * 2),
21
+ height: Math.max(0, rect.height - insetY * 2),
22
+ };
23
+ }
24
+ function takeTop(rect, height) {
25
+ const safeHeight = Math.max(0, Math.min(height, rect.height));
26
+ return {
27
+ slot: {
28
+ x: rect.x,
29
+ y: rect.y,
30
+ width: rect.width,
31
+ height: safeHeight,
32
+ },
33
+ rest: {
34
+ x: rect.x,
35
+ y: rect.y + safeHeight,
36
+ width: rect.width,
37
+ height: Math.max(0, rect.height - safeHeight),
38
+ },
39
+ };
40
+ }
41
+ function takeBottom(rect, height) {
42
+ const safeHeight = Math.max(0, Math.min(height, rect.height));
43
+ return {
44
+ slot: {
45
+ x: rect.x,
46
+ y: rect.y + rect.height - safeHeight,
47
+ width: rect.width,
48
+ height: safeHeight,
49
+ },
50
+ rest: {
51
+ x: rect.x,
52
+ y: rect.y,
53
+ width: rect.width,
54
+ height: Math.max(0, rect.height - safeHeight),
55
+ },
56
+ };
57
+ }
58
+ function addTopGap(rect, gap) {
59
+ return {
60
+ x: rect.x,
61
+ y: rect.y + Math.min(gap, rect.height),
62
+ width: rect.width,
63
+ height: Math.max(0, rect.height - gap),
64
+ };
65
+ }
66
+ function shouldRenderZoneSlot(density, slot) {
67
+ switch (density) {
68
+ case "detail":
69
+ return true;
70
+ case "near":
71
+ return slot === "title" || slot === "type" || slot === "badge";
72
+ case "mid":
73
+ return slot === "title";
74
+ case "far":
75
+ default:
76
+ return false;
77
+ }
78
+ }
79
+ function shouldRenderPathSlot(density, slot) {
80
+ switch (density) {
81
+ case "full":
82
+ return true;
83
+ case "chip":
84
+ return slot === "label";
85
+ case "edge-only":
86
+ case "hidden":
87
+ default:
88
+ return false;
89
+ }
90
+ }
91
+ function computeZoneSlots(params) {
92
+ const { rect, density } = params;
93
+ const slots = {};
94
+ let content = insetRect(rect, ZONE_PADDING_X, ZONE_PADDING_Y);
95
+ if (shouldRenderZoneSlot(density, "title") && content.height > 0) {
96
+ const { slot, rest } = takeTop(content, ZONE_TITLE_HEIGHT);
97
+ slots.title = slot;
98
+ content = addTopGap(rest, ZONE_GAP_Y);
99
+ }
100
+ if (shouldRenderZoneSlot(density, "type") && content.height > 0) {
101
+ const { slot, rest } = takeTop(content, ZONE_TYPE_HEIGHT);
102
+ slots.type = slot;
103
+ content = addTopGap(rest, ZONE_GAP_Y);
104
+ }
105
+ if (shouldRenderZoneSlot(density, "badge") && content.height > 0) {
106
+ const badgeWidth = Math.min(96, content.width);
107
+ slots.badge = {
108
+ x: content.x,
109
+ y: content.y,
110
+ width: badgeWidth,
111
+ height: Math.min(ZONE_BADGE_HEIGHT, content.height),
112
+ };
113
+ content = addTopGap({
114
+ x: content.x,
115
+ y: content.y + Math.min(ZONE_BADGE_HEIGHT, content.height),
116
+ width: content.width,
117
+ height: Math.max(0, content.height - ZONE_BADGE_HEIGHT),
118
+ }, ZONE_GAP_Y);
119
+ }
120
+ if (shouldRenderZoneSlot(density, "footer") && content.height > 0) {
121
+ const { slot, rest } = takeBottom(content, ZONE_FOOTER_HEIGHT);
122
+ slots.footer = slot;
123
+ content = rest;
124
+ }
125
+ if (shouldRenderZoneSlot(density, "body") &&
126
+ content.width > 0 &&
127
+ content.height >= ZONE_BODY_MIN_HEIGHT) {
128
+ slots.body = content;
129
+ }
130
+ return slots;
131
+ }
132
+ function computePathSlots(params) {
133
+ const { rect, density } = params;
134
+ const slots = {};
135
+ let content = insetRect(rect, PATH_PADDING_X, PATH_PADDING_Y);
136
+ if (shouldRenderPathSlot(density, "label") && content.height > 0) {
137
+ const { slot, rest } = takeTop(content, PATH_LABEL_HEIGHT);
138
+ slots.label = slot;
139
+ content = addTopGap(rest, PATH_GAP_Y);
140
+ }
141
+ if (shouldRenderPathSlot(density, "rule") && content.height > 0) {
142
+ const { slot, rest } = takeTop(content, PATH_RULE_HEIGHT);
143
+ slots.rule = slot;
144
+ content = addTopGap(rest, PATH_GAP_Y);
145
+ }
146
+ if (shouldRenderPathSlot(density, "target") && content.height > 0) {
147
+ const { slot, rest } = takeBottom(content, PATH_TARGET_HEIGHT);
148
+ slots.target = slot;
149
+ content = rest;
150
+ }
151
+ if (shouldRenderPathSlot(density, "body") &&
152
+ content.width > 0 &&
153
+ content.height >= PATH_BODY_MIN_HEIGHT) {
154
+ slots.body = content;
155
+ }
156
+ return slots;
157
+ }
158
+ export const defaultComponentLayoutEngine = {
159
+ compute(input) {
160
+ const { graphLayout, density, visibility } = input;
161
+ const zonesById = Object.fromEntries(Object.values(graphLayout.zonesById).map((zone) => {
162
+ const zoneVisibility = visibility.zoneVisibilityById[zone.zoneId];
163
+ const zoneDensity = density.zoneDensityById[zone.zoneId];
164
+ const slots = zoneVisibility?.shouldRenderBody !== false
165
+ ? computeZoneSlots({
166
+ rect: zone.rect,
167
+ density: zoneDensity,
168
+ })
169
+ : {};
170
+ return [
171
+ zone.zoneId,
172
+ {
173
+ zoneId: zone.zoneId,
174
+ slots,
175
+ },
176
+ ];
177
+ }));
178
+ const pathsById = Object.fromEntries(Object.values(graphLayout.pathsById).map((path) => {
179
+ const pathVisibility = visibility.pathVisibilityById[path.pathId];
180
+ const pathDensity = density.pathDensityById[path.pathId];
181
+ const slots = path.rect && pathVisibility?.shouldRenderNode
182
+ ? computePathSlots({
183
+ rect: path.rect,
184
+ density: pathDensity,
185
+ })
186
+ : {};
187
+ return [
188
+ path.pathId,
189
+ {
190
+ pathId: path.pathId,
191
+ slots,
192
+ },
193
+ ];
194
+ }));
195
+ return {
196
+ zonesById,
197
+ pathsById,
198
+ };
199
+ },
200
+ };
@@ -0,0 +1,7 @@
1
+ import type { DebugLayer, RendererDrawInput } from "../types";
2
+ export type DebugDrawInput = RendererDrawInput & {
3
+ layers?: DebugLayer[];
4
+ };
5
+ export declare const debugDrawEngine: {
6
+ draw(input: DebugDrawInput): void;
7
+ };
@@ -0,0 +1,346 @@
1
+ import { isZoneInputEnabled, isZoneOutputEnabled, } from "@zoneflow/core";
2
+ const ANCHOR_SIZE = 8;
3
+ const DEFAULT_DEBUG_LAYERS = [
4
+ "graph-layout",
5
+ "edges",
6
+ "anchors",
7
+ ];
8
+ const drawLayerMap = {
9
+ "graph-layout": drawGraphLayout,
10
+ density: drawDensity,
11
+ visibility: drawVisibility,
12
+ "component-layout": drawComponentLayout,
13
+ edges: drawEdges,
14
+ anchors: drawAnchors,
15
+ viewport: drawViewport,
16
+ };
17
+ function createSvgElement(tag) {
18
+ return document.createElementNS("http://www.w3.org/2000/svg", tag);
19
+ }
20
+ function sortZoneVisualsForRender(pipeline) {
21
+ function getDepth(zoneId) {
22
+ let depth = 0;
23
+ let current = pipeline.graphLayout.zonesById[zoneId]?.zone;
24
+ while (current?.parentZoneId) {
25
+ depth += 1;
26
+ current = pipeline.graphLayout.zonesById[current.parentZoneId]?.zone;
27
+ }
28
+ return depth;
29
+ }
30
+ return Object.values(pipeline.graphLayout.zonesById)
31
+ .map((zone, index) => ({
32
+ zone,
33
+ index,
34
+ depth: getDepth(zone.zoneId),
35
+ }))
36
+ .sort((a, b) => a.depth - b.depth || a.index - b.index)
37
+ .map((entry) => entry.zone);
38
+ }
39
+ function getEdgeColor(kind) {
40
+ return kind === "zone-to-path" ? "#2563eb" : "#0f766e";
41
+ }
42
+ function getBezierCurvePathD(params) {
43
+ const { source, target } = params;
44
+ const sourceLead = Math.min(Math.max(Math.abs(target.x - source.x) * 0.18, 18), 42);
45
+ const leadSourceX = source.x + sourceLead;
46
+ const dx = target.x - leadSourceX;
47
+ const direction = dx >= 0 ? 1 : -1;
48
+ const handle = Math.min(Math.max(Math.abs(dx) * 0.45, 28), 104);
49
+ const control1X = leadSourceX + handle * direction;
50
+ const control2X = target.x - handle * direction;
51
+ return `M ${source.x} ${source.y} L ${leadSourceX} ${source.y} C ${control1X} ${source.y}, ${control2X} ${target.y}, ${target.x} ${target.y}`;
52
+ }
53
+ function getChevronPathD(params) {
54
+ const { target, direction } = params;
55
+ const tipX = target.x - direction * 6;
56
+ const baseX = tipX - direction * 7;
57
+ const topY = target.y - 4;
58
+ const bottomY = target.y + 4;
59
+ return `M ${baseX} ${topY} L ${tipX} ${target.y} L ${baseX} ${bottomY}`;
60
+ }
61
+ function filterPipelineForExclusion(input) {
62
+ const excludedZoneIds = new Set(input.exclusionState?.excludedZoneIds ?? []);
63
+ const excludedPathIds = new Set(input.exclusionState?.excludedPathIds ?? []);
64
+ if (excludedZoneIds.size === 0 && excludedPathIds.size === 0) {
65
+ return input.pipeline;
66
+ }
67
+ return {
68
+ ...input.pipeline,
69
+ graphLayout: {
70
+ ...input.pipeline.graphLayout,
71
+ zonesById: Object.fromEntries(Object.entries(input.pipeline.graphLayout.zonesById).filter(([zoneId]) => !excludedZoneIds.has(zoneId))),
72
+ pathsById: Object.fromEntries(Object.entries(input.pipeline.graphLayout.pathsById).filter(([pathId]) => !excludedPathIds.has(pathId))),
73
+ },
74
+ density: {
75
+ ...input.pipeline.density,
76
+ zoneDensityById: Object.fromEntries(Object.entries(input.pipeline.density.zoneDensityById).filter(([zoneId]) => !excludedZoneIds.has(zoneId))),
77
+ pathDensityById: Object.fromEntries(Object.entries(input.pipeline.density.pathDensityById).filter(([pathId]) => !excludedPathIds.has(pathId))),
78
+ },
79
+ visibility: {
80
+ ...input.pipeline.visibility,
81
+ zoneVisibilityById: Object.fromEntries(Object.entries(input.pipeline.visibility.zoneVisibilityById).filter(([zoneId]) => !excludedZoneIds.has(zoneId))),
82
+ pathVisibilityById: Object.fromEntries(Object.entries(input.pipeline.visibility.pathVisibilityById).filter(([pathId]) => !excludedPathIds.has(pathId))),
83
+ },
84
+ componentLayout: {
85
+ ...input.pipeline.componentLayout,
86
+ zonesById: Object.fromEntries(Object.entries(input.pipeline.componentLayout.zonesById).filter(([zoneId]) => !excludedZoneIds.has(zoneId))),
87
+ pathsById: Object.fromEntries(Object.entries(input.pipeline.componentLayout.pathsById).filter(([pathId]) => !excludedPathIds.has(pathId))),
88
+ },
89
+ };
90
+ }
91
+ export const debugDrawEngine = {
92
+ draw(input) {
93
+ const { host, camera } = input;
94
+ const pipeline = filterPipelineForExclusion(input);
95
+ const layers = input.layers ?? DEFAULT_DEBUG_LAYERS;
96
+ host.innerHTML = "";
97
+ const worldRoot = document.createElement("div");
98
+ worldRoot.style.position = "absolute";
99
+ worldRoot.style.left = "0";
100
+ worldRoot.style.top = "0";
101
+ worldRoot.style.width = "100%";
102
+ worldRoot.style.height = "100%";
103
+ worldRoot.style.transform = `translate(${camera.x}px, ${camera.y}px) scale(${camera.zoom})`;
104
+ worldRoot.style.transformOrigin = "0 0";
105
+ worldRoot.style.pointerEvents = "none";
106
+ const screenRoot = document.createElement("div");
107
+ screenRoot.style.position = "absolute";
108
+ screenRoot.style.left = "0";
109
+ screenRoot.style.top = "0";
110
+ screenRoot.style.width = "100%";
111
+ screenRoot.style.height = "100%";
112
+ screenRoot.style.pointerEvents = "none";
113
+ host.appendChild(worldRoot);
114
+ host.appendChild(screenRoot);
115
+ layers.forEach((layer) => {
116
+ drawLayerMap[layer]?.(worldRoot, pipeline, camera, pipeline.viewportInfo.world);
117
+ });
118
+ if (layers.includes("viewport")) {
119
+ drawHostViewport(screenRoot, pipeline.viewportInfo);
120
+ }
121
+ },
122
+ };
123
+ function getDebugFontSize(camera) {
124
+ return Math.min(24, Math.max(12, 18 / camera.zoom));
125
+ }
126
+ function drawBox(root, rect, color, label, camera) {
127
+ const el = document.createElement("div");
128
+ el.style.position = "absolute";
129
+ el.style.left = `${rect.x}px`;
130
+ el.style.top = `${rect.y}px`;
131
+ el.style.width = `${rect.width}px`;
132
+ el.style.height = `${rect.height}px`;
133
+ el.style.border = `1px dashed ${color}`;
134
+ el.style.boxSizing = "border-box";
135
+ el.style.pointerEvents = "none";
136
+ if (label) {
137
+ const text = document.createElement("div");
138
+ const fontSize = getDebugFontSize(camera);
139
+ text.textContent = String(label);
140
+ text.style.fontSize = `${fontSize}px`;
141
+ text.style.color = color;
142
+ text.style.position = "absolute";
143
+ text.style.left = "2px";
144
+ text.style.top = "2px";
145
+ text.style.pointerEvents = "none";
146
+ text.style.background = "rgba(0,0,0,0.6)";
147
+ text.style.padding = "1px 3px";
148
+ text.style.borderRadius = "3px";
149
+ text.style.fontWeight = "600";
150
+ el.appendChild(text);
151
+ }
152
+ root.appendChild(el);
153
+ }
154
+ function drawAnchor(root, point, color, label, camera) {
155
+ const dot = document.createElement("div");
156
+ dot.style.position = "absolute";
157
+ dot.style.left = `${point.x - ANCHOR_SIZE / 2}px`;
158
+ dot.style.top = `${point.y - ANCHOR_SIZE / 2}px`;
159
+ dot.style.width = `${ANCHOR_SIZE}px`;
160
+ dot.style.height = `${ANCHOR_SIZE}px`;
161
+ dot.style.borderRadius = "999px";
162
+ dot.style.background = color;
163
+ dot.style.border = "1px solid white";
164
+ dot.style.boxSizing = "border-box";
165
+ dot.style.pointerEvents = "none";
166
+ if (label) {
167
+ const text = document.createElement("div");
168
+ const fontSize = getDebugFontSize(camera);
169
+ text.textContent = label;
170
+ text.style.position = "absolute";
171
+ text.style.left = `${point.x + 6}px`;
172
+ text.style.top = `${point.y - 6}px`;
173
+ text.style.fontSize = `${fontSize}px`;
174
+ text.style.color = color;
175
+ text.style.whiteSpace = "nowrap";
176
+ text.style.pointerEvents = "none";
177
+ text.style.background = "rgba(0,0,0,0.6)";
178
+ text.style.padding = "1px 3px";
179
+ text.style.borderRadius = "3px";
180
+ text.style.fontWeight = "600";
181
+ root.appendChild(text);
182
+ }
183
+ root.appendChild(dot);
184
+ }
185
+ function drawGraphLayout(root, pipeline, camera) {
186
+ const { graphLayout } = pipeline;
187
+ sortZoneVisualsForRender(pipeline).forEach((zone) => {
188
+ drawBox(root, zone.rect, "blue", zone.zone.name, camera);
189
+ });
190
+ Object.values(graphLayout.pathsById).forEach((path) => {
191
+ if (path.rect) {
192
+ drawBox(root, path.rect, "green", path.path.name, camera);
193
+ }
194
+ });
195
+ }
196
+ function drawDensity(root, pipeline, camera) {
197
+ const { graphLayout, density } = pipeline;
198
+ Object.values(graphLayout.zonesById).forEach((zone) => {
199
+ const level = density.zoneDensityById[zone.zoneId];
200
+ drawBox(root, zone.rect, "purple", level, camera);
201
+ });
202
+ Object.values(graphLayout.pathsById).forEach((path) => {
203
+ if (!path.rect)
204
+ return;
205
+ const level = density.pathDensityById[path.pathId];
206
+ drawBox(root, path.rect, "magenta", level, camera);
207
+ });
208
+ }
209
+ function drawVisibility(root, pipeline, camera) {
210
+ const { graphLayout, visibility } = pipeline;
211
+ Object.values(graphLayout.zonesById).forEach((zone) => {
212
+ const v = visibility.zoneVisibilityById[zone.zoneId];
213
+ if (!v)
214
+ return;
215
+ const color = !v.isVisible
216
+ ? "rgba(148,163,184,0.45)"
217
+ : v.isPartial
218
+ ? "#f59e0b"
219
+ : "#f97316";
220
+ const label = !v.isVisible
221
+ ? `culled / ${v.emphasis}`
222
+ : v.isPartial
223
+ ? `partial / ${v.emphasis}`
224
+ : `visible / ${v.emphasis}`;
225
+ drawBox(root, zone.rect, color, label, camera);
226
+ });
227
+ Object.values(graphLayout.pathsById).forEach((path) => {
228
+ if (!path.rect)
229
+ return;
230
+ const v = visibility.pathVisibilityById[path.pathId];
231
+ if (!v)
232
+ return;
233
+ const color = !v.isVisible
234
+ ? "rgba(100,100,100,0.35)"
235
+ : v.isPartial
236
+ ? "#d97706"
237
+ : "#ca8a04";
238
+ const label = !v.isVisible
239
+ ? `culled / ${v.emphasis}`
240
+ : v.shouldRenderNode
241
+ ? `node / ${v.emphasis}`
242
+ : `edge-only / ${v.emphasis}`;
243
+ drawBox(root, path.rect, color, label, camera);
244
+ });
245
+ }
246
+ function drawComponentLayout(root, pipeline, camera) {
247
+ const { componentLayout } = pipeline;
248
+ Object.values(componentLayout.zonesById).forEach((zone) => {
249
+ Object.entries(zone.slots).forEach(([name, rect]) => {
250
+ drawBox(root, rect, "red", name, camera);
251
+ });
252
+ });
253
+ Object.values(componentLayout.pathsById ?? {}).forEach((path) => {
254
+ Object.entries(path.slots).forEach(([name, rect]) => {
255
+ drawBox(root, rect, "brown", `path:${name}`, camera);
256
+ });
257
+ });
258
+ }
259
+ function drawEdges(root, pipeline) {
260
+ const { edgesByPathId } = pipeline.graphLayout;
261
+ const svg = createSvgElement("svg");
262
+ svg.style.position = "absolute";
263
+ svg.style.left = "0";
264
+ svg.style.top = "0";
265
+ svg.style.width = "100%";
266
+ svg.style.height = "100%";
267
+ svg.style.overflow = "visible";
268
+ svg.style.pointerEvents = "none";
269
+ Object.values(edgesByPathId)
270
+ .flatMap((edges) => edges)
271
+ .forEach((edge) => {
272
+ const path = createSvgElement("path");
273
+ const stroke = getEdgeColor(edge.kind);
274
+ path.setAttribute("d", getBezierCurvePathD({
275
+ source: edge.source,
276
+ target: edge.target,
277
+ }));
278
+ path.setAttribute("fill", "none");
279
+ path.setAttribute("stroke", stroke);
280
+ path.setAttribute("stroke-width", edge.kind === "zone-to-path" ? "2" : "2.4");
281
+ path.setAttribute("stroke-linecap", "round");
282
+ path.setAttribute("stroke-linejoin", "round");
283
+ svg.appendChild(path);
284
+ const chevron = createSvgElement("path");
285
+ chevron.setAttribute("d", getChevronPathD({
286
+ target: edge.target,
287
+ direction: edge.kind === "path-to-zone" ? 1 : edge.target.x >= edge.source.x ? 1 : -1,
288
+ }));
289
+ chevron.setAttribute("fill", "none");
290
+ chevron.setAttribute("stroke", stroke);
291
+ chevron.setAttribute("stroke-width", edge.kind === "zone-to-path" ? "1.7" : "1.95");
292
+ chevron.setAttribute("stroke-linecap", "round");
293
+ chevron.setAttribute("stroke-linejoin", "round");
294
+ svg.appendChild(chevron);
295
+ });
296
+ root.appendChild(svg);
297
+ }
298
+ function drawAnchors(root, pipeline, camera) {
299
+ const { graphLayout } = pipeline;
300
+ Object.values(graphLayout.zonesById).forEach((zone) => {
301
+ const inlet = zone.anchors?.inlet?.point;
302
+ const outlet = zone.anchors?.outlet?.point;
303
+ if (inlet && isZoneInputEnabled(zone.zone)) {
304
+ drawAnchor(root, inlet, "#2563eb", `${zone.zoneId}:in`, camera);
305
+ }
306
+ if (outlet && isZoneOutputEnabled(zone.zone)) {
307
+ drawAnchor(root, outlet, "#dc2626", `${zone.zoneId}:out`, camera);
308
+ }
309
+ });
310
+ Object.values(graphLayout.pathsById).forEach((path) => {
311
+ if (path.inlet) {
312
+ drawAnchor(root, path.inlet, "#16a34a", `${path.pathId}:in`, camera);
313
+ }
314
+ if (path.outlet) {
315
+ drawAnchor(root, path.outlet, "#ca8a04", `${path.pathId}:out`, camera);
316
+ }
317
+ });
318
+ }
319
+ function drawViewport(root, _pipeline, camera, viewport) {
320
+ drawBox(root, viewport, "#22c55e", "viewport", camera);
321
+ }
322
+ function drawHostViewport(root, viewportInfo) {
323
+ const box = document.createElement("div");
324
+ const { effective, host } = viewportInfo;
325
+ // 실제 host viewport 전체
326
+ const hostBox = document.createElement("div");
327
+ hostBox.style.position = "absolute";
328
+ hostBox.style.left = `${host.x}px`;
329
+ hostBox.style.top = `${host.y}px`;
330
+ hostBox.style.width = `${host.width}px`;
331
+ hostBox.style.height = `${host.height}px`;
332
+ hostBox.style.border = "1px dashed rgba(59,130,246,0.9)";
333
+ hostBox.style.boxSizing = "border-box";
334
+ hostBox.style.pointerEvents = "none";
335
+ // effective viewport
336
+ box.style.position = "absolute";
337
+ box.style.left = `${effective.x}px`;
338
+ box.style.top = `${effective.y}px`;
339
+ box.style.width = `${effective.width}px`;
340
+ box.style.height = `${effective.height}px`;
341
+ box.style.border = "2px solid cyan";
342
+ box.style.boxSizing = "border-box";
343
+ box.style.pointerEvents = "none";
344
+ root.appendChild(hostBox);
345
+ root.appendChild(box);
346
+ }
@@ -0,0 +1,2 @@
1
+ import type { DensityEngine } from "../types";
2
+ export declare const defaultDensityEngine: DensityEngine;
@@ -0,0 +1,47 @@
1
+ function getZoneDensity(size, thresholds) {
2
+ if (size >= thresholds.detail)
3
+ return "detail";
4
+ if (size >= thresholds.near)
5
+ return "near";
6
+ if (size >= thresholds.mid)
7
+ return "mid";
8
+ return "far";
9
+ }
10
+ function getPathDensity(size, thresholds) {
11
+ if (size >= thresholds.full)
12
+ return "full";
13
+ if (size >= thresholds.chip)
14
+ return "chip";
15
+ return "edge-only";
16
+ }
17
+ export const defaultDensityEngine = {
18
+ compute(input) {
19
+ const { graphLayout, base } = input;
20
+ const zoom = base.camera.zoom;
21
+ const theme = base.theme;
22
+ const zoneDensityById = {};
23
+ const pathDensityById = {};
24
+ const zoneThresholds = theme.density.zone;
25
+ const pathThresholds = theme.density.path;
26
+ // --- zone ---
27
+ Object.values(graphLayout.zonesById).forEach((zone) => {
28
+ const rect = zone.rect;
29
+ const size = Math.max(rect.width, rect.height) * zoom;
30
+ zoneDensityById[zone.zoneId] = getZoneDensity(size, zoneThresholds);
31
+ });
32
+ // --- path ---
33
+ Object.values(graphLayout.pathsById).forEach((path) => {
34
+ const rect = path.rect;
35
+ if (!rect) {
36
+ pathDensityById[path.pathId] = "edge-only";
37
+ return;
38
+ }
39
+ const size = Math.max(rect.width, rect.height) * zoom;
40
+ pathDensityById[path.pathId] = getPathDensity(size, pathThresholds);
41
+ });
42
+ return {
43
+ zoneDensityById,
44
+ pathDensityById,
45
+ };
46
+ },
47
+ };
@@ -0,0 +1,2 @@
1
+ import type { DrawEngine } from "../types";
2
+ export declare const domDrawEngine: DrawEngine;