circuitjson-toolkit 1.0.16 → 1.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.
Files changed (155) hide show
  1. package/AGENTS.md +15 -0
  2. package/NOTICE.md +31 -0
  3. package/README.md +267 -107
  4. package/docs/api.md +501 -96
  5. package/docs/capabilities.md +70 -0
  6. package/docs/migration/behaviors.md +45 -0
  7. package/docs/migration/parser.md +60 -0
  8. package/docs/migration/renderers.md +515 -0
  9. package/docs/migration/root.md +740 -0
  10. package/docs/migration.md +120 -0
  11. package/docs/model-format.md +175 -57
  12. package/docs/provenance.md +206 -0
  13. package/docs/release-notes-v1.1.0.md +154 -0
  14. package/docs/testing.md +117 -7
  15. package/package.json +31 -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 +997 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +623 -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
@@ -11,8 +11,12 @@ export class CircuitJsonSchematicSvgArcPath {
11
11
  */
12
12
  static fromThreePoints(start, mid, end) {
13
13
  if (!start || !mid || !end) return ''
14
- const circle = CircuitJsonSchematicSvgArcPath.#circle(start, mid, end)
15
- if (!circle) {
14
+ const geometry = CircuitJsonSchematicSvgArcPath.#geometry(
15
+ start,
16
+ mid,
17
+ end
18
+ )
19
+ if (!geometry) {
16
20
  return (
17
21
  'M ' +
18
22
  CircuitJsonSchematicSvgArcPath.#formatPoint(start) +
@@ -20,6 +24,69 @@ export class CircuitJsonSchematicSvgArcPath {
20
24
  CircuitJsonSchematicSvgArcPath.#formatPoint(end)
21
25
  )
22
26
  }
27
+
28
+ return (
29
+ 'M ' +
30
+ CircuitJsonSchematicSvgArcPath.#formatPoint(start) +
31
+ ' A ' +
32
+ CircuitJsonSchematicSvgArcPath.#formatNumber(
33
+ geometry.circle.radius
34
+ ) +
35
+ ' ' +
36
+ CircuitJsonSchematicSvgArcPath.#formatNumber(
37
+ geometry.circle.radius
38
+ ) +
39
+ ' 0 ' +
40
+ geometry.largeArc +
41
+ ' ' +
42
+ geometry.sweep +
43
+ ' ' +
44
+ CircuitJsonSchematicSvgArcPath.#formatPoint(end)
45
+ )
46
+ }
47
+
48
+ /**
49
+ * Resolves the exact axis-aligned bounds of the rendered three-point arc.
50
+ * @param {{ x: number, y: number } | null} start Start point.
51
+ * @param {{ x: number, y: number } | null} mid Midpoint on the arc.
52
+ * @param {{ x: number, y: number } | null} end End point.
53
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null} Arc bounds.
54
+ */
55
+ static boundsFromThreePoints(start, mid, end) {
56
+ if (!start || !mid || !end) return null
57
+ const geometry = CircuitJsonSchematicSvgArcPath.#geometry(
58
+ start,
59
+ mid,
60
+ end
61
+ )
62
+ if (!geometry) {
63
+ return CircuitJsonSchematicSvgArcPath.#bounds([start, end])
64
+ }
65
+ const points = [start, end]
66
+ for (const angle of [0, Math.PI / 2, Math.PI, (Math.PI * 3) / 2]) {
67
+ if (
68
+ !CircuitJsonSchematicSvgArcPath.#containsAngle(geometry, angle)
69
+ ) {
70
+ continue
71
+ }
72
+ points.push({
73
+ x: geometry.circle.x + Math.cos(angle) * geometry.circle.radius,
74
+ y: geometry.circle.y + Math.sin(angle) * geometry.circle.radius
75
+ })
76
+ }
77
+ return CircuitJsonSchematicSvgArcPath.#bounds(points)
78
+ }
79
+
80
+ /**
81
+ * Resolves the shared circle and directed sweep through three points.
82
+ * @param {{ x: number, y: number }} start Start point.
83
+ * @param {{ x: number, y: number }} mid Midpoint on the arc.
84
+ * @param {{ x: number, y: number }} end End point.
85
+ * @returns {{ circle: { x: number, y: number, radius: number }, startAngle: number, endAngle: number, sweep: 0 | 1, span: number, largeArc: 0 | 1 } | null} Arc geometry.
86
+ */
87
+ static #geometry(start, mid, end) {
88
+ const circle = CircuitJsonSchematicSvgArcPath.#circle(start, mid, end)
89
+ if (!circle) return null
23
90
  const startAngle = CircuitJsonSchematicSvgArcPath.#angle(circle, start)
24
91
  const midAngle = CircuitJsonSchematicSvgArcPath.#angle(circle, mid)
25
92
  const endAngle = CircuitJsonSchematicSvgArcPath.#angle(circle, end)
@@ -39,22 +106,55 @@ export class CircuitJsonSchematicSvgArcPath {
39
106
  endAngle,
40
107
  startAngle
41
108
  )
42
- const largeArc = span > Math.PI ? 1 : 0
109
+ return {
110
+ circle,
111
+ startAngle,
112
+ endAngle,
113
+ sweep,
114
+ span,
115
+ largeArc: span > Math.PI ? 1 : 0
116
+ }
117
+ }
43
118
 
44
- return (
45
- 'M ' +
46
- CircuitJsonSchematicSvgArcPath.#formatPoint(start) +
47
- ' A ' +
48
- CircuitJsonSchematicSvgArcPath.#formatNumber(circle.radius) +
49
- ' ' +
50
- CircuitJsonSchematicSvgArcPath.#formatNumber(circle.radius) +
51
- ' 0 ' +
52
- largeArc +
53
- ' ' +
54
- sweep +
55
- ' ' +
56
- CircuitJsonSchematicSvgArcPath.#formatPoint(end)
57
- )
119
+ /**
120
+ * Returns whether one cardinal angle lies on the directed arc sweep.
121
+ * @param {{ startAngle: number, sweep: 0 | 1, span: number }} geometry Arc geometry.
122
+ * @param {number} angle Candidate angle.
123
+ * @returns {boolean} Whether the angle is visible.
124
+ */
125
+ static #containsAngle(geometry, angle) {
126
+ const delta =
127
+ geometry.sweep === 1
128
+ ? CircuitJsonSchematicSvgArcPath.#positiveDelta(
129
+ geometry.startAngle,
130
+ angle
131
+ )
132
+ : CircuitJsonSchematicSvgArcPath.#positiveDelta(
133
+ angle,
134
+ geometry.startAngle
135
+ )
136
+ return delta <= geometry.span + 0.000001
137
+ }
138
+
139
+ /**
140
+ * Builds finite bounds around a non-empty point list.
141
+ * @param {{ x: number, y: number }[]} points Points to include.
142
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number }} Bounds.
143
+ */
144
+ static #bounds(points) {
145
+ const bounds = {
146
+ minX: Infinity,
147
+ minY: Infinity,
148
+ maxX: -Infinity,
149
+ maxY: -Infinity
150
+ }
151
+ for (const point of points) {
152
+ bounds.minX = Math.min(bounds.minX, point.x)
153
+ bounds.minY = Math.min(bounds.minY, point.y)
154
+ bounds.maxX = Math.max(bounds.maxX, point.x)
155
+ bounds.maxY = Math.max(bounds.maxY, point.y)
156
+ }
157
+ return bounds
58
158
  }
59
159
 
60
160
  /**
@@ -1,3 +1,5 @@
1
+ import { SafeXmlText } from './SafeXmlText.mjs'
2
+
1
3
  /**
2
4
  * Resolves schematic port display metadata from linked source ports.
3
5
  */
@@ -26,10 +28,10 @@ export class CircuitJsonSchematicSvgPortMetadata {
26
28
  */
27
29
  static attributes(element, sourcePort) {
28
30
  const sourcePortId = String(element.source_port_id || '').trim()
29
- const pinNumber = CircuitJsonSchematicSvgPortMetadata.#firstText(
31
+ const pinNumber = CircuitJsonSchematicSvgPortMetadata.#firstText([
30
32
  element.pin_number,
31
33
  sourcePort?.pin_number
32
- )
34
+ ])
33
35
  const direction = String(
34
36
  element.facing_direction || element.direction || ''
35
37
  ).trim()
@@ -69,34 +71,83 @@ export class CircuitJsonSchematicSvgPortMetadata {
69
71
  * Resolves a visible schematic port label.
70
72
  * @param {object} element Schematic port row.
71
73
  * @param {object | null} sourcePort Linked source port row.
74
+ * @param {WeakMap<object, string> | null} [hintCache] Render-scoped hint cache.
72
75
  * @returns {string}
73
76
  */
74
- static label(element, sourcePort) {
75
- return CircuitJsonSchematicSvgPortMetadata.#firstText(
77
+ static label(element, sourcePort, hintCache = null) {
78
+ const direct = CircuitJsonSchematicSvgPortMetadata.#firstText([
76
79
  element.name,
77
80
  element.label,
78
81
  sourcePort?.name,
79
82
  sourcePort?.label,
80
83
  sourcePort?.pin_label,
81
84
  element.pin_number,
82
- sourcePort?.pin_number,
83
- ...(Array.isArray(sourcePort?.port_hints)
84
- ? sourcePort.port_hints
85
- : [])
85
+ sourcePort?.pin_number
86
+ ])
87
+ return direct
88
+ ? direct
89
+ : CircuitJsonSchematicSvgPortMetadata.#sourceHint(
90
+ sourcePort,
91
+ hintCache
92
+ )
93
+ }
94
+
95
+ /**
96
+ * Resolves one source-port hint fallback within a single render snapshot.
97
+ * @param {object | null} sourcePort Source-port row.
98
+ * @param {WeakMap<object, string> | null} hintCache Render-scoped cache.
99
+ * @returns {string} First non-empty hint.
100
+ */
101
+ static #sourceHint(sourcePort, hintCache) {
102
+ if (!sourcePort || typeof sourcePort !== 'object') return ''
103
+ if (hintCache?.has(sourcePort)) {
104
+ return hintCache.get(sourcePort)
105
+ }
106
+ const hints = Array.isArray(sourcePort.port_hints)
107
+ ? sourcePort.port_hints
108
+ : []
109
+ const hint = CircuitJsonSchematicSvgPortMetadata.#firstText(hints)
110
+ hintCache?.set(sourcePort, hint)
111
+ return hint
112
+ }
113
+
114
+ /**
115
+ * Resolves the first non-empty text and whether evaluated values are immutable.
116
+ * @param {unknown[]} values Candidate values.
117
+ * @returns {{ text: string, immutable: boolean }} Text metadata.
118
+ */
119
+ static #firstTextResult(values) {
120
+ let immutable = true
121
+ for (const value of values) {
122
+ immutable =
123
+ immutable &&
124
+ CircuitJsonSchematicSvgPortMetadata.#isImmutableScalar(value)
125
+ const text = String(value ?? '').trim()
126
+ if (text) return { text, immutable }
127
+ }
128
+ return { text: '', immutable }
129
+ }
130
+
131
+ /**
132
+ * Returns whether a label value is immutable primitive data.
133
+ * @param {unknown} value Label value.
134
+ * @returns {boolean} Whether the value can participate in memoization.
135
+ */
136
+ static #isImmutableScalar(value) {
137
+ return (
138
+ value === null ||
139
+ value === undefined ||
140
+ ['string', 'number', 'boolean', 'bigint'].includes(typeof value)
86
141
  )
87
142
  }
88
143
 
89
144
  /**
90
145
  * Resolves the first non-empty display text value.
91
- * @param {...unknown} values Candidate values.
146
+ * @param {unknown[]} values Candidate values.
92
147
  * @returns {string}
93
148
  */
94
- static #firstText(...values) {
95
- return (
96
- values
97
- .map((value) => String(value ?? '').trim())
98
- .find((value) => value) || ''
99
- )
149
+ static #firstText(values) {
150
+ return CircuitJsonSchematicSvgPortMetadata.#firstTextResult(values).text
100
151
  }
101
152
 
102
153
  /**
@@ -105,10 +156,6 @@ export class CircuitJsonSchematicSvgPortMetadata {
105
156
  * @returns {string}
106
157
  */
107
158
  static #escapeHtml(value) {
108
- return String(value ?? '')
109
- .replaceAll('&', '&amp;')
110
- .replaceAll('<', '&lt;')
111
- .replaceAll('>', '&gt;')
112
- .replaceAll('"', '&quot;')
159
+ return SafeXmlText.escape(value)
113
160
  }
114
161
  }
@@ -1,3 +1,5 @@
1
+ import { SafeXmlText } from './SafeXmlText.mjs'
2
+
1
3
  /**
2
4
  * Builds SVG drawing attributes for schematic primitive rows.
3
5
  */
@@ -21,6 +23,8 @@ export class CircuitJsonSchematicSvgPrimitiveAttributes {
21
23
  )
22
24
  const dashArray =
23
25
  CircuitJsonSchematicSvgPrimitiveAttributes.#dashArray(element)
26
+ const lineCap =
27
+ CircuitJsonSchematicSvgPrimitiveAttributes.#lineCap(element)
24
28
 
25
29
  if (strokeWidth !== null) {
26
30
  attributes.push(
@@ -31,20 +35,22 @@ export class CircuitJsonSchematicSvgPrimitiveAttributes {
31
35
  '"'
32
36
  )
33
37
  }
34
- if (strokeColor) {
38
+ const safeStroke = SafeSvgPaint.color(strokeColor)
39
+ const safeFill = SafeSvgPaint.color(fillColor)
40
+ if (safeStroke) {
35
41
  attributes.push(
36
42
  'stroke="' +
37
43
  CircuitJsonSchematicSvgPrimitiveAttributes.#escapeHtml(
38
- strokeColor
44
+ safeStroke
39
45
  ) +
40
46
  '"'
41
47
  )
42
48
  }
43
- if (options.fill !== false && fillColor) {
49
+ if (options.fill !== false && safeFill) {
44
50
  attributes.push(
45
51
  'fill="' +
46
52
  CircuitJsonSchematicSvgPrimitiveAttributes.#escapeHtml(
47
- fillColor
53
+ safeFill
48
54
  ) +
49
55
  '"'
50
56
  )
@@ -58,6 +64,9 @@ export class CircuitJsonSchematicSvgPrimitiveAttributes {
58
64
  '"'
59
65
  )
60
66
  }
67
+ if (lineCap) {
68
+ attributes.push('stroke-linecap="' + lineCap + '"')
69
+ }
61
70
 
62
71
  return attributes.length ? ' ' + attributes.join(' ') : ''
63
72
  }
@@ -87,6 +96,22 @@ export class CircuitJsonSchematicSvgPrimitiveAttributes {
87
96
  * @returns {string}
88
97
  */
89
98
  static #dashArray(element) {
99
+ const dashLength = CircuitJsonSchematicSvgPrimitiveAttributes.#number(
100
+ element?.dash_length ?? element?.dashLength
101
+ )
102
+ const dashGap = CircuitJsonSchematicSvgPrimitiveAttributes.#number(
103
+ element?.dash_gap ?? element?.dashGap
104
+ )
105
+ if (element?.is_dashed && dashLength !== null) {
106
+ return [dashLength, dashGap ?? dashLength]
107
+ .map((entry) =>
108
+ CircuitJsonSchematicSvgPrimitiveAttributes.#formatNumber(
109
+ entry
110
+ )
111
+ )
112
+ .join(' ')
113
+ }
114
+
90
115
  const value =
91
116
  element?.stroke_dasharray ??
92
117
  element?.strokeDasharray ??
@@ -106,6 +131,20 @@ export class CircuitJsonSchematicSvgPrimitiveAttributes {
106
131
  return element?.is_dashed ? '0.4 0.25' : ''
107
132
  }
108
133
 
134
+ /**
135
+ * Resolves one safe SVG stroke line-cap value.
136
+ * @param {object} element CircuitJSON element row.
137
+ * @returns {'butt' | 'round' | 'square' | ''}
138
+ */
139
+ static #lineCap(element) {
140
+ const value = String(
141
+ element?.stroke_linecap ?? element?.strokeLinecap ?? ''
142
+ )
143
+ .trim()
144
+ .toLowerCase()
145
+ return ['butt', 'round', 'square'].includes(value) ? value : ''
146
+ }
147
+
109
148
  /**
110
149
  * Formats one SVG number.
111
150
  * @param {number} value Number value.
@@ -121,10 +160,7 @@ export class CircuitJsonSchematicSvgPrimitiveAttributes {
121
160
  * @returns {string}
122
161
  */
123
162
  static #escapeHtml(value) {
124
- return String(value ?? '')
125
- .replaceAll('&', '&amp;')
126
- .replaceAll('<', '&lt;')
127
- .replaceAll('>', '&gt;')
128
- .replaceAll('"', '&quot;')
163
+ return SafeXmlText.escape(value)
129
164
  }
130
165
  }
166
+ import { SafeSvgPaint } from './SafeSvgPaint.mjs'