@schemd/core 0.3.0 → 0.3.1
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/CHANGELOG.md +18 -0
- package/dist/layout.d.ts +12 -0
- package/dist/layout.js +13 -4
- package/dist/math-label.d.ts +5 -3
- package/dist/math-label.js +14 -28
- package/dist/renderer.js +11 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@schemd/core` are recorded here. Dates describe actual npm publication dates; unpublished versions deliberately use `Unreleased`.
|
|
4
4
|
|
|
5
|
+
## [0.3.1] - 07/20/2026
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Orthogonal routing no longer fails diagrams whose components sit closer than twice the 12-unit clearance margin. The post-routing guard now rejects only physical body clips; escape stubs may legitimately pass through a neighbor's clearance ring, so densely packed parallel wires route as straight traces instead of throwing `Orthogonal route intersects … after routing.`
|
|
10
|
+
- Empty `qgate` detail rows (`parameter=""`, `phase=""`, `matrix=""`) no longer reserve blank text space: layout and renderer now agree that empty details are absent, so such gates keep the canonical shared quantum shell.
|
|
11
|
+
- `embedded-css` output no longer emits keyboard-focusable component and wire groups beneath its `role="img"` root, which flattened them for assistive technology while leaving unlabeled tab stops. Internal `tabindex`/ARIA semantics are now exclusive to `full` mode, and every `full`-mode root is `role="group"` regardless of which semantic hooks are enabled.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- `renderSchematic` skips the redundant geometry revalidation pass when the parser's route cache proves the same frozen document already validated against identical bounds, and computes the AST-serializing signature hash only when no `idPrefix` is supplied. Rendered output is byte-identical; hosts that pass `idPrefix` (such as compile endpoints) no longer pay an `O(document)` serialization per render.
|
|
16
|
+
|
|
17
|
+
### Verified release-candidate measurements
|
|
18
|
+
|
|
19
|
+
- Compiler bundle: 90,294 B minified, 26,479 B gzip — 4,241 B below the 30,720 B gate and 33 B smaller than 0.3.0.
|
|
20
|
+
- Coverage: 100% statements, branches, functions, and lines across 123 tests, including new regressions for sub-clearance routing, single-track barriers, and empty quantum detail rows.
|
|
21
|
+
- Runtime dependencies: zero. No public API, grammar, or geometry contracts changed; no migration is required.
|
|
22
|
+
|
|
5
23
|
## [0.3.0] - 07/19/2026
|
|
6
24
|
|
|
7
25
|
### Added
|
package/dist/layout.d.ts
CHANGED
|
@@ -107,6 +107,18 @@ export declare function rotateQuarterExtents(extents: SchematicHalfExtents, turn
|
|
|
107
107
|
* @returns Relative coordinate between negative and positive half-span.
|
|
108
108
|
*/
|
|
109
109
|
export declare function distributedCoordinate(index: number, count: number, span: number): number;
|
|
110
|
+
/** Total centered height of a quantum glyph carrying `tracks` qubit lines. */
|
|
111
|
+
export declare function quantumTrackSpan(tracks: number): number;
|
|
112
|
+
/**
|
|
113
|
+
* Vertical offset of quantum track `index` within a `tracks`-line glyph.
|
|
114
|
+
*
|
|
115
|
+
* Unlike {@link distributedCoordinate}, tracks are spread *edge-to-edge* so the
|
|
116
|
+
* outermost rails land exactly at ±span/2 — where the connecting bar and body
|
|
117
|
+
* extent terminate. That keeps control dots, targets, and swap crosses on rails
|
|
118
|
+
* a full pitch apart instead of collapsing a two-qubit swap into an overlapped
|
|
119
|
+
* star. Renderer and port geometry both call this, so marks and wires align.
|
|
120
|
+
*/
|
|
121
|
+
export declare function quantumTrackCoordinate(index: number, tracks: number): number;
|
|
110
122
|
/**
|
|
111
123
|
* Compute a classical gate height that scales with its busiest terminal side.
|
|
112
124
|
*
|
package/dist/layout.js
CHANGED
|
@@ -83,11 +83,20 @@ function formatNumber(value) {
|
|
|
83
83
|
export function distributedCoordinate(index, count, span) {
|
|
84
84
|
return ((index + 1) * span) / (count + 1) - span / 2;
|
|
85
85
|
}
|
|
86
|
+
export function quantumTrackSpan(tracks) {
|
|
87
|
+
return Math.max(16, (tracks - 1) * 18);
|
|
88
|
+
}
|
|
89
|
+
export function quantumTrackCoordinate(index, tracks) {
|
|
90
|
+
if (tracks <= 1)
|
|
91
|
+
return 0;
|
|
92
|
+
const span = quantumTrackSpan(tracks);
|
|
93
|
+
return -span / 2 + (index * span) / (tracks - 1);
|
|
94
|
+
}
|
|
86
95
|
export function classicalGateHeight(component) {
|
|
87
96
|
return Math.max(48, Math.max(component.inputs, component.outputs) * 16);
|
|
88
97
|
}
|
|
89
98
|
export function quantumGateDimensions(component) {
|
|
90
|
-
const details = [component.parameter, component.phase, component.matrix].filter((value) => value !== undefined);
|
|
99
|
+
const details = [component.parameter, component.phase, component.matrix].filter((value) => value !== undefined && value !== '');
|
|
91
100
|
let widest = 16;
|
|
92
101
|
if (details.length > 0 && component.kind === 'qgate')
|
|
93
102
|
widest = mathLabelTextWidth(component.label, 8);
|
|
@@ -454,12 +463,12 @@ export function resolvePortPoint(component, port) {
|
|
|
454
463
|
index += component.controls;
|
|
455
464
|
const tracks = Math.max(component.wires, component.controls + component.targets);
|
|
456
465
|
if (port.startsWith('control') || port.startsWith('target')) {
|
|
457
|
-
local = { x: 0, y:
|
|
466
|
+
local = { x: 0, y: quantumTrackCoordinate(index, tracks) };
|
|
458
467
|
}
|
|
459
468
|
else {
|
|
460
469
|
local = {
|
|
461
470
|
x: port.startsWith('out') ? 42 : -42,
|
|
462
|
-
y:
|
|
471
|
+
y: quantumTrackCoordinate(index, tracks)
|
|
463
472
|
};
|
|
464
473
|
}
|
|
465
474
|
break;
|
|
@@ -1091,7 +1100,7 @@ function routeConnectionInternal(connection, components, bounds, cachedObstacles
|
|
|
1091
1100
|
for (const obstacle of obstacleCache) {
|
|
1092
1101
|
if (!(allowFrom && obstacle.id === from.component.id) &&
|
|
1093
1102
|
!(allowTo && obstacle.id === to.component.id) &&
|
|
1094
|
-
segmentIntersectsRectangle(points[index - 1], points[index],
|
|
1103
|
+
segmentIntersectsRectangle(points[index - 1], points[index], obstacle.body)) {
|
|
1095
1104
|
throw new SchematicSyntaxError(`Orthogonal route intersects ${obstacle.id} after routing.`, connection.line);
|
|
1096
1105
|
}
|
|
1097
1106
|
}
|
package/dist/math-label.d.ts
CHANGED
|
@@ -57,9 +57,11 @@ export declare function mathLabelTextWidth(value: string, cellAdvance?: number):
|
|
|
57
57
|
/**
|
|
58
58
|
* Emit escaped inline SVG text with explicit baseline restoration.
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* Baseline shifts are tracked in root-em units, but SVG resolves a `<tspan>`'s
|
|
61
|
+
* `dy="…em"` against *that tspan's own* (scaled) font-size. Each emitted `dy` is
|
|
62
|
+
* therefore the root-em delta divided by the tspan's font scale, so a shift of
|
|
63
|
+
* `d` root-em applied on a `s`-scaled run renders as exactly `d` — nested and
|
|
64
|
+
* consecutive scripts return to the parent baseline with zero drift.
|
|
63
65
|
*
|
|
64
66
|
* @param value - Raw micro-math label.
|
|
65
67
|
* @returns Trusted compiler-owned XML text suitable inside an SVG `<text>` node.
|
package/dist/math-label.js
CHANGED
|
@@ -191,33 +191,19 @@ export function renderMathLabelTspans(value) {
|
|
|
191
191
|
if (segments.length === 1 && segments[0]?.kind === 'text' && segments[0].value === value) {
|
|
192
192
|
return escapeXml(value);
|
|
193
193
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
markup += `<tspan dy="${delta}em" font-size="${fontPercent}%">${escapeXml(segment.value)}</tspan>`;
|
|
205
|
-
previousShift = shift;
|
|
206
|
-
}
|
|
207
|
-
if (previousShift !== 0) {
|
|
208
|
-
markup += `<tspan dy="${Number((-previousShift).toFixed(4))}em" font-size="100%"></tspan>`;
|
|
209
|
-
}
|
|
210
|
-
return markup;
|
|
194
|
+
let previousShift = 0;
|
|
195
|
+
let markup = '';
|
|
196
|
+
for (const segment of segments) {
|
|
197
|
+
const shift = segment.baselineShiftEm ??
|
|
198
|
+
(segment.kind === 'subscript' ? 0.35 : segment.kind === 'superscript' ? -0.55 : 0);
|
|
199
|
+
const scale = segment.fontScale ?? (segment.kind === 'text' ? 1 : 0.7);
|
|
200
|
+
const delta = Number(((shift - previousShift) / scale).toFixed(4));
|
|
201
|
+
const fontPercent = Number((scale * 100).toFixed(2));
|
|
202
|
+
markup += `<tspan dy="${delta}em" font-size="${fontPercent}%">${escapeXml(segment.value)}</tspan>`;
|
|
203
|
+
previousShift = shift;
|
|
211
204
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return `<tspan dy="0">${content}</tspan>`;
|
|
217
|
-
if (segment.kind === 'subscript') {
|
|
218
|
-
return `<tspan dy="0.35em" font-size="70%">${content}</tspan><tspan dy="-0.35em" font-size="100%"></tspan>`;
|
|
219
|
-
}
|
|
220
|
-
return `<tspan dy="-0.55em" font-size="70%">${content}</tspan><tspan dy="0.55em" font-size="100%"></tspan>`;
|
|
221
|
-
})
|
|
222
|
-
.join('');
|
|
205
|
+
if (previousShift !== 0) {
|
|
206
|
+
markup += `<tspan dy="${Number((-previousShift).toFixed(4))}em" font-size="100%"></tspan>`;
|
|
207
|
+
}
|
|
208
|
+
return markup;
|
|
223
209
|
}
|
package/dist/renderer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { classicalGateHeight, componentTextAnchors, distributedCoordinate, enumerateComponentPorts, PORT_HOTSPOT_RADIUS, quantumGateDimensions, routeConnections, validateDocumentGeometry } from './layout.js';
|
|
1
|
+
import { classicalGateHeight, componentTextAnchors, distributedCoordinate, enumerateComponentPorts, PORT_HOTSPOT_RADIUS, quantumGateDimensions, quantumTrackCoordinate, quantumTrackSpan, routeConnections, validateDocumentGeometry } from './layout.js';
|
|
2
2
|
import { MAX_SCHEMATIC_SVG_OUTPUT_BYTES, utf8ByteLength } from './limits.js';
|
|
3
3
|
import { assertParsedSchematicDocument } from './parser.js';
|
|
4
4
|
import { parsedSchematicRoutes } from './route-cache.js';
|
|
@@ -560,14 +560,14 @@ function quantumSpecialShape(component, paint, nodePaint) {
|
|
|
560
560
|
if (component.kind === 'classical-bit' || component.kind === 'classical-register')
|
|
561
561
|
return `<path ${paint} d="M -40 -2 H 40 M -40 2 H 40" />`;
|
|
562
562
|
const tracks = Math.max(component.wires, component.controls + component.targets);
|
|
563
|
-
const span =
|
|
564
|
-
const lines = Array.from({ length: tracks }, (_, index) => `<path ${paint} d="M -42 ${svgNumber(
|
|
563
|
+
const span = quantumTrackSpan(tracks);
|
|
564
|
+
const lines = Array.from({ length: tracks }, (_, index) => `<path ${paint} d="M -42 ${svgNumber(quantumTrackCoordinate(index, tracks))} H 42" />`).join('');
|
|
565
565
|
if (component.kind === 'barrier')
|
|
566
566
|
return `${lines}<path ${paint} stroke-dasharray="4 3" d="M 0 ${svgNumber(-span / 2 - 8)} V ${svgNumber(span / 2 + 8)}" />`;
|
|
567
567
|
if (component.kind === 'delay')
|
|
568
568
|
return `${lines}<rect ${paint} x="-12" y="${svgNumber(-span / 2 - 7)}" width="24" height="${svgNumber(span + 14)}" rx="2" />`;
|
|
569
569
|
const marks = Array.from({ length: tracks }, (_, index) => {
|
|
570
|
-
const y =
|
|
570
|
+
const y = quantumTrackCoordinate(index, tracks);
|
|
571
571
|
if (component.kind === 'swap')
|
|
572
572
|
return `<path ${paint} d="M -6 ${svgNumber(y - 6)} L 6 ${svgNumber(y + 6)} M 6 ${svgNumber(y - 6)} L -6 ${svgNumber(y + 6)}" />`;
|
|
573
573
|
if (component.kind === 'cz')
|
|
@@ -790,7 +790,7 @@ function compactConnectionMarkup(connections, routes, idPrefix, embedVisuals, gl
|
|
|
790
790
|
const labels = batch.labels.join('');
|
|
791
791
|
if (!embedVisuals)
|
|
792
792
|
return trace + endpoints + labels;
|
|
793
|
-
return `<g class="schematic-wire"
|
|
793
|
+
return `<g class="schematic-wire">${trace}<use class="schematic-glow-layer" href="#${traceId}" filter="url(#${glowId})" aria-hidden="true" pointer-events="none" />${endpoints}${labels}</g>`;
|
|
794
794
|
}).join('');
|
|
795
795
|
}
|
|
796
796
|
function componentMetadata(component) {
|
|
@@ -861,7 +861,7 @@ function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, po
|
|
|
861
861
|
? ` data-node-id="${id}" data-node-kind="${component.kind}" data-node-label="${label}" data-component="${id}" data-kind="${component.kind}" data-source-line="${component.line}"${'orientation' in component && component.orientation !== undefined ? ` data-orientation="${component.orientation}"` : ''}`
|
|
862
862
|
: '';
|
|
863
863
|
let accessibility = '';
|
|
864
|
-
if (
|
|
864
|
+
if (mode === 'full') {
|
|
865
865
|
const metadata = componentMetadata(component);
|
|
866
866
|
const ariaLabel = escapeXml(`${component.id}, ${component.kind}, ${component.label}${metadata === '' ? '' : `, ${metadata}`}`);
|
|
867
867
|
accessibility = `${portHooks ? '' : ' tabindex="0"'} role="group" aria-label="${ariaLabel}"`;
|
|
@@ -883,15 +883,13 @@ function componentMarkup(component, index, idPrefix, glowId, mode, nodeHooks, po
|
|
|
883
883
|
export function renderSchematic(document, options) {
|
|
884
884
|
assertParsedSchematicDocument(document);
|
|
885
885
|
const normalized = normalizeCompileOptions(options);
|
|
886
|
-
const components = new Map(document.components.map((component) => [component.id, component]));
|
|
887
886
|
const routedConnections = parsedSchematicRoutes(document, normalized.bounds) ??
|
|
888
|
-
routeConnections(document.connections, components, normalized.bounds);
|
|
889
|
-
|
|
890
|
-
const
|
|
891
|
-
const candidatePrefix = (normalized.idPrefix ?? `schematic-${stableHash(signature)}`).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
887
|
+
validateDocumentGeometry(document, normalized, routeConnections(document.connections, new Map(document.components.map((component) => [component.id, component])), normalized.bounds));
|
|
888
|
+
const signatureHash = () => stableHash(`${normalized.bounds.width}x${normalized.bounds.height}:${normalized.title}:${JSON.stringify(document)}`);
|
|
889
|
+
const candidatePrefix = (normalized.idPrefix ?? `schematic-${signatureHash()}`).replace(/[^A-Za-z0-9_-]/g, '-');
|
|
892
890
|
const safePrefix = /[A-Za-z0-9]/.test(candidatePrefix)
|
|
893
891
|
? candidatePrefix
|
|
894
|
-
: `schematic-${
|
|
892
|
+
: `schematic-${signatureHash()}`;
|
|
895
893
|
const titleId = `${safePrefix}-title`;
|
|
896
894
|
const descriptionId = `${safePrefix}-description`;
|
|
897
895
|
const gridId = `${safePrefix}-grid`;
|
|
@@ -924,7 +922,7 @@ export function renderSchematic(document, options) {
|
|
|
924
922
|
? `<style>${STATIC_SVG_STYLES}${hookStyles}${interactionStyles}</style><pattern id="${gridId}" width="20" height="20" patternUnits="userSpaceOnUse"><path class="schematic-grid-line" d="M20 0H0V20" /></pattern>${glowFilter}`
|
|
925
923
|
: '';
|
|
926
924
|
const definitions = `${embeddedDefinitions}${markerDefinitions}${symbolDefinitions}`;
|
|
927
|
-
const svgRole =
|
|
925
|
+
const svgRole = hooks ? 'group' : 'img';
|
|
928
926
|
writer.append(`<figure class="schematic-frame"${hooks ? ' data-schematic' : ''}><svg class="schematic-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${normalized.bounds.width} ${normalized.bounds.height}" width="${normalized.bounds.width}" height="${normalized.bounds.height}" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" role="${svgRole}" aria-labelledby="${titleId} ${descriptionId}" preserveAspectRatio="xMidYMid meet"><title id="${titleId}">${title}</title><desc id="${descriptionId}">${description}</desc>${definitions === '' ? '' : `<defs>${definitions}</defs>`}${styles ? `<rect class="schematic-surface" width="100%" height="100%" /><rect class="schematic-grid" width="100%" height="100%" fill="url(#${gridId})" />` : ''}<g class="schematic-vectors">`);
|
|
929
927
|
if (wireHooks) {
|
|
930
928
|
for (const [index, connection] of document.connections.entries()) {
|