pcb-scene3d-viewer 1.0.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 (65) hide show
  1. package/AGENTS.md +67 -0
  2. package/COMMERCIAL-LICENSE.md +15 -0
  3. package/CONTRIBUTING.md +14 -0
  4. package/LICENSE +19 -0
  5. package/LICENSES/AGPL-3.0-or-later.txt +235 -0
  6. package/LICENSES/CC-BY-SA-4.0.txt +170 -0
  7. package/LICENSES/LGPL-2.1-or-later.txt +176 -0
  8. package/LICENSES/LicenseRef-PolyForm-Noncommercial-1.0.0.txt +131 -0
  9. package/NOTICE.md +36 -0
  10. package/README.md +128 -0
  11. package/REUSE.toml +16 -0
  12. package/docs/api.md +148 -0
  13. package/docs/circuitjson.md +190 -0
  14. package/docs/model-format.md +117 -0
  15. package/docs/testing.md +23 -0
  16. package/package.json +65 -0
  17. package/spec/library-scope.md +36 -0
  18. package/src/PcbModelArchiveExporter.mjs +320 -0
  19. package/src/PcbScene3dArcUtils.mjs +27 -0
  20. package/src/PcbScene3dBoardAssemblyPlacement.mjs +36 -0
  21. package/src/PcbScene3dBoardAssemblyPresentation.mjs +859 -0
  22. package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +537 -0
  23. package/src/PcbScene3dBoardMaterialPalette.mjs +40 -0
  24. package/src/PcbScene3dBoardShapeFactory.mjs +895 -0
  25. package/src/PcbScene3dBoardSolderMaskFactory.mjs +613 -0
  26. package/src/PcbScene3dCameraRig.mjs +168 -0
  27. package/src/PcbScene3dCircuitJsonAdapter.mjs +545 -0
  28. package/src/PcbScene3dController.mjs +956 -0
  29. package/src/PcbScene3dCopperDetailFilter.mjs +490 -0
  30. package/src/PcbScene3dCopperFactory.mjs +559 -0
  31. package/src/PcbScene3dCopperTextFactory.mjs +534 -0
  32. package/src/PcbScene3dCutoutGeometryFilter.mjs +873 -0
  33. package/src/PcbScene3dDetailCoordinateNormalizer.mjs +65 -0
  34. package/src/PcbScene3dDrillCutoutFilter.mjs +224 -0
  35. package/src/PcbScene3dDrillPathFactory.mjs +362 -0
  36. package/src/PcbScene3dDrillVoidFactory.mjs +268 -0
  37. package/src/PcbScene3dExternalModelLoadOrder.mjs +54 -0
  38. package/src/PcbScene3dExternalModels.mjs +968 -0
  39. package/src/PcbScene3dFallbackVisibility.mjs +82 -0
  40. package/src/PcbScene3dInteractionHints.mjs +56 -0
  41. package/src/PcbScene3dMountRig.mjs +53 -0
  42. package/src/PcbScene3dOutlineBuilder.mjs +210 -0
  43. package/src/PcbScene3dPadFactory.mjs +553 -0
  44. package/src/PcbScene3dPresetState.mjs +48 -0
  45. package/src/PcbScene3dRenderGroupVisibility.mjs +134 -0
  46. package/src/PcbScene3dRuntime.mjs +996 -0
  47. package/src/PcbScene3dRuntimeBoardMeshes.mjs +99 -0
  48. package/src/PcbScene3dSelectionStyler.mjs +252 -0
  49. package/src/PcbScene3dShapePathFactory.mjs +220 -0
  50. package/src/PcbScene3dShellRenderer.mjs +131 -0
  51. package/src/PcbScene3dSilkscreenFactory.mjs +854 -0
  52. package/src/PcbScene3dSilkscreenStrokeWidthResolver.mjs +81 -0
  53. package/src/PcbScene3dStepLoader.mjs +611 -0
  54. package/src/PcbScene3dStrokeFont.mjs +671 -0
  55. package/src/PcbScene3dStrokeGeometryBuilder.mjs +322 -0
  56. package/src/PcbScene3dText.mjs +99 -0
  57. package/src/PcbScene3dTrueTypeTextFactory.mjs +885 -0
  58. package/src/PcbScene3dViaFactory.mjs +176 -0
  59. package/src/PcbScene3dViewCompensation.mjs +109 -0
  60. package/src/PcbScene3dViewScale.mjs +24 -0
  61. package/src/PcbScene3dViewportResize.mjs +35 -0
  62. package/src/PcbScene3dWorkerClient.mjs +123 -0
  63. package/src/index.mjs +1 -0
  64. package/src/scene3d.mjs +44 -0
  65. package/src/styles/scene3d.css +295 -0
@@ -0,0 +1,322 @@
1
+ import { PcbScene3dArcUtils } from './PcbScene3dArcUtils.mjs'
2
+
3
+ /**
4
+ * Appends triangle geometry for PCB stroke-style tracks and arcs.
5
+ */
6
+ export class PcbScene3dStrokeGeometryBuilder {
7
+ static #DEFAULT_MIN_WIDTH = 1
8
+ static #ROUND_CAP_SEGMENTS = 16
9
+
10
+ /**
11
+ * Appends one widened track with round end caps.
12
+ * @param {number[]} positions Position buffer.
13
+ * @param {{ x: number, y: number }} start Start point.
14
+ * @param {{ x: number, y: number }} end End point.
15
+ * @param {number} width Stroke width.
16
+ * @param {number} z Z position.
17
+ * @param {{ minWidth?: number }} [options] Stroke options.
18
+ * @returns {void}
19
+ */
20
+ static appendTrack(positions, start, end, width, z, options = {}) {
21
+ const dx = end.x - start.x
22
+ const dy = end.y - start.y
23
+ const length = Math.hypot(dx, dy)
24
+ const halfWidth =
25
+ PcbScene3dStrokeGeometryBuilder.#safeStrokeWidth(
26
+ width,
27
+ options.minWidth
28
+ ) / 2
29
+
30
+ if (length <= 0.001) {
31
+ PcbScene3dStrokeGeometryBuilder.#appendDiscTriangles(
32
+ positions,
33
+ start,
34
+ halfWidth,
35
+ z
36
+ )
37
+ return
38
+ }
39
+
40
+ const normalX = (-dy / length) * halfWidth
41
+ const normalY = (dx / length) * halfWidth
42
+
43
+ PcbScene3dStrokeGeometryBuilder.#appendQuadTriangles(
44
+ positions,
45
+ { x: start.x + normalX, y: start.y + normalY },
46
+ { x: end.x + normalX, y: end.y + normalY },
47
+ { x: end.x - normalX, y: end.y - normalY },
48
+ { x: start.x - normalX, y: start.y - normalY },
49
+ z
50
+ )
51
+ PcbScene3dStrokeGeometryBuilder.#appendDiscTriangles(
52
+ positions,
53
+ start,
54
+ halfWidth,
55
+ z
56
+ )
57
+ PcbScene3dStrokeGeometryBuilder.#appendDiscTriangles(
58
+ positions,
59
+ end,
60
+ halfWidth,
61
+ z
62
+ )
63
+ }
64
+
65
+ /**
66
+ * Appends one widened arc band with round caps for open arcs.
67
+ * @param {number[]} positions Position buffer.
68
+ * @param {{ x: number, y: number }} center Arc center.
69
+ * @param {{ radius?: number, width?: number, startAngle?: number, endAngle?: number }} arc
70
+ * Arc primitive.
71
+ * @param {number} z Z position.
72
+ * @param {boolean} mirrorY Whether the arc is mirrored around the X axis.
73
+ * @param {{ minWidth?: number, fullCircleEpsilon?: number }} [options]
74
+ * Stroke options.
75
+ * @returns {void}
76
+ */
77
+ static appendArc(positions, center, arc, z, mirrorY, options = {}) {
78
+ const strokeWidth = PcbScene3dStrokeGeometryBuilder.#safeStrokeWidth(
79
+ arc?.width,
80
+ options.minWidth
81
+ )
82
+ const radius = Math.max(Number(arc?.radius || 0), strokeWidth / 2, 0.8)
83
+ const outerRadius = radius + strokeWidth / 2
84
+ const innerRadius = Math.max(radius - strokeWidth / 2, 0)
85
+ const startAngleRad = (Number(arc?.startAngle || 0) * Math.PI) / 180
86
+ const deltaAngleDeg = PcbScene3dArcUtils.resolveSweepDelta(
87
+ Number(arc?.startAngle || 0),
88
+ Number(arc?.endAngle || 0)
89
+ )
90
+ const epsilon = Number(options.fullCircleEpsilon ?? 0.001)
91
+ const isFullCircle =
92
+ Math.abs(deltaAngleDeg) <= epsilon ||
93
+ Math.abs(deltaAngleDeg) >= 360 - epsilon
94
+ const deltaAngleRad = isFullCircle
95
+ ? Math.PI * 2
96
+ : (deltaAngleDeg * Math.PI) / 180
97
+ const segments = Math.max(
98
+ isFullCircle ? 20 : 8,
99
+ Math.ceil((Math.abs(deltaAngleRad) / Math.PI) * 18)
100
+ )
101
+ const yDirection = mirrorY ? -1 : 1
102
+
103
+ for (let index = 0; index < segments; index += 1) {
104
+ PcbScene3dStrokeGeometryBuilder.#appendArcSegmentTriangles(
105
+ positions,
106
+ center,
107
+ {
108
+ innerRadius,
109
+ outerRadius,
110
+ startAngle:
111
+ startAngleRad + (deltaAngleRad * index) / segments,
112
+ endAngle:
113
+ startAngleRad +
114
+ (deltaAngleRad * (index + 1)) / segments,
115
+ yDirection
116
+ },
117
+ z
118
+ )
119
+ }
120
+
121
+ if (!isFullCircle) {
122
+ PcbScene3dStrokeGeometryBuilder.#appendArcCapTriangles(
123
+ positions,
124
+ center,
125
+ radius,
126
+ strokeWidth / 2,
127
+ startAngleRad,
128
+ yDirection,
129
+ z
130
+ )
131
+ PcbScene3dStrokeGeometryBuilder.#appendArcCapTriangles(
132
+ positions,
133
+ center,
134
+ radius,
135
+ strokeWidth / 2,
136
+ startAngleRad + deltaAngleRad,
137
+ yDirection,
138
+ z
139
+ )
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Appends one arc segment band.
145
+ * @param {number[]} positions Position buffer.
146
+ * @param {{ x: number, y: number }} center Arc center.
147
+ * @param {{ innerRadius: number, outerRadius: number, startAngle: number, endAngle: number, yDirection: number }} segment
148
+ * Arc segment geometry.
149
+ * @param {number} z Z position.
150
+ * @returns {void}
151
+ */
152
+ static #appendArcSegmentTriangles(positions, center, segment, z) {
153
+ const outerStart = {
154
+ x: center.x + Math.cos(segment.startAngle) * segment.outerRadius,
155
+ y:
156
+ center.y +
157
+ Math.sin(segment.startAngle) *
158
+ segment.outerRadius *
159
+ segment.yDirection
160
+ }
161
+ const outerEnd = {
162
+ x: center.x + Math.cos(segment.endAngle) * segment.outerRadius,
163
+ y:
164
+ center.y +
165
+ Math.sin(segment.endAngle) *
166
+ segment.outerRadius *
167
+ segment.yDirection
168
+ }
169
+
170
+ if (segment.innerRadius <= 0.001) {
171
+ PcbScene3dStrokeGeometryBuilder.#appendTriangle(
172
+ positions,
173
+ { x: center.x, y: center.y },
174
+ outerStart,
175
+ outerEnd,
176
+ z
177
+ )
178
+ return
179
+ }
180
+
181
+ const innerStart = {
182
+ x: center.x + Math.cos(segment.startAngle) * segment.innerRadius,
183
+ y:
184
+ center.y +
185
+ Math.sin(segment.startAngle) *
186
+ segment.innerRadius *
187
+ segment.yDirection
188
+ }
189
+ const innerEnd = {
190
+ x: center.x + Math.cos(segment.endAngle) * segment.innerRadius,
191
+ y:
192
+ center.y +
193
+ Math.sin(segment.endAngle) *
194
+ segment.innerRadius *
195
+ segment.yDirection
196
+ }
197
+
198
+ PcbScene3dStrokeGeometryBuilder.#appendQuadTriangles(
199
+ positions,
200
+ outerStart,
201
+ outerEnd,
202
+ innerEnd,
203
+ innerStart,
204
+ z
205
+ )
206
+ }
207
+
208
+ /**
209
+ * Appends one rounded arc endpoint.
210
+ * @param {number[]} positions Position buffer.
211
+ * @param {{ x: number, y: number }} center Arc center.
212
+ * @param {number} radius Centerline radius.
213
+ * @param {number} capRadius Cap radius.
214
+ * @param {number} angle Arc endpoint angle.
215
+ * @param {number} yDirection Mirrored Y direction.
216
+ * @param {number} z Z position.
217
+ * @returns {void}
218
+ */
219
+ static #appendArcCapTriangles(
220
+ positions,
221
+ center,
222
+ radius,
223
+ capRadius,
224
+ angle,
225
+ yDirection,
226
+ z
227
+ ) {
228
+ PcbScene3dStrokeGeometryBuilder.#appendDiscTriangles(
229
+ positions,
230
+ {
231
+ x: center.x + Math.cos(angle) * radius,
232
+ y: center.y + Math.sin(angle) * radius * yDirection
233
+ },
234
+ capRadius,
235
+ z
236
+ )
237
+ }
238
+
239
+ /**
240
+ * Appends one rectangle as two triangles.
241
+ * @param {number[]} positions Position buffer.
242
+ * @param {{ x: number, y: number }} a First corner.
243
+ * @param {{ x: number, y: number }} b Second corner.
244
+ * @param {{ x: number, y: number }} c Third corner.
245
+ * @param {{ x: number, y: number }} d Fourth corner.
246
+ * @param {number} z Z position.
247
+ * @returns {void}
248
+ */
249
+ static #appendQuadTriangles(positions, a, b, c, d, z) {
250
+ PcbScene3dStrokeGeometryBuilder.#appendTriangle(positions, a, b, c, z)
251
+ PcbScene3dStrokeGeometryBuilder.#appendTriangle(positions, a, c, d, z)
252
+ }
253
+
254
+ /**
255
+ * Appends one filled circle fan.
256
+ * @param {number[]} positions Position buffer.
257
+ * @param {{ x: number, y: number }} center Disc center.
258
+ * @param {number} radius Disc radius.
259
+ * @param {number} z Z position.
260
+ * @returns {void}
261
+ */
262
+ static #appendDiscTriangles(positions, center, radius, z) {
263
+ const safeRadius = Math.max(Number(radius || 0), 0)
264
+ if (safeRadius <= 0) {
265
+ return
266
+ }
267
+
268
+ for (
269
+ let index = 0;
270
+ index < PcbScene3dStrokeGeometryBuilder.#ROUND_CAP_SEGMENTS;
271
+ index += 1
272
+ ) {
273
+ const startAngle =
274
+ (Math.PI * 2 * index) /
275
+ PcbScene3dStrokeGeometryBuilder.#ROUND_CAP_SEGMENTS
276
+ const endAngle =
277
+ (Math.PI * 2 * (index + 1)) /
278
+ PcbScene3dStrokeGeometryBuilder.#ROUND_CAP_SEGMENTS
279
+
280
+ PcbScene3dStrokeGeometryBuilder.#appendTriangle(
281
+ positions,
282
+ center,
283
+ {
284
+ x: center.x + Math.cos(startAngle) * safeRadius,
285
+ y: center.y + Math.sin(startAngle) * safeRadius
286
+ },
287
+ {
288
+ x: center.x + Math.cos(endAngle) * safeRadius,
289
+ y: center.y + Math.sin(endAngle) * safeRadius
290
+ },
291
+ z
292
+ )
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Resolves a finite stroke width with a caller-specific minimum.
298
+ * @param {unknown} width Authored stroke width.
299
+ * @param {unknown} minWidth Minimum width.
300
+ * @returns {number}
301
+ */
302
+ static #safeStrokeWidth(width, minWidth) {
303
+ return Math.max(
304
+ Number(width) || PcbScene3dStrokeGeometryBuilder.#DEFAULT_MIN_WIDTH,
305
+ Number(minWidth) ||
306
+ PcbScene3dStrokeGeometryBuilder.#DEFAULT_MIN_WIDTH
307
+ )
308
+ }
309
+
310
+ /**
311
+ * Appends one triangle into the position buffer.
312
+ * @param {number[]} positions Position buffer.
313
+ * @param {{ x: number, y: number }} a First point.
314
+ * @param {{ x: number, y: number }} b Second point.
315
+ * @param {{ x: number, y: number }} c Third point.
316
+ * @param {number} z Z position.
317
+ * @returns {void}
318
+ */
319
+ static #appendTriangle(positions, a, b, c, z) {
320
+ positions.push(a.x, a.y, z, b.x, b.y, z, c.x, c.y, z)
321
+ }
322
+ }
@@ -0,0 +1,99 @@
1
+ const FALLBACK_MESSAGES = {
2
+ 'scene3d.boardEnvelopeSuffix': 'mil board envelope',
3
+ 'scene3d.boardPosition': 'Board position',
4
+ 'scene3d.bodyPosition': 'Body position',
5
+ 'scene3d.bodyRotation': 'Body rotation',
6
+ 'scene3d.bomGroups': 'BOM groups',
7
+ 'scene3d.bottom': 'Bottom',
8
+ 'scene3d.companionModelsHint':
9
+ 'Companion models will be used when matching WRL or STEP files are loaded in the session.',
10
+ 'scene3d.componentInspector': 'Component inspector',
11
+ 'scene3d.componentsSuffix': 'components',
12
+ 'scene3d.controlsAria': '3D detail toggles',
13
+ 'scene3d.copperDetail': 'Copper detail',
14
+ 'scene3d.designator': 'Designator',
15
+ 'scene3d.downloadModelsZip': 'Download Models ZIP',
16
+ 'scene3d.downloaded': 'Downloaded',
17
+ 'scene3d.entries': 'entries',
18
+ 'scene3d.entry': 'entry',
19
+ 'scene3d.exportFailed': 'Model ZIP export failed:',
20
+ 'scene3d.externalModel': 'External model',
21
+ 'scene3d.externalModels': 'External models',
22
+ 'scene3d.fallbackBody': 'Fallback body',
23
+ 'scene3d.fallbackBodies': 'Fallback bodies',
24
+ 'scene3d.footprint': 'Footprint',
25
+ 'scene3d.inspectPrompt': 'Click a component to inspect it.',
26
+ 'scene3d.interactiveViewAria': 'Interactive 3D PCB view',
27
+ 'scene3d.isometric': 'Isometric',
28
+ 'scene3d.loading': 'Preparing 3D scene...',
29
+ 'scene3d.model': 'Model',
30
+ 'scene3d.modelFile': 'model file',
31
+ 'scene3d.modelFiles': 'model files',
32
+ 'scene3d.modelRotation': 'Model rotation',
33
+ 'scene3d.mountSide': 'Mount side',
34
+ 'scene3d.noMetadataFor': 'No metadata is available for',
35
+ 'scene3d.noModelsForExport':
36
+ 'No STEP or WRL models were resolved for export.',
37
+ 'scene3d.noPcb': '3D preview is available after loading a PCB document.',
38
+ 'scene3d.pattern': 'Pattern',
39
+ 'scene3d.picked': 'Picked',
40
+ 'scene3d.placements': 'Placements',
41
+ 'scene3d.pointerHint':
42
+ 'Drag to orbit, right-drag to pan, and use the wheel to zoom.',
43
+ 'scene3d.rotation': 'Rotation',
44
+ 'scene3d.skipped': 'Skipped',
45
+ 'scene3d.source': 'Source',
46
+ 'scene3d.startFailed': '3D preview could not start:',
47
+ 'scene3d.stillPreparing': '3D scene is still preparing.',
48
+ 'scene3d.title': '3D preview',
49
+ 'scene3d.to': 'to',
50
+ 'scene3d.toolbarAria': '3D camera presets',
51
+ 'scene3d.top': 'Top',
52
+ 'scene3d.touchHint':
53
+ 'Drag with one finger to orbit, pinch to zoom, and drag with two fingers to pan.',
54
+ 'scene3d.unresolved': 'unresolved'
55
+ }
56
+
57
+ /**
58
+ * Fallback-aware text helpers for viewer-owned UI copy.
59
+ */
60
+ export class PcbScene3dText {
61
+ /**
62
+ * Translates one key with English fallback copy.
63
+ * @param {((key: string) => string) | null | undefined} translate
64
+ * Host translation lookup.
65
+ * @param {string} key Message key.
66
+ * @returns {string}
67
+ */
68
+ static translate(translate, key) {
69
+ if (typeof translate !== 'function') {
70
+ return PcbScene3dText.fallback(key)
71
+ }
72
+
73
+ const value = translate(key)
74
+ if (!value || value === key) {
75
+ return PcbScene3dText.fallback(key)
76
+ }
77
+
78
+ return value
79
+ }
80
+
81
+ /**
82
+ * Creates a stable translator function with English fallback copy.
83
+ * @param {((key: string) => string) | null | undefined} translate
84
+ * Host translation lookup.
85
+ * @returns {(key: string) => string}
86
+ */
87
+ static createTranslator(translate) {
88
+ return (key) => PcbScene3dText.translate(translate, key)
89
+ }
90
+
91
+ /**
92
+ * Returns English fallback copy for one key.
93
+ * @param {string} key Message key.
94
+ * @returns {string}
95
+ */
96
+ static fallback(key) {
97
+ return FALLBACK_MESSAGES[key] || key
98
+ }
99
+ }