@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.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/examples.d.ts +2 -0
- package/dist/examples.js +469 -0
- package/dist/export-image.d.ts +10 -0
- package/dist/export-image.js +47 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.html +13 -0
- package/dist/index.js +18 -0
- package/dist/parser/ast.d.ts +118 -0
- package/dist/parser/ast.js +79 -0
- package/dist/parser/index.d.ts +3 -0
- package/dist/parser/index.js +2 -0
- package/dist/parser/parser.d.ts +2 -0
- package/dist/parser/parser.js +635 -0
- package/dist/renderer/astar-router.d.ts +16 -0
- package/dist/renderer/astar-router.js +532 -0
- package/dist/renderer/gates.d.ts +19 -0
- package/dist/renderer/gates.js +92 -0
- package/dist/renderer/graph.d.ts +31 -0
- package/dist/renderer/graph.js +403 -0
- package/dist/renderer/layout.d.ts +76 -0
- package/dist/renderer/layout.js +3121 -0
- package/dist/renderer/math-renderer.d.ts +16 -0
- package/dist/renderer/math-renderer.js +119 -0
- package/dist/renderer/svg-renderer.d.ts +3 -0
- package/dist/renderer/svg-renderer.js +599 -0
- package/dist/renderer/wires.d.ts +5 -0
- package/dist/renderer/wires.js +12 -0
- package/dist/theme/themes.d.ts +58 -0
- package/dist/theme/themes.js +104 -0
- package/docs/api.adoc +196 -0
- package/docs/ldl-for-llms.md +163 -0
- package/docs/user-guide.adoc +318 -0
- package/package.json +78 -0
- package/spec/render.sh +22 -0
- package/spec/sections/attributes.adoc +80 -0
- package/spec/sections/connections.adoc +39 -0
- package/spec/sections/examples.adoc +212 -0
- package/spec/sections/expressions.adoc +182 -0
- package/spec/sections/file-extension.adoc +5 -0
- package/spec/sections/function-blocks.adoc +120 -0
- package/spec/sections/grammar.adoc +64 -0
- package/spec/sections/hyperlinks.adoc +18 -0
- package/spec/sections/introduction.adoc +16 -0
- package/spec/sections/layout-rules.adoc +491 -0
- package/spec/sections/lexical-conventions.adoc +68 -0
- package/spec/sections/objects.adoc +31 -0
- package/spec/sections/options.adoc +146 -0
- package/spec/sections/ports.adoc +77 -0
- package/spec/sections/rendering-contract.adoc +11 -0
- package/spec/sections/revision-history.adoc +9 -0
- package/spec/sections/styling.adoc +83 -0
- package/spec/sections/svg-symbol-specification.adoc +149 -0
- package/spec/sections/symbol-definitions.adoc +143 -0
- package/spec/sections/templates.adoc +31 -0
- 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 };
|