pcb-scene3d-viewer 1.1.49 → 1.2.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.
- package/README.md +46 -21
- package/docs/api.md +109 -8
- package/docs/circuitjson.md +143 -29
- package/docs/model-format.md +50 -7
- package/docs/release-notes-v1.2.0.md +122 -0
- package/docs/testing.md +30 -1
- package/package.json +6 -3
- package/spec/library-scope.md +10 -2
- package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
- package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
- package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
- package/src/PcbModelArchiveExporter.mjs +28 -112
- package/src/PcbModelArchiveSourceBundle.mjs +326 -0
- package/src/PcbScene3dAabbIndex.mjs +464 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
- package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
- package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
- package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
- package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
- package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
- package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
- package/src/PcbScene3dController.mjs +40 -34
- package/src/PcbScene3dCopperFactory.mjs +36 -22
- package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
- package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
- package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
- package/src/PcbScene3dCopperTextFactory.mjs +12 -4
- package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
- package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
- package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
- package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
- package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
- package/src/PcbScene3dDrillPathFactory.mjs +86 -16
- package/src/PcbScene3dDrillVoidFactory.mjs +35 -10
- package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
- package/src/PcbScene3dExternalModels.mjs +23 -24
- package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
- package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
- package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
- package/src/PcbScene3dModelContent.mjs +236 -0
- package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
- package/src/PcbScene3dModelIdentity.mjs +106 -0
- package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
- package/src/PcbScene3dPreparedPolygon.mjs +709 -0
- package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
- package/src/PcbScene3dRuntime.mjs +45 -45
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
- package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
- package/src/PcbScene3dShellRenderer.mjs +75 -6
- package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
- package/src/PcbScene3dStepLoader.mjs +11 -10
- package/src/PcbScene3dText.mjs +1 -1
- package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
|
@@ -23,18 +23,68 @@ export class PcbScene3dCircuitJsonGeometry {
|
|
|
23
23
|
* @returns {object}
|
|
24
24
|
*/
|
|
25
25
|
static buildBoard(index, options = {}) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
const panelElements = index.elementsByType.get('pcb_panel') || []
|
|
27
|
+
const boardElements = index.elementsByType.get('pcb_board') || []
|
|
28
|
+
const explicitElements = panelElements.length
|
|
29
|
+
? panelElements
|
|
30
|
+
: boardElements
|
|
31
|
+
const elements = explicitElements.length
|
|
32
|
+
? explicitElements
|
|
33
|
+
: [
|
|
34
|
+
PcbScene3dCircuitJsonGeometry.#fauxBoardElement(
|
|
35
|
+
index,
|
|
36
|
+
options
|
|
37
|
+
) || {}
|
|
38
|
+
]
|
|
39
|
+
const sourceContours = elements.map((element) =>
|
|
40
|
+
PcbScene3dCircuitJsonGeometry.#buildBoardContour(
|
|
41
|
+
index,
|
|
42
|
+
element,
|
|
43
|
+
options
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
const bounds =
|
|
47
|
+
PcbScene3dCircuitJsonGeometry.#contourBounds(sourceContours)
|
|
48
|
+
const centerX = (bounds.minX + bounds.maxX) / 2
|
|
49
|
+
const centerY = (bounds.minY + bounds.maxY) / 2
|
|
50
|
+
const contours = sourceContours.map((contour) => ({
|
|
51
|
+
...contour,
|
|
52
|
+
centerX,
|
|
53
|
+
centerY
|
|
54
|
+
}))
|
|
55
|
+
const primary = contours[0]
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
widthMil: bounds.maxX - bounds.minX,
|
|
59
|
+
heightMil: bounds.maxY - bounds.minY,
|
|
60
|
+
thicknessMil: Math.max(
|
|
61
|
+
...contours.map((contour) => contour.thicknessMil)
|
|
62
|
+
),
|
|
63
|
+
minX: bounds.minX,
|
|
64
|
+
minY: bounds.minY,
|
|
65
|
+
centerX,
|
|
66
|
+
centerY,
|
|
67
|
+
segments: primary.segments,
|
|
68
|
+
cutouts: PcbScene3dCircuitJsonGeometry.#uniqueCutouts(contours),
|
|
69
|
+
contours,
|
|
70
|
+
surfaceColor: null,
|
|
71
|
+
edgeColor: null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Builds one independently renderable board or panel contour.
|
|
77
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
78
|
+
* @param {object} boardElement Board or panel element.
|
|
79
|
+
* @param {{ boardDrillQuality?: string }} options Adapter options.
|
|
80
|
+
* @returns {object}
|
|
81
|
+
*/
|
|
82
|
+
static #buildBoardContour(index, boardElement, options) {
|
|
83
|
+
const declaredWidthMil = CircuitJsonUnits.mmToMil(
|
|
34
84
|
boardElement.width,
|
|
35
85
|
DEFAULT_BOARD_WIDTH_MM
|
|
36
86
|
)
|
|
37
|
-
const
|
|
87
|
+
const declaredHeightMil = CircuitJsonUnits.mmToMil(
|
|
38
88
|
boardElement.height,
|
|
39
89
|
DEFAULT_BOARD_HEIGHT_MM
|
|
40
90
|
)
|
|
@@ -43,24 +93,35 @@ export class PcbScene3dCircuitJsonGeometry {
|
|
|
43
93
|
DEFAULT_BOARD_THICKNESS_MM
|
|
44
94
|
)
|
|
45
95
|
const center = CircuitJsonUnits.pointMmToMil(boardElement.center || {})
|
|
46
|
-
const
|
|
47
|
-
|
|
96
|
+
const fallback = {
|
|
97
|
+
minX: center.x - declaredWidthMil / 2,
|
|
98
|
+
minY: center.y - declaredHeightMil / 2,
|
|
99
|
+
widthMil: declaredWidthMil,
|
|
100
|
+
heightMil: declaredHeightMil
|
|
101
|
+
}
|
|
48
102
|
const segments = PcbScene3dCircuitJsonGeometry.#buildBoardSegments(
|
|
49
103
|
boardElement,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
104
|
+
fallback
|
|
105
|
+
)
|
|
106
|
+
const bounds = PcbScene3dCircuitJsonGeometry.#segmentBounds(
|
|
107
|
+
segments,
|
|
108
|
+
fallback
|
|
56
109
|
)
|
|
57
110
|
|
|
58
111
|
return {
|
|
59
|
-
|
|
60
|
-
|
|
112
|
+
sourceId: String(
|
|
113
|
+
boardElement?.pcb_panel_id ||
|
|
114
|
+
boardElement?.panel_id ||
|
|
115
|
+
boardElement?.pcb_board_id ||
|
|
116
|
+
boardElement?.board_id ||
|
|
117
|
+
''
|
|
118
|
+
),
|
|
119
|
+
sourceType: String(boardElement?.type || 'pcb_board'),
|
|
120
|
+
widthMil: bounds.maxX - bounds.minX,
|
|
121
|
+
heightMil: bounds.maxY - bounds.minY,
|
|
61
122
|
thicknessMil,
|
|
62
|
-
minX,
|
|
63
|
-
minY,
|
|
123
|
+
minX: bounds.minX,
|
|
124
|
+
minY: bounds.minY,
|
|
64
125
|
centerX: center.x,
|
|
65
126
|
centerY: center.y,
|
|
66
127
|
segments,
|
|
@@ -74,6 +135,78 @@ export class PcbScene3dCircuitJsonGeometry {
|
|
|
74
135
|
}
|
|
75
136
|
}
|
|
76
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Resolves finite bounds around one contour's ordered segments.
|
|
140
|
+
* @param {object[]} segments Board segments.
|
|
141
|
+
* @param {{ minX: number, minY: number, widthMil: number, heightMil: number }} fallback Rectangle fallback.
|
|
142
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number }}
|
|
143
|
+
*/
|
|
144
|
+
static #segmentBounds(segments, fallback) {
|
|
145
|
+
const points = segments.flatMap((segment) => [
|
|
146
|
+
{ x: Number(segment?.x1), y: Number(segment?.y1) },
|
|
147
|
+
{ x: Number(segment?.x2), y: Number(segment?.y2) }
|
|
148
|
+
])
|
|
149
|
+
const finite = points.filter((point) =>
|
|
150
|
+
Number.isFinite(point.x + point.y)
|
|
151
|
+
)
|
|
152
|
+
if (!finite.length) {
|
|
153
|
+
return {
|
|
154
|
+
minX: fallback.minX,
|
|
155
|
+
minY: fallback.minY,
|
|
156
|
+
maxX: fallback.minX + fallback.widthMil,
|
|
157
|
+
maxY: fallback.minY + fallback.heightMil
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return finite.reduce(
|
|
162
|
+
(bounds, point) => ({
|
|
163
|
+
minX: Math.min(bounds.minX, point.x),
|
|
164
|
+
minY: Math.min(bounds.minY, point.y),
|
|
165
|
+
maxX: Math.max(bounds.maxX, point.x),
|
|
166
|
+
maxY: Math.max(bounds.maxY, point.y)
|
|
167
|
+
}),
|
|
168
|
+
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolves aggregate bounds around every physical contour.
|
|
174
|
+
* @param {object[]} contours Board contours.
|
|
175
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number }}
|
|
176
|
+
*/
|
|
177
|
+
static #contourBounds(contours) {
|
|
178
|
+
return contours.reduce(
|
|
179
|
+
(bounds, contour) => ({
|
|
180
|
+
minX: Math.min(bounds.minX, contour.minX),
|
|
181
|
+
minY: Math.min(bounds.minY, contour.minY),
|
|
182
|
+
maxX: Math.max(bounds.maxX, contour.minX + contour.widthMil),
|
|
183
|
+
maxY: Math.max(bounds.maxY, contour.minY + contour.heightMil)
|
|
184
|
+
}),
|
|
185
|
+
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Flattens contour cutouts without duplicating global cutout rows.
|
|
191
|
+
* @param {object[]} contours Board contours.
|
|
192
|
+
* @returns {object[]}
|
|
193
|
+
*/
|
|
194
|
+
static #uniqueCutouts(contours) {
|
|
195
|
+
const seen = new Set()
|
|
196
|
+
return contours
|
|
197
|
+
.flatMap((contour) => contour.cutouts || [])
|
|
198
|
+
.filter((cutout) => {
|
|
199
|
+
const key =
|
|
200
|
+
cutout.sourceId ||
|
|
201
|
+
JSON.stringify(
|
|
202
|
+
(cutout.points || []).map((point) => [point.x, point.y])
|
|
203
|
+
)
|
|
204
|
+
if (seen.has(key)) return false
|
|
205
|
+
seen.add(key)
|
|
206
|
+
return true
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
77
210
|
/**
|
|
78
211
|
* Builds a generated board element around component bounds when requested.
|
|
79
212
|
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
@@ -175,43 +308,61 @@ export class PcbScene3dCircuitJsonGeometry {
|
|
|
175
308
|
/**
|
|
176
309
|
* Resolves drill opening metadata from circular, pill, and offset fields.
|
|
177
310
|
* @param {object} hole CircuitJSON hole-like element.
|
|
178
|
-
* @
|
|
311
|
+
* @param {object | null} [primitive] Shared normalized hole primitive.
|
|
312
|
+
* @returns {{ center: { x: number, y: number }, diameter: number, width: number, height: number, shape: 'circle' | 'pill' | 'rect', slotLength: number, rotationDeg: number }}
|
|
179
313
|
*/
|
|
180
|
-
static holeDrillSpec(hole) {
|
|
314
|
+
static holeDrillSpec(hole, primitive = null) {
|
|
181
315
|
const center = CircuitJsonUnits.pointMmToMil({
|
|
182
316
|
x: Number(hole?.x || 0) + Number(hole?.hole_offset_x || 0),
|
|
183
317
|
y: Number(hole?.y || 0) + Number(hole?.hole_offset_y || 0)
|
|
184
318
|
})
|
|
185
319
|
const width = CircuitJsonUnits.mmToMil(
|
|
186
|
-
hole?.hole_width || hole?.hole_diameter,
|
|
320
|
+
primitive?.holeWidth || hole?.hole_width || hole?.hole_diameter,
|
|
187
321
|
0
|
|
188
322
|
)
|
|
189
323
|
const height = CircuitJsonUnits.mmToMil(
|
|
190
|
-
|
|
324
|
+
primitive?.holeHeight ||
|
|
325
|
+
hole?.hole_height ||
|
|
326
|
+
hole?.hole_diameter ||
|
|
327
|
+
hole?.hole_width,
|
|
191
328
|
0
|
|
192
329
|
)
|
|
193
330
|
const diameter = PcbScene3dCircuitJsonGeometry.#firstPositive([
|
|
194
|
-
|
|
195
|
-
? CircuitJsonUnits.mmToMil(
|
|
196
|
-
:
|
|
331
|
+
primitive?.holeDiameter
|
|
332
|
+
? CircuitJsonUnits.mmToMil(primitive.holeDiameter, 0)
|
|
333
|
+
: hole?.hole_diameter
|
|
334
|
+
? CircuitJsonUnits.mmToMil(hole.hole_diameter, 0)
|
|
335
|
+
: 0,
|
|
197
336
|
Math.min(width || Infinity, height || Infinity),
|
|
198
337
|
width,
|
|
199
338
|
height
|
|
200
339
|
])
|
|
340
|
+
const shapeText = String(
|
|
341
|
+
primitive?.holeShape || hole?.hole_shape || ''
|
|
342
|
+
).toLowerCase()
|
|
343
|
+
const isRect =
|
|
344
|
+
shapeText.includes('rect') || shapeText.includes('square')
|
|
201
345
|
const isPill =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.includes('
|
|
205
|
-
|
|
346
|
+
!isRect &&
|
|
347
|
+
(shapeText.includes('pill') ||
|
|
348
|
+
shapeText.includes('oval') ||
|
|
349
|
+
(width > 0 && height > 0 && Math.abs(width - height) > 0.001))
|
|
350
|
+
const shape = isRect ? 'rect' : isPill ? 'pill' : 'circle'
|
|
206
351
|
const slotLength = isPill ? Math.max(width, height, diameter) : 0
|
|
207
352
|
const axisRotation = isPill && height > width ? 90 : 0
|
|
208
353
|
|
|
209
354
|
return {
|
|
210
355
|
center,
|
|
211
356
|
diameter,
|
|
357
|
+
width: width || diameter,
|
|
358
|
+
height: height || diameter,
|
|
359
|
+
shape,
|
|
212
360
|
slotLength: slotLength > diameter ? slotLength : 0,
|
|
213
361
|
rotationDeg:
|
|
214
|
-
|
|
362
|
+
Number(
|
|
363
|
+
primitive?.holeRotation ??
|
|
364
|
+
PcbScene3dCircuitJsonGeometry.#rotationDeg(hole)
|
|
365
|
+
) + axisRotation
|
|
215
366
|
}
|
|
216
367
|
}
|
|
217
368
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { CircuitJsonDocumentContext } from 'circuitjson-toolkit'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Performs non-mutating CircuitJSON input detection for viewer routing.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbScene3dCircuitJsonInput {
|
|
7
|
+
/**
|
|
8
|
+
* Returns true for a raw model, canonical document, or prepared context.
|
|
9
|
+
* @param {unknown} value Candidate input.
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
static isModel(value) {
|
|
13
|
+
try {
|
|
14
|
+
if (value instanceof CircuitJsonDocumentContext) return true
|
|
15
|
+
const model = PcbScene3dCircuitJsonInput.#validationModel(value)
|
|
16
|
+
return Boolean(
|
|
17
|
+
model && PcbScene3dCircuitJsonInput.#hasElementRows(model)
|
|
18
|
+
)
|
|
19
|
+
} catch {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks the descriptor-safe structural boundary needed before shared
|
|
26
|
+
* normalization and validation prepare the model.
|
|
27
|
+
* @param {object[]} model Dense model candidate.
|
|
28
|
+
* @returns {boolean} Whether every row exposes an own string type.
|
|
29
|
+
*/
|
|
30
|
+
static #hasElementRows(model) {
|
|
31
|
+
for (let index = 0; index < model.length; index += 1) {
|
|
32
|
+
const element = model[index]
|
|
33
|
+
const type = PcbScene3dCircuitJsonInput.#ownData(element, 'type')
|
|
34
|
+
if (typeof type !== 'string' || !type.trim()) return false
|
|
35
|
+
}
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a non-mutating model view for predicate validation.
|
|
41
|
+
* @param {unknown} value Model or canonical document candidate.
|
|
42
|
+
* @returns {object[] | null}
|
|
43
|
+
*/
|
|
44
|
+
static #validationModel(value) {
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return PcbScene3dCircuitJsonInput.#plainModel(value)
|
|
47
|
+
}
|
|
48
|
+
if (
|
|
49
|
+
PcbScene3dCircuitJsonInput.#ownData(value, 'schema') !==
|
|
50
|
+
'ecad-toolkit.document.v1'
|
|
51
|
+
) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
const model = PcbScene3dCircuitJsonInput.#ownData(value, 'model')
|
|
55
|
+
return Array.isArray(model)
|
|
56
|
+
? PcbScene3dCircuitJsonInput.#plainModel(model)
|
|
57
|
+
: null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Removes safe legacy array metadata without freezing caller values.
|
|
62
|
+
* @param {object[]} model CircuitJSON model candidate.
|
|
63
|
+
* @returns {object[] | null}
|
|
64
|
+
*/
|
|
65
|
+
static #plainModel(model) {
|
|
66
|
+
let prototype
|
|
67
|
+
let descriptors
|
|
68
|
+
try {
|
|
69
|
+
prototype = Object.getPrototypeOf(model)
|
|
70
|
+
descriptors = Object.getOwnPropertyDescriptors(model)
|
|
71
|
+
} catch {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
const length = descriptors.length?.value
|
|
75
|
+
if (
|
|
76
|
+
prototype !== Array.prototype ||
|
|
77
|
+
!Number.isSafeInteger(length) ||
|
|
78
|
+
length < 0
|
|
79
|
+
) {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const keys = Reflect.ownKeys(descriptors)
|
|
84
|
+
const plain = keys.length === length + 1 ? null : new Array(length)
|
|
85
|
+
for (let index = 0; index < length; index += 1) {
|
|
86
|
+
const descriptor = descriptors[String(index)]
|
|
87
|
+
if (
|
|
88
|
+
!descriptor ||
|
|
89
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
90
|
+
descriptor.enumerable !== true
|
|
91
|
+
) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
if (plain) plain[index] = descriptor.value
|
|
95
|
+
}
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
if (key === 'length') continue
|
|
98
|
+
const index =
|
|
99
|
+
typeof key === 'string' && /^(?:0|[1-9]\d*)$/u.test(key)
|
|
100
|
+
? Number(key)
|
|
101
|
+
: -1
|
|
102
|
+
if (Number.isSafeInteger(index) && index < length) continue
|
|
103
|
+
const descriptor = descriptors[key]
|
|
104
|
+
if (
|
|
105
|
+
typeof key !== 'string' ||
|
|
106
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
107
|
+
descriptor.enumerable !== true
|
|
108
|
+
) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return plain || model
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reads one own data property without invoking caller accessors.
|
|
117
|
+
* @param {unknown} value Record candidate.
|
|
118
|
+
* @param {string} name Property name.
|
|
119
|
+
* @returns {unknown} Own data value or undefined.
|
|
120
|
+
*/
|
|
121
|
+
static #ownData(value, name) {
|
|
122
|
+
if (!value || typeof value !== 'object') return undefined
|
|
123
|
+
try {
|
|
124
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, name)
|
|
125
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
126
|
+
? descriptor.value
|
|
127
|
+
: undefined
|
|
128
|
+
} catch {
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes canonical CircuitJSON CAD model asset references.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dCircuitJsonModelAsset {
|
|
5
|
+
/**
|
|
6
|
+
* Resolves one viewer-supported canonical model asset reference.
|
|
7
|
+
* @param {unknown} asset Model asset candidate.
|
|
8
|
+
* @returns {{ format: string, sourceUrl: string } | null}
|
|
9
|
+
*/
|
|
10
|
+
static reference(asset) {
|
|
11
|
+
if (!asset || typeof asset !== 'object') return null
|
|
12
|
+
const sourceUrl = String(
|
|
13
|
+
asset.project_relative_path || asset.url || ''
|
|
14
|
+
).trim()
|
|
15
|
+
const format = PcbScene3dCircuitJsonModelAsset.#format(asset, sourceUrl)
|
|
16
|
+
return sourceUrl && format ? { format, sourceUrl } : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a supported CAD format from canonical and retained metadata.
|
|
21
|
+
* @param {object} asset Model asset metadata.
|
|
22
|
+
* @param {string} sourceUrl Canonical model asset path.
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
static #format(asset, sourceUrl) {
|
|
26
|
+
const text = [asset.format, asset.mimetype, sourceUrl, asset.url]
|
|
27
|
+
.map(String)
|
|
28
|
+
.join(' ')
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
if (/\bglb\b|\.glb(?:[?#]|$)/u.test(text)) return 'glb'
|
|
31
|
+
if (/\bgltf\b|\.gltf(?:[?#]|$)/u.test(text)) return 'gltf'
|
|
32
|
+
if (/\b3mf\b|\.3mf(?:[?#]|$)/u.test(text)) return '3mf'
|
|
33
|
+
if (/\bobj\b|\.obj(?:[?#]|$)/u.test(text)) return 'obj'
|
|
34
|
+
if (/\bstl\b|\.stl(?:[?#]|$)/u.test(text)) return 'stl'
|
|
35
|
+
if (/\bwrl\b|vrml|\.wrl(?:[?#]|$)/u.test(text)) return 'wrl'
|
|
36
|
+
return /step|stp|\.step(?:[?#]|$)|\.stp(?:[?#]|$)/u.test(text)
|
|
37
|
+
? 'step'
|
|
38
|
+
: ''
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -15,25 +15,19 @@ import { PcbScene3dText } from './PcbScene3dText.mjs'
|
|
|
15
15
|
export class PcbScene3dController {
|
|
16
16
|
/** @type {HTMLElement | null} */
|
|
17
17
|
#viewportNode
|
|
18
|
-
|
|
19
18
|
/** @type {any} */
|
|
20
19
|
#documentModel
|
|
21
|
-
|
|
22
20
|
/** @type {HTMLElement | null} */
|
|
23
21
|
#rootNode
|
|
24
|
-
|
|
25
22
|
/** @type {HTMLElement | null} */
|
|
26
23
|
#diagnosticsNode
|
|
27
|
-
|
|
28
24
|
/** @type {HTMLElement | null} */
|
|
29
25
|
#selectionNode
|
|
30
|
-
|
|
31
26
|
/** @type {HTMLElement | null} */
|
|
32
27
|
#adjustmentHostNode
|
|
33
28
|
|
|
34
29
|
/** @type {PcbScene3dAdjustmentControlBinder | null} */
|
|
35
30
|
#adjustmentControls
|
|
36
|
-
|
|
37
31
|
/** @type {PcbScene3dSelectionVisibilityBinder | null} */
|
|
38
32
|
#visibilityControls
|
|
39
33
|
/** @type {Array<{ node: EventTarget, type: string, listener: (event: any) => void }>} */
|
|
@@ -41,19 +35,16 @@ export class PcbScene3dController {
|
|
|
41
35
|
|
|
42
36
|
/** @type {Map<string, { component: any | null, externalPlacement: any | null, staticBodyPlacement: any | null }>} */
|
|
43
37
|
#selectionIndex
|
|
44
|
-
|
|
45
38
|
/** @type {Map<string, { scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }>} */
|
|
46
39
|
#componentAdjustments
|
|
47
|
-
|
|
48
40
|
/** @type {string} */
|
|
49
41
|
#selectedComponentKey
|
|
50
|
-
|
|
51
42
|
/** @type {any | null} */
|
|
52
43
|
#runtime
|
|
53
|
-
|
|
54
44
|
/** @type {any | null} */
|
|
55
45
|
#sceneDescription
|
|
56
|
-
|
|
46
|
+
/** @type {object | undefined} */
|
|
47
|
+
#modelLoaderOptions
|
|
57
48
|
/** @type {{ prepareScene?: (documentModel: any, sessionAssets?: any[]) => Promise<any>, dispose?: () => void } | null} */
|
|
58
49
|
#scenePrepClient
|
|
59
50
|
|
|
@@ -87,7 +78,7 @@ export class PcbScene3dController {
|
|
|
87
78
|
/**
|
|
88
79
|
* @param {HTMLElement} viewportNode
|
|
89
80
|
* @param {any} documentModel
|
|
90
|
-
* @param {{ rootNode?: HTMLElement | null, documentId?: string, onComponentSelectionChange?: ((change: { documentId: string, componentKey: string, source?: string }) => void) | null, sessionAssets?: any[], autoSearchMissingModels?: boolean, circuitJson?: object[], sceneDescription?: any, buildScene?: (documentModel: any, options: { modelRegistry: any }) => any, createModelRegistry?: (documentModel: any, sessionAssets: any[]) => any, createRuntime?: (viewportNode: HTMLElement, sceneDescription: any, hooks: { setDiagnostics: (messages: string[]) => void, setSelection: (selection: any | null) => void, translate?: ((key: string) => string) | null }) => { setPreset?: (preset: string) => void, setToggle?: (toggleName: string, enabled: boolean) => void, dispose?: () => void, whenReady?: () => Promise<void> | void }, scenePrepClient?: { prepareScene?: (documentModel: any, sessionAssets?: any[]) => Promise<any>, dispose?: () => void } | null, exportArchive?: (options: { archiveBaseName?: string, sceneDescription?: any }) => Promise<{ archiveName: string, archiveBytes: Uint8Array, exportedEntries: any[], skippedEntries: any[] }>, downloadArchive?: (archiveName: string, archiveBytes: Uint8Array) => Promise<void> | void, renderAdjustmentControlsInSelection?: boolean, setLoadingVisible?: (visible: boolean) => void, translate?: ((key: string) => string) | null }} [options]
|
|
81
|
+
* @param {{ rootNode?: HTMLElement | null, documentId?: string, onComponentSelectionChange?: ((change: { documentId: string, componentKey: string, source?: string }) => void) | null, sessionAssets?: any[], autoSearchMissingModels?: boolean, circuitJson?: object[], sceneDescription?: any, modelLoaderOptions?: object, buildScene?: (documentModel: any, options: { modelRegistry: any }) => any, createModelRegistry?: (documentModel: any, sessionAssets: any[]) => any, createRuntime?: (viewportNode: HTMLElement, sceneDescription: any, hooks: { setDiagnostics: (messages: string[]) => void, setSelection: (selection: any | null) => void, translate?: ((key: string) => string) | null, modelLoaderOptions?: object }) => { setPreset?: (preset: string) => void, setToggle?: (toggleName: string, enabled: boolean) => void, dispose?: () => void, whenReady?: () => Promise<void> | void }, scenePrepClient?: { prepareScene?: (documentModel: any, sessionAssets?: any[]) => Promise<any>, dispose?: () => void } | null, exportArchive?: (options: { archiveBaseName?: string, sceneDescription?: any }) => Promise<{ archiveName: string, archiveBytes: Uint8Array, exportedEntries: any[], skippedEntries: any[] }>, downloadArchive?: (archiveName: string, archiveBytes: Uint8Array) => Promise<void> | void, renderAdjustmentControlsInSelection?: boolean, setLoadingVisible?: (visible: boolean) => void, translate?: ((key: string) => string) | null }} [options]
|
|
91
82
|
*/
|
|
92
83
|
constructor(viewportNode, documentModel, options = {}) {
|
|
93
84
|
this.#viewportNode = viewportNode
|
|
@@ -107,6 +98,7 @@ export class PcbScene3dController {
|
|
|
107
98
|
this.#listeners = []
|
|
108
99
|
this.#scenePrepClient = options.scenePrepClient || null
|
|
109
100
|
this.#sceneDescription = null
|
|
101
|
+
this.#modelLoaderOptions = options.modelLoaderOptions
|
|
110
102
|
this.#exportArchive =
|
|
111
103
|
options.exportArchive ||
|
|
112
104
|
((exportOptions) =>
|
|
@@ -188,15 +180,6 @@ export class PcbScene3dController {
|
|
|
188
180
|
this.#visibilityControls.bindSelectionNode(this.#selectionNode)
|
|
189
181
|
this.#setSelection(null)
|
|
190
182
|
this.#setLoadingVisible(true)
|
|
191
|
-
const circuitJsonModel = PcbScene3dController.#resolveCircuitJsonModel(
|
|
192
|
-
options,
|
|
193
|
-
this.#documentModel
|
|
194
|
-
)
|
|
195
|
-
if (circuitJsonModel) {
|
|
196
|
-
this.#initializePreparedScene(circuitJsonModel, options)
|
|
197
|
-
return
|
|
198
|
-
}
|
|
199
|
-
|
|
200
183
|
if (options.sceneDescription) {
|
|
201
184
|
this.#initializePreparedScene(options.sceneDescription, options)
|
|
202
185
|
return
|
|
@@ -207,6 +190,15 @@ export class PcbScene3dController {
|
|
|
207
190
|
return
|
|
208
191
|
}
|
|
209
192
|
|
|
193
|
+
const circuitJsonModel = PcbScene3dController.#resolveCircuitJsonModel(
|
|
194
|
+
options,
|
|
195
|
+
this.#documentModel
|
|
196
|
+
)
|
|
197
|
+
if (circuitJsonModel) {
|
|
198
|
+
this.#initializePreparedScene(circuitJsonModel, options)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
210
202
|
this.#initializeSceneSync(options)
|
|
211
203
|
}
|
|
212
204
|
|
|
@@ -427,6 +419,12 @@ export class PcbScene3dController {
|
|
|
427
419
|
* @returns {any}
|
|
428
420
|
*/
|
|
429
421
|
#prepareSceneDescriptionSync(options) {
|
|
422
|
+
const circuitJsonModel = PcbScene3dController.#resolveCircuitJsonModel(
|
|
423
|
+
options,
|
|
424
|
+
this.#documentModel
|
|
425
|
+
)
|
|
426
|
+
if (circuitJsonModel) return circuitJsonModel
|
|
427
|
+
|
|
430
428
|
const buildScene = options.buildScene
|
|
431
429
|
if (typeof buildScene !== 'function') {
|
|
432
430
|
throw new Error(
|
|
@@ -450,12 +448,14 @@ export class PcbScene3dController {
|
|
|
450
448
|
/**
|
|
451
449
|
* Mounts the runtime for one prepared scene description.
|
|
452
450
|
* @param {any} sceneDescription
|
|
453
|
-
* @param {{ createRuntime?: (viewportNode: HTMLElement, sceneDescription: any, hooks: { setDiagnostics: (messages: string[]) => void, setSelection: (selection: any | null) => void, translate?: ((key: string) => string) | null }) => { setPreset?: (preset: string) => void, setToggle?: (toggleName: string, enabled: boolean) => void, dispose?: () => void, whenReady?: () => Promise<void> | void } }} options
|
|
451
|
+
* @param {{ modelLoaderOptions?: object, createRuntime?: (viewportNode: HTMLElement, sceneDescription: any, hooks: { setDiagnostics: (messages: string[]) => void, setSelection: (selection: any | null) => void, translate?: ((key: string) => string) | null, modelLoaderOptions?: object }) => { setPreset?: (preset: string) => void, setToggle?: (toggleName: string, enabled: boolean) => void, dispose?: () => void, whenReady?: () => Promise<void> | void } }} options
|
|
454
452
|
* @returns {void}
|
|
455
453
|
*/
|
|
456
454
|
#mountScene(sceneDescription, options) {
|
|
457
|
-
const renderModel =
|
|
458
|
-
|
|
455
|
+
const renderModel = PcbScene3dController.#normalizeSceneDescription(
|
|
456
|
+
sceneDescription,
|
|
457
|
+
options
|
|
458
|
+
)
|
|
459
459
|
this.#sceneDescription = renderModel
|
|
460
460
|
this.#selectionIndex =
|
|
461
461
|
PcbScene3dSelectionIndexBuilder.build(renderModel)
|
|
@@ -472,7 +472,8 @@ export class PcbScene3dController {
|
|
|
472
472
|
setDiagnostics: (messages) => this.#setDiagnostics(messages),
|
|
473
473
|
setSelection: (selection) =>
|
|
474
474
|
this.#handleRuntimeSelection(selection),
|
|
475
|
-
translate: this.#translate
|
|
475
|
+
translate: this.#translate,
|
|
476
|
+
modelLoaderOptions: options.modelLoaderOptions
|
|
476
477
|
})
|
|
477
478
|
this.#applyInitialToggles()
|
|
478
479
|
if (!this.#autoSearchMissingModels) {
|
|
@@ -621,7 +622,8 @@ export class PcbScene3dController {
|
|
|
621
622
|
archiveBaseName: PcbScene3dController.#resolveArchiveBaseName(
|
|
622
623
|
this.#documentModel
|
|
623
624
|
),
|
|
624
|
-
sceneDescription: this.#sceneDescription
|
|
625
|
+
sceneDescription: this.#sceneDescription,
|
|
626
|
+
modelLoaderOptions: this.#modelLoaderOptions
|
|
625
627
|
})
|
|
626
628
|
|
|
627
629
|
if (this.#isDisposed) {
|
|
@@ -908,35 +910,34 @@ export class PcbScene3dController {
|
|
|
908
910
|
if (
|
|
909
911
|
PcbScene3dCircuitJsonAdapter.isCircuitJsonModel(options.circuitJson)
|
|
910
912
|
) {
|
|
911
|
-
return options.circuitJson
|
|
913
|
+
return PcbScene3dCircuitJsonAdapter.prepare(options.circuitJson)
|
|
912
914
|
}
|
|
913
915
|
if (
|
|
914
916
|
PcbScene3dCircuitJsonAdapter.isDirectCircuitJsonModel(documentModel)
|
|
915
917
|
) {
|
|
916
|
-
return documentModel
|
|
918
|
+
return PcbScene3dCircuitJsonAdapter.prepare(documentModel)
|
|
917
919
|
}
|
|
918
920
|
return null
|
|
919
921
|
}
|
|
920
922
|
|
|
921
923
|
/**
|
|
922
|
-
* Converts direct CircuitJSON inputs into the renderer scene model.
|
|
923
924
|
* @param {any} sceneDescription Scene description or CircuitJSON model.
|
|
925
|
+
* @param {object} options Controller options forwarded to CircuitJSON adaptation.
|
|
924
926
|
* @returns {any}
|
|
925
927
|
*/
|
|
926
|
-
static #normalizeSceneDescription(sceneDescription) {
|
|
928
|
+
static #normalizeSceneDescription(sceneDescription, options) {
|
|
927
929
|
const normalizedScene =
|
|
928
930
|
PcbScene3dCircuitJsonAdapter.isDirectCircuitJsonModel(
|
|
929
931
|
sceneDescription
|
|
930
932
|
)
|
|
931
|
-
? PcbScene3dCircuitJsonAdapter.build(sceneDescription)
|
|
933
|
+
? PcbScene3dCircuitJsonAdapter.build(sceneDescription, options)
|
|
932
934
|
: sceneDescription
|
|
933
|
-
|
|
934
935
|
return PcbScene3dExternalPlacementDefaults.apply(normalizedScene)
|
|
935
936
|
}
|
|
936
937
|
|
|
937
938
|
/**
|
|
938
939
|
* Resolves one archive base name from the mounted document metadata.
|
|
939
|
-
* @param {{ summary?: { title?: string }, fileName?: string } | null} documentModel
|
|
940
|
+
* @param {{ summary?: { title?: string }, fileName?: string, source?: { fileName?: string }, document?: { source?: { fileName?: string } } } | null} documentModel
|
|
940
941
|
* @returns {string}
|
|
941
942
|
*/
|
|
942
943
|
static #resolveArchiveBaseName(documentModel) {
|
|
@@ -945,7 +946,12 @@ export class PcbScene3dController {
|
|
|
945
946
|
return summaryTitle
|
|
946
947
|
}
|
|
947
948
|
|
|
948
|
-
const fileName = String(
|
|
949
|
+
const fileName = String(
|
|
950
|
+
documentModel?.source?.fileName ||
|
|
951
|
+
documentModel?.document?.source?.fileName ||
|
|
952
|
+
documentModel?.fileName ||
|
|
953
|
+
''
|
|
954
|
+
).trim()
|
|
949
955
|
if (fileName) {
|
|
950
956
|
return fileName.replace(/\.[^.]+$/, '')
|
|
951
957
|
}
|