@weasel-js/diagram 1.4.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 orochi235
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @weasel-js/diagram
2
+
3
+ Node-link diagramming for weasel — flowcharts, pipelines, code-flow diagrams,
4
+ simple visual programming.
5
+
6
+ Any scene node becomes a diagram participant by carrying the `DiagramNode`
7
+ trait; nothing has to be authored through this package to take part. Ports
8
+ default to anchors on the node's own bounds, so a node needs to say nothing to
9
+ be connectable. An optional body builder composes ordinary scene nodes for
10
+ nodes that should *look* like a flowchart box.
11
+
12
+ Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
13
+ scene-graph canvas kit for React. See the
14
+ [API reference](https://orochi235.github.io/weasel/api/).
15
+
16
+ ## Install
17
+
18
+ ```sh
19
+ npm install @weasel-js/diagram
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ import { portsOf, COMPASS } from '@weasel-js/diagram';
26
+ ```
27
+
28
+ ## License
29
+
30
+ MIT
@@ -0,0 +1,308 @@
1
+ import * as _weasel_js_core from '@weasel-js/core';
2
+ import { Path, Vec2, PoseProjection } from '@weasel-js/core';
3
+
4
+ /**
5
+ * The shapes a built body can wear, as paths in the node's bounds.
6
+ *
7
+ * These are the flowchart vocabulary — a decision is a diamond, an I/O step a
8
+ * parallelogram, a terminator a stadium — plus `'rect'` for everything else and
9
+ * a `Path` escape hatch for a shape the vocabulary does not have. The path is
10
+ * built in world coordinates from the bounds it is given, the same way the
11
+ * kit's own `clipFromPose` factories are.
12
+ */
13
+
14
+ /** An axis-aligned box, in the shape `PoseProjection.getBounds` returns. */
15
+ interface Bounds {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ }
21
+ /** A named flowchart shape, or a path a consumer built themselves. */
22
+ type Outline = 'rect' | 'diamond' | 'stadium' | 'parallelogram' | Path;
23
+ /** The path `outline` describes inside `bounds`. A zero-area box gives a
24
+ * zero-area rect rather than a degenerate polygon. */
25
+ declare function outlinePath(outline: Outline, bounds: Bounds): Path;
26
+
27
+ /**
28
+ * Where a port sits on its node, in **normalized bounds coordinates**:
29
+ * `(0, 0)` is the top-left corner of the node's AABB and `(1, 1)` the
30
+ * bottom-right. `COMPASS` names the eight positions almost everything wants.
31
+ *
32
+ * Normalized rather than absolute because a node's size is authored data that
33
+ * resize, align and distribute all write — a port fixed at an offset would
34
+ * drift off the shape the first time one of them ran.
35
+ */
36
+ interface PortAnchor {
37
+ u: number;
38
+ v: number;
39
+ }
40
+ /** One attachment point declared on a node. */
41
+ interface PortSpec {
42
+ /** Unique within the node. Edges name this, so it outlives a re-layout. */
43
+ id: string;
44
+ at: PortAnchor;
45
+ /** Consulted by a connect gesture's validity predicate. Uninterpreted here. */
46
+ type?: string;
47
+ /** The direction an edge leaves along, overriding the one the anchor
48
+ * implies. `null` means "no preferred direction" — a router goes straight
49
+ * at it. */
50
+ normal?: Vec2 | null;
51
+ }
52
+ /**
53
+ * The trait that makes an existing scene node a diagram participant.
54
+ *
55
+ * Attached to a node's `data`, not a node kind the plugin mints: a text block,
56
+ * an image, a path, a group or a plain rect all take part by carrying this,
57
+ * and nothing has to be authored through this package to be connectable.
58
+ */
59
+ interface DiagramNode {
60
+ /** Defaults to {@link DEFAULT_PORTS} — the four edge midpoints of the
61
+ * node's own bounds, which every node has. */
62
+ ports?: readonly PortSpec[];
63
+ /** Layout must not move this node. */
64
+ pinned?: boolean;
65
+ }
66
+ /** A port resolved against a node's current pose: where it is in world
67
+ * coordinates, and which way an edge leaves it. */
68
+ interface Port {
69
+ id: string;
70
+ /** The node this port belongs to. */
71
+ nodeId: string;
72
+ /** World point an edge attaches to. */
73
+ point: Vec2;
74
+ /** Unit vector pointing away from the node, or `null` when the port
75
+ * declared no direction. Rotated with the node. */
76
+ normal: Vec2 | null;
77
+ type?: string;
78
+ }
79
+
80
+ /** One line of body content. */
81
+ type Row =
82
+ /** Text. `label` is the node's name; `field` is a named value. */
83
+ {
84
+ kind: 'label';
85
+ text: string;
86
+ style?: RowTextStyle;
87
+ } | {
88
+ kind: 'field';
89
+ label: string;
90
+ value: string;
91
+ style?: RowTextStyle;
92
+ }
93
+ /** A row that carries ports on its own left and right edges — the
94
+ * visual-programming shape, where an operator's inputs line up with the
95
+ * rows they feed. */
96
+ | {
97
+ kind: 'ports';
98
+ left?: readonly RowPort[];
99
+ right?: readonly RowPort[];
100
+ height?: number;
101
+ }
102
+ /** Reserved space a consumer paints into. */
103
+ | {
104
+ kind: 'slot';
105
+ height: number;
106
+ id?: string;
107
+ };
108
+ /** A port declared on a row rather than on the node's perimeter. */
109
+ interface RowPort {
110
+ id: string;
111
+ /** Label shown beside it, and what the row measures against. */
112
+ label?: string;
113
+ type?: string;
114
+ }
115
+ /** The subset of a text style the body measures against. Deliberately small:
116
+ * a full `ResolvedTextStyle` belongs to whatever paints the row. */
117
+ interface RowTextStyle {
118
+ fontSize?: number;
119
+ fontFamily?: string;
120
+ bold?: boolean;
121
+ }
122
+ /** Measures one run of text. Returns its advance width and its line height. */
123
+ type MeasureRowText = (text: string, style: RowTextStyle) => {
124
+ width: number;
125
+ height: number;
126
+ };
127
+ interface BodySpec {
128
+ outline: Outline;
129
+ rows: readonly Row[];
130
+ /** Space between the outline and its rows. Default 8. */
131
+ padding?: number;
132
+ /** Space between adjacent rows. Default 4. */
133
+ gap?: number;
134
+ }
135
+ /** The floor a body's content puts under its node's size. */
136
+ interface BodyFloor {
137
+ minWidth: number;
138
+ minHeight: number;
139
+ }
140
+ /** Where one row landed, in the same frame as the bounds it was laid out in.
141
+ * A `ports` row's own ports anchor to this box. */
142
+ interface RowBox extends Bounds {
143
+ row: Row;
144
+ index: number;
145
+ }
146
+ /** Adapts the kit's `measureText` for a caller holding a 2D context. Pass the
147
+ * same context the renderer measures with, or the floor will not match what
148
+ * is painted. */
149
+ declare function canvasMeasure(ctx: {
150
+ measureText(text: string): {
151
+ width: number;
152
+ };
153
+ font: string;
154
+ }): MeasureRowText;
155
+ /**
156
+ * The smallest box `spec`'s rows fit in. A node's pose is maxed against this,
157
+ * never set to it.
158
+ */
159
+ declare function measureBody(spec: BodySpec, measure?: MeasureRowText): BodyFloor;
160
+ /**
161
+ * The authored size, grown to fit the body. Never shrinks: a row removed
162
+ * leaves the node the size the author last saw it at, which is the only
163
+ * behavior that survives a resize the author did on purpose.
164
+ */
165
+ declare function sizeToBody<TPose extends Bounds>(pose: TPose, floor: BodyFloor): TPose;
166
+ /** Where each row sits inside `bounds`, top to bottom. Rows keep their
167
+ * measured heights; the leftover goes unclaimed at the bottom rather than
168
+ * being distributed, so a row does not move when a sibling grows. */
169
+ declare function layoutBody(spec: BodySpec, bounds: Bounds, measure?: MeasureRowText): RowBox[];
170
+ /**
171
+ * The `DiagramNode` trait a built body implies: perimeter ports for the
172
+ * outline, plus one port per `ports`-row entry, anchored to that row's own
173
+ * left or right edge.
174
+ *
175
+ * Anchors are normalized against `bounds`, so they survive the node being
176
+ * resized — which is the whole reason `PortAnchor` is normalized.
177
+ */
178
+ declare function bodyTrait(spec: BodySpec, bounds: Bounds, measure?: MeasureRowText): DiagramNode;
179
+ /** The outline path for a built body at `bounds` — what paints it, and what a
180
+ * perimeter-hugging port will eventually be placed on. */
181
+ declare function bodyOutline(spec: BodySpec, bounds: Bounds): _weasel_js_core.Path;
182
+
183
+ /**
184
+ * Reading the `DiagramNode` trait off a scene node.
185
+ *
186
+ * Two ways to attach it, both of which this module's default reader answers:
187
+ *
188
+ * - **On the node.** `data.diagram` holds the trait. One node, one answer;
189
+ * what a connect gesture writes when the author drags a new edge.
190
+ * - **By kind.** A consumer declares once which data shapes take part, and
191
+ * every matching node gets the trait without being stamped. This mirrors
192
+ * `createNodeRouting` in the kit — a predicate over `data`, first match
193
+ * wins — and is the shape to reach for when the participants are a class
194
+ * of node rather than a hand-picked set.
195
+ *
196
+ * A reader is a plain function, so a consumer whose data lives somewhere else
197
+ * entirely passes their own and never touches either form.
198
+ */
199
+
200
+ /** The key on a node's `data` the default reader looks under. */
201
+ declare const DIAGRAM_TRAIT_KEY = "diagram";
202
+ /** All a reader looks at. Deliberately not `SceneNode`: naming `TPose` there
203
+ * would put it in a contravariant position for no gain, since nothing here
204
+ * reads a pose. */
205
+ interface DiagramNodeLike {
206
+ id: string;
207
+ kind: 'leaf' | 'container';
208
+ data: unknown;
209
+ }
210
+ /** Answers "is this node a diagram participant, and on what terms" — `null`
211
+ * for a node that takes no part. */
212
+ type DiagramNodeReader = (node: DiagramNodeLike) => DiagramNode | null;
213
+ /** One class of participant, declared once for every node whose data matches.
214
+ * Mirrors `NodeRoutingEntry`. */
215
+ interface DiagramNodeEntry {
216
+ /** Unique within a reader; the same vocabulary as `NodeRoutingEntry.name`. */
217
+ name: string;
218
+ matches: (data: unknown) => boolean;
219
+ /** The trait every matching node carries. A function when it varies with the
220
+ * node's own data — a pipeline stage whose ports come from its rows. */
221
+ trait: DiagramNode | ((data: unknown) => DiagramNode);
222
+ }
223
+ /** The trait on the node's own `data.diagram`, or `null`. */
224
+ declare const dataKeyReader: DiagramNodeReader;
225
+ /**
226
+ * A reader over declared kinds. A trait on the node's own data still wins, so
227
+ * a single node can opt out of, or override, whatever its kind says.
228
+ *
229
+ * Throws on a duplicate `name`, for the reason `mergeContributions` does: a
230
+ * class of node silently losing its ports is what this registry exists to make
231
+ * loud.
232
+ */
233
+ declare function createDiagramNodes(entries: readonly DiagramNodeEntry[]): DiagramNodeReader;
234
+ /** The trait `node` carries, through `read` (default: its own `data.diagram`). */
235
+ declare function diagramNodeOf(node: DiagramNodeLike, read?: DiagramNodeReader): DiagramNode | null;
236
+ /** True when layout must leave this node where it is. */
237
+ declare function isPinned(node: DiagramNodeLike, read?: DiagramNodeReader): boolean;
238
+
239
+ /**
240
+ * Where a node's ports are, in world coordinates.
241
+ *
242
+ * A port is placed in the node's **unrotated** bounds, then carried through
243
+ * the pose's rotation — the same pivot every other kit reader uses, the AABB
244
+ * center. Bounds come from the pose descriptor rather than from `pose.width`
245
+ * and friends, so a node whose pose is a path, an ellipse or anything else a
246
+ * consumer wired is connectable on the same terms as a rect.
247
+ */
248
+
249
+ /** The eight anchors almost everything wants, named the way a compass is. */
250
+ declare const COMPASS: {
251
+ readonly nw: {
252
+ readonly u: 0;
253
+ readonly v: 0;
254
+ };
255
+ readonly n: {
256
+ readonly u: 0.5;
257
+ readonly v: 0;
258
+ };
259
+ readonly ne: {
260
+ readonly u: 1;
261
+ readonly v: 0;
262
+ };
263
+ readonly e: {
264
+ readonly u: 1;
265
+ readonly v: 0.5;
266
+ };
267
+ readonly se: {
268
+ readonly u: 1;
269
+ readonly v: 1;
270
+ };
271
+ readonly s: {
272
+ readonly u: 0.5;
273
+ readonly v: 1;
274
+ };
275
+ readonly sw: {
276
+ readonly u: 0;
277
+ readonly v: 1;
278
+ };
279
+ readonly w: {
280
+ readonly u: 0;
281
+ readonly v: 0.5;
282
+ };
283
+ };
284
+ /**
285
+ * What a node carrying no `ports` gets: the four edge midpoints of its own
286
+ * bounds. Every node has bounds, so every node is connectable without saying
287
+ * anything.
288
+ */
289
+ declare const DEFAULT_PORTS: readonly PortSpec[];
290
+ interface PortsOptions<TPose> {
291
+ /** Reads the AABB a port is placed in, and the node's rotation. Defaults to
292
+ * the kit's `AUTO_POSE_DESCRIPTOR`, which handles rect and path poses. */
293
+ geometry?: PoseProjection<TPose>;
294
+ /** How the `DiagramNode` trait is read off a node. */
295
+ read?: DiagramNodeReader;
296
+ }
297
+ /**
298
+ * Every port on `node`, resolved against `pose`.
299
+ *
300
+ * Pass the pose the node is **painted** at — `effectivePose(scene, node)` —
301
+ * not `node.pose`. A port that answers from the document pose while the node
302
+ * is mid-drag puts an edge somewhere the user can see the node is not.
303
+ */
304
+ declare function portsOf<TPose>(node: DiagramNodeLike, pose: TPose, opts?: PortsOptions<TPose>): Port[];
305
+ /** One named port, or `undefined` when the node has no such port. */
306
+ declare function portOf<TPose>(node: DiagramNodeLike, pose: TPose, portId: string, opts?: PortsOptions<TPose>): Port | undefined;
307
+
308
+ export { type BodyFloor, type BodySpec, type Bounds, COMPASS, DEFAULT_PORTS, DIAGRAM_TRAIT_KEY, type DiagramNode, type DiagramNodeEntry, type DiagramNodeLike, type DiagramNodeReader, type MeasureRowText, type Outline, type Port, type PortAnchor, type PortSpec, type PortsOptions, type Row, type RowBox, type RowPort, type RowTextStyle, bodyOutline, bodyTrait, canvasMeasure, createDiagramNodes, dataKeyReader, diagramNodeOf, isPinned, layoutBody, measureBody, outlinePath, portOf, portsOf, sizeToBody };
package/dist/index.js ADDED
@@ -0,0 +1,236 @@
1
+ import { rectPath, polygonFromPoints, PathBuilder, AUTO_POSE_DESCRIPTOR, rotatePoint } from '@weasel-js/core';
2
+
3
+ // src/outline.ts
4
+ var LEAN = 0.2;
5
+ var KAPPA = 0.5522847498307936;
6
+ function outlinePath(outline, bounds) {
7
+ if (typeof outline !== "string") return outline;
8
+ const { x, y, width: w, height: h } = bounds;
9
+ if (w <= 0 || h <= 0) return rectPath(x, y, Math.max(w, 0), Math.max(h, 0));
10
+ switch (outline) {
11
+ case "rect":
12
+ return rectPath(x, y, w, h);
13
+ case "diamond":
14
+ return polygonFromPoints([
15
+ { x: x + w / 2, y },
16
+ { x: x + w, y: y + h / 2 },
17
+ { x: x + w / 2, y: y + h },
18
+ { x, y: y + h / 2 }
19
+ ]);
20
+ case "parallelogram": {
21
+ const lean = Math.min(w * LEAN, w / 2);
22
+ return polygonFromPoints([
23
+ { x: x + lean, y },
24
+ { x: x + w, y },
25
+ { x: x + w - lean, y: y + h },
26
+ { x, y: y + h }
27
+ ]);
28
+ }
29
+ case "stadium":
30
+ return stadiumPath(x, y, w, h);
31
+ }
32
+ }
33
+ function stadiumPath(x, y, w, h) {
34
+ const r = Math.min(w, h) / 2;
35
+ const k = r * KAPPA;
36
+ const b = new PathBuilder();
37
+ if (w >= h) {
38
+ const left2 = x + r;
39
+ const right2 = x + w - r;
40
+ const top2 = y;
41
+ const bottom2 = y + h;
42
+ const midY = y + h / 2;
43
+ return b.moveTo(left2, top2).lineTo(right2, top2).curveTo(right2 + k, top2, x + w, midY - k, x + w, midY).curveTo(x + w, midY + k, right2 + k, bottom2, right2, bottom2).lineTo(left2, bottom2).curveTo(left2 - k, bottom2, x, midY + k, x, midY).curveTo(x, midY - k, left2 - k, top2, left2, top2).close().build();
44
+ }
45
+ const top = y + r;
46
+ const bottom = y + h - r;
47
+ const left = x;
48
+ const right = x + w;
49
+ const midX = x + w / 2;
50
+ return b.moveTo(right, top).lineTo(right, bottom).curveTo(right, bottom + k, midX + k, y + h, midX, y + h).curveTo(midX - k, y + h, left, bottom + k, left, bottom).lineTo(left, top).curveTo(left, top - k, midX - k, y, midX, y).curveTo(midX + k, y, right, top - k, right, top).close().build();
51
+ }
52
+
53
+ // src/trait.ts
54
+ var DIAGRAM_TRAIT_KEY = "diagram";
55
+ var dataKeyReader = (node) => {
56
+ const data = node.data;
57
+ if (data === null || typeof data !== "object") return null;
58
+ const trait = data[DIAGRAM_TRAIT_KEY];
59
+ return trait !== null && typeof trait === "object" ? trait : null;
60
+ };
61
+ function createDiagramNodes(entries) {
62
+ const seen = /* @__PURE__ */ new Set();
63
+ for (const entry of entries) {
64
+ if (seen.has(entry.name)) {
65
+ throw new Error(`createDiagramNodes: duplicate entry name "${entry.name}"`);
66
+ }
67
+ seen.add(entry.name);
68
+ }
69
+ return (node) => {
70
+ const own = dataKeyReader(node);
71
+ if (own !== null) return own;
72
+ for (const entry of entries) {
73
+ if (!entry.matches(node.data)) continue;
74
+ return typeof entry.trait === "function" ? entry.trait(node.data) : entry.trait;
75
+ }
76
+ return null;
77
+ };
78
+ }
79
+ function diagramNodeOf(node, read = dataKeyReader) {
80
+ return read(node);
81
+ }
82
+ function isPinned(node, read) {
83
+ return diagramNodeOf(node, read)?.pinned === true;
84
+ }
85
+
86
+ // src/ports.ts
87
+ var COMPASS = {
88
+ nw: { u: 0, v: 0 },
89
+ n: { u: 0.5, v: 0 },
90
+ ne: { u: 1, v: 0 },
91
+ e: { u: 1, v: 0.5 },
92
+ se: { u: 1, v: 1 },
93
+ s: { u: 0.5, v: 1 },
94
+ sw: { u: 0, v: 1 },
95
+ w: { u: 0, v: 0.5 }
96
+ };
97
+ var DEFAULT_PORTS = Object.freeze([
98
+ { id: "n", at: COMPASS.n },
99
+ { id: "e", at: COMPASS.e },
100
+ { id: "s", at: COMPASS.s },
101
+ { id: "w", at: COMPASS.w }
102
+ ]);
103
+ function normalOf(at) {
104
+ const x = at.u * 2 - 1;
105
+ const y = at.v * 2 - 1;
106
+ const len = Math.hypot(x, y);
107
+ if (len === 0) return null;
108
+ return { x: x / len, y: y / len };
109
+ }
110
+ function portsOf(node, pose, opts = {}) {
111
+ const trait = diagramNodeOf(node, opts.read);
112
+ if (trait === null) return [];
113
+ const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
114
+ const bounds = geometry.getBounds(pose);
115
+ const rotation = geometry.getRotation?.(pose) ?? 0;
116
+ const cx = bounds.x + bounds.width / 2;
117
+ const cy = bounds.y + bounds.height / 2;
118
+ return (trait.ports ?? DEFAULT_PORTS).map((spec) => {
119
+ const flat = {
120
+ x: bounds.x + spec.at.u * bounds.width,
121
+ y: bounds.y + spec.at.v * bounds.height
122
+ };
123
+ const normal = spec.normal === void 0 ? normalOf(spec.at) : spec.normal;
124
+ if (rotation === 0) {
125
+ return { id: spec.id, nodeId: node.id, point: flat, normal, ...typeOf(spec) };
126
+ }
127
+ return {
128
+ id: spec.id,
129
+ nodeId: node.id,
130
+ point: rotatePoint(flat.x, flat.y, cx, cy, rotation),
131
+ // A direction rotates about the origin, not about the node's center.
132
+ normal: normal === null ? null : rotatePoint(normal.x, normal.y, 0, 0, rotation),
133
+ ...typeOf(spec)
134
+ };
135
+ });
136
+ }
137
+ function portOf(node, pose, portId, opts = {}) {
138
+ return portsOf(node, pose, opts).find((p) => p.id === portId);
139
+ }
140
+ function typeOf(spec) {
141
+ return spec.type === void 0 ? {} : { type: spec.type };
142
+ }
143
+
144
+ // src/body.ts
145
+ var DEFAULT_PADDING = 8;
146
+ var DEFAULT_GAP = 4;
147
+ var FALLBACK_LINE = 16;
148
+ function canvasMeasure(ctx) {
149
+ return (text, style) => {
150
+ const size = style.fontSize ?? FALLBACK_LINE;
151
+ ctx.font = `${style.bold === true ? "bold " : ""}${size}px ${style.fontFamily ?? "sans-serif"}`;
152
+ return { width: ctx.measureText(text).width, height: Math.ceil(size * 1.2) };
153
+ };
154
+ }
155
+ function measureRow(row, measure) {
156
+ const text = (s, style) => measure === void 0 ? { width: s.length * ((style?.fontSize ?? FALLBACK_LINE) * 0.6), height: FALLBACK_LINE } : measure(s, style ?? {});
157
+ switch (row.kind) {
158
+ case "label":
159
+ return text(row.text, row.style);
160
+ case "field": {
161
+ const l = text(row.label, row.style);
162
+ const v = text(row.value, row.style);
163
+ return { width: l.width + DEFAULT_GAP * 2 + v.width, height: Math.max(l.height, v.height) };
164
+ }
165
+ case "ports": {
166
+ const side = (ports) => (ports ?? []).reduce((w, p) => Math.max(w, p.label === void 0 ? 0 : text(p.label).width), 0);
167
+ const rows = Math.max(row.left?.length ?? 0, row.right?.length ?? 0, 1);
168
+ return {
169
+ width: side(row.left) + DEFAULT_GAP * 2 + side(row.right),
170
+ height: row.height ?? rows * FALLBACK_LINE
171
+ };
172
+ }
173
+ case "slot":
174
+ return { width: 0, height: row.height };
175
+ }
176
+ }
177
+ function measureBody(spec, measure) {
178
+ const padding = spec.padding ?? DEFAULT_PADDING;
179
+ const gap = spec.gap ?? DEFAULT_GAP;
180
+ let width = 0;
181
+ let height = 0;
182
+ for (const [i, row] of spec.rows.entries()) {
183
+ const m = measureRow(row, measure);
184
+ width = Math.max(width, m.width);
185
+ height += m.height + (i > 0 ? gap : 0);
186
+ }
187
+ return { minWidth: width + padding * 2, minHeight: height + padding * 2 };
188
+ }
189
+ function sizeToBody(pose, floor) {
190
+ const width = Math.max(pose.width, floor.minWidth);
191
+ const height = Math.max(pose.height, floor.minHeight);
192
+ if (width === pose.width && height === pose.height) return pose;
193
+ return { ...pose, width, height };
194
+ }
195
+ function layoutBody(spec, bounds, measure) {
196
+ const padding = spec.padding ?? DEFAULT_PADDING;
197
+ const gap = spec.gap ?? DEFAULT_GAP;
198
+ const width = Math.max(bounds.width - padding * 2, 0);
199
+ let y = bounds.y + padding;
200
+ return spec.rows.map((row, index) => {
201
+ const h = measureRow(row, measure).height;
202
+ const box = { row, index, x: bounds.x + padding, y, width, height: h };
203
+ y += h + gap;
204
+ return box;
205
+ });
206
+ }
207
+ function bodyTrait(spec, bounds, measure) {
208
+ const ports = [
209
+ { id: "n", at: COMPASS.n },
210
+ { id: "e", at: COMPASS.e },
211
+ { id: "s", at: COMPASS.s },
212
+ { id: "w", at: COMPASS.w }
213
+ ];
214
+ if (bounds.height <= 0) return { ports };
215
+ for (const box of layoutBody(spec, bounds, measure)) {
216
+ if (box.row.kind !== "ports") continue;
217
+ const v = (box.y + box.height / 2 - bounds.y) / bounds.height;
218
+ for (const p of box.row.left ?? []) {
219
+ ports.push(withType({ id: p.id, at: { u: 0, v } }, p.type));
220
+ }
221
+ for (const p of box.row.right ?? []) {
222
+ ports.push(withType({ id: p.id, at: { u: 1, v } }, p.type));
223
+ }
224
+ }
225
+ return { ports };
226
+ }
227
+ function withType(spec, type) {
228
+ return type === void 0 ? spec : { ...spec, type };
229
+ }
230
+ function bodyOutline(spec, bounds) {
231
+ return outlinePath(spec.outline, bounds);
232
+ }
233
+
234
+ export { COMPASS, DEFAULT_PORTS, DIAGRAM_TRAIT_KEY, bodyOutline, bodyTrait, canvasMeasure, createDiagramNodes, dataKeyReader, diagramNodeOf, isPinned, layoutBody, measureBody, outlinePath, portOf, portsOf, sizeToBody };
235
+ //# sourceMappingURL=index.js.map
236
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/outline.ts","../src/trait.ts","../src/ports.ts","../src/body.ts"],"names":["left","right","top","bottom"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The shapes a built body can wear, as paths in the node's bounds.\n *\n * These are the flowchart vocabulary — a decision is a diamond, an I/O step a\n * parallelogram, a terminator a stadium — plus `'rect'` for everything else and\n * a `Path` escape hatch for a shape the vocabulary does not have. The path is\n * built in world coordinates from the bounds it is given, the same way the\n * kit's own `clipFromPose` factories are.\n */\nimport { PathBuilder, polygonFromPoints, rectPath, type Path } from '@weasel-js/core';\n\n/** An axis-aligned box, in the shape `PoseProjection.getBounds` returns. */\nexport interface Bounds {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** A named flowchart shape, or a path a consumer built themselves. */\nexport type Outline = 'rect' | 'diamond' | 'stadium' | 'parallelogram' | Path;\n\n/** How far a parallelogram's top edge leans past its bottom one, as a\n * fraction of the box's width. The flowchart convention. */\nconst LEAN = 0.2;\n\n/** Cubic approximation of a quarter circle. A stadium's ends are two of these\n * each, which is the same construction `ellipsePath` uses. */\nconst KAPPA = 0.5522847498307936;\n\n/** The path `outline` describes inside `bounds`. A zero-area box gives a\n * zero-area rect rather than a degenerate polygon. */\nexport function outlinePath(outline: Outline, bounds: Bounds): Path {\n if (typeof outline !== 'string') return outline;\n const { x, y, width: w, height: h } = bounds;\n if (w <= 0 || h <= 0) return rectPath(x, y, Math.max(w, 0), Math.max(h, 0));\n\n switch (outline) {\n case 'rect':\n return rectPath(x, y, w, h);\n\n case 'diamond':\n return polygonFromPoints([\n { x: x + w / 2, y },\n { x: x + w, y: y + h / 2 },\n { x: x + w / 2, y: y + h },\n { x, y: y + h / 2 },\n ]);\n\n case 'parallelogram': {\n // The lean is clamped so a narrow box cannot fold through itself.\n const lean = Math.min(w * LEAN, w / 2);\n return polygonFromPoints([\n { x: x + lean, y },\n { x: x + w, y },\n { x: x + w - lean, y: y + h },\n { x, y: y + h },\n ]);\n }\n\n case 'stadium':\n return stadiumPath(x, y, w, h);\n }\n}\n\n/** A rect with semicircular ends — the Minkowski sum of a segment and a disc,\n * which puts the flat edges along the box's *longer* axis. A tall box is a\n * pill standing up, not an ellipse. */\nfunction stadiumPath(x: number, y: number, w: number, h: number): Path {\n const r = Math.min(w, h) / 2;\n const k = r * KAPPA;\n const b = new PathBuilder();\n\n if (w >= h) {\n const left = x + r;\n const right = x + w - r;\n const top = y;\n const bottom = y + h;\n const midY = y + h / 2;\n return b\n .moveTo(left, top)\n .lineTo(right, top)\n .curveTo(right + k, top, x + w, midY - k, x + w, midY)\n .curveTo(x + w, midY + k, right + k, bottom, right, bottom)\n .lineTo(left, bottom)\n .curveTo(left - k, bottom, x, midY + k, x, midY)\n .curveTo(x, midY - k, left - k, top, left, top)\n .close()\n .build();\n }\n\n const top = y + r;\n const bottom = y + h - r;\n const left = x;\n const right = x + w;\n const midX = x + w / 2;\n return b\n .moveTo(right, top)\n .lineTo(right, bottom)\n .curveTo(right, bottom + k, midX + k, y + h, midX, y + h)\n .curveTo(midX - k, y + h, left, bottom + k, left, bottom)\n .lineTo(left, top)\n .curveTo(left, top - k, midX - k, y, midX, y)\n .curveTo(midX + k, y, right, top - k, right, top)\n .close()\n .build();\n}\n","/**\n * Reading the `DiagramNode` trait off a scene node.\n *\n * Two ways to attach it, both of which this module's default reader answers:\n *\n * - **On the node.** `data.diagram` holds the trait. One node, one answer;\n * what a connect gesture writes when the author drags a new edge.\n * - **By kind.** A consumer declares once which data shapes take part, and\n * every matching node gets the trait without being stamped. This mirrors\n * `createNodeRouting` in the kit — a predicate over `data`, first match\n * wins — and is the shape to reach for when the participants are a class\n * of node rather than a hand-picked set.\n *\n * A reader is a plain function, so a consumer whose data lives somewhere else\n * entirely passes their own and never touches either form.\n */\nimport type { DiagramNode } from './types';\n\n/** The key on a node's `data` the default reader looks under. */\nexport const DIAGRAM_TRAIT_KEY = 'diagram';\n\n/** All a reader looks at. Deliberately not `SceneNode`: naming `TPose` there\n * would put it in a contravariant position for no gain, since nothing here\n * reads a pose. */\nexport interface DiagramNodeLike {\n id: string;\n kind: 'leaf' | 'container';\n data: unknown;\n}\n\n/** Answers \"is this node a diagram participant, and on what terms\" — `null`\n * for a node that takes no part. */\nexport type DiagramNodeReader = (node: DiagramNodeLike) => DiagramNode | null;\n\n/** One class of participant, declared once for every node whose data matches.\n * Mirrors `NodeRoutingEntry`. */\nexport interface DiagramNodeEntry {\n /** Unique within a reader; the same vocabulary as `NodeRoutingEntry.name`. */\n name: string;\n matches: (data: unknown) => boolean;\n /** The trait every matching node carries. A function when it varies with the\n * node's own data — a pipeline stage whose ports come from its rows. */\n trait: DiagramNode | ((data: unknown) => DiagramNode);\n}\n\n/** The trait on the node's own `data.diagram`, or `null`. */\nexport const dataKeyReader: DiagramNodeReader = (node) => {\n const data = node.data;\n if (data === null || typeof data !== 'object') return null;\n const trait = (data as Record<string, unknown>)[DIAGRAM_TRAIT_KEY];\n return trait !== null && typeof trait === 'object' ? (trait as DiagramNode) : null;\n};\n\n/**\n * A reader over declared kinds. A trait on the node's own data still wins, so\n * a single node can opt out of, or override, whatever its kind says.\n *\n * Throws on a duplicate `name`, for the reason `mergeContributions` does: a\n * class of node silently losing its ports is what this registry exists to make\n * loud.\n */\nexport function createDiagramNodes(entries: readonly DiagramNodeEntry[]): DiagramNodeReader {\n const seen = new Set<string>();\n for (const entry of entries) {\n if (seen.has(entry.name)) {\n throw new Error(`createDiagramNodes: duplicate entry name \"${entry.name}\"`);\n }\n seen.add(entry.name);\n }\n return (node) => {\n const own = dataKeyReader(node);\n if (own !== null) return own;\n for (const entry of entries) {\n if (!entry.matches(node.data)) continue;\n return typeof entry.trait === 'function' ? entry.trait(node.data) : entry.trait;\n }\n return null;\n };\n}\n\n/** The trait `node` carries, through `read` (default: its own `data.diagram`). */\nexport function diagramNodeOf(\n node: DiagramNodeLike,\n read: DiagramNodeReader = dataKeyReader,\n): DiagramNode | null {\n return read(node);\n}\n\n/** True when layout must leave this node where it is. */\nexport function isPinned(node: DiagramNodeLike, read?: DiagramNodeReader): boolean {\n return diagramNodeOf(node, read)?.pinned === true;\n}\n","/**\n * Where a node's ports are, in world coordinates.\n *\n * A port is placed in the node's **unrotated** bounds, then carried through\n * the pose's rotation — the same pivot every other kit reader uses, the AABB\n * center. Bounds come from the pose descriptor rather than from `pose.width`\n * and friends, so a node whose pose is a path, an ellipse or anything else a\n * consumer wired is connectable on the same terms as a rect.\n */\nimport {\n AUTO_POSE_DESCRIPTOR,\n rotatePoint,\n type PoseProjection,\n type Vec2,\n} from '@weasel-js/core';\nimport { diagramNodeOf, type DiagramNodeLike, type DiagramNodeReader } from './trait';\nimport type { Port, PortAnchor, PortSpec } from './types';\n\n/** The eight anchors almost everything wants, named the way a compass is. */\nexport const COMPASS = {\n nw: { u: 0, v: 0 },\n n: { u: 0.5, v: 0 },\n ne: { u: 1, v: 0 },\n e: { u: 1, v: 0.5 },\n se: { u: 1, v: 1 },\n s: { u: 0.5, v: 1 },\n sw: { u: 0, v: 1 },\n w: { u: 0, v: 0.5 },\n} as const satisfies Record<string, PortAnchor>;\n\n/**\n * What a node carrying no `ports` gets: the four edge midpoints of its own\n * bounds. Every node has bounds, so every node is connectable without saying\n * anything.\n */\nexport const DEFAULT_PORTS: readonly PortSpec[] = Object.freeze([\n { id: 'n', at: COMPASS.n },\n { id: 'e', at: COMPASS.e },\n { id: 's', at: COMPASS.s },\n { id: 'w', at: COMPASS.w },\n]);\n\nexport interface PortsOptions<TPose> {\n /** Reads the AABB a port is placed in, and the node's rotation. Defaults to\n * the kit's `AUTO_POSE_DESCRIPTOR`, which handles rect and path poses. */\n geometry?: PoseProjection<TPose>;\n /** How the `DiagramNode` trait is read off a node. */\n read?: DiagramNodeReader;\n}\n\n/**\n * The direction the anchor implies: away from the center of the bounds.\n *\n * `(2u-1, 2v-1)` is the anchor in a box that runs -1..1, which is exactly that\n * direction — an edge midpoint gets the axis-aligned normal, a corner gets the\n * diagonal. It is deliberately not scaled by the node's aspect: this is a\n * heading a router leaves along, not a point on the outline. A port at the\n * dead center has no direction, and says so.\n */\nfunction normalOf(at: PortAnchor): Vec2 | null {\n const x = at.u * 2 - 1;\n const y = at.v * 2 - 1;\n const len = Math.hypot(x, y);\n if (len === 0) return null;\n return { x: x / len, y: y / len };\n}\n\n/**\n * Every port on `node`, resolved against `pose`.\n *\n * Pass the pose the node is **painted** at — `effectivePose(scene, node)` —\n * not `node.pose`. A port that answers from the document pose while the node\n * is mid-drag puts an edge somewhere the user can see the node is not.\n */\nexport function portsOf<TPose>(\n node: DiagramNodeLike,\n pose: TPose,\n opts: PortsOptions<TPose> = {},\n): Port[] {\n const trait = diagramNodeOf(node, opts.read);\n if (trait === null) return [];\n const geometry = opts.geometry ?? (AUTO_POSE_DESCRIPTOR as PoseProjection<TPose>);\n const bounds = geometry.getBounds(pose);\n const rotation = geometry.getRotation?.(pose) ?? 0;\n const cx = bounds.x + bounds.width / 2;\n const cy = bounds.y + bounds.height / 2;\n\n return (trait.ports ?? DEFAULT_PORTS).map((spec) => {\n const flat: Vec2 = {\n x: bounds.x + spec.at.u * bounds.width,\n y: bounds.y + spec.at.v * bounds.height,\n };\n const normal = spec.normal === undefined ? normalOf(spec.at) : spec.normal;\n if (rotation === 0) {\n return { id: spec.id, nodeId: node.id, point: flat, normal, ...typeOf(spec) };\n }\n return {\n id: spec.id,\n nodeId: node.id,\n point: rotatePoint(flat.x, flat.y, cx, cy, rotation),\n // A direction rotates about the origin, not about the node's center.\n normal: normal === null ? null : rotatePoint(normal.x, normal.y, 0, 0, rotation),\n ...typeOf(spec),\n };\n });\n}\n\n/** One named port, or `undefined` when the node has no such port. */\nexport function portOf<TPose>(\n node: DiagramNodeLike,\n pose: TPose,\n portId: string,\n opts: PortsOptions<TPose> = {},\n): Port | undefined {\n return portsOf(node, pose, opts).find((p) => p.id === portId);\n}\n\n/** Spread rather than assigned, so a port with no type has no `type` key at\n * all — `exactOptionalPropertyTypes` rejects an explicit `undefined`. */\nfunction typeOf(spec: PortSpec): { type?: string } {\n return spec.type === undefined ? {} : { type: spec.type };\n}\n","/**\n * The optional body builder: what a node looks like when it should read as a\n * flowchart box or a visual-programming operator, rather than as whatever\n * scene node it already was.\n *\n * **The body measures a floor; the author sets the rest.** Rows measure to a\n * `{ minWidth, minHeight }` and the node's pose is the max of that and what\n * the author gave it. Adding a port row can grow a node; nothing here ever\n * shrinks one back. Because the pose stays authored data, resize, align,\n * distribute, guides, snapping and undo need no special case, and a node that\n * did not come from the builder just has whatever size it already had.\n *\n * Text measurement is a seam rather than an import: measuring needs a 2D\n * context, which a geometry module has no business owning. `canvasMeasure`\n * adapts the kit's own `measureText` for a caller that has one.\n */\nimport { outlinePath, type Bounds, type Outline } from './outline';\nimport { COMPASS } from './ports';\nimport type { DiagramNode, PortSpec } from './types';\n\n/** One line of body content. */\nexport type Row =\n /** Text. `label` is the node's name; `field` is a named value. */\n | { kind: 'label'; text: string; style?: RowTextStyle }\n | { kind: 'field'; label: string; value: string; style?: RowTextStyle }\n /** A row that carries ports on its own left and right edges — the\n * visual-programming shape, where an operator's inputs line up with the\n * rows they feed. */\n | { kind: 'ports'; left?: readonly RowPort[]; right?: readonly RowPort[]; height?: number }\n /** Reserved space a consumer paints into. */\n | { kind: 'slot'; height: number; id?: string };\n\n/** A port declared on a row rather than on the node's perimeter. */\nexport interface RowPort {\n id: string;\n /** Label shown beside it, and what the row measures against. */\n label?: string;\n type?: string;\n}\n\n/** The subset of a text style the body measures against. Deliberately small:\n * a full `ResolvedTextStyle` belongs to whatever paints the row. */\nexport interface RowTextStyle {\n fontSize?: number;\n fontFamily?: string;\n bold?: boolean;\n}\n\n/** Measures one run of text. Returns its advance width and its line height. */\nexport type MeasureRowText = (text: string, style: RowTextStyle) => { width: number; height: number };\n\nexport interface BodySpec {\n outline: Outline;\n rows: readonly Row[];\n /** Space between the outline and its rows. Default 8. */\n padding?: number;\n /** Space between adjacent rows. Default 4. */\n gap?: number;\n}\n\n/** The floor a body's content puts under its node's size. */\nexport interface BodyFloor {\n minWidth: number;\n minHeight: number;\n}\n\n/** Where one row landed, in the same frame as the bounds it was laid out in.\n * A `ports` row's own ports anchor to this box. */\nexport interface RowBox extends Bounds {\n row: Row;\n index: number;\n}\n\nconst DEFAULT_PADDING = 8;\nconst DEFAULT_GAP = 4;\n/** What a row is worth when nothing measures its text. Enough that a body\n * laid out headlessly is not zero-height — a zero-height body renders as an\n * empty page with every unit test still green. */\nconst FALLBACK_LINE = 16;\n\n/** Adapts the kit's `measureText` for a caller holding a 2D context. Pass the\n * same context the renderer measures with, or the floor will not match what\n * is painted. */\nexport function canvasMeasure(\n ctx: { measureText(text: string): { width: number } ; font: string },\n): MeasureRowText {\n return (text, style) => {\n const size = style.fontSize ?? FALLBACK_LINE;\n ctx.font = `${style.bold === true ? 'bold ' : ''}${size}px ${style.fontFamily ?? 'sans-serif'}`;\n return { width: ctx.measureText(text).width, height: Math.ceil(size * 1.2) };\n };\n}\n\n/** What each row is worth on its own, before padding and gaps. */\nfunction measureRow(row: Row, measure: MeasureRowText | undefined): { width: number; height: number } {\n const text = (s: string, style?: RowTextStyle): { width: number; height: number } =>\n measure === undefined\n ? { width: s.length * (((style?.fontSize ?? FALLBACK_LINE) * 0.6)), height: FALLBACK_LINE }\n : measure(s, style ?? {});\n\n switch (row.kind) {\n case 'label':\n return text(row.text, row.style);\n case 'field': {\n // Label and value sit on one line with a gap between them.\n const l = text(row.label, row.style);\n const v = text(row.value, row.style);\n return { width: l.width + DEFAULT_GAP * 2 + v.width, height: Math.max(l.height, v.height) };\n }\n case 'ports': {\n const side = (ports: readonly RowPort[] | undefined): number =>\n (ports ?? []).reduce((w, p) => Math.max(w, p.label === undefined ? 0 : text(p.label).width), 0);\n const rows = Math.max(row.left?.length ?? 0, row.right?.length ?? 0, 1);\n return {\n width: side(row.left) + DEFAULT_GAP * 2 + side(row.right),\n height: row.height ?? rows * FALLBACK_LINE,\n };\n }\n case 'slot':\n return { width: 0, height: row.height };\n }\n}\n\n/**\n * The smallest box `spec`'s rows fit in. A node's pose is maxed against this,\n * never set to it.\n */\nexport function measureBody(spec: BodySpec, measure?: MeasureRowText): BodyFloor {\n const padding = spec.padding ?? DEFAULT_PADDING;\n const gap = spec.gap ?? DEFAULT_GAP;\n let width = 0;\n let height = 0;\n for (const [i, row] of spec.rows.entries()) {\n const m = measureRow(row, measure);\n width = Math.max(width, m.width);\n height += m.height + (i > 0 ? gap : 0);\n }\n return { minWidth: width + padding * 2, minHeight: height + padding * 2 };\n}\n\n/**\n * The authored size, grown to fit the body. Never shrinks: a row removed\n * leaves the node the size the author last saw it at, which is the only\n * behavior that survives a resize the author did on purpose.\n */\nexport function sizeToBody<TPose extends Bounds>(pose: TPose, floor: BodyFloor): TPose {\n const width = Math.max(pose.width, floor.minWidth);\n const height = Math.max(pose.height, floor.minHeight);\n if (width === pose.width && height === pose.height) return pose;\n return { ...pose, width, height };\n}\n\n/** Where each row sits inside `bounds`, top to bottom. Rows keep their\n * measured heights; the leftover goes unclaimed at the bottom rather than\n * being distributed, so a row does not move when a sibling grows. */\nexport function layoutBody(\n spec: BodySpec,\n bounds: Bounds,\n measure?: MeasureRowText,\n): RowBox[] {\n const padding = spec.padding ?? DEFAULT_PADDING;\n const gap = spec.gap ?? DEFAULT_GAP;\n const width = Math.max(bounds.width - padding * 2, 0);\n let y = bounds.y + padding;\n return spec.rows.map((row, index) => {\n const h = measureRow(row, measure).height;\n const box: RowBox = { row, index, x: bounds.x + padding, y, width, height: h };\n y += h + gap;\n return box;\n });\n}\n\n/**\n * The `DiagramNode` trait a built body implies: perimeter ports for the\n * outline, plus one port per `ports`-row entry, anchored to that row's own\n * left or right edge.\n *\n * Anchors are normalized against `bounds`, so they survive the node being\n * resized — which is the whole reason `PortAnchor` is normalized.\n */\nexport function bodyTrait(spec: BodySpec, bounds: Bounds, measure?: MeasureRowText): DiagramNode {\n const ports: PortSpec[] = [\n { id: 'n', at: COMPASS.n },\n { id: 'e', at: COMPASS.e },\n { id: 's', at: COMPASS.s },\n { id: 'w', at: COMPASS.w },\n ];\n if (bounds.height <= 0) return { ports };\n\n for (const box of layoutBody(spec, bounds, measure)) {\n if (box.row.kind !== 'ports') continue;\n const v = (box.y + box.height / 2 - bounds.y) / bounds.height;\n for (const p of box.row.left ?? []) {\n ports.push(withType({ id: p.id, at: { u: 0, v } }, p.type));\n }\n for (const p of box.row.right ?? []) {\n ports.push(withType({ id: p.id, at: { u: 1, v } }, p.type));\n }\n }\n return { ports };\n}\n\nfunction withType(spec: PortSpec, type: string | undefined): PortSpec {\n return type === undefined ? spec : { ...spec, type };\n}\n\n/** The outline path for a built body at `bounds` — what paints it, and what a\n * perimeter-hugging port will eventually be placed on. */\nexport function bodyOutline(spec: BodySpec, bounds: Bounds) {\n return outlinePath(spec.outline, bounds);\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@weasel-js/diagram",
3
+ "version": "1.4.3",
4
+ "description": "Node-link diagramming for weasel: any scene node becomes a diagram participant, with ports on its perimeter and an optional body builder for flowchart-shaped boxes.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "peerDependencies": {
19
+ "react": ">=18",
20
+ "@weasel-js/core": "1.4.3"
21
+ },
22
+ "author": "orochi235",
23
+ "homepage": "https://orochi235.github.io/weasel/",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/orochi235/weasel.git",
27
+ "directory": "packages/diagram"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/orochi235/weasel/issues"
31
+ },
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsup"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "provenance": true
46
+ }
47
+ }