pcb-scene3d-viewer 1.1.22 → 1.1.44
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/AGENTS.md +4 -3
- package/README.md +3 -2
- package/docs/api.md +59 -1
- package/docs/circuitjson.md +206 -22
- package/docs/model-format.md +65 -3
- package/package.json +2 -2
- package/spec/library-scope.md +2 -2
- package/src/CircuitJsonCadModelAssetResolver.mjs +261 -0
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +49 -10
- package/src/PcbAssemblyComponentMeshBuilder.mjs +842 -0
- package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
- package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
- package/src/PcbAssemblyGeometryBuildProgress.mjs +3 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +94 -174
- package/src/PcbAssemblyGltfModelMeshParser.mjs +969 -0
- package/src/PcbAssemblyGltfValidator.mjs +460 -0
- package/src/PcbAssemblyGltfWriter.mjs +978 -0
- package/src/PcbAssemblyModelMeshLoader.mjs +330 -2
- package/src/PcbAssemblyPadMeshBuilder.mjs +9 -7
- package/src/PcbAssemblyTextModelMeshParser.mjs +725 -0
- package/src/PcbModelArchiveExporter.mjs +17 -2
- package/src/PcbScene3dCircuitJsonAdapter.mjs +585 -246
- package/src/PcbScene3dCircuitJsonCopperPourBuilder.mjs +298 -0
- package/src/PcbScene3dCircuitJsonDocumentationArtworkBuilder.mjs +606 -0
- package/src/PcbScene3dCircuitJsonGeometry.mjs +542 -0
- package/src/PcbScene3dCircuitJsonLayer.mjs +110 -0
- package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
- package/src/PcbScene3dCircuitJsonModelUrlResolver.mjs +142 -0
- package/src/PcbScene3dCircuitJsonSilkscreenBuilder.mjs +729 -0
- package/src/PcbScene3dCircuitJsonSolderPasteBuilder.mjs +359 -0
- package/src/PcbScene3dCircuitJsonThermalSpokeBuilder.mjs +167 -0
- package/src/PcbScene3dCircuitJsonTraceRouteBuilder.mjs +335 -0
- package/src/PcbScene3dComponentVisibility.mjs +9 -6
- 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/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 +10 -7
- package/src/PcbScene3dFallbackBodyFactory.mjs +45 -4
- package/src/PcbScene3dFallbackVisibility.mjs +17 -32
- package/src/PcbScene3dFootprintBodyBuilder.mjs +894 -0
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
- package/src/PcbScene3dModelSeatingPolicy.mjs +58 -0
- package/src/PcbScene3dPadFactory.mjs +35 -2
- package/src/PcbScene3dRenderGroupVisibility.mjs +10 -13
- package/src/PcbScene3dRuntime.mjs +99 -91
- package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
- package/src/PcbScene3dRuntimeSurfaceArtworkLoader.mjs +91 -0
- package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
- package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
- package/src/PcbScene3dSelectionMarkerFactory.mjs +368 -0
- package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
- package/src/PcbScene3dSelectionStyler.mjs +52 -2
- package/src/PcbScene3dStaticBodyFactory.mjs +30 -8
- package/src/PcbScene3dStepLoader.mjs +42 -6
- package/src/PcbScene3dText.mjs +1 -0
- package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
- package/src/PcbScene3dViaFactory.mjs +27 -7
- package/src/scene3d.mjs +4 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges CAD component model asset metadata to scene model resolution.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonCadModelAssetResolver {
|
|
5
|
+
/**
|
|
6
|
+
* Adds URL fields derived from CAD `model_asset` records.
|
|
7
|
+
* @param {object | object[]} documentModel Document model.
|
|
8
|
+
* @returns {object | object[]}
|
|
9
|
+
*/
|
|
10
|
+
static withModelAssetUrls(documentModel) {
|
|
11
|
+
const elements =
|
|
12
|
+
CircuitJsonCadModelAssetResolver.#elements(documentModel)
|
|
13
|
+
if (
|
|
14
|
+
!elements.some((element) =>
|
|
15
|
+
CircuitJsonCadModelAssetResolver.#modelAssetUrlField(element)
|
|
16
|
+
)
|
|
17
|
+
) {
|
|
18
|
+
return documentModel
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const nextElements = elements.map((element) =>
|
|
22
|
+
CircuitJsonCadModelAssetResolver.#withModelAssetUrl(element)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(documentModel)) {
|
|
26
|
+
Object.assign(nextElements, {
|
|
27
|
+
sourceFormat: documentModel.sourceFormat,
|
|
28
|
+
kind: documentModel.kind,
|
|
29
|
+
fileName: documentModel.fileName,
|
|
30
|
+
bom: documentModel.bom
|
|
31
|
+
})
|
|
32
|
+
return nextElements
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(documentModel?.elements)) {
|
|
36
|
+
return { ...documentModel, elements: nextElements }
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(documentModel?.circuitJson)) {
|
|
39
|
+
return { ...documentModel, circuitJson: nextElements }
|
|
40
|
+
}
|
|
41
|
+
return documentModel
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Adds a session-asset-aware model URL resolver.
|
|
46
|
+
* @param {object} options Scene preparation options.
|
|
47
|
+
* @returns {object}
|
|
48
|
+
*/
|
|
49
|
+
static withSessionAssetResolver(options = {}) {
|
|
50
|
+
const sessionAssets = Array.isArray(options.sessionAssets)
|
|
51
|
+
? options.sessionAssets
|
|
52
|
+
: []
|
|
53
|
+
const callerResolver =
|
|
54
|
+
typeof options.modelUrlResolver === 'function'
|
|
55
|
+
? options.modelUrlResolver
|
|
56
|
+
: null
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...options,
|
|
60
|
+
modelUrlResolver: (url, context) =>
|
|
61
|
+
CircuitJsonCadModelAssetResolver.#resolveModelUrl(
|
|
62
|
+
url,
|
|
63
|
+
context,
|
|
64
|
+
sessionAssets,
|
|
65
|
+
callerResolver
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Adds a model URL field to one CAD component when possible.
|
|
72
|
+
* @param {object} element Element row.
|
|
73
|
+
* @returns {object}
|
|
74
|
+
*/
|
|
75
|
+
static #withModelAssetUrl(element) {
|
|
76
|
+
const field =
|
|
77
|
+
CircuitJsonCadModelAssetResolver.#modelAssetUrlField(element)
|
|
78
|
+
if (!field) return element
|
|
79
|
+
const asset = element.model_asset || {}
|
|
80
|
+
const sourceUrl = String(
|
|
81
|
+
asset.project_relative_path || asset.url || ''
|
|
82
|
+
).trim()
|
|
83
|
+
if (!sourceUrl) return element
|
|
84
|
+
return { ...element, [field]: sourceUrl }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolves a model URL field for one CAD component asset.
|
|
89
|
+
* @param {object} element Element row.
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
static #modelAssetUrlField(element) {
|
|
93
|
+
if (element?.type !== 'cad_component') return ''
|
|
94
|
+
const asset = element.model_asset
|
|
95
|
+
if (!asset || typeof asset !== 'object') return ''
|
|
96
|
+
if (CircuitJsonCadModelAssetResolver.#hasModelUrl(element)) return ''
|
|
97
|
+
const format = CircuitJsonCadModelAssetResolver.#format(asset)
|
|
98
|
+
return (
|
|
99
|
+
{
|
|
100
|
+
'3mf': 'model_3mf_url',
|
|
101
|
+
glb: 'model_glb_url',
|
|
102
|
+
gltf: 'model_gltf_url',
|
|
103
|
+
obj: 'model_obj_url',
|
|
104
|
+
step: 'model_step_url',
|
|
105
|
+
stl: 'model_stl_url',
|
|
106
|
+
wrl: 'model_wrl_url'
|
|
107
|
+
}[format] || ''
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolves a URL to a session asset or caller-provided value.
|
|
113
|
+
* @param {string} url Model URL.
|
|
114
|
+
* @param {object} context Resolver context.
|
|
115
|
+
* @param {object[]} sessionAssets Session asset rows.
|
|
116
|
+
* @param {((url: string, context: object) => any) | null} callerResolver Caller resolver.
|
|
117
|
+
* @returns {object | string | null}
|
|
118
|
+
*/
|
|
119
|
+
static #resolveModelUrl(url, context, sessionAssets, callerResolver) {
|
|
120
|
+
const matchedAsset =
|
|
121
|
+
CircuitJsonCadModelAssetResolver.#matchingSessionAsset(
|
|
122
|
+
url,
|
|
123
|
+
context,
|
|
124
|
+
sessionAssets
|
|
125
|
+
)
|
|
126
|
+
if (matchedAsset) {
|
|
127
|
+
return matchedAsset
|
|
128
|
+
}
|
|
129
|
+
return callerResolver ? callerResolver(url, context) : null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Finds a session asset matching one CAD model URL.
|
|
134
|
+
* @param {string} url Model URL.
|
|
135
|
+
* @param {object} context Resolver context.
|
|
136
|
+
* @param {object[]} sessionAssets Session assets.
|
|
137
|
+
* @returns {object | null}
|
|
138
|
+
*/
|
|
139
|
+
static #matchingSessionAsset(url, context, sessionAssets) {
|
|
140
|
+
const candidates = CircuitJsonCadModelAssetResolver.#urlCandidates(
|
|
141
|
+
url,
|
|
142
|
+
context
|
|
143
|
+
)
|
|
144
|
+
return (
|
|
145
|
+
sessionAssets.find((asset) =>
|
|
146
|
+
CircuitJsonCadModelAssetResolver.#assetMatches(
|
|
147
|
+
asset,
|
|
148
|
+
candidates
|
|
149
|
+
)
|
|
150
|
+
) || null
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Builds normalized URL/path candidates for matching.
|
|
156
|
+
* @param {string} url Model URL.
|
|
157
|
+
* @param {object} context Resolver context.
|
|
158
|
+
* @returns {Set<string>}
|
|
159
|
+
*/
|
|
160
|
+
static #urlCandidates(url, context) {
|
|
161
|
+
return new Set(
|
|
162
|
+
[
|
|
163
|
+
url,
|
|
164
|
+
context?.cadComponent?.model_asset?.project_relative_path,
|
|
165
|
+
context?.cadComponent?.model_asset?.url
|
|
166
|
+
]
|
|
167
|
+
.map((value) => CircuitJsonCadModelAssetResolver.#key(value))
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Returns true when a session asset matches a model URL.
|
|
174
|
+
* @param {object} asset Session asset.
|
|
175
|
+
* @param {Set<string>} candidates URL candidates.
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
static #assetMatches(asset, candidates) {
|
|
179
|
+
return [
|
|
180
|
+
asset?.relativePath,
|
|
181
|
+
asset?.sourceUrl,
|
|
182
|
+
asset?.url,
|
|
183
|
+
asset?.name
|
|
184
|
+
].some((value) =>
|
|
185
|
+
candidates.has(CircuitJsonCadModelAssetResolver.#key(value))
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolves a supported model format from asset metadata.
|
|
191
|
+
* @param {object} asset Model asset metadata.
|
|
192
|
+
* @returns {string}
|
|
193
|
+
*/
|
|
194
|
+
static #format(asset) {
|
|
195
|
+
const text = String(
|
|
196
|
+
asset.format ||
|
|
197
|
+
asset.mimetype ||
|
|
198
|
+
asset.project_relative_path ||
|
|
199
|
+
asset.url ||
|
|
200
|
+
''
|
|
201
|
+
).toLowerCase()
|
|
202
|
+
if (/\bglb\b|\.glb(?:[?#]|$)/u.test(text)) return 'glb'
|
|
203
|
+
if (/\bgltf\b|\.gltf(?:[?#]|$)/u.test(text)) return 'gltf'
|
|
204
|
+
if (/\b3mf\b|\.3mf(?:[?#]|$)/u.test(text)) return '3mf'
|
|
205
|
+
if (/\bobj\b|\.obj(?:[?#]|$)/u.test(text)) return 'obj'
|
|
206
|
+
if (/\bstl\b|\.stl(?:[?#]|$)/u.test(text)) return 'stl'
|
|
207
|
+
if (/\bwrl\b|vrml|\.wrl(?:[?#]|$)/u.test(text)) return 'wrl'
|
|
208
|
+
if (/step|stp|\.step(?:[?#]|$)|\.stp(?:[?#]|$)/u.test(text)) {
|
|
209
|
+
return 'step'
|
|
210
|
+
}
|
|
211
|
+
return ''
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns true when a CAD component already has a model URL.
|
|
216
|
+
* @param {object} element Element row.
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
static #hasModelUrl(element) {
|
|
220
|
+
return [
|
|
221
|
+
'model_step_url',
|
|
222
|
+
'model_stp_url',
|
|
223
|
+
'model_3mf_url',
|
|
224
|
+
'model_wrl_url',
|
|
225
|
+
'model_vrml_url',
|
|
226
|
+
'model_glb_url',
|
|
227
|
+
'model_gltf_url',
|
|
228
|
+
'model_stl_url',
|
|
229
|
+
'model_obj_url'
|
|
230
|
+
].some((field) => String(element?.[field] || '').trim())
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Reads element rows from document wrappers.
|
|
235
|
+
* @param {object | object[]} documentModel Document model.
|
|
236
|
+
* @returns {object[]}
|
|
237
|
+
*/
|
|
238
|
+
static #elements(documentModel) {
|
|
239
|
+
if (Array.isArray(documentModel)) return documentModel
|
|
240
|
+
if (Array.isArray(documentModel?.elements))
|
|
241
|
+
return documentModel.elements
|
|
242
|
+
if (Array.isArray(documentModel?.circuitJson)) {
|
|
243
|
+
return documentModel.circuitJson
|
|
244
|
+
}
|
|
245
|
+
return []
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Normalizes a path or URL key.
|
|
250
|
+
* @param {unknown} value Raw path.
|
|
251
|
+
* @returns {string}
|
|
252
|
+
*/
|
|
253
|
+
static #key(value) {
|
|
254
|
+
return String(value || '')
|
|
255
|
+
.trim()
|
|
256
|
+
.split(/[?#]/u)[0]
|
|
257
|
+
.replace(/^https?:\/\/[^/]+\//iu, '')
|
|
258
|
+
.replace(/^\/+/u, '')
|
|
259
|
+
.toLowerCase()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import earcut from 'earcut'
|
|
2
2
|
import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
|
|
3
3
|
|
|
4
|
-
const MAX_HOLE_POINTS =
|
|
4
|
+
const MAX_HOLE_POINTS = 64
|
|
5
5
|
const MIN_HOLE_POINTS = 8
|
|
6
6
|
const GEOMETRY_EPSILON = 0.001
|
|
7
|
+
const DRILL_QUALITY_POINTS = {
|
|
8
|
+
low: 16,
|
|
9
|
+
medium: 24,
|
|
10
|
+
high: 48
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Builds PCB substrate meshes, including through-board drill cutouts.
|
|
@@ -145,15 +150,20 @@ export class PcbAssemblyBoardSubstrateBuilder {
|
|
|
145
150
|
* @returns {number[][][]}
|
|
146
151
|
*/
|
|
147
152
|
static #boardHoleLoops(sceneDescription, outer) {
|
|
153
|
+
const explicitLoops =
|
|
154
|
+
PcbAssemblyBoardSubstrateBuilder.#explicitCutoutLoops(
|
|
155
|
+
sceneDescription
|
|
156
|
+
)
|
|
148
157
|
const primitiveLoops =
|
|
149
158
|
PcbAssemblyBoardSubstrateBuilder.#primitiveHoleLoops(
|
|
150
159
|
sceneDescription
|
|
151
160
|
)
|
|
152
|
-
const
|
|
161
|
+
const fallbackLoops = primitiveLoops.length
|
|
153
162
|
? primitiveLoops
|
|
154
163
|
: PcbAssemblyBoardSubstrateBuilder.#drillCutoutLoops(
|
|
155
164
|
sceneDescription
|
|
156
165
|
)
|
|
166
|
+
const rawLoops = [...explicitLoops, ...fallbackLoops]
|
|
157
167
|
const seen = new Set()
|
|
158
168
|
const holes = []
|
|
159
169
|
|
|
@@ -189,6 +199,19 @@ export class PcbAssemblyBoardSubstrateBuilder {
|
|
|
189
199
|
return holes
|
|
190
200
|
}
|
|
191
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Reads explicit board cutout contours from the scene board metadata.
|
|
204
|
+
* @param {object} sceneDescription Prepared scene description.
|
|
205
|
+
* @returns {Array<Array<object | number[]>>}
|
|
206
|
+
*/
|
|
207
|
+
static #explicitCutoutLoops(sceneDescription) {
|
|
208
|
+
return PcbAssemblyBoardSubstrateBuilder.#array(
|
|
209
|
+
sceneDescription?.board?.cutouts
|
|
210
|
+
)
|
|
211
|
+
.map((cutout) => cutout?.points || cutout?.vertices || cutout)
|
|
212
|
+
.filter((loop) => Array.isArray(loop))
|
|
213
|
+
}
|
|
214
|
+
|
|
192
215
|
/**
|
|
193
216
|
* Reads existing drill cutout contours from scene silkscreen details.
|
|
194
217
|
* @param {object} sceneDescription Prepared scene description.
|
|
@@ -210,12 +233,16 @@ export class PcbAssemblyBoardSubstrateBuilder {
|
|
|
210
233
|
*/
|
|
211
234
|
static #primitiveHoleLoops(sceneDescription) {
|
|
212
235
|
const detail = sceneDescription?.detail || {}
|
|
236
|
+
const pointCount =
|
|
237
|
+
PcbAssemblyBoardSubstrateBuilder.#drillQualityPointCount(
|
|
238
|
+
detail?.drillQuality
|
|
239
|
+
)
|
|
213
240
|
return [
|
|
214
241
|
...PcbAssemblyBoardSubstrateBuilder.#array(detail.pads).map((pad) =>
|
|
215
|
-
PcbAssemblyBoardSubstrateBuilder.#padHoleLoop(pad)
|
|
242
|
+
PcbAssemblyBoardSubstrateBuilder.#padHoleLoop(pad, pointCount)
|
|
216
243
|
),
|
|
217
244
|
...PcbAssemblyBoardSubstrateBuilder.#array(detail.vias).map((via) =>
|
|
218
|
-
PcbAssemblyBoardSubstrateBuilder.#viaHoleLoop(via)
|
|
245
|
+
PcbAssemblyBoardSubstrateBuilder.#viaHoleLoop(via, pointCount)
|
|
219
246
|
)
|
|
220
247
|
].filter(Boolean)
|
|
221
248
|
}
|
|
@@ -223,9 +250,10 @@ export class PcbAssemblyBoardSubstrateBuilder {
|
|
|
223
250
|
/**
|
|
224
251
|
* Builds a fallback pad drill loop.
|
|
225
252
|
* @param {object} pad Pad primitive.
|
|
253
|
+
* @param {number} pointCount Circular drill point count.
|
|
226
254
|
* @returns {number[][] | null}
|
|
227
255
|
*/
|
|
228
|
-
static #padHoleLoop(pad) {
|
|
256
|
+
static #padHoleLoop(pad, pointCount) {
|
|
229
257
|
const diameter = PcbAssemblyBoardSubstrateBuilder.#firstPositive([
|
|
230
258
|
pad?.holeDiameter,
|
|
231
259
|
pad?.drillDiameter,
|
|
@@ -238,8 +266,8 @@ export class PcbAssemblyBoardSubstrateBuilder {
|
|
|
238
266
|
return null
|
|
239
267
|
}
|
|
240
268
|
|
|
241
|
-
const x = Number(pad?.x)
|
|
242
|
-
const y = Number(pad?.y)
|
|
269
|
+
const x = Number(pad?.x) + Number(pad?.holeOffsetX || 0)
|
|
270
|
+
const y = Number(pad?.y) + Number(pad?.holeOffsetY || 0)
|
|
243
271
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
244
272
|
return null
|
|
245
273
|
}
|
|
@@ -265,15 +293,16 @@ export class PcbAssemblyBoardSubstrateBuilder {
|
|
|
265
293
|
)
|
|
266
294
|
}
|
|
267
295
|
|
|
268
|
-
return PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2,
|
|
296
|
+
return PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2, pointCount)
|
|
269
297
|
}
|
|
270
298
|
|
|
271
299
|
/**
|
|
272
300
|
* Builds a fallback via drill loop.
|
|
273
301
|
* @param {object} via Via primitive.
|
|
302
|
+
* @param {number} pointCount Circular drill point count.
|
|
274
303
|
* @returns {number[][] | null}
|
|
275
304
|
*/
|
|
276
|
-
static #viaHoleLoop(via) {
|
|
305
|
+
static #viaHoleLoop(via, pointCount) {
|
|
277
306
|
const diameter = PcbAssemblyBoardSubstrateBuilder.#firstPositive([
|
|
278
307
|
via?.holeDiameter,
|
|
279
308
|
via?.drillDiameter,
|
|
@@ -284,7 +313,7 @@ export class PcbAssemblyBoardSubstrateBuilder {
|
|
|
284
313
|
const y = Number(via?.y)
|
|
285
314
|
|
|
286
315
|
return diameter && Number.isFinite(x) && Number.isFinite(y)
|
|
287
|
-
? PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2,
|
|
316
|
+
? PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2, pointCount)
|
|
288
317
|
: null
|
|
289
318
|
}
|
|
290
319
|
|
|
@@ -458,6 +487,16 @@ export class PcbAssemblyBoardSubstrateBuilder {
|
|
|
458
487
|
return 0
|
|
459
488
|
}
|
|
460
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Resolves circular drill sampling from scene detail quality metadata.
|
|
492
|
+
* @param {unknown} value Candidate quality.
|
|
493
|
+
* @returns {number}
|
|
494
|
+
*/
|
|
495
|
+
static #drillQualityPointCount(value) {
|
|
496
|
+
const quality = String(value || 'medium').toLowerCase()
|
|
497
|
+
return DRILL_QUALITY_POINTS[quality] || DRILL_QUALITY_POINTS.medium
|
|
498
|
+
}
|
|
499
|
+
|
|
461
500
|
/**
|
|
462
501
|
* Normalizes a value to an array.
|
|
463
502
|
* @param {unknown} value Candidate value.
|