pcb-scene3d-viewer 1.3.2 → 1.3.4

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.
@@ -41,6 +41,10 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
41
41
  sceneDescription,
42
42
  placement
43
43
  )
44
+ if (siblingPlacements.length <= 1) {
45
+ return
46
+ }
47
+
44
48
  const packageRecords =
45
49
  PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#packageRecords(
46
50
  sceneDescription,
@@ -117,11 +121,7 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
117
121
  String(placement?.projection?.source || '').toLowerCase() ===
118
122
  'model-bounds' &&
119
123
  String(placement?.externalModel?.origin || '').toLowerCase() ===
120
- 'embedded' &&
121
- PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#siblingPlacements(
122
- sceneDescription,
123
- placement
124
- ).length > 1
124
+ 'embedded'
125
125
  )
126
126
  }
127
127
 
@@ -191,18 +191,29 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
191
191
  * @returns {{ placement: object, padCenter: { x: number, y: number }, anchorOffset: { x: number, y: number } }[]}
192
192
  */
193
193
  static #packageRecords(sceneDescription, placements) {
194
+ // Reuse ownership indexes across the sibling group, while rebuilding
195
+ // them per application so edits to scene rows remain observable.
196
+ const componentsByDesignator =
197
+ PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#componentsByDesignator(
198
+ sceneDescription
199
+ )
200
+ const padsByComponent =
201
+ PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#padsByComponent(
202
+ sceneDescription
203
+ )
194
204
  return (Array.isArray(placements) ? placements : [])
195
205
  .map((placement) => {
196
206
  const component =
197
207
  PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#resolveComponent(
198
- sceneDescription,
208
+ componentsByDesignator,
199
209
  placement
200
210
  )
201
211
  const padCenter =
202
212
  PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#ownedPackagePadCenter(
203
213
  sceneDescription,
204
214
  component,
205
- placement
215
+ placement,
216
+ padsByComponent
206
217
  )
207
218
  if (!padCenter) {
208
219
  return null
@@ -225,9 +236,15 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
225
236
  * @param {object | null | undefined} sceneDescription Scene description.
226
237
  * @param {object | null} component Scene component.
227
238
  * @param {object | null | undefined} placement External placement.
239
+ * @param {Map<number, object[]>} padsByComponent Pads indexed by owner.
228
240
  * @returns {{ x: number, y: number } | null}
229
241
  */
230
- static #ownedPackagePadCenter(sceneDescription, component, placement) {
242
+ static #ownedPackagePadCenter(
243
+ sceneDescription,
244
+ component,
245
+ placement,
246
+ padsByComponent
247
+ ) {
231
248
  if (
232
249
  !PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#isPackageComponent(
233
250
  component
@@ -247,18 +264,12 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
247
264
  PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#isBottomPlacement(
248
265
  placement
249
266
  )
250
- const points = (
251
- Array.isArray(sceneDescription?.detail?.pads)
252
- ? sceneDescription.detail.pads
253
- : []
254
- )
255
- .filter(
256
- (pad) =>
257
- Number(pad?.componentIndex) === componentIndex &&
258
- PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#isSurfacePad(
259
- pad,
260
- isBottom
261
- )
267
+ const points = (padsByComponent.get(componentIndex) || [])
268
+ .filter((pad) =>
269
+ PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#isSurfacePad(
270
+ pad,
271
+ isBottom
272
+ )
262
273
  )
263
274
  .map((pad) => ({
264
275
  x: Number(pad?.x || 0) - centerX,
@@ -482,23 +493,58 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
482
493
  }
483
494
 
484
495
  /**
485
- * Resolves the scene component for one placement.
496
+ * Indexes scene pads once for all owners in a repeated package group.
497
+ * @param {object | null | undefined} sceneDescription Scene description.
498
+ * @returns {Map<number, object[]>}
499
+ */
500
+ static #padsByComponent(sceneDescription) {
501
+ const index = new Map()
502
+ const pads = Array.isArray(sceneDescription?.detail?.pads)
503
+ ? sceneDescription.detail.pads
504
+ : []
505
+ for (const pad of pads) {
506
+ const componentIndex = Number(pad?.componentIndex)
507
+ if (!Number.isFinite(componentIndex)) {
508
+ continue
509
+ }
510
+ let ownedPads = index.get(componentIndex)
511
+ if (!ownedPads) {
512
+ ownedPads = []
513
+ index.set(componentIndex, ownedPads)
514
+ }
515
+ ownedPads.push(pad)
516
+ }
517
+ return index
518
+ }
519
+
520
+ /**
521
+ * Indexes the first component for each normalized owner designator.
486
522
  * @param {object | null | undefined} sceneDescription Scene description.
523
+ * @returns {Map<string, object>}
524
+ */
525
+ static #componentsByDesignator(sceneDescription) {
526
+ const index = new Map()
527
+ const components = Array.isArray(sceneDescription?.components)
528
+ ? sceneDescription.components
529
+ : []
530
+ for (const component of components) {
531
+ const designator = String(component?.designator || '').trim()
532
+ if (designator && !index.has(designator)) {
533
+ index.set(designator, component)
534
+ }
535
+ }
536
+ return index
537
+ }
538
+
539
+ /**
540
+ * Resolves the scene component for one placement.
541
+ * @param {Map<string, object>} componentsByDesignator Components indexed by owner.
487
542
  * @param {object | null | undefined} placement External placement.
488
543
  * @returns {object | null}
489
544
  */
490
- static #resolveComponent(sceneDescription, placement) {
545
+ static #resolveComponent(componentsByDesignator, placement) {
491
546
  const designator = String(placement?.designator || '').trim()
492
- if (!designator || !Array.isArray(sceneDescription?.components)) {
493
- return null
494
- }
495
-
496
- return (
497
- sceneDescription.components.find(
498
- (component) =>
499
- String(component?.designator || '').trim() === designator
500
- ) || null
501
- )
547
+ return componentsByDesignator.get(designator) || null
502
548
  }
503
549
 
504
550
  /**
@@ -6,12 +6,13 @@ export class PcbScene3dExternalModelSourceOriginPolicy {
6
6
  * Checks whether an explicit owner anchor should keep source-origin repair
7
7
  * disabled. Translucent cover-sized bodies use corner-origin STEP data, so
8
8
  * they still need the embedded source-origin correction.
9
- * @param {{ bodyOpacity?: number | string, modelTransform?: { ownerAnchorOffsetMil?: object }, projection?: { boundsMil?: { width?: number, depth?: number } } }} placement Placement metadata.
9
+ * @param {{ bodyOpacity?: number | string, modelTransform?: { ownerAnchorOffsetMil?: object, preserveSourceAnchor?: boolean }, projection?: { boundsMil?: { width?: number, depth?: number } } }} placement Placement metadata.
10
10
  * @returns {boolean}
11
11
  */
12
12
  static shouldSkipOwnerAnchoredAdjustment(placement) {
13
13
  return (
14
- Boolean(placement?.modelTransform?.ownerAnchorOffsetMil) &&
14
+ (Boolean(placement?.modelTransform?.ownerAnchorOffsetMil) ||
15
+ placement?.modelTransform?.preserveSourceAnchor === true) &&
15
16
  !PcbScene3dExternalModelSourceOriginPolicy.#isTransparentCoverSizedPlacement(
16
17
  placement
17
18
  )
@@ -362,7 +362,16 @@ export class PcbScene3dExternalModels {
362
362
  viewCompensationGroup
363
363
  )
364
364
  const modelTransform = placement?.modelTransform || {}
365
- const modelRotation = modelTransform.rotationDeg || {}
365
+ const authoredModelRotation = modelTransform.rotationDeg || {}
366
+ const modelRotation =
367
+ PcbScene3dExternalModels.#resolveEmbeddedModelRotation(
368
+ placement,
369
+ modelGroup,
370
+ authoredModelRotation
371
+ )
372
+ if (modelRotation !== authoredModelRotation) {
373
+ modelGroup.userData.scene3dEmbeddedAxisTiltRepair = true
374
+ }
366
375
  const modelOffset =
367
376
  PcbScene3dExternalModels.#resolveModelOffset(modelTransform)
368
377
  const sourceOriginAdjustment =
@@ -595,6 +604,73 @@ export class PcbScene3dExternalModels {
595
604
  }
596
605
  }
597
606
 
607
+ /**
608
+ * Removes a duplicated quarter-turn when the imported STEP assembly has
609
+ * already exchanged its authored depth and height axes. Native STEP point
610
+ * envelopes describe pre-assembly coordinates, while OCCT returns the
611
+ * fully placed mesh, so comparing both frames is required before applying
612
+ * the separate Altium body tilt.
613
+ * @param {{ externalModel?: { origin?: string }, projection?: { source?: string, boundsMil?: { depth?: number, height?: number } } }} placement Placement metadata.
614
+ * @param {{ userData?: { scene3dSourceBoundsMil?: { sizeY?: number, sizeZ?: number } } }} modelGroup Loaded model group.
615
+ * @param {{ x?: number, y?: number, z?: number }} modelRotation Authored model rotation.
616
+ * @returns {{ x?: number, y?: number, z?: number }}
617
+ */
618
+ static #resolveEmbeddedModelRotation(placement, modelGroup, modelRotation) {
619
+ const sourceBounds = modelGroup?.userData?.scene3dSourceBoundsMil || {}
620
+ const projectionBounds = placement?.projection?.boundsMil || {}
621
+ const sourceY = Math.abs(Number(sourceBounds.sizeY || 0))
622
+ const sourceZ = Math.abs(Number(sourceBounds.sizeZ || 0))
623
+ const projectedDepth = Math.abs(Number(projectionBounds.depth || 0))
624
+ const projectedHeight = Math.abs(Number(projectionBounds.height || 0))
625
+ const tilt = PcbScene3dExternalModels.#normalizeAngle(modelRotation?.x)
626
+ const directError =
627
+ PcbScene3dExternalModels.#relativeDimensionError(
628
+ sourceY,
629
+ projectedDepth
630
+ ) +
631
+ PcbScene3dExternalModels.#relativeDimensionError(
632
+ sourceZ,
633
+ projectedHeight
634
+ )
635
+ const exchangedError =
636
+ PcbScene3dExternalModels.#relativeDimensionError(
637
+ sourceY,
638
+ projectedHeight
639
+ ) +
640
+ PcbScene3dExternalModels.#relativeDimensionError(
641
+ sourceZ,
642
+ projectedDepth
643
+ )
644
+
645
+ if (
646
+ String(placement?.externalModel?.origin || '').toLowerCase() !==
647
+ 'embedded' ||
648
+ String(placement?.projection?.source || '').toLowerCase() !==
649
+ 'model-bounds' ||
650
+ (tilt !== 90 && tilt !== 270) ||
651
+ Math.min(sourceY, sourceZ, projectedDepth, projectedHeight) <= 0 ||
652
+ Math.max(projectedDepth, projectedHeight) /
653
+ Math.min(projectedDepth, projectedHeight) <
654
+ 1.25 ||
655
+ exchangedError > 0.2 ||
656
+ directError - exchangedError < 0.5
657
+ ) {
658
+ return modelRotation
659
+ }
660
+
661
+ return { ...modelRotation, x: 0 }
662
+ }
663
+
664
+ /**
665
+ * Measures a scale-independent dimension mismatch.
666
+ * @param {number} actual Imported dimension.
667
+ * @param {number} expected Authored dimension.
668
+ * @returns {number}
669
+ */
670
+ static #relativeDimensionError(actual, expected) {
671
+ return Math.abs(actual - expected) / Math.max(actual, expected, 1)
672
+ }
673
+
598
674
  /**
599
675
  * Checks whether source Z is the model's intentional edge-extension axis
600
676
  * instead of a square-package source-origin bias.
@@ -0,0 +1,87 @@
1
+ import { PcbScene3dBoardSolderMaskFactory } from './PcbScene3dBoardSolderMaskFactory.mjs'
2
+ import { PcbScene3dCopperDetailGroupBuilder } from './PcbScene3dCopperDetailGroupBuilder.mjs'
3
+ import { PcbScene3dDetailCoordinateNormalizer } from './PcbScene3dDetailCoordinateNormalizer.mjs'
4
+ import { PcbScene3dDrillVoidFactory } from './PcbScene3dDrillVoidFactory.mjs'
5
+ import { PcbScene3dRuntimeBoardMeshes } from './PcbScene3dRuntimeBoardMeshes.mjs'
6
+
7
+ /** Runs the same exact generated-geometry factories on workers or the caller. */
8
+ export class PcbScene3dGeneratedGeometryBuilder {
9
+ /**
10
+ * Builds one independent generated runtime stage.
11
+ * @param {any} THREE Three.js namespace.
12
+ * @param {'board' | 'copper'} kind Geometry stage.
13
+ * @param {object} sceneDescription Normalized scene description.
14
+ * @returns {any}
15
+ */
16
+ static build(THREE, kind, sceneDescription) {
17
+ const normalizePoint =
18
+ PcbScene3dDetailCoordinateNormalizer.create(sceneDescription)
19
+ const board = sceneDescription.board
20
+ if (kind === 'copper') {
21
+ return PcbScene3dCopperDetailGroupBuilder.build(
22
+ THREE,
23
+ sceneDescription,
24
+ board.thicknessMil / 2 + 0.05,
25
+ normalizePoint
26
+ )
27
+ }
28
+ if (kind !== 'board')
29
+ throw new Error('Unknown generated geometry stage: ' + kind)
30
+ const group = new THREE.Group()
31
+ group.add(
32
+ PcbScene3dRuntimeBoardMeshes.buildBoardMesh(
33
+ THREE,
34
+ sceneDescription,
35
+ normalizePoint
36
+ )
37
+ )
38
+ group.add(
39
+ PcbScene3dBoardSolderMaskFactory.buildGroup(
40
+ THREE,
41
+ sceneDescription,
42
+ normalizePoint
43
+ )
44
+ )
45
+ group.add(
46
+ PcbScene3dDrillVoidFactory.buildGroup(
47
+ THREE,
48
+ sceneDescription.detail,
49
+ board.thicknessMil / 2,
50
+ -board.thicknessMil / 2,
51
+ normalizePoint,
52
+ {
53
+ enabled: true,
54
+ board,
55
+ hasBoardAssemblyModel: Boolean(
56
+ sceneDescription.boardAssemblyModel
57
+ ),
58
+ sourceFormat: sceneDescription.sourceFormat
59
+ }
60
+ )
61
+ )
62
+ group.add(
63
+ PcbScene3dRuntimeBoardMeshes.buildBoardOutline(
64
+ THREE,
65
+ sceneDescription,
66
+ normalizePoint
67
+ )
68
+ )
69
+ return group
70
+ }
71
+
72
+ /**
73
+ * Keeps component model payloads outside geometry-worker messages.
74
+ * @param {object} sceneDescription Normalized scene description.
75
+ * @returns {object}
76
+ */
77
+ static workerInput(sceneDescription) {
78
+ return {
79
+ board: sceneDescription.board,
80
+ detail: sceneDescription.detail,
81
+ texts: sceneDescription.texts,
82
+ sourceFormat: sceneDescription.sourceFormat,
83
+ coordinateSystem: sceneDescription.coordinateSystem,
84
+ boardAssemblyModel: Boolean(sceneDescription.boardAssemblyModel)
85
+ }
86
+ }
87
+ }