@schemd/core 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 +726 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +7 -0
- package/dist/layout.d.ts +151 -0
- package/dist/layout.js +592 -0
- package/dist/limits.d.ts +31 -0
- package/dist/limits.js +25 -0
- package/dist/marked-extension.d.ts +23 -0
- package/dist/marked-extension.js +75 -0
- package/dist/math-label.d.ts +50 -0
- package/dist/math-label.js +151 -0
- package/dist/parser.d.ts +56 -0
- package/dist/parser.js +598 -0
- package/dist/renderer.d.ts +44 -0
- package/dist/renderer.js +485 -0
- package/dist/types.d.ts +248 -0
- package/dist/types.js +25 -0
- package/package.json +56 -0
package/dist/layout.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import { CLASSICAL_GATE_KINDS, SchematicSyntaxError } from './types.js';
|
|
2
|
+
import { mathLabelGlyphLength } from './math-label.js';
|
|
3
|
+
export const PORT_HOTSPOT_RADIUS = 4;
|
|
4
|
+
export const SCHEMATIC_OBSTACLE_CLEARANCE = 12;
|
|
5
|
+
export const SCHEMATIC_BRIDGE_RADIUS = 5;
|
|
6
|
+
const MAX_OBSTACLE_PASSES = 16;
|
|
7
|
+
const CROSSING_BUCKET_SIZE = 64;
|
|
8
|
+
const DESIGNATOR_BASELINE_GAP = 10;
|
|
9
|
+
const LABEL_BASELINE_GAP = 19;
|
|
10
|
+
const TEXT_ASCENT_GUTTER = 12;
|
|
11
|
+
const TEXT_DESCENT_GUTTER = 5;
|
|
12
|
+
const TEXT_ADVANCE = 7;
|
|
13
|
+
const TEXT_HORIZONTAL_GUTTER = 4;
|
|
14
|
+
const PASSIVE_INPUT_PORTS = new Set(['in', 'left', 'l']);
|
|
15
|
+
const PASSIVE_OUTPUT_PORTS = new Set(['out', 'right', 'r']);
|
|
16
|
+
const DIODE_ANODE_PORTS = new Set(['anode', 'a']);
|
|
17
|
+
const DIODE_CATHODE_PORTS = new Set(['cathode', 'k', 'c']);
|
|
18
|
+
const TRANSISTOR_CONTROL_PORTS = new Set(['base', 'gate', 'b', 'g']);
|
|
19
|
+
const TRANSISTOR_UPPER_PORTS = new Set(['collector', 'drain', 'c', 'd']);
|
|
20
|
+
const TRANSISTOR_LOWER_PORTS = new Set(['emitter', 'source', 'e', 's']);
|
|
21
|
+
function formatNumber(value) {
|
|
22
|
+
const rounded = Math.abs(value) < 0.0005 ? 0 : Number(value.toFixed(3));
|
|
23
|
+
return String(rounded);
|
|
24
|
+
}
|
|
25
|
+
export function distributedCoordinate(index, count, span) {
|
|
26
|
+
return ((index + 1) * span) / (count + 1) - span / 2;
|
|
27
|
+
}
|
|
28
|
+
export function classicalGateHeight(component) {
|
|
29
|
+
return Math.max(48, Math.max(component.inputs, component.outputs) * 16);
|
|
30
|
+
}
|
|
31
|
+
function isClassicalGate(component) {
|
|
32
|
+
return CLASSICAL_GATE_KINDS.includes(component.kind);
|
|
33
|
+
}
|
|
34
|
+
function indexedPortPoint(component, port) {
|
|
35
|
+
const output = port.startsWith('out');
|
|
36
|
+
const count = output ? component.outputs : component.inputs;
|
|
37
|
+
const suffix = port.match(/\d+$/)?.[0];
|
|
38
|
+
const index = suffix === undefined ? 0 : Number(suffix) - 1;
|
|
39
|
+
return {
|
|
40
|
+
x: component.x + (output ? 48 : -48),
|
|
41
|
+
y: component.y + distributedCoordinate(index, count, classicalGateHeight(component))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function positionIcPin(component, side, index) {
|
|
45
|
+
const pins = component.pins[side];
|
|
46
|
+
if (!Number.isInteger(index) || index < 0 || index >= pins.length) {
|
|
47
|
+
throw new RangeError(`IC pin index ${index} is outside ${component.id}.${side}.`);
|
|
48
|
+
}
|
|
49
|
+
if (side === 'left' || side === 'right') {
|
|
50
|
+
return {
|
|
51
|
+
x: component.x +
|
|
52
|
+
(side === 'left' ? -component.bodyWidth / 2 - 16 : component.bodyWidth / 2 + 16),
|
|
53
|
+
y: component.y + distributedCoordinate(index, pins.length, component.bodyHeight)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
x: component.x + distributedCoordinate(index, pins.length, component.bodyWidth),
|
|
58
|
+
y: component.y +
|
|
59
|
+
(side === 'top' ? -component.bodyHeight / 2 - 16 : component.bodyHeight / 2 + 16)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function findIcPin(component, port) {
|
|
63
|
+
const sides = ['left', 'right', 'top', 'bottom'];
|
|
64
|
+
for (const side of sides) {
|
|
65
|
+
const index = component.pins[side].indexOf(port);
|
|
66
|
+
if (index >= 0)
|
|
67
|
+
return positionIcPin(component, side, index);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
function icPinPoint(component, port) {
|
|
72
|
+
if (port === 'in' || port === 'out') {
|
|
73
|
+
const canonical = findIcPin(component, `${port}1`);
|
|
74
|
+
if (canonical !== undefined)
|
|
75
|
+
return canonical;
|
|
76
|
+
const aliasSides = port === 'in' ? ['left', 'top'] : ['right', 'bottom'];
|
|
77
|
+
for (const side of aliasSides) {
|
|
78
|
+
if (component.pins[side].length > 0)
|
|
79
|
+
return positionIcPin(component, side, 0);
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return findIcPin(component, port);
|
|
84
|
+
}
|
|
85
|
+
export function resolvePortPoint(component, port) {
|
|
86
|
+
if (isClassicalGate(component))
|
|
87
|
+
return indexedPortPoint(component, port);
|
|
88
|
+
const left = { x: component.x - 42, y: component.y };
|
|
89
|
+
const right = { x: component.x + 42, y: component.y };
|
|
90
|
+
switch (component.kind) {
|
|
91
|
+
case 'resistor':
|
|
92
|
+
case 'capacitor':
|
|
93
|
+
case 'inductor':
|
|
94
|
+
if (PASSIVE_INPUT_PORTS.has(port))
|
|
95
|
+
return left;
|
|
96
|
+
if (PASSIVE_OUTPUT_PORTS.has(port))
|
|
97
|
+
return right;
|
|
98
|
+
break;
|
|
99
|
+
case 'diode':
|
|
100
|
+
if (DIODE_ANODE_PORTS.has(port))
|
|
101
|
+
return left;
|
|
102
|
+
if (DIODE_CATHODE_PORTS.has(port))
|
|
103
|
+
return right;
|
|
104
|
+
break;
|
|
105
|
+
case 'transistor':
|
|
106
|
+
if (TRANSISTOR_CONTROL_PORTS.has(port))
|
|
107
|
+
return left;
|
|
108
|
+
if (TRANSISTOR_UPPER_PORTS.has(port)) {
|
|
109
|
+
return { x: component.x + 42, y: component.y - 22 };
|
|
110
|
+
}
|
|
111
|
+
if (TRANSISTOR_LOWER_PORTS.has(port)) {
|
|
112
|
+
return { x: component.x + 42, y: component.y + 22 };
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
case 'port':
|
|
116
|
+
if (port === 'in')
|
|
117
|
+
return left;
|
|
118
|
+
if (port === 'out')
|
|
119
|
+
return right;
|
|
120
|
+
break;
|
|
121
|
+
case 'ground':
|
|
122
|
+
if (port === 'in')
|
|
123
|
+
return { x: component.x, y: component.y - 42 };
|
|
124
|
+
break;
|
|
125
|
+
case 'hadamard':
|
|
126
|
+
case 'qgate':
|
|
127
|
+
if (port === 'in')
|
|
128
|
+
return { x: component.x - 48, y: component.y };
|
|
129
|
+
if (port === 'out')
|
|
130
|
+
return { x: component.x + 48, y: component.y };
|
|
131
|
+
break;
|
|
132
|
+
case 'cnot':
|
|
133
|
+
if (port === 'in')
|
|
134
|
+
return left;
|
|
135
|
+
if (port === 'out')
|
|
136
|
+
return right;
|
|
137
|
+
if (port === 'control')
|
|
138
|
+
return { x: component.x, y: component.y - 16 };
|
|
139
|
+
if (port === 'target')
|
|
140
|
+
return { x: component.x, y: component.y + 16 };
|
|
141
|
+
break;
|
|
142
|
+
case 'ic': {
|
|
143
|
+
const point = icPinPoint(component, port);
|
|
144
|
+
if (point !== undefined)
|
|
145
|
+
return point;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`Validated port ${component.id}.${port} is missing.`);
|
|
150
|
+
}
|
|
151
|
+
function namedPort(component, id) {
|
|
152
|
+
return { id, point: resolvePortPoint(component, id) };
|
|
153
|
+
}
|
|
154
|
+
export function enumerateComponentPorts(component) {
|
|
155
|
+
if (isClassicalGate(component)) {
|
|
156
|
+
return [
|
|
157
|
+
...Array.from({ length: component.inputs }, (_, index) => namedPort(component, `in${index + 1}`)),
|
|
158
|
+
...Array.from({ length: component.outputs }, (_, index) => namedPort(component, `out${index + 1}`))
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
switch (component.kind) {
|
|
162
|
+
case 'resistor':
|
|
163
|
+
case 'capacitor':
|
|
164
|
+
case 'inductor':
|
|
165
|
+
case 'port':
|
|
166
|
+
case 'hadamard':
|
|
167
|
+
case 'qgate':
|
|
168
|
+
return [namedPort(component, 'in'), namedPort(component, 'out')];
|
|
169
|
+
case 'diode':
|
|
170
|
+
return [namedPort(component, 'anode'), namedPort(component, 'cathode')];
|
|
171
|
+
case 'transistor': {
|
|
172
|
+
const bipolar = component.transistorType === 'npn' || component.transistorType === 'pnp';
|
|
173
|
+
return bipolar
|
|
174
|
+
? [
|
|
175
|
+
namedPort(component, 'base'),
|
|
176
|
+
namedPort(component, 'collector'),
|
|
177
|
+
namedPort(component, 'emitter')
|
|
178
|
+
]
|
|
179
|
+
: [
|
|
180
|
+
namedPort(component, 'gate'),
|
|
181
|
+
namedPort(component, 'drain'),
|
|
182
|
+
namedPort(component, 'source')
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
case 'ground':
|
|
186
|
+
return [namedPort(component, 'in')];
|
|
187
|
+
case 'cnot':
|
|
188
|
+
return [
|
|
189
|
+
namedPort(component, 'in'),
|
|
190
|
+
namedPort(component, 'out'),
|
|
191
|
+
namedPort(component, 'control'),
|
|
192
|
+
namedPort(component, 'target')
|
|
193
|
+
];
|
|
194
|
+
case 'ic': {
|
|
195
|
+
const sides = ['left', 'right', 'top', 'bottom'];
|
|
196
|
+
return sides.flatMap((side) => component.pins[side].map((id, index) => ({
|
|
197
|
+
id,
|
|
198
|
+
point: positionIcPin(component, side, index)
|
|
199
|
+
})));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function endpointPoint(endpoint, components) {
|
|
204
|
+
const component = components.get(endpoint.componentId);
|
|
205
|
+
if (component === undefined) {
|
|
206
|
+
throw new Error(`Validated component ${endpoint.componentId} is missing.`);
|
|
207
|
+
}
|
|
208
|
+
return resolvePortPoint(component, endpoint.port);
|
|
209
|
+
}
|
|
210
|
+
function samePoint(left, right) {
|
|
211
|
+
return Math.abs(left.x - right.x) < 0.0005 && Math.abs(left.y - right.y) < 0.0005;
|
|
212
|
+
}
|
|
213
|
+
function compactOrthogonalPoints(points) {
|
|
214
|
+
const deduplicated = [];
|
|
215
|
+
for (const point of points) {
|
|
216
|
+
if (!samePoint(deduplicated.at(-1) ?? { x: Number.NaN, y: Number.NaN }, point)) {
|
|
217
|
+
deduplicated.push(point);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
let index = 1;
|
|
221
|
+
while (index < deduplicated.length - 1) {
|
|
222
|
+
const previous = deduplicated[index - 1];
|
|
223
|
+
const current = deduplicated[index];
|
|
224
|
+
const next = deduplicated[index + 1];
|
|
225
|
+
if ((previous.x === current.x && current.x === next.x) ||
|
|
226
|
+
(previous.y === current.y && current.y === next.y)) {
|
|
227
|
+
deduplicated.splice(index, 1);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
index += 1;
|
|
231
|
+
}
|
|
232
|
+
return deduplicated;
|
|
233
|
+
}
|
|
234
|
+
function orthogonalPath(points) {
|
|
235
|
+
const first = points[0];
|
|
236
|
+
let path = `M ${formatNumber(first.x)} ${formatNumber(first.y)}`;
|
|
237
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
238
|
+
const previous = points[index - 1];
|
|
239
|
+
const point = points[index];
|
|
240
|
+
if (previous.y === point.y)
|
|
241
|
+
path += ` H ${formatNumber(point.x)}`;
|
|
242
|
+
else
|
|
243
|
+
path += ` V ${formatNumber(point.y)}`;
|
|
244
|
+
}
|
|
245
|
+
return path;
|
|
246
|
+
}
|
|
247
|
+
function expandedComponentRectangle(component, clearance) {
|
|
248
|
+
const { halfWidth, halfHeight } = componentHalfExtents(component);
|
|
249
|
+
return {
|
|
250
|
+
minX: component.x - halfWidth - clearance,
|
|
251
|
+
minY: component.y - halfHeight - clearance,
|
|
252
|
+
maxX: component.x + halfWidth + clearance,
|
|
253
|
+
maxY: component.y + halfHeight + clearance
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
export function componentObstacleRectangle(component, clearance = SCHEMATIC_OBSTACLE_CLEARANCE) {
|
|
257
|
+
if (!Number.isFinite(clearance) || clearance < 0) {
|
|
258
|
+
throw new RangeError('Component obstacle clearance must be a finite non-negative number.');
|
|
259
|
+
}
|
|
260
|
+
return expandedComponentRectangle(component, clearance);
|
|
261
|
+
}
|
|
262
|
+
function segmentIntersectsRectangle(start, end, rectangle) {
|
|
263
|
+
if (start.y === end.y) {
|
|
264
|
+
return (start.y > rectangle.minY &&
|
|
265
|
+
start.y < rectangle.maxY &&
|
|
266
|
+
Math.max(start.x, end.x) > rectangle.minX &&
|
|
267
|
+
Math.min(start.x, end.x) < rectangle.maxX);
|
|
268
|
+
}
|
|
269
|
+
return (start.x > rectangle.minX &&
|
|
270
|
+
start.x < rectangle.maxX &&
|
|
271
|
+
Math.max(start.y, end.y) > rectangle.minY &&
|
|
272
|
+
Math.min(start.y, end.y) < rectangle.maxY);
|
|
273
|
+
}
|
|
274
|
+
function candidateCollisionCount(points, obstacles) {
|
|
275
|
+
let collisions = 0;
|
|
276
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
277
|
+
for (const obstacle of obstacles) {
|
|
278
|
+
if (segmentIntersectsRectangle(points[index - 1], points[index], obstacle))
|
|
279
|
+
collisions += 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return collisions;
|
|
283
|
+
}
|
|
284
|
+
function candidateLength(points) {
|
|
285
|
+
let length = 0;
|
|
286
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
287
|
+
length +=
|
|
288
|
+
Math.abs(points[index].x - points[index - 1].x) +
|
|
289
|
+
Math.abs(points[index].y - points[index - 1].y);
|
|
290
|
+
}
|
|
291
|
+
return length;
|
|
292
|
+
}
|
|
293
|
+
function detourRoute(routeStart, routeEnd, obstacle, obstacles) {
|
|
294
|
+
const candidates = [
|
|
295
|
+
[
|
|
296
|
+
routeStart,
|
|
297
|
+
{ x: routeStart.x, y: obstacle.minY },
|
|
298
|
+
{ x: routeEnd.x, y: obstacle.minY },
|
|
299
|
+
routeEnd
|
|
300
|
+
],
|
|
301
|
+
[
|
|
302
|
+
routeStart,
|
|
303
|
+
{ x: routeStart.x, y: obstacle.maxY },
|
|
304
|
+
{ x: routeEnd.x, y: obstacle.maxY },
|
|
305
|
+
routeEnd
|
|
306
|
+
],
|
|
307
|
+
[
|
|
308
|
+
routeStart,
|
|
309
|
+
{ x: obstacle.minX, y: routeStart.y },
|
|
310
|
+
{ x: obstacle.minX, y: routeEnd.y },
|
|
311
|
+
routeEnd
|
|
312
|
+
],
|
|
313
|
+
[
|
|
314
|
+
routeStart,
|
|
315
|
+
{ x: obstacle.maxX, y: routeStart.y },
|
|
316
|
+
{ x: obstacle.maxX, y: routeEnd.y },
|
|
317
|
+
routeEnd
|
|
318
|
+
]
|
|
319
|
+
];
|
|
320
|
+
return [...candidates].sort((left, right) => {
|
|
321
|
+
const collisionDifference = candidateCollisionCount(left, obstacles) - candidateCollisionCount(right, obstacles);
|
|
322
|
+
if (collisionDifference !== 0)
|
|
323
|
+
return collisionDifference;
|
|
324
|
+
return candidateLength(left) - candidateLength(right);
|
|
325
|
+
})[0];
|
|
326
|
+
}
|
|
327
|
+
function avoidComponentObstacles(points, obstacles) {
|
|
328
|
+
let routed = compactOrthogonalPoints(points);
|
|
329
|
+
for (let pass = 0; pass < MAX_OBSTACLE_PASSES; pass += 1) {
|
|
330
|
+
let collision;
|
|
331
|
+
for (let segmentIndex = 1; segmentIndex < routed.length && collision === undefined; segmentIndex += 1) {
|
|
332
|
+
for (const obstacle of obstacles) {
|
|
333
|
+
if (segmentIntersectsRectangle(routed[segmentIndex - 1], routed[segmentIndex], obstacle)) {
|
|
334
|
+
collision = { segmentIndex, obstacle };
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (collision === undefined)
|
|
340
|
+
break;
|
|
341
|
+
routed = compactOrthogonalPoints(detourRoute(routed[0], routed.at(-1), collision.obstacle, obstacles));
|
|
342
|
+
}
|
|
343
|
+
return routed;
|
|
344
|
+
}
|
|
345
|
+
export function routeConnection(connection, components) {
|
|
346
|
+
const start = endpointPoint(connection.from, components);
|
|
347
|
+
const end = endpointPoint(connection.to, components);
|
|
348
|
+
const sx = formatNumber(start.x);
|
|
349
|
+
const sy = formatNumber(start.y);
|
|
350
|
+
const ex = formatNumber(end.x);
|
|
351
|
+
const ey = formatNumber(end.y);
|
|
352
|
+
if (connection.curve === 'bezier') {
|
|
353
|
+
const middleX = (start.x + end.x) / 2;
|
|
354
|
+
const controlA = { x: middleX, y: start.y };
|
|
355
|
+
const controlB = { x: middleX, y: end.y };
|
|
356
|
+
return {
|
|
357
|
+
d: `M ${sx} ${sy} C ${formatNumber(controlA.x)} ${formatNumber(controlA.y)}, ${formatNumber(controlB.x)} ${formatNumber(controlB.y)}, ${ex} ${ey}`,
|
|
358
|
+
points: [start, controlA, controlB, end]
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (connection.curve === 'ortho') {
|
|
362
|
+
const middleX = (start.x + end.x) / 2;
|
|
363
|
+
const cornerA = { x: middleX, y: start.y };
|
|
364
|
+
const cornerB = { x: middleX, y: end.y };
|
|
365
|
+
const obstacles = Array.from(components.values())
|
|
366
|
+
.filter((component) => component.id !== connection.from.componentId && component.id !== connection.to.componentId)
|
|
367
|
+
.map((component) => componentObstacleRectangle(component));
|
|
368
|
+
const points = avoidComponentObstacles([start, cornerA, cornerB, end], obstacles);
|
|
369
|
+
return {
|
|
370
|
+
d: orthogonalPath(points),
|
|
371
|
+
points: points
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return { d: `M ${sx} ${sy} L ${ex} ${ey}`, points: [start, end] };
|
|
375
|
+
}
|
|
376
|
+
function axisSegments(route, routeIndex) {
|
|
377
|
+
const segments = [];
|
|
378
|
+
for (let index = 1; index < route.points.length; index += 1) {
|
|
379
|
+
const start = route.points[index - 1];
|
|
380
|
+
const end = route.points[index];
|
|
381
|
+
if (start.x === end.x && start.y !== end.y) {
|
|
382
|
+
segments.push({ routeIndex, segmentIndex: index - 1, start, end, orientation: 'vertical' });
|
|
383
|
+
}
|
|
384
|
+
else if (start.y === end.y && start.x !== end.x) {
|
|
385
|
+
segments.push({ routeIndex, segmentIndex: index - 1, start, end, orientation: 'horizontal' });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return segments;
|
|
389
|
+
}
|
|
390
|
+
function segmentBucketKeys(segment) {
|
|
391
|
+
const minX = Math.floor(Math.min(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
392
|
+
const maxX = Math.floor(Math.max(segment.start.x, segment.end.x) / CROSSING_BUCKET_SIZE);
|
|
393
|
+
const minY = Math.floor(Math.min(segment.start.y, segment.end.y) / CROSSING_BUCKET_SIZE);
|
|
394
|
+
const maxY = Math.floor(Math.max(segment.start.y, segment.end.y) / CROSSING_BUCKET_SIZE);
|
|
395
|
+
const keys = [];
|
|
396
|
+
for (let x = minX; x <= maxX; x += 1) {
|
|
397
|
+
for (let y = minY; y <= maxY; y += 1)
|
|
398
|
+
keys.push(`${x}:${y}`);
|
|
399
|
+
}
|
|
400
|
+
return keys;
|
|
401
|
+
}
|
|
402
|
+
function segmentCrossing(left, right) {
|
|
403
|
+
if (left.orientation === right.orientation)
|
|
404
|
+
return undefined;
|
|
405
|
+
const horizontal = left.orientation === 'horizontal' ? left : right;
|
|
406
|
+
const vertical = left.orientation === 'vertical' ? left : right;
|
|
407
|
+
const point = { x: vertical.start.x, y: horizontal.start.y };
|
|
408
|
+
if (point.x <= Math.min(horizontal.start.x, horizontal.end.x) ||
|
|
409
|
+
point.x >= Math.max(horizontal.start.x, horizontal.end.x) ||
|
|
410
|
+
point.y <= Math.min(vertical.start.y, vertical.end.y) ||
|
|
411
|
+
point.y >= Math.max(vertical.start.y, vertical.end.y)) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
return point;
|
|
415
|
+
}
|
|
416
|
+
function bridgedOrthogonalPath(route, crossings) {
|
|
417
|
+
if (crossings.size === 0)
|
|
418
|
+
return route;
|
|
419
|
+
const first = route.points[0];
|
|
420
|
+
let path = `M ${formatNumber(first.x)} ${formatNumber(first.y)}`;
|
|
421
|
+
const boundsPoints = [...route.points];
|
|
422
|
+
for (let index = 1; index < route.points.length; index += 1) {
|
|
423
|
+
const start = route.points[index - 1];
|
|
424
|
+
const end = route.points[index];
|
|
425
|
+
const segmentCrossings = [...(crossings.get(index - 1) ?? [])];
|
|
426
|
+
const horizontal = start.y === end.y;
|
|
427
|
+
const direction = Math.sign(horizontal ? end.x - start.x : end.y - start.y);
|
|
428
|
+
const coordinate = horizontal ? 'x' : 'y';
|
|
429
|
+
segmentCrossings.sort((left, right) => direction * (left[coordinate] - right[coordinate]));
|
|
430
|
+
if (horizontal) {
|
|
431
|
+
for (const crossing of segmentCrossings) {
|
|
432
|
+
const before = crossing.x - direction * SCHEMATIC_BRIDGE_RADIUS;
|
|
433
|
+
const after = crossing.x + direction * SCHEMATIC_BRIDGE_RADIUS;
|
|
434
|
+
path += ` H ${formatNumber(before)} A ${SCHEMATIC_BRIDGE_RADIUS} ${SCHEMATIC_BRIDGE_RADIUS} 0 0 ${direction > 0 ? 1 : 0} ${formatNumber(after)} ${formatNumber(crossing.y)}`;
|
|
435
|
+
boundsPoints.push({ x: crossing.x, y: crossing.y - SCHEMATIC_BRIDGE_RADIUS });
|
|
436
|
+
}
|
|
437
|
+
path += ` H ${formatNumber(end.x)}`;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
for (const crossing of segmentCrossings) {
|
|
441
|
+
const before = crossing.y - direction * SCHEMATIC_BRIDGE_RADIUS;
|
|
442
|
+
const after = crossing.y + direction * SCHEMATIC_BRIDGE_RADIUS;
|
|
443
|
+
path += ` V ${formatNumber(before)} A ${SCHEMATIC_BRIDGE_RADIUS} ${SCHEMATIC_BRIDGE_RADIUS} 0 0 ${direction > 0 ? 0 : 1} ${formatNumber(crossing.x)} ${formatNumber(after)}`;
|
|
444
|
+
boundsPoints.push({ x: crossing.x + SCHEMATIC_BRIDGE_RADIUS, y: crossing.y });
|
|
445
|
+
}
|
|
446
|
+
path += ` V ${formatNumber(end.y)}`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
d: path,
|
|
451
|
+
points: boundsPoints
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
export function routeConnections(connections, components) {
|
|
455
|
+
const routes = connections.map((connection) => routeConnection(connection, components));
|
|
456
|
+
const buckets = new Map();
|
|
457
|
+
const crossingsByRoute = new Map();
|
|
458
|
+
for (let routeIndex = 0; routeIndex < routes.length; routeIndex += 1) {
|
|
459
|
+
const route = routes[routeIndex];
|
|
460
|
+
for (const segment of axisSegments(route, routeIndex)) {
|
|
461
|
+
const visited = new Set();
|
|
462
|
+
for (const key of segmentBucketKeys(segment)) {
|
|
463
|
+
for (const previous of buckets.get(key) ?? []) {
|
|
464
|
+
const identity = `${previous.routeIndex}:${previous.segmentIndex}`;
|
|
465
|
+
if (visited.has(identity))
|
|
466
|
+
continue;
|
|
467
|
+
visited.add(identity);
|
|
468
|
+
const crossing = segmentCrossing(segment, previous);
|
|
469
|
+
if (crossing === undefined)
|
|
470
|
+
continue;
|
|
471
|
+
let routeCrossings = crossingsByRoute.get(routeIndex);
|
|
472
|
+
if (routeCrossings === undefined) {
|
|
473
|
+
routeCrossings = new Map();
|
|
474
|
+
crossingsByRoute.set(routeIndex, routeCrossings);
|
|
475
|
+
}
|
|
476
|
+
const points = routeCrossings.get(segment.segmentIndex) ?? [];
|
|
477
|
+
if (!points.some((point) => samePoint(point, crossing)))
|
|
478
|
+
points.push(crossing);
|
|
479
|
+
routeCrossings.set(segment.segmentIndex, points);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for (const segment of axisSegments(route, routeIndex)) {
|
|
484
|
+
for (const key of segmentBucketKeys(segment)) {
|
|
485
|
+
const bucket = buckets.get(key) ?? [];
|
|
486
|
+
bucket.push(segment);
|
|
487
|
+
buckets.set(key, bucket);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return routes.map((route, index) => bridgedOrthogonalPath(route, crossingsByRoute.get(index) ?? new Map()));
|
|
492
|
+
}
|
|
493
|
+
function componentHalfExtents(component) {
|
|
494
|
+
let halfWidth;
|
|
495
|
+
let halfHeight;
|
|
496
|
+
if (isClassicalGate(component)) {
|
|
497
|
+
halfWidth = 48;
|
|
498
|
+
halfHeight = classicalGateHeight(component) / 2;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
switch (component.kind) {
|
|
502
|
+
case 'resistor':
|
|
503
|
+
case 'capacitor':
|
|
504
|
+
case 'inductor':
|
|
505
|
+
halfWidth = 42;
|
|
506
|
+
halfHeight = 18;
|
|
507
|
+
break;
|
|
508
|
+
case 'diode':
|
|
509
|
+
halfWidth = 42;
|
|
510
|
+
halfHeight = component.diodeType === 'led' ? 30 : 20;
|
|
511
|
+
break;
|
|
512
|
+
case 'transistor':
|
|
513
|
+
halfWidth = 42;
|
|
514
|
+
halfHeight = 38;
|
|
515
|
+
break;
|
|
516
|
+
case 'port':
|
|
517
|
+
halfWidth = 42;
|
|
518
|
+
halfHeight = 18;
|
|
519
|
+
break;
|
|
520
|
+
case 'ground':
|
|
521
|
+
halfWidth = 22;
|
|
522
|
+
halfHeight = 42;
|
|
523
|
+
break;
|
|
524
|
+
case 'hadamard':
|
|
525
|
+
case 'qgate':
|
|
526
|
+
halfWidth = 48;
|
|
527
|
+
halfHeight = 30;
|
|
528
|
+
break;
|
|
529
|
+
case 'cnot':
|
|
530
|
+
halfWidth = 42;
|
|
531
|
+
halfHeight = 26;
|
|
532
|
+
break;
|
|
533
|
+
case 'ic':
|
|
534
|
+
halfWidth = component.bodyWidth / 2 + 16;
|
|
535
|
+
halfHeight = component.bodyHeight / 2 + 16;
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return { halfWidth, halfHeight };
|
|
540
|
+
}
|
|
541
|
+
export function componentTextAnchors(component) {
|
|
542
|
+
const { halfHeight } = componentHalfExtents(component);
|
|
543
|
+
return {
|
|
544
|
+
designatorY: -halfHeight - DESIGNATOR_BASELINE_GAP,
|
|
545
|
+
labelY: halfHeight + LABEL_BASELINE_GAP,
|
|
546
|
+
designatorWidth: Array.from(component.id).length * TEXT_ADVANCE,
|
|
547
|
+
labelWidth: mathLabelGlyphLength(component.label) * TEXT_ADVANCE
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
export function componentRectangle(component) {
|
|
551
|
+
const { halfWidth } = componentHalfExtents(component);
|
|
552
|
+
const anchors = componentTextAnchors(component);
|
|
553
|
+
const textHalfWidth = Math.max(anchors.designatorWidth, anchors.labelWidth) / 2 + TEXT_HORIZONTAL_GUTTER;
|
|
554
|
+
const boundedHalfWidth = Math.max(halfWidth, textHalfWidth);
|
|
555
|
+
const rectangle = {
|
|
556
|
+
minX: component.x - boundedHalfWidth,
|
|
557
|
+
minY: component.y + anchors.designatorY - TEXT_ASCENT_GUTTER,
|
|
558
|
+
maxX: component.x + boundedHalfWidth,
|
|
559
|
+
maxY: component.y + anchors.labelY + TEXT_DESCENT_GUTTER
|
|
560
|
+
};
|
|
561
|
+
for (const port of enumerateComponentPorts(component)) {
|
|
562
|
+
rectangle.minX = Math.min(rectangle.minX, port.point.x - PORT_HOTSPOT_RADIUS);
|
|
563
|
+
rectangle.minY = Math.min(rectangle.minY, port.point.y - PORT_HOTSPOT_RADIUS);
|
|
564
|
+
rectangle.maxX = Math.max(rectangle.maxX, port.point.x + PORT_HOTSPOT_RADIUS);
|
|
565
|
+
rectangle.maxY = Math.max(rectangle.maxY, port.point.y + PORT_HOTSPOT_RADIUS);
|
|
566
|
+
}
|
|
567
|
+
return rectangle;
|
|
568
|
+
}
|
|
569
|
+
function rectangleInsideBounds(rectangle, bounds) {
|
|
570
|
+
return (rectangle.minX >= 0 &&
|
|
571
|
+
rectangle.minY >= 0 &&
|
|
572
|
+
rectangle.maxX <= bounds.width &&
|
|
573
|
+
rectangle.maxY <= bounds.height);
|
|
574
|
+
}
|
|
575
|
+
function pointInsideBounds(point, bounds) {
|
|
576
|
+
return point.x >= 0 && point.y >= 0 && point.x <= bounds.width && point.y <= bounds.height;
|
|
577
|
+
}
|
|
578
|
+
export function validateDocumentGeometry(document, fence) {
|
|
579
|
+
for (const component of document.components) {
|
|
580
|
+
if (!rectangleInsideBounds(componentRectangle(component), fence.bounds)) {
|
|
581
|
+
throw new SchematicSyntaxError(`${component.id} geometry exceeds the declared ${fence.bounds.width}x${fence.bounds.height} bounds.`, component.line);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const components = new Map(document.components.map((component) => [component.id, component]));
|
|
585
|
+
const routedConnections = routeConnections(document.connections, components);
|
|
586
|
+
for (const [index, connection] of document.connections.entries()) {
|
|
587
|
+
const routed = routedConnections[index];
|
|
588
|
+
if (!routed.points.every((point) => pointInsideBounds(point, fence.bounds))) {
|
|
589
|
+
throw new SchematicSyntaxError('Connection trace exceeds the declared schematic bounds.', connection.line);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard compiler budgets shared by the parser, renderer, and Marked integration.
|
|
3
|
+
*
|
|
4
|
+
* `parseSchematic` and `renderSchematic` enforce these limits for one diagram.
|
|
5
|
+
* `schematicMarkedExtension` additionally enforces them cumulatively across all
|
|
6
|
+
* schematic fences encountered during one Marked document pass.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
/** Maximum UTF-16 source characters accepted in one compilation pass. */
|
|
11
|
+
export declare const MAX_SCHEMATIC_SOURCE_CHARACTERS = 131072;
|
|
12
|
+
/** Maximum component declarations accepted in one document. */
|
|
13
|
+
export declare const MAX_SCHEMATIC_COMPONENTS = 512;
|
|
14
|
+
/** Maximum directed connections accepted in one document. */
|
|
15
|
+
export declare const MAX_SCHEMATIC_CONNECTIONS = 2048;
|
|
16
|
+
/** Maximum UTF-8 bytes the bounded SVG writer may emit. */
|
|
17
|
+
export declare const MAX_SCHEMATIC_SVG_OUTPUT_BYTES = 2097152;
|
|
18
|
+
/** Frozen runtime-readable form of every compiler allocation ceiling. */
|
|
19
|
+
export declare const SCHEMATIC_LIMITS: Readonly<{
|
|
20
|
+
sourceCharacters: 131072;
|
|
21
|
+
components: 512;
|
|
22
|
+
connections: 2048;
|
|
23
|
+
svgOutputBytes: 2097152;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Return the exact UTF-8 byte length without allocating an encoded copy.
|
|
27
|
+
*
|
|
28
|
+
* @param value - JavaScript string whose encoded output cost is required.
|
|
29
|
+
* @returns Number of UTF-8 bytes, including four-byte astral code points.
|
|
30
|
+
*/
|
|
31
|
+
export declare function utf8ByteLength(value: string): number;
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const MAX_SCHEMATIC_SOURCE_CHARACTERS = 131_072;
|
|
2
|
+
export const MAX_SCHEMATIC_COMPONENTS = 512;
|
|
3
|
+
export const MAX_SCHEMATIC_CONNECTIONS = 2_048;
|
|
4
|
+
export const MAX_SCHEMATIC_SVG_OUTPUT_BYTES = 2_097_152;
|
|
5
|
+
export const SCHEMATIC_LIMITS = Object.freeze({
|
|
6
|
+
sourceCharacters: MAX_SCHEMATIC_SOURCE_CHARACTERS,
|
|
7
|
+
components: MAX_SCHEMATIC_COMPONENTS,
|
|
8
|
+
connections: MAX_SCHEMATIC_CONNECTIONS,
|
|
9
|
+
svgOutputBytes: MAX_SCHEMATIC_SVG_OUTPUT_BYTES
|
|
10
|
+
});
|
|
11
|
+
export function utf8ByteLength(value) {
|
|
12
|
+
let bytes = 0;
|
|
13
|
+
for (const character of value) {
|
|
14
|
+
const codePoint = character.codePointAt(0);
|
|
15
|
+
if (codePoint <= 0x7f)
|
|
16
|
+
bytes += 1;
|
|
17
|
+
else if (codePoint <= 0x7ff)
|
|
18
|
+
bytes += 2;
|
|
19
|
+
else if (codePoint <= 0xffff)
|
|
20
|
+
bytes += 3;
|
|
21
|
+
else
|
|
22
|
+
bytes += 4;
|
|
23
|
+
}
|
|
24
|
+
return bytes;
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-only Marked adapter for compiling fenced `schemd` diagrams during SSR.
|
|
3
|
+
*
|
|
4
|
+
* The module imports no Marked runtime. Its factory returns ordinary extension
|
|
5
|
+
* hooks to a host-owned parser and enforces both per-diagram compiler ceilings
|
|
6
|
+
* and cumulative limits across each Markdown document pass.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import type { MarkedExtension } from 'marked';
|
|
11
|
+
import { type SchematicMarkedOptions } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Create a bounded Marked extension that compiles `schemd` fences into inline SVG.
|
|
14
|
+
*
|
|
15
|
+
* The returned extension keeps aggregate per-document budgets in closure state,
|
|
16
|
+
* resets them from Marked's `preprocess` hook, and recognizes only the canonical
|
|
17
|
+
* `schemd` language identifier.
|
|
18
|
+
*
|
|
19
|
+
* @param options - Output mode, accessible-title fallback, dynamic mode resolver,
|
|
20
|
+
* and trusted server-side diagnostic renderer.
|
|
21
|
+
* @returns A Marked extension suitable for a server-owned `Marked` instance.
|
|
22
|
+
*/
|
|
23
|
+
export declare function schematicMarkedExtension(options?: SchematicMarkedOptions): MarkedExtension;
|