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.
Files changed (155) hide show
  1. package/NOTICE.md +31 -0
  2. package/README.md +274 -107
  3. package/docs/api.md +507 -96
  4. package/docs/capabilities.md +70 -0
  5. package/docs/migration/behaviors.md +45 -0
  6. package/docs/migration/parser.md +60 -0
  7. package/docs/migration/renderers.md +515 -0
  8. package/docs/migration/root.md +740 -0
  9. package/docs/migration.md +126 -0
  10. package/docs/model-format.md +175 -57
  11. package/docs/provenance.md +206 -0
  12. package/docs/release-notes-v1.1.0.md +154 -0
  13. package/docs/release-notes-v1.1.1.md +24 -0
  14. package/docs/testing.md +122 -7
  15. package/package.json +32 -5
  16. package/spec/api-baseline-v1.0.17.json +1 -0
  17. package/spec/baseline-provenance-v1.0.17.json +7 -0
  18. package/spec/circuitjson-schema-snapshot.json +321 -0
  19. package/spec/circuitjson-schema-source.json +28 -0
  20. package/spec/feature-preservation.json +1 -0
  21. package/spec/library-scope.md +27 -20
  22. package/src/capabilities.mjs +1 -0
  23. package/src/core/ArchiveEntryPath.mjs +93 -0
  24. package/src/core/ArchiveLimits.mjs +31 -0
  25. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  26. package/src/core/AsyncInputOwnership.mjs +56 -0
  27. package/src/core/AttachedValueLimits.mjs +67 -0
  28. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  29. package/src/core/CircuitJsonDocument.mjs +19 -61
  30. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  31. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  32. package/src/core/CircuitJsonIndexer.mjs +274 -194
  33. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  34. package/src/core/CircuitJsonParser.mjs +75 -13
  35. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  36. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  37. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  38. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  39. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  40. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  41. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  42. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  43. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  44. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  45. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  46. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  47. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  48. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  49. package/src/core/CircuitJsonUnits.mjs +13 -87
  50. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  51. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  52. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  53. package/src/core/ManufacturingService.mjs +323 -0
  54. package/src/core/Parser.mjs +343 -0
  55. package/src/core/ParserOptions.mjs +333 -0
  56. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  57. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  58. package/src/core/PcbInteractionIndex.mjs +368 -0
  59. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  60. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  61. package/src/core/ProjectLoader.mjs +975 -0
  62. package/src/core/SimulationService.mjs +790 -0
  63. package/src/core/ToolkitCapabilities.mjs +130 -0
  64. package/src/core/ZipArchiveInspector.mjs +649 -0
  65. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  66. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  67. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  68. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  69. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  70. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  71. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  72. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  73. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  74. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  75. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  76. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  77. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  78. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  79. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  80. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  81. package/src/core/contracts/DocumentResult.mjs +198 -0
  82. package/src/core/contracts/ProjectResult.mjs +96 -0
  83. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  84. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  85. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  86. package/src/core/contracts/ToolkitError.mjs +176 -0
  87. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  88. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  89. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  90. package/src/core/query/CircuitTraversal.mjs +343 -0
  91. package/src/core/query/ComponentGrouping.mjs +275 -0
  92. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  93. package/src/core/query/QueryService.mjs +435 -0
  94. package/src/core/query/RegexPattern.mjs +75 -0
  95. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  96. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  97. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  98. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  99. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  100. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  101. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  102. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  103. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  104. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  105. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  106. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  107. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  108. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  109. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  110. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  111. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  112. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  113. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  114. package/src/core/worker/ParserWorkerClient.mjs +999 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +650 -0
  117. package/src/core/worker/WorkerResponseData.mjs +905 -0
  118. package/src/extensions.mjs +37 -0
  119. package/src/index.mjs +14 -9
  120. package/src/interaction.mjs +2 -0
  121. package/src/manufacturing.mjs +1 -0
  122. package/src/parser.mjs +12 -2
  123. package/src/project.mjs +5 -0
  124. package/src/query.mjs +1 -0
  125. package/src/renderers.mjs +3 -29
  126. package/src/scene3d.mjs +3 -0
  127. package/src/simulation.mjs +1 -0
  128. package/src/styles/renderers.css +24 -0
  129. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  130. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  131. package/src/testing/runToolkitContract.mjs +705 -0
  132. package/src/testing.mjs +3 -0
  133. package/src/ui/BomTableRenderer.mjs +304 -0
  134. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  135. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  136. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  137. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  138. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  139. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  140. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  141. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  142. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  143. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  144. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  145. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  146. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  147. package/src/ui/PcbSvgRenderer.mjs +41 -0
  148. package/src/ui/SafeSvgPaint.mjs +26 -0
  149. package/src/ui/SafeXmlText.mjs +60 -0
  150. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  151. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  152. package/src/ui/SchematicTableGeometry.mjs +319 -0
  153. package/src/ui/SchematicTextAnchor.mjs +55 -0
  154. package/src/ui/SchematicTextBounds.mjs +98 -0
  155. 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
+ }