@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.
- package/dist/anchors.d.ts +13 -0
- package/dist/anchors.js +22 -0
- package/dist/engines/componentLayoutEngine.d.ts +2 -0
- package/dist/engines/componentLayoutEngine.js +200 -0
- package/dist/engines/debugDrawEngine.d.ts +7 -0
- package/dist/engines/debugDrawEngine.js +346 -0
- package/dist/engines/densityEngine.d.ts +2 -0
- package/dist/engines/densityEngine.js +47 -0
- package/dist/engines/drawEngine.d.ts +2 -0
- package/dist/engines/drawEngine.js +681 -0
- package/dist/engines/graphLayoutEngine.d.ts +6 -0
- package/dist/engines/graphLayoutEngine.js +255 -0
- package/dist/engines/visibilityEngine.d.ts +2 -0
- package/dist/engines/visibilityEngine.js +76 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/pipeline.d.ts +8 -0
- package/dist/pipeline.js +25 -0
- package/dist/renderer.d.ts +2 -0
- package/dist/renderer.js +137 -0
- package/dist/theme.d.ts +23 -0
- package/dist/theme.js +1 -0
- package/dist/themes/defaultTheme.d.ts +9 -0
- package/dist/themes/defaultTheme.js +46 -0
- package/dist/types.d.ts +253 -0
- package/dist/types.js +1 -0
- package/package.json +23 -0
|
@@ -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 {};
|
package/dist/anchors.js
ADDED
|
@@ -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,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,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,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
|
+
};
|