circuitjson-toolkit 1.0.17 → 1.1.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/NOTICE.md +31 -0
- package/README.md +274 -107
- package/docs/api.md +507 -96
- package/docs/capabilities.md +70 -0
- package/docs/migration/behaviors.md +45 -0
- package/docs/migration/parser.md +60 -0
- package/docs/migration/renderers.md +515 -0
- package/docs/migration/root.md +740 -0
- package/docs/migration.md +126 -0
- package/docs/model-format.md +175 -57
- package/docs/provenance.md +206 -0
- package/docs/release-notes-v1.1.0.md +154 -0
- package/docs/release-notes-v1.1.1.md +24 -0
- package/docs/testing.md +122 -7
- package/package.json +32 -5
- package/spec/api-baseline-v1.0.17.json +1 -0
- package/spec/baseline-provenance-v1.0.17.json +7 -0
- package/spec/circuitjson-schema-snapshot.json +321 -0
- package/spec/circuitjson-schema-source.json +28 -0
- package/spec/feature-preservation.json +1 -0
- package/spec/library-scope.md +27 -20
- package/src/capabilities.mjs +1 -0
- package/src/core/ArchiveEntryPath.mjs +93 -0
- package/src/core/ArchiveLimits.mjs +31 -0
- package/src/core/ArchiveLimitsValidator.mjs +107 -0
- package/src/core/AsyncInputOwnership.mjs +56 -0
- package/src/core/AttachedValueLimits.mjs +67 -0
- package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
- package/src/core/CircuitJsonDocument.mjs +19 -61
- package/src/core/CircuitJsonElementTypes.mjs +10 -0
- package/src/core/CircuitJsonElementValidator.mjs +98 -847
- package/src/core/CircuitJsonIndexer.mjs +274 -194
- package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
- package/src/core/CircuitJsonParser.mjs +75 -13
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
- package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
- package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
- package/src/core/CircuitJsonUnitParsers.mjs +101 -0
- package/src/core/CircuitJsonUnits.mjs +13 -87
- package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
- package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
- package/src/core/CircuitJsonValidationUnits.mjs +6 -0
- package/src/core/ManufacturingService.mjs +323 -0
- package/src/core/Parser.mjs +343 -0
- package/src/core/ParserOptions.mjs +333 -0
- package/src/core/PcbBoundsSelectionModel.mjs +55 -19
- package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
- package/src/core/PcbInteractionIndex.mjs +368 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
- package/src/core/ProjectAsyncInputOwner.mjs +70 -0
- package/src/core/ProjectLoader.mjs +975 -0
- package/src/core/SimulationService.mjs +790 -0
- package/src/core/ToolkitCapabilities.mjs +130 -0
- package/src/core/ZipArchiveInspector.mjs +649 -0
- package/src/core/context/BinaryDataSnapshot.mjs +217 -0
- package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
- package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
- package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
- package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
- package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
- package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
- package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
- package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
- package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
- package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
- package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
- package/src/core/context/PcbSpatialIndex.mjs +701 -0
- package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
- package/src/core/context/StructuredDataSnapshot.mjs +683 -0
- package/src/core/contracts/DocumentResult.mjs +198 -0
- package/src/core/contracts/ProjectResult.mjs +96 -0
- package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
- package/src/core/contracts/ToolkitAsset.mjs +493 -0
- package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
- package/src/core/contracts/ToolkitError.mjs +176 -0
- package/src/core/contracts/ToolkitProgress.mjs +89 -0
- package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
- package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
- package/src/core/query/CircuitTraversal.mjs +343 -0
- package/src/core/query/ComponentGrouping.mjs +275 -0
- package/src/core/query/QueryNetlistBuilder.mjs +306 -0
- package/src/core/query/QueryService.mjs +435 -0
- package/src/core/query/RegexPattern.mjs +75 -0
- package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
- package/src/core/rendering/CanonicalBomRows.mjs +92 -0
- package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
- package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
- package/src/core/rendering/PcbRenderPlan.mjs +429 -0
- package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
- package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
- package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
- package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
- package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
- package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
- package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
- package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
- package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
- package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
- package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
- package/src/core/scene3d/Scene3dOptions.mjs +363 -0
- package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
- package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
- package/src/core/worker/ParserWorkerClient.mjs +999 -0
- package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
- package/src/core/worker/WorkerRequestData.mjs +650 -0
- package/src/core/worker/WorkerResponseData.mjs +905 -0
- package/src/extensions.mjs +37 -0
- package/src/index.mjs +14 -9
- package/src/interaction.mjs +2 -0
- package/src/manufacturing.mjs +1 -0
- package/src/parser.mjs +12 -2
- package/src/project.mjs +5 -0
- package/src/query.mjs +1 -0
- package/src/renderers.mjs +3 -29
- package/src/scene3d.mjs +3 -0
- package/src/simulation.mjs +1 -0
- package/src/styles/renderers.css +24 -0
- package/src/testing/ToolkitContractFixtures.mjs +124 -0
- package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
- package/src/testing/runToolkitContract.mjs +705 -0
- package/src/testing.mjs +3 -0
- package/src/ui/BomTableRenderer.mjs +304 -0
- package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
- package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
- package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
- package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
- package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
- package/src/ui/PcbSvgRenderer.mjs +41 -0
- package/src/ui/SafeSvgPaint.mjs +26 -0
- package/src/ui/SafeXmlText.mjs +60 -0
- package/src/ui/SchematicGeometryBounds.mjs +540 -0
- package/src/ui/SchematicSvgRenderer.mjs +110 -0
- package/src/ui/SchematicTableGeometry.mjs +319 -0
- package/src/ui/SchematicTextAnchor.mjs +55 -0
- package/src/ui/SchematicTextBounds.mjs +98 -0
- package/src/workers/parser.worker.mjs +59 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { CircuitJsonUnits } from '../core/CircuitJsonUnits.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves shared schematic table rectangles and nine-point anchors.
|
|
5
|
+
*/
|
|
6
|
+
export class SchematicTableGeometry {
|
|
7
|
+
/**
|
|
8
|
+
* Builds table models keyed by their canonical ids.
|
|
9
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
10
|
+
* @returns {Map<string, object>} Table models.
|
|
11
|
+
*/
|
|
12
|
+
static models(index) {
|
|
13
|
+
const models = new Map()
|
|
14
|
+
for (const element of index.elementsByType.get('schematic_table') ||
|
|
15
|
+
[]) {
|
|
16
|
+
const model = SchematicTableGeometry.model(element)
|
|
17
|
+
if (model) models.set(model.id, model)
|
|
18
|
+
}
|
|
19
|
+
return models
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds one reusable schematic table model.
|
|
24
|
+
* @param {object} element Table element.
|
|
25
|
+
* @returns {object | null} Table model.
|
|
26
|
+
*/
|
|
27
|
+
static model(element) {
|
|
28
|
+
const id = String(element.schematic_table_id || '').trim()
|
|
29
|
+
const columns = SchematicTableGeometry.lengths(
|
|
30
|
+
element.column_widths || element.columnWidths || element.columns
|
|
31
|
+
)
|
|
32
|
+
const rows = SchematicTableGeometry.lengths(
|
|
33
|
+
element.row_heights || element.rowHeights || element.rows
|
|
34
|
+
)
|
|
35
|
+
const rect = SchematicTableGeometry.rect(
|
|
36
|
+
element,
|
|
37
|
+
SchematicTableGeometry.sum(columns),
|
|
38
|
+
SchematicTableGeometry.sum(rows)
|
|
39
|
+
)
|
|
40
|
+
if (!id || !rect) return null
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
element,
|
|
44
|
+
...rect,
|
|
45
|
+
columns,
|
|
46
|
+
rows,
|
|
47
|
+
columnOffsets: SchematicTableGeometry.#offsets(columns),
|
|
48
|
+
rowOffsets: SchematicTableGeometry.#offsets(rows),
|
|
49
|
+
padding: CircuitJsonUnits.length(element.cell_padding, 0),
|
|
50
|
+
borderWidth:
|
|
51
|
+
CircuitJsonUnits.optionalLength(element.border_width) ??
|
|
52
|
+
CircuitJsonUnits.optionalLength(element.borderWidth)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolves one complete table rectangle from its grid dimensions.
|
|
58
|
+
* @param {object} element Table element.
|
|
59
|
+
* @returns {{ x: number, y: number, width: number, height: number } | null} Table rectangle.
|
|
60
|
+
*/
|
|
61
|
+
static tableRect(element) {
|
|
62
|
+
const columns = SchematicTableGeometry.lengths(
|
|
63
|
+
element.column_widths || element.columnWidths || element.columns
|
|
64
|
+
)
|
|
65
|
+
const rows = SchematicTableGeometry.lengths(
|
|
66
|
+
element.row_heights || element.rowHeights || element.rows
|
|
67
|
+
)
|
|
68
|
+
return SchematicTableGeometry.rect(
|
|
69
|
+
element,
|
|
70
|
+
SchematicTableGeometry.sum(columns),
|
|
71
|
+
SchematicTableGeometry.sum(rows)
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolves a rectangle from center/size or anchor metadata.
|
|
77
|
+
* @param {object} element Element row.
|
|
78
|
+
* @param {number} [fallbackWidth] Fallback width.
|
|
79
|
+
* @param {number} [fallbackHeight] Fallback height.
|
|
80
|
+
* @returns {{ x: number, y: number, width: number, height: number } | null} Rectangle.
|
|
81
|
+
*/
|
|
82
|
+
static rect(element, fallbackWidth = 0, fallbackHeight = 0) {
|
|
83
|
+
const size = SchematicTableGeometry.#size(
|
|
84
|
+
element,
|
|
85
|
+
fallbackWidth,
|
|
86
|
+
fallbackHeight
|
|
87
|
+
)
|
|
88
|
+
const anchor = CircuitJsonUnits.optionalPoint(element.anchor_position)
|
|
89
|
+
if (size && anchor) {
|
|
90
|
+
return SchematicTableGeometry.#anchoredRect(
|
|
91
|
+
anchor,
|
|
92
|
+
size,
|
|
93
|
+
element.anchor
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
const center = CircuitJsonUnits.optionalPoint(element.center || element)
|
|
97
|
+
if (!center || !size) return null
|
|
98
|
+
return {
|
|
99
|
+
x: center.x - size.width / 2,
|
|
100
|
+
y: center.y - size.height / 2,
|
|
101
|
+
...size
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolves one direct or grid-addressed table-cell rectangle.
|
|
107
|
+
* @param {object} element Cell element.
|
|
108
|
+
* @param {object | undefined} table Parent table model.
|
|
109
|
+
* @returns {object | null} Cell rectangle.
|
|
110
|
+
*/
|
|
111
|
+
static cellRect(element, table) {
|
|
112
|
+
if (!table?.columns?.length || !table?.rows?.length) {
|
|
113
|
+
return SchematicTableGeometry.rect(element)
|
|
114
|
+
}
|
|
115
|
+
const row = SchematicTableGeometry.#index(
|
|
116
|
+
element.row ??
|
|
117
|
+
element.row_index ??
|
|
118
|
+
element.rowIndex ??
|
|
119
|
+
element.start_row_index
|
|
120
|
+
)
|
|
121
|
+
const column = SchematicTableGeometry.#index(
|
|
122
|
+
element.column ??
|
|
123
|
+
element.col ??
|
|
124
|
+
element.column_index ??
|
|
125
|
+
element.columnIndex ??
|
|
126
|
+
element.start_column_index
|
|
127
|
+
)
|
|
128
|
+
if (row === null || column === null) return null
|
|
129
|
+
if (row >= table.rows.length || column >= table.columns.length) {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
const endRow = SchematicTableGeometry.#index(element.end_row_index)
|
|
133
|
+
const endColumn = SchematicTableGeometry.#index(
|
|
134
|
+
element.end_column_index
|
|
135
|
+
)
|
|
136
|
+
const rowSpan = SchematicTableGeometry.#span(
|
|
137
|
+
element.row_span ??
|
|
138
|
+
element.rowSpan ??
|
|
139
|
+
(endRow === null ? undefined : endRow - row + 1)
|
|
140
|
+
)
|
|
141
|
+
const columnSpan = SchematicTableGeometry.#span(
|
|
142
|
+
element.col_span ??
|
|
143
|
+
element.column_span ??
|
|
144
|
+
element.colSpan ??
|
|
145
|
+
(endColumn === null ? undefined : endColumn - column + 1)
|
|
146
|
+
)
|
|
147
|
+
const columnEnd = Math.min(column + columnSpan, table.columns.length)
|
|
148
|
+
const rowEnd = Math.min(row + rowSpan, table.rows.length)
|
|
149
|
+
const width =
|
|
150
|
+
(table.columnOffsets[columnEnd] ?? 0) -
|
|
151
|
+
(table.columnOffsets[column] ?? 0)
|
|
152
|
+
const height =
|
|
153
|
+
(table.rowOffsets[rowEnd] ?? 0) - (table.rowOffsets[row] ?? 0)
|
|
154
|
+
if (width <= 0 || height <= 0) return null
|
|
155
|
+
return {
|
|
156
|
+
x: table.x + (table.columnOffsets[column] || 0),
|
|
157
|
+
y: table.y + (table.rowOffsets[row] || 0),
|
|
158
|
+
width,
|
|
159
|
+
height
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolves one table-cell text anchor shared by bounds and SVG output.
|
|
165
|
+
* @param {object} element Cell element.
|
|
166
|
+
* @param {object | undefined} table Parent table model.
|
|
167
|
+
* @param {object} rect Cell rectangle.
|
|
168
|
+
* @returns {{ x: number, y: number, anchor: string, baseline: string, namedAnchor: string }} Text position.
|
|
169
|
+
*/
|
|
170
|
+
static textPosition(element, table, rect) {
|
|
171
|
+
const padding = Number(table?.padding || 0)
|
|
172
|
+
const horizontal = String(
|
|
173
|
+
element.horizontal_align || element.horizontalAlign || ''
|
|
174
|
+
).toLowerCase()
|
|
175
|
+
const vertical = String(
|
|
176
|
+
element.vertical_align || element.verticalAlign || ''
|
|
177
|
+
).toLowerCase()
|
|
178
|
+
const horizontalName =
|
|
179
|
+
horizontal === 'left' || horizontal === 'start'
|
|
180
|
+
? 'left'
|
|
181
|
+
: horizontal === 'right' || horizontal === 'end'
|
|
182
|
+
? 'right'
|
|
183
|
+
: 'center'
|
|
184
|
+
const verticalName =
|
|
185
|
+
vertical === 'top'
|
|
186
|
+
? 'top'
|
|
187
|
+
: vertical === 'bottom'
|
|
188
|
+
? 'bottom'
|
|
189
|
+
: 'center'
|
|
190
|
+
return {
|
|
191
|
+
x:
|
|
192
|
+
horizontalName === 'left'
|
|
193
|
+
? rect.x + padding
|
|
194
|
+
: horizontalName === 'right'
|
|
195
|
+
? rect.x + rect.width - padding
|
|
196
|
+
: rect.x + rect.width / 2,
|
|
197
|
+
y:
|
|
198
|
+
verticalName === 'top'
|
|
199
|
+
? rect.y + padding
|
|
200
|
+
: verticalName === 'bottom'
|
|
201
|
+
? rect.y + rect.height - padding
|
|
202
|
+
: rect.y + rect.height / 2,
|
|
203
|
+
anchor:
|
|
204
|
+
horizontalName === 'left'
|
|
205
|
+
? 'start'
|
|
206
|
+
: horizontalName === 'right'
|
|
207
|
+
? 'end'
|
|
208
|
+
: 'middle',
|
|
209
|
+
baseline:
|
|
210
|
+
verticalName === 'top'
|
|
211
|
+
? 'hanging'
|
|
212
|
+
: verticalName === 'bottom'
|
|
213
|
+
? 'text-after-edge'
|
|
214
|
+
: 'central',
|
|
215
|
+
namedAnchor: `${verticalName}_${horizontalName}`
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Resolves an array of positive table lengths.
|
|
221
|
+
* @param {unknown} value Length array candidate.
|
|
222
|
+
* @returns {number[]} Positive lengths.
|
|
223
|
+
*/
|
|
224
|
+
static lengths(value) {
|
|
225
|
+
return (Array.isArray(value) ? value : [])
|
|
226
|
+
.map((entry) =>
|
|
227
|
+
CircuitJsonUnits.optionalLength(
|
|
228
|
+
entry?.width ?? entry?.height ?? entry
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
.filter((entry) => entry !== null && entry > 0)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Sums numeric table lengths.
|
|
236
|
+
* @param {number[]} values Values.
|
|
237
|
+
* @returns {number} Sum.
|
|
238
|
+
*/
|
|
239
|
+
static sum(values) {
|
|
240
|
+
return values.reduce((total, value) => total + value, 0)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Builds cumulative boundary offsets for a length list.
|
|
245
|
+
* @param {number[]} values Length values.
|
|
246
|
+
* @returns {number[]} Cumulative boundaries from zero through total.
|
|
247
|
+
*/
|
|
248
|
+
static #offsets(values) {
|
|
249
|
+
const offsets = [0]
|
|
250
|
+
let total = 0
|
|
251
|
+
for (const value of values) {
|
|
252
|
+
total += value
|
|
253
|
+
offsets.push(total)
|
|
254
|
+
}
|
|
255
|
+
return offsets
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolves a zero-based row or column index.
|
|
260
|
+
* @param {unknown} value Index candidate.
|
|
261
|
+
* @returns {number | null} Index.
|
|
262
|
+
*/
|
|
263
|
+
static #index(value) {
|
|
264
|
+
const number = Number(value)
|
|
265
|
+
return Number.isInteger(number) && number >= 0 ? number : null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Resolves a positive row or column span.
|
|
270
|
+
* @param {unknown} value Span candidate.
|
|
271
|
+
* @returns {number} Span.
|
|
272
|
+
*/
|
|
273
|
+
static #span(value) {
|
|
274
|
+
const number = Number(value)
|
|
275
|
+
return Number.isInteger(number) && number > 0 ? number : 1
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Resolves a positive width/height pair.
|
|
280
|
+
* @param {object} element Element row.
|
|
281
|
+
* @param {number} fallbackWidth Fallback width.
|
|
282
|
+
* @param {number} fallbackHeight Fallback height.
|
|
283
|
+
* @returns {{ width: number, height: number } | null} Size.
|
|
284
|
+
*/
|
|
285
|
+
static #size(element, fallbackWidth, fallbackHeight) {
|
|
286
|
+
const size = CircuitJsonUnits.optionalSize(element.size || element)
|
|
287
|
+
const width = size?.width ?? fallbackWidth
|
|
288
|
+
const height = size?.height ?? fallbackHeight
|
|
289
|
+
return width > 0 && height > 0 ? { width, height } : null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Builds a rectangle from an exact nine-point anchor.
|
|
294
|
+
* @param {{ x: number, y: number }} point Anchor point.
|
|
295
|
+
* @param {{ width: number, height: number }} size Size.
|
|
296
|
+
* @param {unknown} anchor Anchor name.
|
|
297
|
+
* @returns {{ x: number, y: number, width: number, height: number }} Rectangle.
|
|
298
|
+
*/
|
|
299
|
+
static #anchoredRect(point, size, anchor) {
|
|
300
|
+
const text = String(anchor || 'top_left').toLowerCase()
|
|
301
|
+
const [vertical, horizontal] =
|
|
302
|
+
text === 'center' || text === 'middle'
|
|
303
|
+
? ['center', 'center']
|
|
304
|
+
: text.split('_')
|
|
305
|
+
const x =
|
|
306
|
+
horizontal === 'right'
|
|
307
|
+
? point.x - size.width
|
|
308
|
+
: horizontal === 'center' || horizontal === 'middle'
|
|
309
|
+
? point.x - size.width / 2
|
|
310
|
+
: point.x
|
|
311
|
+
const y =
|
|
312
|
+
vertical === 'bottom'
|
|
313
|
+
? point.y - size.height
|
|
314
|
+
: vertical === 'center' || vertical === 'middle'
|
|
315
|
+
? point.y - size.height / 2
|
|
316
|
+
: point.y
|
|
317
|
+
return { x, y, ...size }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves standard schematic text anchors for SVG and bounds geometry.
|
|
3
|
+
*/
|
|
4
|
+
export class SchematicTextAnchor {
|
|
5
|
+
/**
|
|
6
|
+
* Resolves one named anchor into horizontal and vertical alignment.
|
|
7
|
+
* @param {unknown} value Anchor candidate.
|
|
8
|
+
* @returns {{ horizontal: 'left' | 'center' | 'right', vertical: 'top' | 'center' | 'bottom' | 'baseline', textAnchor: 'start' | 'middle' | 'end', baseline: 'hanging' | 'central' | 'text-after-edge' | '' }} Alignment.
|
|
9
|
+
*/
|
|
10
|
+
static resolve(value) {
|
|
11
|
+
const text = String(value || '').toLowerCase()
|
|
12
|
+
const horizontal =
|
|
13
|
+
text === 'right' || text.endsWith('_right')
|
|
14
|
+
? 'right'
|
|
15
|
+
: text === 'center' ||
|
|
16
|
+
text === 'middle' ||
|
|
17
|
+
text === 'top' ||
|
|
18
|
+
text === 'bottom' ||
|
|
19
|
+
text.endsWith('_center') ||
|
|
20
|
+
text.endsWith('_middle')
|
|
21
|
+
? 'center'
|
|
22
|
+
: 'left'
|
|
23
|
+
const vertical =
|
|
24
|
+
text === 'top' || text.startsWith('top_')
|
|
25
|
+
? 'top'
|
|
26
|
+
: text === 'bottom' || text.startsWith('bottom_')
|
|
27
|
+
? 'bottom'
|
|
28
|
+
: text === 'center' ||
|
|
29
|
+
text === 'middle' ||
|
|
30
|
+
text === 'left' ||
|
|
31
|
+
text === 'right' ||
|
|
32
|
+
text.startsWith('center_') ||
|
|
33
|
+
text.startsWith('middle_')
|
|
34
|
+
? 'center'
|
|
35
|
+
: 'baseline'
|
|
36
|
+
return {
|
|
37
|
+
horizontal,
|
|
38
|
+
vertical,
|
|
39
|
+
textAnchor:
|
|
40
|
+
horizontal === 'right'
|
|
41
|
+
? 'end'
|
|
42
|
+
: horizontal === 'center'
|
|
43
|
+
? 'middle'
|
|
44
|
+
: 'start',
|
|
45
|
+
baseline:
|
|
46
|
+
vertical === 'top'
|
|
47
|
+
? 'hanging'
|
|
48
|
+
: vertical === 'bottom'
|
|
49
|
+
? 'text-after-edge'
|
|
50
|
+
: vertical === 'center'
|
|
51
|
+
? 'central'
|
|
52
|
+
: ''
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estimates conservative bounds for SVG text in schematic user units.
|
|
3
|
+
*/
|
|
4
|
+
export class SchematicTextBounds {
|
|
5
|
+
/**
|
|
6
|
+
* Resolves rotated text bounds around its anchor point.
|
|
7
|
+
* @param {{ x: number, y: number }} point Text anchor point.
|
|
8
|
+
* @param {unknown} value Text value.
|
|
9
|
+
* @param {{ fontSize?: number, anchor?: unknown, rotation?: number }} [options] Text geometry.
|
|
10
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number }} Bounds.
|
|
11
|
+
*/
|
|
12
|
+
static resolve(point, value, options = {}) {
|
|
13
|
+
const lines = String(value ?? '').split(/\r?\n/u)
|
|
14
|
+
const fontSize =
|
|
15
|
+
Number.isFinite(options.fontSize) && options.fontSize > 0
|
|
16
|
+
? options.fontSize
|
|
17
|
+
: 1
|
|
18
|
+
let characters = 1
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
characters = Math.max(characters, [...line].length)
|
|
21
|
+
}
|
|
22
|
+
const width = Math.max(fontSize * 0.3, characters * fontSize)
|
|
23
|
+
const height = Math.max(fontSize, lines.length * fontSize * 1.2)
|
|
24
|
+
const origin = SchematicTextBounds.#origin(
|
|
25
|
+
point,
|
|
26
|
+
width,
|
|
27
|
+
height,
|
|
28
|
+
options.anchor
|
|
29
|
+
)
|
|
30
|
+
const corners = [
|
|
31
|
+
{ x: origin.x, y: origin.y },
|
|
32
|
+
{ x: origin.x + width, y: origin.y },
|
|
33
|
+
{ x: origin.x + width, y: origin.y + height },
|
|
34
|
+
{ x: origin.x, y: origin.y + height }
|
|
35
|
+
].map((corner) =>
|
|
36
|
+
SchematicTextBounds.#rotate(
|
|
37
|
+
corner,
|
|
38
|
+
point,
|
|
39
|
+
Number(options.rotation) || 0
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
return {
|
|
43
|
+
minX: Math.min(...corners.map((corner) => corner.x)),
|
|
44
|
+
minY: Math.min(...corners.map((corner) => corner.y)),
|
|
45
|
+
maxX: Math.max(...corners.map((corner) => corner.x)),
|
|
46
|
+
maxY: Math.max(...corners.map((corner) => corner.y))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the top-left text box origin from a named anchor.
|
|
52
|
+
* @param {{ x: number, y: number }} point Anchor point.
|
|
53
|
+
* @param {number} width Text width.
|
|
54
|
+
* @param {number} height Text height.
|
|
55
|
+
* @param {unknown} anchor Named anchor.
|
|
56
|
+
* @returns {{ x: number, y: number }} Top-left origin.
|
|
57
|
+
*/
|
|
58
|
+
static #origin(point, width, height, anchor) {
|
|
59
|
+
const { horizontal, vertical } = SchematicTextAnchor.resolve(anchor)
|
|
60
|
+
return {
|
|
61
|
+
x:
|
|
62
|
+
horizontal === 'right'
|
|
63
|
+
? point.x - width
|
|
64
|
+
: horizontal === 'center'
|
|
65
|
+
? point.x - width / 2
|
|
66
|
+
: point.x,
|
|
67
|
+
y:
|
|
68
|
+
vertical === 'top'
|
|
69
|
+
? point.y
|
|
70
|
+
: vertical === 'bottom'
|
|
71
|
+
? point.y - height
|
|
72
|
+
: vertical === 'center'
|
|
73
|
+
? point.y - height / 2
|
|
74
|
+
: point.y - height
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Rotates one corner around the text anchor.
|
|
80
|
+
* @param {{ x: number, y: number }} point Corner.
|
|
81
|
+
* @param {{ x: number, y: number }} center Rotation center.
|
|
82
|
+
* @param {number} degrees Counter-clockwise degrees.
|
|
83
|
+
* @returns {{ x: number, y: number }} Rotated corner.
|
|
84
|
+
*/
|
|
85
|
+
static #rotate(point, center, degrees) {
|
|
86
|
+
if (!degrees) return point
|
|
87
|
+
const radians = (degrees * Math.PI) / 180
|
|
88
|
+
const cos = Math.cos(radians)
|
|
89
|
+
const sin = Math.sin(radians)
|
|
90
|
+
const x = point.x - center.x
|
|
91
|
+
const y = point.y - center.y
|
|
92
|
+
return {
|
|
93
|
+
x: center.x + x * cos - y * sin,
|
|
94
|
+
y: center.y + x * sin + y * cos
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
import { SchematicTextAnchor } from './SchematicTextAnchor.mjs'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Parser } from '../core/Parser.mjs'
|
|
2
|
+
import { ProjectLoader } from '../core/ProjectLoader.mjs'
|
|
3
|
+
import { AsyncInputOwnership } from '../core/AsyncInputOwnership.mjs'
|
|
4
|
+
import { ToolkitWorkerProtocol } from '../core/worker/ToolkitWorkerProtocol.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses one protocol payload through the canonical direct async path.
|
|
8
|
+
* @param {{ input: unknown, options?: object }} payload Worker payload.
|
|
9
|
+
* @param {{ signal: AbortSignal, onProgress: Function }} runtime Protocol runtime.
|
|
10
|
+
* @returns {Promise<object>} Canonical document result.
|
|
11
|
+
*/
|
|
12
|
+
async function parseInWorker(payload, runtime) {
|
|
13
|
+
return await Parser.parseAsync(
|
|
14
|
+
AsyncInputOwnership.markParser(payload.input),
|
|
15
|
+
{
|
|
16
|
+
...(payload.options || {}),
|
|
17
|
+
worker: false,
|
|
18
|
+
signal: runtime.signal,
|
|
19
|
+
onProgress: runtime.onProgress
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Loads one project payload through the canonical direct async path.
|
|
26
|
+
* @param {{ entries: unknown, options?: object }} payload Worker payload.
|
|
27
|
+
* @param {{ signal: AbortSignal, onProgress: Function }} runtime Protocol runtime.
|
|
28
|
+
* @returns {Promise<object>} Canonical project result.
|
|
29
|
+
*/
|
|
30
|
+
async function loadProjectInWorker(payload, runtime) {
|
|
31
|
+
return await ProjectLoader.loadAsync(
|
|
32
|
+
AsyncInputOwnership.markProject(payload.entries),
|
|
33
|
+
{
|
|
34
|
+
...(payload.options || {}),
|
|
35
|
+
worker: false,
|
|
36
|
+
signal: runtime.signal,
|
|
37
|
+
onProgress: runtime.onProgress
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Installs the shared parser/project protocol on one worker-like scope.
|
|
44
|
+
* @param {unknown} scope Worker global scope.
|
|
45
|
+
* @returns {{ dispose: () => void }} Protocol installation.
|
|
46
|
+
*/
|
|
47
|
+
export function installParserWorker(scope) {
|
|
48
|
+
return ToolkitWorkerProtocol.install(scope, {
|
|
49
|
+
parse: parseInWorker,
|
|
50
|
+
loadProject: loadProjectInWorker
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
typeof globalThis.addEventListener === 'function' &&
|
|
56
|
+
typeof globalThis.postMessage === 'function'
|
|
57
|
+
) {
|
|
58
|
+
installParserWorker(globalThis)
|
|
59
|
+
}
|