pcb-scene3d-viewer 1.1.21 → 1.1.31
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 +3 -2
- package/docs/api.md +27 -1
- package/docs/circuitjson.md +63 -18
- package/docs/model-format.md +13 -2
- package/package.json +1 -1
- package/spec/library-scope.md +2 -2
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +25 -5
- package/src/PcbAssemblyComponentMeshBuilder.mjs +762 -0
- package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
- package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +41 -301
- package/src/PcbAssemblyGltfModelMeshParser.mjs +912 -0
- package/src/PcbAssemblyGltfValidator.mjs +460 -0
- package/src/PcbAssemblyGltfWriter.mjs +801 -0
- package/src/PcbAssemblyMeshUtils.mjs +117 -0
- package/src/PcbAssemblyModelMeshLoader.mjs +18 -0
- package/src/PcbAssemblyPadMeshBuilder.mjs +394 -0
- package/src/PcbAssemblyStepWriter.mjs +24 -2
- package/src/PcbAssemblyTextModelMeshParser.mjs +602 -0
- package/src/PcbModelArchiveExporter.mjs +521 -7
- package/src/PcbScene3dCircuitJsonAdapter.mjs +409 -7
- package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
- package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +242 -0
- package/src/PcbScene3dComponentVisibility.mjs +100 -5
- package/src/PcbScene3dController.mjs +25 -55
- package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
- package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
- package/src/PcbScene3dCopperFactory.mjs +99 -85
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
- package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
- package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
- package/src/PcbScene3dExternalCompanionFallback.mjs +202 -0
- package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
- package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
- package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
- package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
- package/src/PcbScene3dExternalModels.mjs +16 -7
- package/src/PcbScene3dFallbackBodyFactory.mjs +105 -0
- package/src/PcbScene3dFallbackVisibility.mjs +58 -1
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
- package/src/PcbScene3dPadFactory.mjs +35 -2
- package/src/PcbScene3dRenderGroupVisibility.mjs +8 -1
- package/src/PcbScene3dRuntime.mjs +94 -100
- package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
- package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
- package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
- package/src/PcbScene3dSelectionMarkerFactory.mjs +333 -0
- package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
- package/src/PcbScene3dSelectionStyler.mjs +52 -2
- package/src/PcbScene3dStaticBodyFactory.mjs +263 -0
- package/src/PcbScene3dText.mjs +1 -0
- package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
- package/src/PcbScene3dViaFactory.mjs +27 -7
- package/src/scene3d.mjs +3 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const COORDINATE_EPSILON = 0.001
|
|
2
|
+
const AREA_EPSILON = 0.001
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes saved copper fill rings before triangulation and export.
|
|
6
|
+
*/
|
|
7
|
+
export class PcbAssemblyFillRingNormalizer {
|
|
8
|
+
/**
|
|
9
|
+
* Normalizes one ring into a clean, optionally oriented loop.
|
|
10
|
+
* @param {unknown[]} points Candidate point rows.
|
|
11
|
+
* @param {{ role?: string, shapeIndex?: number, ringIndex?: number, winding?: 'positive' | 'negative' }} options Normalization options.
|
|
12
|
+
* @returns {{ loop: number[][], diagnostic: object | null, area: number }}
|
|
13
|
+
*/
|
|
14
|
+
static normalize(points, options = {}) {
|
|
15
|
+
const rows = PcbAssemblyFillRingNormalizer.#pointRows(points)
|
|
16
|
+
const hasNonFinitePoint = rows.some((row) => !row.isFinite)
|
|
17
|
+
if (hasNonFinitePoint) {
|
|
18
|
+
return PcbAssemblyFillRingNormalizer.#result(
|
|
19
|
+
[],
|
|
20
|
+
'non-finite-point',
|
|
21
|
+
options,
|
|
22
|
+
rows.length,
|
|
23
|
+
0
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const loop = PcbAssemblyFillRingNormalizer.#cleanLoop(
|
|
28
|
+
rows.map((row) => [row.x, row.y])
|
|
29
|
+
)
|
|
30
|
+
const signedArea = PcbAssemblyFillRingNormalizer.signedArea(loop)
|
|
31
|
+
const area = Math.abs(signedArea)
|
|
32
|
+
if (loop.length < 3) {
|
|
33
|
+
return PcbAssemblyFillRingNormalizer.#result(
|
|
34
|
+
loop,
|
|
35
|
+
'too-few-points',
|
|
36
|
+
options,
|
|
37
|
+
loop.length,
|
|
38
|
+
area
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
if (area <= AREA_EPSILON) {
|
|
42
|
+
return PcbAssemblyFillRingNormalizer.#result(
|
|
43
|
+
loop,
|
|
44
|
+
'near-zero-area',
|
|
45
|
+
options,
|
|
46
|
+
loop.length,
|
|
47
|
+
area
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
loop: PcbAssemblyFillRingNormalizer.#orientLoop(
|
|
53
|
+
loop,
|
|
54
|
+
signedArea,
|
|
55
|
+
options.winding
|
|
56
|
+
),
|
|
57
|
+
diagnostic: null,
|
|
58
|
+
area
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Checks whether one loop has enough non-collinear area.
|
|
64
|
+
* @param {number[][]} loop Candidate loop.
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
static isValidLoop(loop) {
|
|
68
|
+
return (
|
|
69
|
+
Array.isArray(loop) &&
|
|
70
|
+
loop.length >= 3 &&
|
|
71
|
+
Math.abs(PcbAssemblyFillRingNormalizer.signedArea(loop)) >
|
|
72
|
+
AREA_EPSILON
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Computes signed polygon area in source units.
|
|
78
|
+
* @param {number[][]} loop Candidate loop.
|
|
79
|
+
* @returns {number}
|
|
80
|
+
*/
|
|
81
|
+
static signedArea(loop) {
|
|
82
|
+
let area = 0
|
|
83
|
+
for (let index = 0; index < (loop || []).length; index += 1) {
|
|
84
|
+
const current = loop[index]
|
|
85
|
+
const next = loop[(index + 1) % loop.length]
|
|
86
|
+
area += current[0] * next[1] - next[0] * current[1]
|
|
87
|
+
}
|
|
88
|
+
return area / 2
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Normalizes candidate point rows into numeric values.
|
|
93
|
+
* @param {unknown[]} points Candidate point rows.
|
|
94
|
+
* @returns {{ x: number, y: number, isFinite: boolean }[]}
|
|
95
|
+
*/
|
|
96
|
+
static #pointRows(points) {
|
|
97
|
+
return (Array.isArray(points) ? points : []).map((point) => {
|
|
98
|
+
const x = Number(Array.isArray(point) ? point[0] : point?.x)
|
|
99
|
+
const y = Number(Array.isArray(point) ? point[1] : point?.y)
|
|
100
|
+
return {
|
|
101
|
+
x,
|
|
102
|
+
y,
|
|
103
|
+
isFinite: Number.isFinite(x) && Number.isFinite(y)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Removes invalid, repeated, and closing points from one loop.
|
|
110
|
+
* @param {number[][]} points Candidate numeric points.
|
|
111
|
+
* @returns {number[][]}
|
|
112
|
+
*/
|
|
113
|
+
static #cleanLoop(points) {
|
|
114
|
+
const loop = []
|
|
115
|
+
for (const point of points || []) {
|
|
116
|
+
const x = Number(point?.[0])
|
|
117
|
+
const y = Number(point?.[1])
|
|
118
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const previous = loop[loop.length - 1]
|
|
123
|
+
if (
|
|
124
|
+
previous &&
|
|
125
|
+
Math.abs(previous[0] - x) < COORDINATE_EPSILON &&
|
|
126
|
+
Math.abs(previous[1] - y) < COORDINATE_EPSILON
|
|
127
|
+
) {
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
loop.push([x, y])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const first = loop[0]
|
|
134
|
+
const last = loop[loop.length - 1]
|
|
135
|
+
if (
|
|
136
|
+
first &&
|
|
137
|
+
last &&
|
|
138
|
+
Math.abs(first[0] - last[0]) < COORDINATE_EPSILON &&
|
|
139
|
+
Math.abs(first[1] - last[1]) < COORDINATE_EPSILON
|
|
140
|
+
) {
|
|
141
|
+
loop.pop()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return loop
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Orients one loop to the requested signed-area winding.
|
|
149
|
+
* @param {number[][]} loop Candidate loop.
|
|
150
|
+
* @param {number} signedArea Signed loop area.
|
|
151
|
+
* @param {'positive' | 'negative' | undefined} winding Requested winding.
|
|
152
|
+
* @returns {number[][]}
|
|
153
|
+
*/
|
|
154
|
+
static #orientLoop(loop, signedArea, winding) {
|
|
155
|
+
if (winding === 'positive' && signedArea < 0) {
|
|
156
|
+
return [...loop].reverse()
|
|
157
|
+
}
|
|
158
|
+
if (winding === 'negative' && signedArea > 0) {
|
|
159
|
+
return [...loop].reverse()
|
|
160
|
+
}
|
|
161
|
+
return loop
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Builds a normalization result.
|
|
166
|
+
* @param {number[][]} loop Normalized loop.
|
|
167
|
+
* @param {string} reason Drop reason.
|
|
168
|
+
* @param {{ role?: string, shapeIndex?: number, ringIndex?: number }} options Normalization options.
|
|
169
|
+
* @param {number} pointCount Clean point count.
|
|
170
|
+
* @param {number} area Absolute loop area.
|
|
171
|
+
* @returns {{ loop: number[][], diagnostic: object, area: number }}
|
|
172
|
+
*/
|
|
173
|
+
static #result(loop, reason, options, pointCount, area) {
|
|
174
|
+
return {
|
|
175
|
+
loop,
|
|
176
|
+
diagnostic: PcbAssemblyFillRingNormalizer.#diagnostic(
|
|
177
|
+
reason,
|
|
178
|
+
options,
|
|
179
|
+
pointCount,
|
|
180
|
+
area
|
|
181
|
+
),
|
|
182
|
+
area
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Builds one dropped-ring diagnostic row.
|
|
188
|
+
* @param {string} reason Drop reason.
|
|
189
|
+
* @param {{ role?: string, shapeIndex?: number, ringIndex?: number }} options Normalization options.
|
|
190
|
+
* @param {number} pointCount Clean point count.
|
|
191
|
+
* @param {number} area Absolute loop area.
|
|
192
|
+
* @returns {object}
|
|
193
|
+
*/
|
|
194
|
+
static #diagnostic(reason, options, pointCount, area) {
|
|
195
|
+
const diagnostic = {
|
|
196
|
+
reason,
|
|
197
|
+
role: String(options.role || 'ring'),
|
|
198
|
+
pointCount,
|
|
199
|
+
area
|
|
200
|
+
}
|
|
201
|
+
if (Number.isInteger(options.shapeIndex)) {
|
|
202
|
+
diagnostic.shapeIndex = options.shapeIndex
|
|
203
|
+
}
|
|
204
|
+
if (Number.isInteger(options.ringIndex)) {
|
|
205
|
+
diagnostic.ringIndex = options.ringIndex
|
|
206
|
+
}
|
|
207
|
+
return diagnostic
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { PcbAssemblyBoardSubstrateBuilder } from './PcbAssemblyBoardSubstrateBuilder.mjs'
|
|
2
|
+
import { PcbAssemblyComponentMeshBuilder } from './PcbAssemblyComponentMeshBuilder.mjs'
|
|
3
|
+
import { PcbAssemblyFillGeometryResolver } from './PcbAssemblyFillGeometryResolver.mjs'
|
|
2
4
|
import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
|
|
5
|
+
import { PcbAssemblyPadMeshBuilder } from './PcbAssemblyPadMeshBuilder.mjs'
|
|
3
6
|
|
|
4
7
|
const COPPER_THICKNESS_MIL = 2.2
|
|
5
8
|
const SILKSCREEN_THICKNESS_MIL = 0.8
|
|
6
9
|
const BOARD_COLOR = [0.05, 0.32, 0.18]
|
|
7
10
|
const COPPER_COLOR = [0.85, 0.62, 0.12]
|
|
8
11
|
const SILKSCREEN_COLOR = [0.96, 0.95, 0.9]
|
|
9
|
-
const COMPONENT_COLOR = [0.55, 0.56, 0.58]
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Converts a prepared 3D scene description into exportable faceted meshes.
|
|
@@ -15,7 +17,7 @@ export class PcbAssemblyGeometryBuilder {
|
|
|
15
17
|
/**
|
|
16
18
|
* Builds assembly meshes and diagnostics.
|
|
17
19
|
* @param {{ board?: object, detail?: object, components?: object[], externalPlacements?: object[] }} sceneDescription Prepared scene description.
|
|
18
|
-
* @param {{ modelMeshLoader?: (placement: object) => Promise<object | object[]>, progress?: { advance?: (units: number, message: string) => Promise<void>, finish?: (message: string) => Promise<void> } }} [options] Build options.
|
|
20
|
+
* @param {{ modelMeshLoader?: (placement: object) => Promise<object | object[]>, includeModels?: boolean, renderFallbackBodies?: boolean, progress?: { advance?: (units: number, message: string) => Promise<void>, finish?: (message: string) => Promise<void> } }} [options] Build options.
|
|
19
21
|
* @returns {Promise<{ meshes: object[], diagnostics: object[] }>}
|
|
20
22
|
*/
|
|
21
23
|
static async build(sceneDescription, options = {}) {
|
|
@@ -35,23 +37,20 @@ export class PcbAssemblyGeometryBuilder {
|
|
|
35
37
|
progress
|
|
36
38
|
)
|
|
37
39
|
const pcbMeshes = [...boardMeshes, ...copperMeshes, ...silkscreenMeshes]
|
|
40
|
+
const componentResult = await PcbAssemblyComponentMeshBuilder.build(
|
|
41
|
+
sceneDescription,
|
|
42
|
+
options,
|
|
43
|
+
progress
|
|
44
|
+
)
|
|
45
|
+
diagnostics.push(...componentResult.diagnostics)
|
|
38
46
|
const meshes = [
|
|
39
47
|
...PcbAssemblyGeometryBuilder.#translateMeshes(
|
|
40
48
|
pcbMeshes,
|
|
41
49
|
PcbAssemblyGeometryBuilder.#boardLocalOffset(sceneDescription)
|
|
42
50
|
),
|
|
43
|
-
...
|
|
44
|
-
sceneDescription,
|
|
45
|
-
options,
|
|
46
|
-
diagnostics,
|
|
47
|
-
progress
|
|
48
|
-
))
|
|
51
|
+
...componentResult.meshes
|
|
49
52
|
]
|
|
50
53
|
|
|
51
|
-
PcbAssemblyGeometryBuilder.#appendMissingModelDiagnostics(
|
|
52
|
-
sceneDescription,
|
|
53
|
-
diagnostics
|
|
54
|
-
)
|
|
55
54
|
await progress?.finish?.('Geometry meshes ready')
|
|
56
55
|
return {
|
|
57
56
|
meshes: meshes.filter((mesh) => mesh?.vertices?.length),
|
|
@@ -149,14 +148,14 @@ export class PcbAssemblyGeometryBuilder {
|
|
|
149
148
|
for (let index = 0; index < fills.length; index += 1) {
|
|
150
149
|
const fill = fills[index]
|
|
151
150
|
const side = PcbAssemblyGeometryBuilder.#resolveSide(fill)
|
|
152
|
-
const
|
|
151
|
+
const fillMeshes = PcbAssemblyGeometryBuilder.#fillMeshes(
|
|
153
152
|
'copper-' + side + '-fill-' + (index + 1),
|
|
154
|
-
|
|
153
|
+
fill,
|
|
155
154
|
side === 'bottom' ? bottomZ : topZ,
|
|
156
155
|
COPPER_THICKNESS_MIL,
|
|
157
156
|
COPPER_COLOR
|
|
158
157
|
)
|
|
159
|
-
|
|
158
|
+
meshes.push(...fillMeshes)
|
|
160
159
|
await progress?.advance?.(
|
|
161
160
|
1,
|
|
162
161
|
'Building copper fills ' + (index + 1) + '/' + fills.length
|
|
@@ -166,19 +165,21 @@ export class PcbAssemblyGeometryBuilder {
|
|
|
166
165
|
const pads = PcbAssemblyGeometryBuilder.#array(detail.pads)
|
|
167
166
|
for (let index = 0; index < pads.length; index += 1) {
|
|
168
167
|
const pad = pads[index]
|
|
169
|
-
const topMesh =
|
|
168
|
+
const topMesh = PcbAssemblyPadMeshBuilder.build(
|
|
170
169
|
'pad-top-' + (index + 1),
|
|
171
170
|
pad,
|
|
172
171
|
'top',
|
|
173
172
|
topZ,
|
|
174
|
-
COPPER_THICKNESS_MIL
|
|
173
|
+
COPPER_THICKNESS_MIL,
|
|
174
|
+
COPPER_COLOR
|
|
175
175
|
)
|
|
176
|
-
const bottomMesh =
|
|
176
|
+
const bottomMesh = PcbAssemblyPadMeshBuilder.build(
|
|
177
177
|
'pad-bottom-' + (index + 1),
|
|
178
178
|
pad,
|
|
179
179
|
'bottom',
|
|
180
180
|
bottomZ,
|
|
181
|
-
COPPER_THICKNESS_MIL
|
|
181
|
+
COPPER_THICKNESS_MIL,
|
|
182
|
+
COPPER_COLOR
|
|
182
183
|
)
|
|
183
184
|
if (topMesh) meshes.push(topMesh)
|
|
184
185
|
await progress?.advance?.(
|
|
@@ -389,164 +390,6 @@ export class PcbAssemblyGeometryBuilder {
|
|
|
389
390
|
}
|
|
390
391
|
}
|
|
391
392
|
|
|
392
|
-
/**
|
|
393
|
-
* Builds component model meshes.
|
|
394
|
-
* @param {{ externalPlacements?: object[] }} sceneDescription Prepared scene description.
|
|
395
|
-
* @param {{ modelMeshLoader?: (placement: object) => Promise<object | object[]> }} options Build options.
|
|
396
|
-
* @param {object[]} diagnostics Mutable diagnostics list.
|
|
397
|
-
* @param {{ advance?: (units: number, message: string) => Promise<void> } | null} progress Progress tracker.
|
|
398
|
-
* @returns {Promise<object[]>}
|
|
399
|
-
*/
|
|
400
|
-
static async #buildComponentMeshes(
|
|
401
|
-
sceneDescription,
|
|
402
|
-
options,
|
|
403
|
-
diagnostics,
|
|
404
|
-
progress = null
|
|
405
|
-
) {
|
|
406
|
-
const loader =
|
|
407
|
-
typeof options.modelMeshLoader === 'function'
|
|
408
|
-
? options.modelMeshLoader
|
|
409
|
-
: null
|
|
410
|
-
if (!loader) {
|
|
411
|
-
return []
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const meshes = []
|
|
415
|
-
const placements = PcbAssemblyGeometryBuilder.#array(
|
|
416
|
-
sceneDescription?.externalPlacements
|
|
417
|
-
).filter((placement) => placement?.externalModel)
|
|
418
|
-
for (
|
|
419
|
-
let placementIndex = 0;
|
|
420
|
-
placementIndex < placements.length;
|
|
421
|
-
placementIndex += 1
|
|
422
|
-
) {
|
|
423
|
-
const placement = placements[placementIndex]
|
|
424
|
-
|
|
425
|
-
try {
|
|
426
|
-
const loaded = await loader(placement)
|
|
427
|
-
const loadedMeshes = Array.isArray(loaded) ? loaded : [loaded]
|
|
428
|
-
loadedMeshes.filter(Boolean).forEach((mesh, index) => {
|
|
429
|
-
const designator = PcbAssemblyMeshUtils.safeName(
|
|
430
|
-
placement?.designator || 'component'
|
|
431
|
-
)
|
|
432
|
-
const transformed = PcbAssemblyMeshUtils.transformMesh(
|
|
433
|
-
{
|
|
434
|
-
...mesh,
|
|
435
|
-
name:
|
|
436
|
-
'component-' +
|
|
437
|
-
designator +
|
|
438
|
-
(loadedMeshes.length > 1
|
|
439
|
-
? '-' + (index + 1)
|
|
440
|
-
: ''),
|
|
441
|
-
color: mesh.color || COMPONENT_COLOR
|
|
442
|
-
},
|
|
443
|
-
placement
|
|
444
|
-
)
|
|
445
|
-
meshes.push(transformed)
|
|
446
|
-
})
|
|
447
|
-
PcbAssemblyGeometryBuilder.#appendModelDiagnostic(
|
|
448
|
-
placement,
|
|
449
|
-
diagnostics
|
|
450
|
-
)
|
|
451
|
-
} catch (error) {
|
|
452
|
-
diagnostics.push(
|
|
453
|
-
PcbAssemblyGeometryBuilder.#diagnostic(
|
|
454
|
-
'warning',
|
|
455
|
-
'component_model_conversion_failed',
|
|
456
|
-
'Could not convert 3D model for ' +
|
|
457
|
-
String(placement?.designator || 'component') +
|
|
458
|
-
': ' +
|
|
459
|
-
String(error?.message || error)
|
|
460
|
-
)
|
|
461
|
-
)
|
|
462
|
-
}
|
|
463
|
-
await progress?.advance?.(
|
|
464
|
-
1,
|
|
465
|
-
'Loading component models ' +
|
|
466
|
-
(placementIndex + 1) +
|
|
467
|
-
'/' +
|
|
468
|
-
placements.length
|
|
469
|
-
)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return meshes
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Adds a format-specific component model diagnostic.
|
|
477
|
-
* @param {object} placement Component placement.
|
|
478
|
-
* @param {object[]} diagnostics Mutable diagnostics list.
|
|
479
|
-
* @returns {void}
|
|
480
|
-
*/
|
|
481
|
-
static #appendModelDiagnostic(placement, diagnostics) {
|
|
482
|
-
const format = String(
|
|
483
|
-
placement?.externalModel?.format || ''
|
|
484
|
-
).toLowerCase()
|
|
485
|
-
if (format === 'wrl' || format === 'vrml') {
|
|
486
|
-
diagnostics.push(
|
|
487
|
-
PcbAssemblyGeometryBuilder.#diagnostic(
|
|
488
|
-
'info',
|
|
489
|
-
'component_wrl_faceted_step',
|
|
490
|
-
'Converted WRL model for ' +
|
|
491
|
-
String(placement?.designator || 'component') +
|
|
492
|
-
' as faceted geometry.'
|
|
493
|
-
)
|
|
494
|
-
)
|
|
495
|
-
return
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (format === 'step' || format === 'stp') {
|
|
499
|
-
diagnostics.push(
|
|
500
|
-
PcbAssemblyGeometryBuilder.#diagnostic(
|
|
501
|
-
'info',
|
|
502
|
-
'component_step_faceted_export',
|
|
503
|
-
'Included STEP model for ' +
|
|
504
|
-
String(placement?.designator || 'component') +
|
|
505
|
-
' through export mesh geometry.'
|
|
506
|
-
)
|
|
507
|
-
)
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Adds diagnostics for components without resolved external models.
|
|
513
|
-
* @param {{ components?: object[], externalPlacements?: object[] }} sceneDescription Scene description.
|
|
514
|
-
* @param {object[]} diagnostics Mutable diagnostics list.
|
|
515
|
-
* @returns {void}
|
|
516
|
-
*/
|
|
517
|
-
static #appendMissingModelDiagnostics(sceneDescription, diagnostics) {
|
|
518
|
-
const placementDesignators = new Set(
|
|
519
|
-
PcbAssemblyGeometryBuilder.#array(
|
|
520
|
-
sceneDescription?.externalPlacements
|
|
521
|
-
)
|
|
522
|
-
.map((placement) => String(placement?.designator || '').trim())
|
|
523
|
-
.filter(Boolean)
|
|
524
|
-
)
|
|
525
|
-
|
|
526
|
-
PcbAssemblyGeometryBuilder.#array(sceneDescription?.components).forEach(
|
|
527
|
-
(component) => {
|
|
528
|
-
const designator = String(component?.designator || '').trim()
|
|
529
|
-
if (
|
|
530
|
-
!designator ||
|
|
531
|
-
component?.externalModel ||
|
|
532
|
-
placementDesignators.has(designator)
|
|
533
|
-
) {
|
|
534
|
-
return
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
diagnostics.push(
|
|
538
|
-
PcbAssemblyGeometryBuilder.#diagnostic(
|
|
539
|
-
'warning',
|
|
540
|
-
'component_model_missing',
|
|
541
|
-
'No resolved 3D model was available for ' +
|
|
542
|
-
designator +
|
|
543
|
-
'.'
|
|
544
|
-
)
|
|
545
|
-
)
|
|
546
|
-
}
|
|
547
|
-
)
|
|
548
|
-
}
|
|
549
|
-
|
|
550
393
|
/**
|
|
551
394
|
* Builds one widened track mesh.
|
|
552
395
|
* @param {string} name Mesh name.
|
|
@@ -584,133 +427,30 @@ export class PcbAssemblyGeometryBuilder {
|
|
|
584
427
|
}
|
|
585
428
|
|
|
586
429
|
/**
|
|
587
|
-
* Builds
|
|
430
|
+
* Builds filled polygon meshes.
|
|
588
431
|
* @param {string} name Mesh name.
|
|
589
|
-
* @param {object}
|
|
590
|
-
* @param {'top' | 'bottom'} side Pad side.
|
|
432
|
+
* @param {object} fill Filled primitive.
|
|
591
433
|
* @param {number} z Center Z.
|
|
592
|
-
* @param {number} thickness
|
|
593
|
-
* @
|
|
594
|
-
|
|
595
|
-
static #padMesh(name, pad, side, z, thickness) {
|
|
596
|
-
const size = PcbAssemblyGeometryBuilder.#padSize(pad, side)
|
|
597
|
-
if (!size) {
|
|
598
|
-
return null
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const offset = PcbAssemblyGeometryBuilder.#padOffset(pad, side)
|
|
602
|
-
const x = Number(pad?.x || 0) + offset.x
|
|
603
|
-
const y = Number(pad?.y || 0) + offset.y
|
|
604
|
-
const shape = PcbAssemblyGeometryBuilder.#padShape(pad, side)
|
|
605
|
-
const isCircle =
|
|
606
|
-
Math.abs(size.width - size.height) < 0.001 && shape !== 2
|
|
607
|
-
const mesh = isCircle
|
|
608
|
-
? PcbAssemblyMeshUtils.cylinder(name, {
|
|
609
|
-
x,
|
|
610
|
-
y,
|
|
611
|
-
z,
|
|
612
|
-
radius: size.width / 2,
|
|
613
|
-
height: thickness,
|
|
614
|
-
color: COPPER_COLOR
|
|
615
|
-
})
|
|
616
|
-
: PcbAssemblyMeshUtils.box(name, {
|
|
617
|
-
x,
|
|
618
|
-
y,
|
|
619
|
-
z,
|
|
620
|
-
width: size.width,
|
|
621
|
-
depth: size.height,
|
|
622
|
-
height: thickness,
|
|
623
|
-
color: COPPER_COLOR
|
|
624
|
-
})
|
|
625
|
-
|
|
626
|
-
return PcbAssemblyGeometryBuilder.#rotateMeshAroundZ(
|
|
627
|
-
mesh,
|
|
628
|
-
Number(pad?.rotation || 0),
|
|
629
|
-
x,
|
|
630
|
-
y
|
|
631
|
-
)
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Resolves side-specific pad dimensions.
|
|
636
|
-
* @param {object} pad Pad primitive.
|
|
637
|
-
* @param {'top' | 'bottom'} side Pad side.
|
|
638
|
-
* @returns {{ width: number, height: number } | null}
|
|
639
|
-
*/
|
|
640
|
-
static #padSize(pad, side) {
|
|
641
|
-
const prefix = side === 'bottom' ? 'Bottom' : 'Top'
|
|
642
|
-
let width = PcbAssemblyGeometryBuilder.#firstPositive([
|
|
643
|
-
pad?.['size' + prefix + 'X']
|
|
644
|
-
])
|
|
645
|
-
let height = PcbAssemblyGeometryBuilder.#firstPositive([
|
|
646
|
-
pad?.['size' + prefix + 'Y']
|
|
647
|
-
])
|
|
648
|
-
|
|
649
|
-
if (side === 'top' && !width) {
|
|
650
|
-
width = PcbAssemblyGeometryBuilder.#firstPositive([
|
|
651
|
-
pad?.width,
|
|
652
|
-
pad?.sizeX,
|
|
653
|
-
pad?.diameter,
|
|
654
|
-
pad?.sizeMidX,
|
|
655
|
-
pad?.sizeBottomX
|
|
656
|
-
])
|
|
657
|
-
}
|
|
658
|
-
if (side === 'top' && !height) {
|
|
659
|
-
height = PcbAssemblyGeometryBuilder.#firstPositive([
|
|
660
|
-
pad?.height,
|
|
661
|
-
pad?.sizeY,
|
|
662
|
-
pad?.diameter,
|
|
663
|
-
pad?.sizeMidY,
|
|
664
|
-
pad?.sizeBottomY,
|
|
665
|
-
width
|
|
666
|
-
])
|
|
667
|
-
}
|
|
668
|
-
if (width && !height) {
|
|
669
|
-
height = width
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
return width && height
|
|
673
|
-
? {
|
|
674
|
-
width: Math.max(width, 1),
|
|
675
|
-
height: Math.max(height, 1)
|
|
676
|
-
}
|
|
677
|
-
: null
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Resolves side-specific pad center offset.
|
|
682
|
-
* @param {object} pad Pad primitive.
|
|
683
|
-
* @param {'top' | 'bottom'} side Pad side.
|
|
684
|
-
* @returns {{ x: number, y: number }}
|
|
685
|
-
*/
|
|
686
|
-
static #padOffset(pad, side) {
|
|
687
|
-
const prefix = side === 'bottom' ? 'Bottom' : 'Top'
|
|
688
|
-
|
|
689
|
-
return {
|
|
690
|
-
x: Number(pad?.['offset' + prefix + 'X'] || 0),
|
|
691
|
-
y: Number(pad?.['offset' + prefix + 'Y'] || 0)
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* Resolves the renderer pad shape code for one side.
|
|
697
|
-
* @param {object} pad Pad primitive.
|
|
698
|
-
* @param {'top' | 'bottom'} side Pad side.
|
|
699
|
-
* @returns {number}
|
|
434
|
+
* @param {number} thickness Mesh thickness.
|
|
435
|
+
* @param {number[]} color Mesh color.
|
|
436
|
+
* @returns {object[]}
|
|
700
437
|
*/
|
|
701
|
-
static #
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
438
|
+
static #fillMeshes(name, fill, z, thickness, color) {
|
|
439
|
+
const loopSets = PcbAssemblyFillGeometryResolver.resolveAll(fill)
|
|
440
|
+
return loopSets
|
|
441
|
+
.map((loops, index) =>
|
|
442
|
+
PcbAssemblyMeshUtils.prismWithHoles(
|
|
443
|
+
loopSets.length > 1
|
|
444
|
+
? name + '-island-' + (index + 1)
|
|
445
|
+
: name,
|
|
446
|
+
loops.outer,
|
|
447
|
+
loops.holes,
|
|
448
|
+
z,
|
|
449
|
+
thickness,
|
|
450
|
+
color
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
.filter(Boolean)
|
|
714
454
|
}
|
|
715
455
|
|
|
716
456
|
/**
|