@openpowershift/logic-diagram-language 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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/dist/examples.d.ts +2 -0
  4. package/dist/examples.js +469 -0
  5. package/dist/export-image.d.ts +10 -0
  6. package/dist/export-image.js +47 -0
  7. package/dist/index.d.ts +10 -0
  8. package/dist/index.html +13 -0
  9. package/dist/index.js +18 -0
  10. package/dist/parser/ast.d.ts +118 -0
  11. package/dist/parser/ast.js +79 -0
  12. package/dist/parser/index.d.ts +3 -0
  13. package/dist/parser/index.js +2 -0
  14. package/dist/parser/parser.d.ts +2 -0
  15. package/dist/parser/parser.js +635 -0
  16. package/dist/renderer/astar-router.d.ts +16 -0
  17. package/dist/renderer/astar-router.js +532 -0
  18. package/dist/renderer/gates.d.ts +19 -0
  19. package/dist/renderer/gates.js +92 -0
  20. package/dist/renderer/graph.d.ts +31 -0
  21. package/dist/renderer/graph.js +403 -0
  22. package/dist/renderer/layout.d.ts +76 -0
  23. package/dist/renderer/layout.js +3121 -0
  24. package/dist/renderer/math-renderer.d.ts +16 -0
  25. package/dist/renderer/math-renderer.js +119 -0
  26. package/dist/renderer/svg-renderer.d.ts +3 -0
  27. package/dist/renderer/svg-renderer.js +599 -0
  28. package/dist/renderer/wires.d.ts +5 -0
  29. package/dist/renderer/wires.js +12 -0
  30. package/dist/theme/themes.d.ts +58 -0
  31. package/dist/theme/themes.js +104 -0
  32. package/docs/api.adoc +196 -0
  33. package/docs/ldl-for-llms.md +163 -0
  34. package/docs/user-guide.adoc +318 -0
  35. package/package.json +78 -0
  36. package/spec/render.sh +22 -0
  37. package/spec/sections/attributes.adoc +80 -0
  38. package/spec/sections/connections.adoc +39 -0
  39. package/spec/sections/examples.adoc +212 -0
  40. package/spec/sections/expressions.adoc +182 -0
  41. package/spec/sections/file-extension.adoc +5 -0
  42. package/spec/sections/function-blocks.adoc +120 -0
  43. package/spec/sections/grammar.adoc +64 -0
  44. package/spec/sections/hyperlinks.adoc +18 -0
  45. package/spec/sections/introduction.adoc +16 -0
  46. package/spec/sections/layout-rules.adoc +491 -0
  47. package/spec/sections/lexical-conventions.adoc +68 -0
  48. package/spec/sections/objects.adoc +31 -0
  49. package/spec/sections/options.adoc +146 -0
  50. package/spec/sections/ports.adoc +77 -0
  51. package/spec/sections/rendering-contract.adoc +11 -0
  52. package/spec/sections/revision-history.adoc +9 -0
  53. package/spec/sections/styling.adoc +83 -0
  54. package/spec/sections/svg-symbol-specification.adoc +149 -0
  55. package/spec/sections/symbol-definitions.adoc +143 -0
  56. package/spec/sections/templates.adoc +31 -0
  57. package/spec/spec.adoc +49 -0
@@ -0,0 +1,403 @@
1
+ // Flatten the AST: associative AND/OR chains are merged into single multi-input gates; blocks and
2
+ // leaves pass through. (NOT stays a gate here; BUBBLES absorption happens in buildGraph.)
3
+ export function flattenGate(node) {
4
+ if (node.kind === 'port')
5
+ return node;
6
+ if (node.kind === 'symbolRef')
7
+ return node;
8
+ if (node.kind === 'block')
9
+ return { ...node, inputs: node.inputs.map(flattenGate) };
10
+ if (node.kind !== 'gate')
11
+ return node;
12
+ const flatInputs = node.inputs.map(flattenGate);
13
+ if (node.gateType === 'AND' || node.gateType === 'OR') {
14
+ const merged = [];
15
+ for (const input of flatInputs) {
16
+ // Merge associative same-type chains, but never absorb a child gate that carries an
17
+ // explicit instance id (AND#ID) — that is a distinct, named gate and must stay its own
18
+ // node so its .Name/.Description meta resolves.
19
+ if (input.kind === 'gate' && input.gateType === node.gateType && input.id === undefined) {
20
+ merged.push(...input.inputs);
21
+ }
22
+ else {
23
+ merged.push(input);
24
+ }
25
+ }
26
+ return { kind: 'gate', gateType: node.gateType, id: node.id, inputs: merged };
27
+ }
28
+ return { kind: 'gate', gateType: node.gateType, id: node.id, inputs: flatInputs };
29
+ }
30
+ // Build the flattened logic graph from the parsed diagram: resolve every assignment into shared
31
+ // signal nodes (consumed intermediates fan out instead of duplicating), apply BUBBLES inversion
32
+ // absorption, and assign depths. Pure with respect to geometry — only the semantic model.
33
+ export function buildGraph(diagram, portMeta, opts, uid) {
34
+ const flatOutputs = diagram.outputs.map(o => ({
35
+ ...o,
36
+ expression: flattenGate(o.expression),
37
+ }));
38
+ const metaMap = new Map();
39
+ for (const m of portMeta) {
40
+ let e = metaMap.get(m.identifier);
41
+ if (!e) {
42
+ e = {};
43
+ metaMap.set(m.identifier, e);
44
+ }
45
+ if (m.property === 'Name')
46
+ e.name = m.value;
47
+ if (m.property === 'Description')
48
+ e.description = m.value;
49
+ }
50
+ const nodes = new Map();
51
+ const inputMap = new Map();
52
+ const exprMap = new Map();
53
+ const blockMap = new Map(); // function-block instances (dedup by id/structure)
54
+ const nameDriver = new Map(); // intermediate name -> resolved driver node id
55
+ const nameDriverPort = new Map(); // its output port selector (e.g. NQ)
56
+ // Default output port of a function block when none is selected (SR exposes Q/NQ).
57
+ const defaultPort = (blockType) => (blockType === 'SR' ? 'Q' : 'OUT');
58
+ // The source output-port name a child reference points at (block .Q/.NQ, symbol port, or the
59
+ // selector carried by a referenced intermediate signal).
60
+ const portOf = (child) => child.kind === 'block' ? (child.port ?? defaultPort(child.blockType)).toUpperCase()
61
+ : child.kind === 'symbolRef' ? child.portName
62
+ : child.kind === 'port' && nameDriverPort.has(child.name) ? nameDriverPort.get(child.name)
63
+ : undefined;
64
+ const outputMeta = new Map();
65
+ for (const m of portMeta) {
66
+ let e = outputMeta.get(m.identifier);
67
+ if (!e) {
68
+ e = {};
69
+ outputMeta.set(m.identifier, e);
70
+ }
71
+ if (m.property === 'Name')
72
+ e.name = m.value;
73
+ if (m.property === 'Description')
74
+ e.description = m.value;
75
+ }
76
+ // Each LHS name defines a signal. A name referenced inside ANOTHER name's expression is a
77
+ // CONSUMED intermediate: it is not drawn as an output — its driver fans out to the consumers —
78
+ // unless it has no consumer (a sink), references itself (feedback/seal-in), or is forced with
79
+ // `NAME.OUT = TRUE`.
80
+ const definitionExpr = new Map();
81
+ for (const o of flatOutputs)
82
+ if (!definitionExpr.has(o.name))
83
+ definitionExpr.set(o.name, o.expression);
84
+ const consumedByOther = new Set();
85
+ const selfRef = new Set();
86
+ const scanRefs = (owner, n) => {
87
+ if (n.kind === 'port') {
88
+ if (definitionExpr.has(n.name))
89
+ (n.name === owner ? selfRef : consumedByOther).add(n.name);
90
+ }
91
+ else if (n.kind === 'gate' || n.kind === 'block') {
92
+ for (const c of n.inputs)
93
+ scanRefs(owner, c);
94
+ }
95
+ };
96
+ for (const o of flatOutputs)
97
+ scanRefs(o.name, o.expression);
98
+ const forceOut = new Set();
99
+ for (const m of portMeta)
100
+ if (m.property === 'Out' && /^(true|1|yes|on)$/i.test(m.value.trim()))
101
+ forceOut.add(m.identifier);
102
+ const isShownOutput = (name) => !consumedByOther.has(name) || forceOut.has(name) || selfRef.has(name);
103
+ // Output nodes exist only for SHOWN names (sinks, feedback, or forced). A consumed intermediate
104
+ // resolves to its driver wherever referenced, so its signal is shared instead of duplicated.
105
+ const outputIdByName = new Map();
106
+ for (const output of flatOutputs) {
107
+ if (outputIdByName.has(output.name) || !isShownOutput(output.name))
108
+ continue;
109
+ const outputId = uid('out');
110
+ const meta = outputMeta.get(output.name);
111
+ nodes.set(outputId, {
112
+ id: outputId, kind: 'output', label: output.name,
113
+ name: meta?.name, description: meta?.description, depth: 0, inputIds: [],
114
+ });
115
+ outputIdByName.set(output.name, outputId);
116
+ }
117
+ const resolvingNames = new Set();
118
+ function makeInput(name) {
119
+ if (!inputMap.has(name)) {
120
+ const id = uid('in');
121
+ const meta = metaMap.get(name);
122
+ nodes.set(id, { id, kind: 'input', label: name, name: meta?.name, description: meta?.description, depth: 0, inputIds: [] });
123
+ inputMap.set(name, id);
124
+ }
125
+ return inputMap.get(name);
126
+ }
127
+ // Resolve a defined name to its driver node, memoised. A name referenced while it is itself
128
+ // being resolved is a cycle → loop back to its output node (feedback), or an input if unshown.
129
+ function resolveName(name) {
130
+ if (nameDriver.has(name))
131
+ return nameDriver.get(name);
132
+ if (resolvingNames.has(name))
133
+ return outputIdByName.get(name) ?? makeInput(name);
134
+ resolvingNames.add(name);
135
+ const driver = resolve(definitionExpr.get(name));
136
+ resolvingNames.delete(name);
137
+ nameDriver.set(name, driver);
138
+ nameDriverPort.set(name, portOf(definitionExpr.get(name)));
139
+ return driver;
140
+ }
141
+ function resolve(node) {
142
+ if (node.kind === 'port') {
143
+ if (definitionExpr.has(node.name))
144
+ return resolveName(node.name); // shared intermediate/output
145
+ return makeInput(node.name);
146
+ }
147
+ if (node.kind === 'symbolRef') {
148
+ // A block-type symbol reference (e.g. `FB#PROT.ALARM`) is a port-assigned output: it
149
+ // reuses the existing block instance (same B#id key as the block-call form) and adds the
150
+ // selected port to its usedPorts, or creates a new no-input block if no prior
151
+ // instantiation exists. This allows binding multiple outputs to one block without
152
+ // repeating its arguments: `TRIP = FB#PROT(...).TRIP` + `ALARM = FB#PROT.ALARM`.
153
+ const blockTypes = new Set(['TIMER', 'SR', 'RISING', 'FALLING', 'COMPARE', 'FB']);
154
+ if (blockTypes.has(node.symbolName)) {
155
+ const bt = node.symbolName;
156
+ const key = node.id ? `B#${node.id}` : `B:${bt}()`;
157
+ let blockId = blockMap.get(key);
158
+ if (!blockId) {
159
+ const meta = node.id ? metaMap.get(node.id) : undefined;
160
+ blockId = uid(bt.toLowerCase());
161
+ nodes.set(blockId, {
162
+ id: blockId, kind: 'gate', gateType: bt, blockType: bt,
163
+ params: {}, name: meta?.name, description: meta?.description,
164
+ depth: 0, inputIds: [], inputPorts: [], inputLabels: [],
165
+ usedPorts: new Set(),
166
+ });
167
+ blockMap.set(key, blockId);
168
+ }
169
+ const port = (node.portName ?? defaultPort(bt)).toUpperCase();
170
+ nodes.get(blockId).usedPorts.add(port);
171
+ return blockId;
172
+ }
173
+ // A non-block symbol reference (e.g. a custom symbol) stays a bare gate.
174
+ const id = uid('sym');
175
+ nodes.set(id, {
176
+ id, kind: 'gate', gateType: node.symbolName,
177
+ depth: 0, inputIds: [],
178
+ });
179
+ return id;
180
+ }
181
+ if (node.kind === 'gate') {
182
+ const inputIds = node.inputs.map(i => resolve(i));
183
+ const inputPorts = node.inputs.map(portOf);
184
+ // Canonical key for deduplication: sort inputs for commutative operators.
185
+ // A gate with an explicit instance id (AND#MYID) is NEVER deduplicated — each id is
186
+ // a distinct instance. The id is also the key for .Name / .Description meta lookup.
187
+ const keyInputIds = (node.gateType === 'AND' || node.gateType === 'OR')
188
+ ? [...inputIds].sort()
189
+ : inputIds;
190
+ const key = node.id ? `G#${node.id}` : `${node.gateType}(${keyInputIds.join(',')})`;
191
+ const existing = exprMap.get(key);
192
+ if (existing)
193
+ return existing;
194
+ const id = uid(node.gateType.toLowerCase());
195
+ // Feedback inputs (an output node) do NOT contribute depth — the back-edge would
196
+ // otherwise create a cycle / push the gate to the far right.
197
+ const depth = Math.max(...inputIds.map(iid => {
198
+ const n = nodes.get(iid);
199
+ return n && n.kind === 'output' ? 0 : n?.depth ?? 0;
200
+ }), 0) + 1;
201
+ const meta = node.id ? metaMap.get(node.id) : undefined;
202
+ nodes.set(id, {
203
+ id, kind: 'gate', gateType: node.gateType,
204
+ label: node.id, name: meta?.name, description: meta?.description,
205
+ depth, inputIds, inputPorts,
206
+ });
207
+ exprMap.set(key, id);
208
+ return id;
209
+ }
210
+ if (node.kind === 'block') {
211
+ const inputIds = node.inputs.map(i => resolve(i));
212
+ const key = node.id
213
+ ? `B#${node.id}`
214
+ : `B:${node.blockType}(${inputIds.join(',')};${JSON.stringify(node.params)})`;
215
+ let id = blockMap.get(key);
216
+ if (!id) {
217
+ const inputPorts = node.inputs.map(portOf);
218
+ const depth = Math.max(...inputIds.map(iid => {
219
+ const n = nodes.get(iid);
220
+ return n && n.kind === 'output' ? 0 : n?.depth ?? 0;
221
+ }), 0) + 1;
222
+ const meta = node.id ? metaMap.get(node.id) : undefined;
223
+ id = uid(node.blockType.toLowerCase());
224
+ nodes.set(id, {
225
+ id, kind: 'gate', gateType: node.blockType, blockType: node.blockType,
226
+ label: node.id, params: node.params, name: meta?.name, description: meta?.description,
227
+ depth, inputIds, inputPorts, inputLabels: node.inputLabels, usedPorts: new Set(),
228
+ });
229
+ blockMap.set(key, id);
230
+ }
231
+ nodes.get(id).usedPorts.add((node.port ?? defaultPort(node.blockType)).toUpperCase());
232
+ return id;
233
+ }
234
+ return uid('unknown');
235
+ }
236
+ for (const output of flatOutputs) {
237
+ const outputId = outputIdByName.get(output.name);
238
+ if (!outputId)
239
+ continue; // consumed intermediate — not drawn, only its driver is shared
240
+ const driver = resolveName(output.name);
241
+ const on = nodes.get(outputId);
242
+ on.inputIds = [driver];
243
+ on.inputPorts = [portOf(output.expression)];
244
+ on.depth = (nodes.get(driver)?.depth ?? 0) + 1;
245
+ }
246
+ // A consumed intermediate (not drawn as an output) can still be labelled at its fan-out
247
+ // junction by setting NAME.Name / NAME.Description — a net label on the shared signal.
248
+ const intermediateLabels = [];
249
+ for (const [name, driverId] of nameDriver) {
250
+ if (isShownOutput(name))
251
+ continue; // shown signals are labelled at their output node
252
+ const meta = metaMap.get(name);
253
+ if (meta && (meta.name || meta.description)) {
254
+ intermediateLabels.push({ driverId, port: nameDriverPort.get(name), name: meta.name ?? name, description: meta.description });
255
+ }
256
+ }
257
+ // INVERSION = BUBBLES: absorb NOT gates into downstream ports with inversion bubbles
258
+ if (opts.inversion === 'BUBBLES') {
259
+ const notNodes = Array.from(nodes.values())
260
+ .filter(n => n.kind === 'gate' && n.gateType === 'NOT');
261
+ // Build NOT chain info: for each NOT, walk to the ultimate non-NOT source
262
+ // and record the cumulative inversion depth.
263
+ const notChainInfo = new Map();
264
+ for (const notNode of notNodes) {
265
+ if (notNode.inputIds.length !== 1)
266
+ continue;
267
+ let sourceId = notNode.inputIds[0];
268
+ let depth = 1;
269
+ while (true) {
270
+ const sourceNode = nodes.get(sourceId);
271
+ if (sourceNode && sourceNode.kind === 'gate' && sourceNode.gateType === 'NOT' && sourceNode.inputIds.length === 1) {
272
+ depth++;
273
+ sourceId = sourceNode.inputIds[0];
274
+ }
275
+ else {
276
+ break;
277
+ }
278
+ }
279
+ notChainInfo.set(notNode.id, { sourceId, inversionDepth: depth });
280
+ }
281
+ // A NOT-only through-connection (single NOT feeding straight to an output, with NO gate
282
+ // consumer) is kept as an explicit NOT gate even under BUBBLES — a bare `O = NOT A` reads
283
+ // better as an explicit NOT symbol than a lone port bubble (spec: Tier 3.6). Identify those
284
+ // NOTs and exclude them from absorption so their consumer's input stays the NOT itself.
285
+ const notConsumers = new Map();
286
+ for (const n of nodes.values()) {
287
+ for (const inputId of n.inputIds) {
288
+ const a = notConsumers.get(inputId) ?? [];
289
+ a.push(n.id);
290
+ notConsumers.set(inputId, a);
291
+ }
292
+ }
293
+ const protectedNotIds = new Set();
294
+ for (const notNode of notNodes) {
295
+ const info = notChainInfo.get(notNode.id);
296
+ // Protect a single-inversion NOT (depth 1) whose source is an INPUT and whose only
297
+ // consumers are OUTPUT nodes — a bare `O = NOT A` reads better as an explicit NOT gate
298
+ // than a lone port bubble (spec: Tier 3.6). NOT wrapping a gate (e.g. `NOT (A AND B)`)
299
+ // is NOT protected: the bubble on the gate output is the conventional BUBBLES rendering.
300
+ if (info && info.inversionDepth === 1) {
301
+ const source = nodes.get(info.sourceId);
302
+ const isInputSource = source?.kind === 'input';
303
+ const consumers = notConsumers.get(notNode.id) ?? [];
304
+ if (isInputSource && consumers.length > 0 && consumers.every(cid => nodes.get(cid)?.kind === 'output')) {
305
+ protectedNotIds.add(notNode.id);
306
+ }
307
+ }
308
+ }
309
+ // For each non-NOT node, walk each input through NOT chains to find
310
+ // the ultimate source. Track inversion depth per input for bubble marking.
311
+ const inputInversionDepth = new Map();
312
+ for (const otherNode of nodes.values()) {
313
+ if (otherNode.kind === 'gate' && otherNode.gateType === 'NOT')
314
+ continue;
315
+ for (let i = 0; i < otherNode.inputIds.length; i++) {
316
+ let ref = otherNode.inputIds[i];
317
+ let totalInversion = 0;
318
+ while (notChainInfo.has(ref) && !protectedNotIds.has(ref)) {
319
+ totalInversion += notChainInfo.get(ref).inversionDepth;
320
+ otherNode.inputIds[i] = notChainInfo.get(ref).sourceId;
321
+ ref = notChainInfo.get(ref).sourceId;
322
+ }
323
+ if (totalInversion > 0) {
324
+ if (!inputInversionDepth.has(otherNode.id)) {
325
+ inputInversionDepth.set(otherNode.id, new Map());
326
+ }
327
+ inputInversionDepth.get(otherNode.id).set(i, totalInversion);
328
+ }
329
+ }
330
+ }
331
+ // Mark bubbled inputs/outputs based on inversion depth
332
+ // Odd inversion → bubble; even → cancel out
333
+ for (const [nodeId, depthMap] of inputInversionDepth) {
334
+ const node = nodes.get(nodeId);
335
+ if (!node)
336
+ continue;
337
+ for (const [inputIdx, depth] of depthMap) {
338
+ if (depth % 2 === 1) {
339
+ const sourceId = node.inputIds[inputIdx];
340
+ const sourceNode = nodes.get(sourceId);
341
+ if (sourceNode && sourceNode.kind === 'gate' && !sourceNode.blockType && sourceNode.gateType !== 'NOT') {
342
+ // Source is a plain logic gate (AND/OR/…): absorb the inversion as an output-side
343
+ // bubble on that gate (the conventional NAND/NOR rendering).
344
+ sourceNode.bubbledOutput = true;
345
+ }
346
+ else {
347
+ // Source is an input port, an output node, or a FUNCTION BLOCK (TIMER/SR/RISING/…).
348
+ // A block carries named I/O ports and is not drawn with an output bubble, so the
349
+ // inversion belongs on THIS consumer's input port (a bubble on the gate input),
350
+ // not on the block's output — otherwise the NOT renders nowhere. See `NOT WIN`
351
+ // feeding an AND: the bubble sits on the AND's timer input.
352
+ if (!node.invertedInputs)
353
+ node.invertedInputs = new Set();
354
+ node.invertedInputs.add(inputIdx);
355
+ }
356
+ }
357
+ // Even inversion: both NOTs cancel, no bubble needed
358
+ }
359
+ }
360
+ // Remove all NOT nodes EXCEPT protected through-connections (kept as explicit gates).
361
+ for (const n of notNodes) {
362
+ if (!protectedNotIds.has(n.id))
363
+ nodes.delete(n.id);
364
+ }
365
+ // Recalculate depths after removing NOTs — nodes may have shallower depths now
366
+ const depthOrder = Array.from(nodes.values()).sort((a, b) => a.depth - b.depth);
367
+ for (const node of depthOrder) {
368
+ if (node.kind === 'input') {
369
+ node.depth = 0;
370
+ }
371
+ else {
372
+ const inputDepths = node.inputIds
373
+ .map(id => nodes.get(id)?.depth ?? 0);
374
+ node.depth = (inputDepths.length > 0 ? Math.max(...inputDepths) : 0) + 1;
375
+ }
376
+ }
377
+ // Compress depth values to remove gaps
378
+ const usedDepths = [...new Set(Array.from(nodes.values()).map(n => n.depth))].sort((a, b) => a - b);
379
+ const depthRemap = new Map();
380
+ usedDepths.forEach((d, i) => depthRemap.set(d, i));
381
+ for (const node of nodes.values()) {
382
+ node.depth = depthRemap.get(node.depth) ?? node.depth;
383
+ }
384
+ }
385
+ // Push outputs to a higher depth if they share a column with any gate node.
386
+ // This prevents wires from passing through intermediate gate bodies when an
387
+ // output's depth column coincides with a gate's (e.g. when a shared subexpression
388
+ // feeds both a multi-input gate and a direct output).
389
+ {
390
+ const maxCheckDepth = Math.max(...Array.from(nodes.values()).map(n => n.depth), 0) + 5;
391
+ for (let depth = 0; depth <= maxCheckDepth; depth++) {
392
+ const hasGate = Array.from(nodes.values()).some(n => n.kind === 'gate' && n.depth === depth);
393
+ if (!hasGate)
394
+ continue;
395
+ for (const node of nodes.values()) {
396
+ if (node.kind === 'output' && node.depth === depth) {
397
+ node.depth++;
398
+ }
399
+ }
400
+ }
401
+ }
402
+ return { nodes, intermediateLabels };
403
+ }
@@ -0,0 +1,76 @@
1
+ import type { Diagram, PortMeta, RenderOptions } from '../parser/ast.js';
2
+ export interface LayoutPort {
3
+ name: string;
4
+ absX: number;
5
+ absY: number;
6
+ bubbled?: boolean;
7
+ bubbledOutput?: boolean;
8
+ style?: 'CIRCLE' | 'SQUARE';
9
+ label?: string;
10
+ }
11
+ export interface LayoutNode {
12
+ id: string;
13
+ gateType: string;
14
+ label?: string;
15
+ name?: string;
16
+ description?: string;
17
+ absX: number;
18
+ absY: number;
19
+ width: number;
20
+ height: number;
21
+ inputs: LayoutPort[];
22
+ outputs: LayoutPort[];
23
+ depth: number;
24
+ barsMode?: boolean;
25
+ blockType?: string;
26
+ params?: Record<string, string>;
27
+ }
28
+ export interface LayoutWire {
29
+ id: string;
30
+ points: {
31
+ x: number;
32
+ y: number;
33
+ }[];
34
+ fromId: string;
35
+ toId: string;
36
+ feedback?: boolean;
37
+ }
38
+ export interface LayoutJunction {
39
+ x: number;
40
+ y: number;
41
+ }
42
+ export interface LayoutLabel {
43
+ x: number;
44
+ y: number;
45
+ width: number;
46
+ height: number;
47
+ anchorX: number;
48
+ anchorY: number;
49
+ driverId: string;
50
+ leaderX?: number;
51
+ leaderY?: number;
52
+ name?: string;
53
+ description?: string;
54
+ }
55
+ export interface LayoutResult {
56
+ nodes: LayoutNode[];
57
+ wires: LayoutWire[];
58
+ junctions: LayoutJunction[];
59
+ labels: LayoutLabel[];
60
+ width: number;
61
+ height: number;
62
+ options: RenderOptions;
63
+ }
64
+ declare const MIN_PORT_GAP = 25;
65
+ declare const MIN_DOGLEG = 30;
66
+ export declare function layoutDiagram(diagram: Diagram, portMeta?: PortMeta[], options?: RenderOptions): LayoutResult;
67
+ export interface WireCrossing {
68
+ wire1From: string;
69
+ wire1To: string;
70
+ wire2From: string;
71
+ wire2To: string;
72
+ x: number;
73
+ y: number;
74
+ }
75
+ export declare function findWireCrossings(wires: LayoutWire[], junctions: LayoutJunction[]): WireCrossing[];
76
+ export { MIN_PORT_GAP, MIN_DOGLEG };