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,82 @@
1
+ /**
2
+ * Manages when procedural fallback bodies should stay visible alongside
3
+ * external 3D models.
4
+ */
5
+ export class PcbScene3dFallbackVisibility {
6
+ /**
7
+ * Registers one fallback render root under a component designator.
8
+ * @param {Map<string, Set<any>>} fallbackRoots
9
+ * @param {string} designator
10
+ * @param {any} rootObject
11
+ * @returns {void}
12
+ */
13
+ static registerFallbackRoot(fallbackRoots, designator, rootObject) {
14
+ if (!(fallbackRoots instanceof Map) || !rootObject) {
15
+ return
16
+ }
17
+
18
+ const normalizedDesignator = String(designator || '').trim()
19
+ if (!normalizedDesignator) {
20
+ return
21
+ }
22
+
23
+ if (!fallbackRoots.has(normalizedDesignator)) {
24
+ fallbackRoots.set(normalizedDesignator, new Set())
25
+ }
26
+
27
+ fallbackRoots.get(normalizedDesignator)?.add(rootObject)
28
+ }
29
+
30
+ /**
31
+ * Marks one designator as having a successfully loaded external model.
32
+ * @param {Set<string>} loadedDesignators
33
+ * @param {string} designator
34
+ * @returns {void}
35
+ */
36
+ static markExternalModelLoaded(loadedDesignators, designator) {
37
+ if (!(loadedDesignators instanceof Set)) {
38
+ return
39
+ }
40
+
41
+ const normalizedDesignator = String(designator || '').trim()
42
+ if (!normalizedDesignator) {
43
+ return
44
+ }
45
+
46
+ loadedDesignators.add(normalizedDesignator)
47
+ }
48
+
49
+ /**
50
+ * Applies the current visibility policy to all registered fallback roots.
51
+ * Fallbacks stay visible when their toggle is enabled and either no
52
+ * external model loaded for that designator or external models are hidden.
53
+ * @param {Map<string, Set<any>>} fallbackRoots
54
+ * @param {Set<string>} loadedDesignators
55
+ * @param {{ 'fallback-bodies'?: boolean, 'external-models'?: boolean }} toggles
56
+ * @returns {void}
57
+ */
58
+ static applyVisibility(fallbackRoots, loadedDesignators, toggles) {
59
+ if (!(fallbackRoots instanceof Map)) {
60
+ return
61
+ }
62
+
63
+ const showFallbackBodies = Boolean(toggles?.['fallback-bodies'])
64
+ const showExternalModels = Boolean(toggles?.['external-models'])
65
+
66
+ fallbackRoots.forEach((roots, designator) => {
67
+ const hideForLoadedExternal =
68
+ showExternalModels &&
69
+ loadedDesignators instanceof Set &&
70
+ loadedDesignators.has(String(designator || '').trim())
71
+
72
+ roots?.forEach?.((rootObject) => {
73
+ if (!rootObject) {
74
+ return
75
+ }
76
+
77
+ rootObject.visible =
78
+ showFallbackBodies && !hideForLoadedExternal
79
+ })
80
+ })
81
+ }
82
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Centralizes pointer and touch interaction defaults for the 3D PCB viewport.
3
+ */
4
+ export class PcbScene3dInteractionHints {
5
+ /**
6
+ * Applies explicit OrbitControls input mappings for desktop and touch.
7
+ * @param {{ mouseButtons?: any, touches?: any }} controls
8
+ * @param {{ MOUSE?: any, TOUCH?: any }} THREE
9
+ * @returns {void}
10
+ */
11
+ static configureControls(controls, THREE) {
12
+ if (!controls) {
13
+ return
14
+ }
15
+
16
+ if (THREE?.MOUSE) {
17
+ controls.mouseButtons = {
18
+ LEFT: THREE.MOUSE.ROTATE,
19
+ MIDDLE: THREE.MOUSE.DOLLY,
20
+ RIGHT: THREE.MOUSE.PAN
21
+ }
22
+ }
23
+
24
+ if (THREE?.TOUCH) {
25
+ controls.touches = {
26
+ ONE: THREE.TOUCH.ROTATE,
27
+ TWO: THREE.TOUCH.DOLLY_PAN
28
+ }
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Resolves the default interaction copy for the current pointer type.
34
+ * @param {{ matchMedia?: (query: string) => { matches?: boolean } }} [environment]
35
+ * @param {((key: string) => string) | null} [translate] Translation lookup.
36
+ * @returns {string}
37
+ */
38
+ static resolveDefaultMessage(
39
+ environment = globalThis.window,
40
+ translate = null
41
+ ) {
42
+ if (environment?.matchMedia?.('(pointer: coarse)')?.matches) {
43
+ if (typeof translate === 'function') {
44
+ const value = translate('scene3d.touchHint')
45
+ if (value && value !== 'scene3d.touchHint') return value
46
+ }
47
+ return 'Drag with one finger to orbit, pinch to zoom, and drag with two fingers to pan.'
48
+ }
49
+
50
+ if (typeof translate === 'function') {
51
+ const value = translate('scene3d.pointerHint')
52
+ if (value && value !== 'scene3d.pointerHint') return value
53
+ }
54
+ return 'Drag to orbit, right-drag to pan, and use the wheel to zoom.'
55
+ }
56
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Builds a stable transform hierarchy for one PCB-mounted 3D object.
3
+ */
4
+ export class PcbScene3dMountRig {
5
+ /**
6
+ * Creates one transform rig that keeps the XY anchor fixed while flipping
7
+ * bottom-side content around the board face.
8
+ * @param {any} THREE
9
+ * @param {{ mountSide?: string, rotationDeg?: number, positionMil?: { x?: number, y?: number, z?: number } }} placement
10
+ * @returns {{ rootGroup: any, orientationGroup: any, sideGroup: any, faceGroup: any }}
11
+ */
12
+ static create(THREE, placement) {
13
+ const rootGroup = new THREE.Group()
14
+ const orientationGroup = new THREE.Group()
15
+ const sideGroup = new THREE.Group()
16
+ const faceGroup = new THREE.Group()
17
+ const positionMil = placement?.positionMil || {}
18
+
19
+ rootGroup.position.set(
20
+ Number(positionMil.x || 0),
21
+ Number(positionMil.y || 0),
22
+ 0
23
+ )
24
+ orientationGroup.rotation.z =
25
+ (Number(placement?.rotationDeg || 0) * Math.PI) / 180
26
+ if (PcbScene3dMountRig.#isBottomSide(placement?.mountSide)) {
27
+ sideGroup.rotation.y = Math.PI
28
+ sideGroup.rotation.z = Math.PI
29
+ }
30
+
31
+ faceGroup.position.z = Math.abs(Number(positionMil.z || 0))
32
+
33
+ sideGroup.add(faceGroup)
34
+ orientationGroup.add(sideGroup)
35
+ rootGroup.add(orientationGroup)
36
+
37
+ return {
38
+ rootGroup,
39
+ orientationGroup,
40
+ sideGroup,
41
+ faceGroup
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Returns true when one placement belongs on the underside of the board.
47
+ * @param {string | undefined} mountSide
48
+ * @returns {boolean}
49
+ */
50
+ static #isBottomSide(mountSide) {
51
+ return String(mountSide || 'top').toLowerCase() === 'bottom'
52
+ }
53
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Builds stable board-outline commands for the 3D PCB runtime.
3
+ */
4
+ export class PcbScene3dOutlineBuilder {
5
+ static #DEGENERATE_LINE_EPSILON = 0.01
6
+
7
+ /**
8
+ * Converts one board outline into local move/line/arc commands.
9
+ * @param {{ centerX?: number, centerY?: number, segments?: Array<Record<string, number | string>> }} board
10
+ * @returns {Array<Record<string, number | boolean>>}
11
+ */
12
+ static buildCommands(board) {
13
+ const segments = Array.isArray(board?.segments) ? board.segments : []
14
+ if (!segments.length) {
15
+ return []
16
+ }
17
+
18
+ const centerX = Number(board?.centerX || 0)
19
+ const centerY = Number(board?.centerY || 0)
20
+ const firstPoint = PcbScene3dOutlineBuilder.#toLocalPoint(
21
+ Number(segments[0].x1 || 0),
22
+ Number(segments[0].y1 || 0),
23
+ centerX,
24
+ centerY
25
+ )
26
+ const commands = [
27
+ {
28
+ type: 'move',
29
+ x: firstPoint.x,
30
+ y: firstPoint.y
31
+ }
32
+ ]
33
+
34
+ for (const segment of segments) {
35
+ if (segment.type === 'arc') {
36
+ const arcCommand = PcbScene3dOutlineBuilder.#buildArcCommand(
37
+ segment,
38
+ centerX,
39
+ centerY
40
+ )
41
+ if (arcCommand) {
42
+ commands.push(arcCommand)
43
+ }
44
+ continue
45
+ }
46
+
47
+ const lineCommand = PcbScene3dOutlineBuilder.#buildLineCommand(
48
+ segment,
49
+ centerX,
50
+ centerY
51
+ )
52
+ if (lineCommand) {
53
+ commands.push(lineCommand)
54
+ }
55
+ }
56
+
57
+ return commands
58
+ }
59
+
60
+ /**
61
+ * Converts one outline line segment into a local line command.
62
+ * @param {Record<string, number | string>} segment
63
+ * @param {number} centerX
64
+ * @param {number} centerY
65
+ * @returns {{ type: 'line', x: number, y: number } | null}
66
+ */
67
+ static #buildLineCommand(segment, centerX, centerY) {
68
+ const startPoint = PcbScene3dOutlineBuilder.#toLocalPoint(
69
+ Number(segment.x1 || 0),
70
+ Number(segment.y1 || 0),
71
+ centerX,
72
+ centerY
73
+ )
74
+ const endPoint = PcbScene3dOutlineBuilder.#toLocalPoint(
75
+ Number(segment.x2 || 0),
76
+ Number(segment.y2 || 0),
77
+ centerX,
78
+ centerY
79
+ )
80
+
81
+ if (
82
+ PcbScene3dOutlineBuilder.#distanceBetween(startPoint, endPoint) <
83
+ PcbScene3dOutlineBuilder.#DEGENERATE_LINE_EPSILON
84
+ ) {
85
+ return null
86
+ }
87
+
88
+ return {
89
+ type: 'line',
90
+ x: endPoint.x,
91
+ y: endPoint.y
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Converts one outline arc segment into a local arc command traced from
97
+ * the ordered segment endpoints instead of the serialized angle fields.
98
+ * @param {Record<string, number | string>} segment
99
+ * @param {number} centerX
100
+ * @param {number} centerY
101
+ * @returns {{ type: 'arc', cx: number, cy: number, radius: number, startX: number, startY: number, endX: number, endY: number, startAngleRad: number, endAngleRad: number, clockwise: boolean } | null}
102
+ */
103
+ static #buildArcCommand(segment, centerX, centerY) {
104
+ const arcCenter = PcbScene3dOutlineBuilder.#toLocalPoint(
105
+ Number(segment.cx || 0),
106
+ Number(segment.cy || 0),
107
+ centerX,
108
+ centerY
109
+ )
110
+ const startPoint = PcbScene3dOutlineBuilder.#toLocalPoint(
111
+ Number(segment.x1 || 0),
112
+ Number(segment.y1 || 0),
113
+ centerX,
114
+ centerY
115
+ )
116
+ const endPoint = PcbScene3dOutlineBuilder.#toLocalPoint(
117
+ Number(segment.x2 || 0),
118
+ Number(segment.y2 || 0),
119
+ centerX,
120
+ centerY
121
+ )
122
+ const radius =
123
+ Math.max(
124
+ Number(segment.radius || 0),
125
+ PcbScene3dOutlineBuilder.#distanceBetween(
126
+ arcCenter,
127
+ startPoint
128
+ ),
129
+ PcbScene3dOutlineBuilder.#distanceBetween(arcCenter, endPoint)
130
+ ) || 0
131
+
132
+ if (!radius) {
133
+ return null
134
+ }
135
+
136
+ const startAngleRad = Math.atan2(
137
+ startPoint.y - arcCenter.y,
138
+ startPoint.x - arcCenter.x
139
+ )
140
+ const endAngleRad = Math.atan2(
141
+ endPoint.y - arcCenter.y,
142
+ endPoint.x - arcCenter.x
143
+ )
144
+ const deltaAngle = PcbScene3dOutlineBuilder.#resolveShortDeltaAngle(
145
+ startAngleRad,
146
+ endAngleRad
147
+ )
148
+
149
+ return {
150
+ type: 'arc',
151
+ cx: arcCenter.x,
152
+ cy: arcCenter.y,
153
+ radius,
154
+ startX: startPoint.x,
155
+ startY: startPoint.y,
156
+ endX: endPoint.x,
157
+ endY: endPoint.y,
158
+ startAngleRad,
159
+ endAngleRad,
160
+ clockwise: deltaAngle < 0
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Converts one board-space point into local centered scene coordinates.
166
+ * @param {number} x
167
+ * @param {number} y
168
+ * @param {number} centerX
169
+ * @param {number} centerY
170
+ * @returns {{ x: number, y: number }}
171
+ */
172
+ static #toLocalPoint(x, y, centerX, centerY) {
173
+ return {
174
+ x: Number(x || 0) - centerX,
175
+ y: Number(y || 0) - centerY
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Measures one point-to-point distance.
181
+ * @param {{ x: number, y: number }} left
182
+ * @param {{ x: number, y: number }} right
183
+ * @returns {number}
184
+ */
185
+ static #distanceBetween(left, right) {
186
+ const deltaX = Number(right.x || 0) - Number(left.x || 0)
187
+ const deltaY = Number(right.y || 0) - Number(left.y || 0)
188
+ return Math.hypot(deltaX, deltaY)
189
+ }
190
+
191
+ /**
192
+ * Resolves the shortest signed delta between two radians.
193
+ * @param {number} startAngleRad
194
+ * @param {number} endAngleRad
195
+ * @returns {number}
196
+ */
197
+ static #resolveShortDeltaAngle(startAngleRad, endAngleRad) {
198
+ let deltaAngle = endAngleRad - startAngleRad
199
+
200
+ if (deltaAngle > Math.PI) {
201
+ deltaAngle -= Math.PI * 2
202
+ }
203
+
204
+ if (deltaAngle < -Math.PI) {
205
+ deltaAngle += Math.PI * 2
206
+ }
207
+
208
+ return deltaAngle
209
+ }
210
+ }