kymostudio 0.1.0

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,176 @@
1
+ /**
2
+ * Data model for the container diagram — JS mirror of `src/model.py`.
3
+ *
4
+ * Components, regions and edges are plain object literals (no classes).
5
+ * `anchor(node, side)` and `resolveAnchors(edge, src, dst)` mimic the
6
+ * Python implementations 1:1.
7
+ */
8
+
9
+ /** @type {Record<string, [number, number]>} */
10
+ export const SHAPE_HALF = {
11
+ "circle": [38, 38],
12
+ "cube": [40, 40],
13
+ "cube-big": [50, 50],
14
+ "box": [35, 35],
15
+ "cylinder": [35, 35],
16
+ "hex": [35, 32], // flat-top hexagon — wider than tall
17
+ "annotation": [0, 0],
18
+ "aws-tile": [32, 32],
19
+ "aws-tile-hero": [40, 40],
20
+ "badge": [14, 14],
21
+ "image": [32, 32],
22
+ };
23
+
24
+ /** @type {Record<string, number>} */
25
+ export const LABEL_HEIGHT = {
26
+ "circle": 38,
27
+ "cube": 42,
28
+ "cube-big": 48,
29
+ "box": 38,
30
+ "cylinder": 38,
31
+ "hex": 40,
32
+ "annotation": 0,
33
+ "aws-tile": 48,
34
+ "aws-tile-hero": 48,
35
+ "badge": 0,
36
+ "image": 26,
37
+ };
38
+
39
+ // ── Factories (mirror Python @dataclass with defaults) ────────────────
40
+
41
+ export function makeComponent({
42
+ id, name = "", subtitle = "", icon = "", shape = "box", accent = "green",
43
+ pos = [0, 0],
44
+ parent = null, align = null, alignGap = 24, alignOffset = [0, 0],
45
+ }) {
46
+ return {
47
+ id, name, subtitle, icon, shape, accent, pos,
48
+ parent, align, alignGap, alignOffset,
49
+ };
50
+ }
51
+
52
+ export function makeRegion({
53
+ id, label = "", bounds = [0, 0, 0, 0], contains = [],
54
+ padding = [24, 24], paddingBottom = null, style = "outer",
55
+ icon = null, layout = null, pos = null, gap = 24, align = "center",
56
+ visible = true, borderDash = null, borderStroke = null,
57
+ labelAnchor = "middle", labelPosition = null,
58
+ }) {
59
+ return {
60
+ id, label, bounds, contains, padding, paddingBottom, style, icon,
61
+ layout, pos, gap, align, visible, borderDash, borderStroke,
62
+ labelAnchor, labelPosition,
63
+ };
64
+ }
65
+
66
+ export function makeEdge({
67
+ src, dst, label = "", style = "gray",
68
+ srcAnchor = null, dstAnchor = null,
69
+ route = "auto", via = [],
70
+ srcOffset = [0, 0], dstOffset = [0, 0],
71
+ labelOffset = [0, 0], labelAnchor = "mid",
72
+ labelSmall = false, labelPos = null,
73
+ dashed = false, noArrow = false,
74
+ trunkOffset = 0, sharedPort = false,
75
+ }) {
76
+ return {
77
+ src, dst, label, style, srcAnchor, dstAnchor, route, via,
78
+ srcOffset, dstOffset, labelOffset, labelAnchor, labelSmall, labelPos,
79
+ dashed, noArrow, trunkOffset, sharedPort,
80
+ };
81
+ }
82
+
83
+ export function makeDiagram({
84
+ width = 0, height = 0, title = "", subtitle = "",
85
+ components = [], regions = [], edges = [], layoutTrees = [],
86
+ } = {}) {
87
+ return { width, height, title, subtitle, components, regions, edges, layoutTrees };
88
+ }
89
+
90
+ // ── Lookups & geometry helpers ────────────────────────────────────────
91
+
92
+ export function componentHalf(c) {
93
+ return SHAPE_HALF[c.shape];
94
+ }
95
+
96
+ export function regionHalf(r) {
97
+ const [, , w, h] = r.bounds;
98
+ return [(w / 2) | 0, (h / 2) | 0];
99
+ }
100
+
101
+ /**
102
+ * Edge attach point on `node` for a given side.
103
+ * For a Component, `bottom` pushes past `LABEL_HEIGHT` when the
104
+ * component actually has a name or subtitle.
105
+ */
106
+ export function anchor(node, side) {
107
+ // Region path — has `bounds`, no `pos`.
108
+ if (node.bounds !== undefined && node.pos === undefined) {
109
+ return regionAnchor(node, side);
110
+ }
111
+ // Component path — has `pos`.
112
+ return componentAnchor(node, side);
113
+ }
114
+
115
+ function componentAnchor(c, side) {
116
+ const [cx, cy] = c.pos;
117
+ const [hw, hh] = SHAPE_HALF[c.shape];
118
+ const labelled = (c.name && c.name.length > 0) || (c.subtitle && c.subtitle.length > 0);
119
+ const lh = labelled ? (LABEL_HEIGHT[c.shape] || 0) : 0;
120
+ switch (side) {
121
+ case "top": return [cx, cy - hh];
122
+ case "right": return [cx + hw, cy];
123
+ case "bottom": return [cx, cy + hh + lh];
124
+ case "left": return [cx - hw, cy];
125
+ case "center": return [cx, cy];
126
+ }
127
+ throw new Error(`anchor: bad side ${side}`);
128
+ }
129
+
130
+ function regionAnchor(r, side) {
131
+ const [x, y, w, h] = r.bounds;
132
+ switch (side) {
133
+ case "top": return [x + ((w / 2) | 0), y];
134
+ case "right": return [x + w, y + ((h / 2) | 0)];
135
+ case "bottom": return [x + ((w / 2) | 0), y + h];
136
+ case "left": return [x, y + ((h / 2) | 0)];
137
+ case "center": return [x + ((w / 2) | 0), y + ((h / 2) | 0)];
138
+ }
139
+ throw new Error(`anchor: bad side ${side}`);
140
+ }
141
+
142
+ /**
143
+ * Pick effective (srcAnchor, dstAnchor) for an edge. `null` slots are
144
+ * filled from geometry: horizontal-biased — vertical wins only when
145
+ * `|dy| > 2·|dx|`.
146
+ */
147
+ export function resolveAnchors(e, src, dst) {
148
+ let { srcAnchor: sa, dstAnchor: da } = e;
149
+ if (sa !== null && da !== null) return [sa, da];
150
+ const [scx, scy] = anchor(src, "center");
151
+ const [dcx, dcy] = anchor(dst, "center");
152
+ const dx = dcx - scx, dy = dcy - scy;
153
+ let autoSa, autoDa;
154
+ if (Math.abs(dy) > 2 * Math.abs(dx)) {
155
+ [autoSa, autoDa] = dy >= 0 ? ["bottom", "top"] : ["top", "bottom"];
156
+ } else {
157
+ [autoSa, autoDa] = dx >= 0 ? ["right", "left"] : ["left", "right"];
158
+ }
159
+ return [sa || autoSa, da || autoDa];
160
+ }
161
+
162
+ // ── Diagram node lookups ──────────────────────────────────────────────
163
+
164
+ export function getComponent(d, id) {
165
+ const c = d.components.find(c => c.id === id);
166
+ if (!c) throw new Error(`component ${JSON.stringify(id)} not in diagram`);
167
+ return c;
168
+ }
169
+
170
+ export function getNode(d, id) {
171
+ const c = d.components.find(c => c.id === id);
172
+ if (c) return c;
173
+ const r = d.regions.find(r => r.id === id);
174
+ if (r) return r;
175
+ throw new Error(`node ${JSON.stringify(id)} not in diagram (checked components + regions)`);
176
+ }