@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,255 @@
1
+ export const DEFAULT_PATH_NODE_WIDTH = 120;
2
+ export const DEFAULT_PATH_NODE_HEIGHT = 32;
3
+ export const DEFAULT_PATH_NODE_OFFSET_X = 32;
4
+ export const DEFAULT_PATH_NODE_GAP_Y = 40;
5
+ function typedEntries(record) {
6
+ return Object.entries(record);
7
+ }
8
+ function typedValues(record) {
9
+ return Object.values(record);
10
+ }
11
+ function rectFromLayout(layout) {
12
+ return {
13
+ x: layout.x,
14
+ y: layout.y,
15
+ width: layout.width ?? 0,
16
+ height: layout.height ?? 0,
17
+ };
18
+ }
19
+ function centerOfRect(rect) {
20
+ return {
21
+ x: rect.x + rect.width / 2,
22
+ y: rect.y + rect.height / 2,
23
+ };
24
+ }
25
+ function resolveAnchorRect(worldPos, rect) {
26
+ if (!rect)
27
+ return undefined;
28
+ return {
29
+ ...rect,
30
+ x: worldPos.x + rect.x,
31
+ y: worldPos.y + rect.y,
32
+ };
33
+ }
34
+ /**
35
+ * relative layout -> absolute layout
36
+ */
37
+ function resolveLayout(model, layoutModel) {
38
+ const resolvedZoneLayouts = {};
39
+ const resolvedPathLayouts = {};
40
+ const zoneCache = new Map();
41
+ function resolveZonePosition(zoneId) {
42
+ if (zoneCache.has(zoneId)) {
43
+ return zoneCache.get(zoneId);
44
+ }
45
+ const zone = model.zonesById[zoneId];
46
+ const layout = layoutModel.zoneLayoutsById[zoneId];
47
+ if (!zone || !layout) {
48
+ const fallback = { x: 0, y: 0 };
49
+ zoneCache.set(zoneId, fallback);
50
+ return fallback;
51
+ }
52
+ if (!zone.parentZoneId) {
53
+ const rootPos = { x: layout.x, y: layout.y };
54
+ zoneCache.set(zoneId, rootPos);
55
+ return rootPos;
56
+ }
57
+ const parentPos = resolveZonePosition(zone.parentZoneId);
58
+ const worldPos = {
59
+ x: parentPos.x + layout.x,
60
+ y: parentPos.y + layout.y,
61
+ };
62
+ zoneCache.set(zoneId, worldPos);
63
+ return worldPos;
64
+ }
65
+ for (const [typedZoneId, layout] of typedEntries(layoutModel.zoneLayoutsById)) {
66
+ const worldPos = resolveZonePosition(typedZoneId);
67
+ const anchors = layout.anchors;
68
+ const resolvedAnchors = {
69
+ inlet: {
70
+ point: {
71
+ x: worldPos.x + anchors.inlet.point.x,
72
+ y: worldPos.y + anchors.inlet.point.y,
73
+ },
74
+ rect: resolveAnchorRect(worldPos, anchors.inlet.rect),
75
+ },
76
+ outlet: {
77
+ point: {
78
+ x: worldPos.x + anchors.outlet.point.x,
79
+ y: worldPos.y + anchors.outlet.point.y,
80
+ },
81
+ rect: resolveAnchorRect(worldPos, anchors.outlet.rect),
82
+ },
83
+ };
84
+ resolvedZoneLayouts[typedZoneId] = {
85
+ ...layout,
86
+ x: worldPos.x,
87
+ y: worldPos.y,
88
+ anchors: resolvedAnchors,
89
+ };
90
+ }
91
+ for (const [pathId, pathLayout] of typedEntries(layoutModel.pathLayoutsById)) {
92
+ resolvedPathLayouts[pathId] = {
93
+ ...pathLayout,
94
+ };
95
+ }
96
+ return {
97
+ ...layoutModel,
98
+ zoneLayoutsById: resolvedZoneLayouts,
99
+ pathLayoutsById: resolvedPathLayouts,
100
+ };
101
+ }
102
+ function resolvePathNodeRect(params) {
103
+ const { layoutModel, pathId, sourceOutlet, fallbackIndex } = params;
104
+ const pathLayout = layoutModel.pathLayoutsById[pathId];
105
+ const preferredComponentLayout = pathLayout?.componentLayoutsById?.body ??
106
+ pathLayout?.componentLayoutsById?.label;
107
+ if (preferredComponentLayout) {
108
+ return rectFromLayout(preferredComponentLayout);
109
+ }
110
+ const routeOffset = pathLayout?.routeOffset;
111
+ return {
112
+ x: sourceOutlet.x + DEFAULT_PATH_NODE_OFFSET_X + (routeOffset?.x ?? 0),
113
+ y: sourceOutlet.y -
114
+ DEFAULT_PATH_NODE_HEIGHT / 2 +
115
+ fallbackIndex * DEFAULT_PATH_NODE_GAP_Y +
116
+ (routeOffset?.y ?? 0),
117
+ width: DEFAULT_PATH_NODE_WIDTH,
118
+ height: DEFAULT_PATH_NODE_HEIGHT,
119
+ };
120
+ }
121
+ function resolvePathNodeAnchors(rect) {
122
+ return {
123
+ inlet: {
124
+ x: rect.x,
125
+ y: rect.y + rect.height / 2,
126
+ },
127
+ outlet: {
128
+ x: rect.x + rect.width,
129
+ y: rect.y + rect.height / 2,
130
+ },
131
+ };
132
+ }
133
+ function createZoneVisualNodes(params) {
134
+ const { model, layoutModel } = params;
135
+ const result = {};
136
+ for (const [typedZoneId, zone] of typedEntries(model.zonesById)) {
137
+ const zoneLayout = layoutModel.zoneLayoutsById[typedZoneId];
138
+ if (!zoneLayout)
139
+ continue;
140
+ result[typedZoneId] = {
141
+ universeId: model.universeId,
142
+ zoneId: typedZoneId,
143
+ zone,
144
+ rect: rectFromLayout(zoneLayout),
145
+ anchors: zoneLayout.anchors,
146
+ };
147
+ }
148
+ return result;
149
+ }
150
+ function createPathVisualNodes(params) {
151
+ const { model, layoutModel, zonesById } = params;
152
+ const result = {};
153
+ for (const zone of typedValues(model.zonesById)) {
154
+ const sourceZoneVisual = zonesById[zone.id];
155
+ if (!sourceZoneVisual)
156
+ continue;
157
+ zone.pathIds.forEach((pathId, index) => {
158
+ const path = zone.pathsById[pathId];
159
+ if (!path)
160
+ return;
161
+ const targetZoneId = path.target?.universeId === model.universeId
162
+ ? path.target.zoneId
163
+ : null;
164
+ const sourceOutlet = sourceZoneVisual.anchors.outlet?.point ??
165
+ centerOfRect(sourceZoneVisual.rect);
166
+ const rect = resolvePathNodeRect({
167
+ layoutModel,
168
+ pathId,
169
+ sourceOutlet,
170
+ fallbackIndex: index,
171
+ });
172
+ const anchors = resolvePathNodeAnchors(rect);
173
+ result[pathId] = {
174
+ universeId: model.universeId,
175
+ pathId,
176
+ sourceZoneId: zone.id,
177
+ targetZoneId,
178
+ path,
179
+ rect,
180
+ inlet: anchors.inlet,
181
+ outlet: anchors.outlet,
182
+ };
183
+ });
184
+ }
185
+ return result;
186
+ }
187
+ function createEdgeVisuals(params) {
188
+ const { model, zonesById, pathsById } = params;
189
+ const result = {};
190
+ for (const zone of typedValues(model.zonesById)) {
191
+ const sourceZoneVisual = zonesById[zone.id];
192
+ if (!sourceZoneVisual)
193
+ continue;
194
+ zone.pathIds.forEach((pathId) => {
195
+ const pathVisual = pathsById[pathId];
196
+ if (!pathVisual)
197
+ return;
198
+ const targetZoneVisual = pathVisual.targetZoneId
199
+ ? zonesById[pathVisual.targetZoneId]
200
+ : undefined;
201
+ const zoneOutlet = sourceZoneVisual.anchors.outlet?.point ??
202
+ centerOfRect(sourceZoneVisual.rect);
203
+ const pathInlet = pathVisual.inlet ??
204
+ (pathVisual.rect ? centerOfRect(pathVisual.rect) : zoneOutlet);
205
+ const pathOutlet = pathVisual.outlet ??
206
+ (pathVisual.rect ? centerOfRect(pathVisual.rect) : zoneOutlet);
207
+ const targetInlet = targetZoneVisual
208
+ ? (targetZoneVisual.anchors.inlet?.point ??
209
+ centerOfRect(targetZoneVisual.rect))
210
+ : pathOutlet;
211
+ result[pathId] = [
212
+ {
213
+ id: `${pathId}:z2p`,
214
+ pathId,
215
+ kind: "zone-to-path",
216
+ source: zoneOutlet,
217
+ target: pathInlet,
218
+ },
219
+ {
220
+ id: `${pathId}:p2z`,
221
+ pathId,
222
+ kind: "path-to-zone",
223
+ source: pathOutlet,
224
+ target: targetInlet,
225
+ },
226
+ ];
227
+ });
228
+ }
229
+ return result;
230
+ }
231
+ export const defaultGraphLayoutEngine = {
232
+ compute(input) {
233
+ const { model, layoutModel } = input;
234
+ const resolvedLayout = resolveLayout(model, layoutModel);
235
+ const zonesById = createZoneVisualNodes({
236
+ model,
237
+ layoutModel: resolvedLayout,
238
+ });
239
+ const pathsById = createPathVisualNodes({
240
+ model,
241
+ layoutModel: resolvedLayout,
242
+ zonesById,
243
+ });
244
+ const edgesByPathId = createEdgeVisuals({
245
+ model,
246
+ zonesById,
247
+ pathsById,
248
+ });
249
+ return {
250
+ zonesById,
251
+ pathsById,
252
+ edgesByPathId,
253
+ };
254
+ },
255
+ };
@@ -0,0 +1,2 @@
1
+ import type { VisibilityEngine } from "../types";
2
+ export declare const defaultVisibilityEngine: VisibilityEngine;
@@ -0,0 +1,76 @@
1
+ function intersects(a, b) {
2
+ return !(a.x + a.width <= b.x ||
3
+ b.x + b.width <= a.x ||
4
+ a.y + a.height <= b.y ||
5
+ b.y + b.height <= a.y);
6
+ }
7
+ function containsFully(outer, inner) {
8
+ return (inner.x >= outer.x &&
9
+ inner.y >= outer.y &&
10
+ inner.x + inner.width <= outer.x + outer.width &&
11
+ inner.y + inner.height <= outer.y + outer.height);
12
+ }
13
+ function resolveZoneEmphasis(isVisible, isPartial) {
14
+ if (!isVisible)
15
+ return "hidden";
16
+ if (isPartial)
17
+ return "dim";
18
+ return "strong";
19
+ }
20
+ function resolvePathEmphasis(isVisible, isPartial) {
21
+ if (!isVisible)
22
+ return "hidden";
23
+ if (isPartial)
24
+ return "dim";
25
+ return "normal";
26
+ }
27
+ export const defaultVisibilityEngine = {
28
+ compute(input) {
29
+ const { base, graphLayout, density } = input;
30
+ const worldViewport = base.viewportInfo.world;
31
+ const zoneVisibilityById = {};
32
+ const pathVisibilityById = {};
33
+ Object.values(graphLayout.zonesById).forEach((zone) => {
34
+ const isVisible = intersects(worldViewport, zone.rect);
35
+ const isPartial = isVisible && !containsFully(worldViewport, zone.rect);
36
+ const emphasis = resolveZoneEmphasis(isVisible, isPartial);
37
+ zoneVisibilityById[zone.zoneId] = {
38
+ isVisible,
39
+ isPartial,
40
+ shouldRenderBody: isVisible,
41
+ shouldRenderContent: isVisible && zone.rect.width > 0 && zone.rect.height > 0,
42
+ emphasis,
43
+ };
44
+ });
45
+ Object.values(graphLayout.pathsById).forEach((path) => {
46
+ const rect = path.rect;
47
+ if (!rect) {
48
+ pathVisibilityById[path.pathId] = {
49
+ isVisible: false,
50
+ isPartial: false,
51
+ shouldRenderNode: false,
52
+ shouldRenderEdge: true,
53
+ shouldRenderLabel: false,
54
+ emphasis: "hidden",
55
+ };
56
+ return;
57
+ }
58
+ const isVisible = intersects(worldViewport, rect);
59
+ const isPartial = isVisible && !containsFully(worldViewport, rect);
60
+ const emphasis = resolvePathEmphasis(isVisible, isPartial);
61
+ const densityMode = density.pathDensityById[path.pathId];
62
+ pathVisibilityById[path.pathId] = {
63
+ isVisible,
64
+ isPartial,
65
+ shouldRenderNode: isVisible && densityMode !== "edge-only",
66
+ shouldRenderEdge: true,
67
+ shouldRenderLabel: isVisible && densityMode !== "edge-only",
68
+ emphasis,
69
+ };
70
+ });
71
+ return {
72
+ zoneVisibilityById,
73
+ pathVisibilityById,
74
+ };
75
+ },
76
+ };
@@ -0,0 +1,11 @@
1
+ export * from "./theme";
2
+ export * from "./types";
3
+ export * from "./anchors";
4
+ export * from "./pipeline";
5
+ export * from "./renderer";
6
+ export * from "./engines/graphLayoutEngine";
7
+ export * from "./engines/densityEngine";
8
+ export * from "./engines/visibilityEngine";
9
+ export * from "./engines/componentLayoutEngine";
10
+ export * from "./engines/drawEngine";
11
+ export * from "./engines/debugDrawEngine";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export * from "./theme";
2
+ export * from "./types";
3
+ export * from "./anchors";
4
+ export * from "./pipeline";
5
+ export * from "./renderer";
6
+ export * from "./engines/graphLayoutEngine";
7
+ export * from "./engines/densityEngine";
8
+ export * from "./engines/visibilityEngine";
9
+ export * from "./engines/componentLayoutEngine";
10
+ export * from "./engines/drawEngine";
11
+ export * from "./engines/debugDrawEngine";
@@ -0,0 +1,8 @@
1
+ import type { ComponentLayoutEngine, DensityEngine, GraphLayoutEngine, RenderPipelineInput, RenderPipelineResult, VisibilityEngine } from "./types";
2
+ export type RenderPipelineEngines = {
3
+ graphLayoutEngine: GraphLayoutEngine;
4
+ densityEngine: DensityEngine;
5
+ visibilityEngine: VisibilityEngine;
6
+ componentLayoutEngine: ComponentLayoutEngine;
7
+ };
8
+ export declare function runRenderPipeline(input: RenderPipelineInput, engines: RenderPipelineEngines): RenderPipelineResult;
@@ -0,0 +1,25 @@
1
+ export function runRenderPipeline(input, engines) {
2
+ const graphLayout = engines.graphLayoutEngine.compute(input);
3
+ const density = engines.densityEngine.compute({
4
+ base: input,
5
+ graphLayout,
6
+ });
7
+ const visibility = engines.visibilityEngine.compute({
8
+ base: input,
9
+ graphLayout,
10
+ density,
11
+ });
12
+ const componentLayout = engines.componentLayoutEngine.compute({
13
+ base: input,
14
+ graphLayout,
15
+ density,
16
+ visibility,
17
+ });
18
+ return {
19
+ viewportInfo: input.viewportInfo,
20
+ graphLayout,
21
+ density,
22
+ visibility,
23
+ componentLayout,
24
+ };
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { ZoneflowRenderer } from "./types";
2
+ export declare function createRenderer(): ZoneflowRenderer;
@@ -0,0 +1,137 @@
1
+ import { resolveTheme } from "./themes/defaultTheme";
2
+ import { runRenderPipeline } from "./pipeline";
3
+ import { defaultGraphLayoutEngine } from "./engines/graphLayoutEngine";
4
+ import { defaultDensityEngine } from "./engines/densityEngine";
5
+ import { defaultVisibilityEngine } from "./engines/visibilityEngine";
6
+ import { defaultComponentLayoutEngine } from "./engines/componentLayoutEngine";
7
+ import { domDrawEngine } from "./engines/drawEngine";
8
+ import { debugDrawEngine } from "./engines/debugDrawEngine";
9
+ const DEFAULT_CAMERA = {
10
+ x: 0,
11
+ y: 0,
12
+ zoom: 1,
13
+ };
14
+ function ensureHostBaseStyle(host) {
15
+ host.style.position = "relative";
16
+ host.style.overflow = "hidden";
17
+ }
18
+ function getHostViewport(host) {
19
+ return {
20
+ x: 0,
21
+ y: 0,
22
+ width: host.clientWidth,
23
+ height: host.clientHeight,
24
+ };
25
+ }
26
+ function getEffectiveViewport(hostViewport, viewport) {
27
+ if (viewport?.enabled) {
28
+ const offsetX = viewport.offsetX ?? 0;
29
+ const offsetY = viewport.offsetY ?? 0;
30
+ return {
31
+ x: offsetX,
32
+ y: offsetY,
33
+ width: viewport.width,
34
+ height: viewport.height,
35
+ };
36
+ }
37
+ return {
38
+ x: 0,
39
+ y: 0,
40
+ width: hostViewport.width,
41
+ height: hostViewport.height,
42
+ };
43
+ }
44
+ function getWorldViewport(camera, effectiveViewport) {
45
+ return {
46
+ x: (effectiveViewport.x - camera.x) / camera.zoom,
47
+ y: (effectiveViewport.y - camera.y) / camera.zoom,
48
+ width: effectiveViewport.width / camera.zoom,
49
+ height: effectiveViewport.height / camera.zoom,
50
+ };
51
+ }
52
+ function resolveViewportInfo(host, camera, input) {
53
+ const hostViewport = getHostViewport(host);
54
+ const effectiveViewport = getEffectiveViewport(hostViewport, input.viewport);
55
+ const worldViewport = getWorldViewport(camera, effectiveViewport);
56
+ return {
57
+ host: hostViewport,
58
+ effective: effectiveViewport,
59
+ world: worldViewport,
60
+ };
61
+ }
62
+ export function createRenderer() {
63
+ let host = null;
64
+ return {
65
+ mount(container) {
66
+ host = container;
67
+ ensureHostBaseStyle(host);
68
+ },
69
+ update(input) {
70
+ if (!host)
71
+ return;
72
+ const { model, layoutModel, theme, textScale = "md", camera = DEFAULT_CAMERA, graphLayoutEngine = defaultGraphLayoutEngine, densityEngine = defaultDensityEngine, visibilityEngine = defaultVisibilityEngine, componentLayoutEngine = defaultComponentLayoutEngine, drawEngine = domDrawEngine, zoneComponentRenderers, pathComponentRenderers, interactionHandlers, exclusionState, debug, } = input;
73
+ const mergedTheme = resolveTheme(theme);
74
+ const viewportInfo = resolveViewportInfo(host, camera, input);
75
+ const pipeline = runRenderPipeline({
76
+ model,
77
+ layoutModel,
78
+ camera,
79
+ viewportInfo,
80
+ theme: mergedTheme,
81
+ textScale,
82
+ }, {
83
+ graphLayoutEngine,
84
+ densityEngine,
85
+ visibilityEngine,
86
+ componentLayoutEngine,
87
+ });
88
+ if (debug?.enabled) {
89
+ debugDrawEngine.draw({
90
+ host,
91
+ model,
92
+ layoutModel,
93
+ camera,
94
+ viewportInfo,
95
+ theme: mergedTheme,
96
+ textScale,
97
+ pipeline,
98
+ exclusionState,
99
+ layers: debug.layers ?? ["graph-layout", "edges", "anchors"],
100
+ });
101
+ return {
102
+ viewportInfo,
103
+ pipeline,
104
+ mounts: {
105
+ zones: [],
106
+ paths: [],
107
+ },
108
+ };
109
+ }
110
+ const mounts = drawEngine.draw({
111
+ host,
112
+ model,
113
+ layoutModel,
114
+ camera,
115
+ viewportInfo,
116
+ theme: mergedTheme,
117
+ textScale,
118
+ pipeline,
119
+ zoneComponentRenderers,
120
+ pathComponentRenderers,
121
+ interactionHandlers,
122
+ exclusionState,
123
+ });
124
+ return {
125
+ viewportInfo,
126
+ pipeline,
127
+ mounts,
128
+ };
129
+ },
130
+ destroy() {
131
+ if (host) {
132
+ host.innerHTML = "";
133
+ }
134
+ host = null;
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,23 @@
1
+ export type ZoneflowTheme = {
2
+ background: string;
3
+ zoneTitle: string;
4
+ zoneSubtext: string;
5
+ zoneContainerBorder: string;
6
+ zoneActionBorder: string;
7
+ zoneBadgeBg: string;
8
+ pathLabel: string;
9
+ pathEdge: string;
10
+ selection: string;
11
+ density: {
12
+ zone: {
13
+ detail: number;
14
+ near: number;
15
+ mid: number;
16
+ };
17
+ path: {
18
+ full: number;
19
+ chip: number;
20
+ };
21
+ };
22
+ };
23
+ export type TextScaleLevel = "sm" | "md" | "lg";
package/dist/theme.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { ZoneflowTheme } from "../theme";
2
+ /**
3
+ * 기본 테마 (모든 필수 값 포함)
4
+ */
5
+ export declare const defaultTheme: ZoneflowTheme;
6
+ /**
7
+ * Partial theme을 받아서 완전한 theme으로 보정
8
+ */
9
+ export declare function resolveTheme(theme?: Partial<ZoneflowTheme>): ZoneflowTheme;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 기본 테마 (모든 필수 값 포함)
3
+ */
4
+ export const defaultTheme = {
5
+ background: "#f3f6fb",
6
+ zoneTitle: "#0f172a",
7
+ zoneSubtext: "#5f6f86",
8
+ zoneContainerBorder: "#cbd5e1",
9
+ zoneActionBorder: "#f59e0b",
10
+ zoneBadgeBg: "#e0f2fe",
11
+ pathLabel: "#1e293b",
12
+ pathEdge: "#7a8aa0",
13
+ selection: "#2e90fa",
14
+ density: {
15
+ zone: {
16
+ detail: 200,
17
+ near: 140,
18
+ mid: 90,
19
+ },
20
+ path: {
21
+ full: 120,
22
+ chip: 60,
23
+ },
24
+ },
25
+ };
26
+ /**
27
+ * Partial theme을 받아서 완전한 theme으로 보정
28
+ */
29
+ export function resolveTheme(theme) {
30
+ if (!theme)
31
+ return defaultTheme;
32
+ return {
33
+ ...defaultTheme,
34
+ ...theme,
35
+ density: {
36
+ zone: {
37
+ ...defaultTheme.density.zone,
38
+ ...theme.density?.zone,
39
+ },
40
+ path: {
41
+ ...defaultTheme.density.path,
42
+ ...theme.density?.path,
43
+ },
44
+ },
45
+ };
46
+ }