pcb-scene3d-viewer 1.3.2 → 1.3.3

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 CHANGED
@@ -42,6 +42,11 @@ short-circuits non-subdividing copper-fill clipping without changing its
42
42
  observable geometry. See the
43
43
  [1.3.2 release notes](docs/release-notes-v1.3.2.md).
44
44
 
45
+ Version 1.3.3 preserves validated STEP source anchors and avoids applying a
46
+ second quarter-turn when the imported assembly envelope proves that its depth
47
+ and height axes are already exchanged. See the
48
+ [1.3.3 release notes](docs/release-notes-v1.3.3.md).
49
+
45
50
  ## CircuitJSON 1.1 convergence
46
51
 
47
52
  Version 1.2.2 accepts the common document and prepared-context shapes returned
@@ -150,6 +155,7 @@ const controller = new PcbScene3dController(viewportNode, document)
150
155
 
151
156
  - [API](docs/api.md)
152
157
  - [CircuitJSON usage](docs/circuitjson.md)
158
+ - [1.3.3 release notes](docs/release-notes-v1.3.3.md)
153
159
  - [1.3.2 release notes](docs/release-notes-v1.3.2.md)
154
160
  - [1.3.1 release notes](docs/release-notes-v1.3.1.md)
155
161
  - [1.2.2 release notes](docs/release-notes-v1.2.2.md)
@@ -186,6 +186,13 @@ Embedded STEP models can use:
186
186
  }
187
187
  ```
188
188
 
189
+ For imported STEP assemblies, the runtime compares the fully assembled mesh
190
+ envelope with an authored model-bounds projection before applying a separate
191
+ quarter-turn around the model X axis. When the imported assembly has already
192
+ exchanged its depth and height axes, the duplicate tilt is omitted. Toolkits
193
+ can set `modelTransform.preserveSourceAnchor: true` when an authored body origin
194
+ must remain fixed without requesting component-center recovery.
195
+
189
196
  ## Detail Primitives
190
197
 
191
198
  The runtime expects pre-normalized primitive lists for:
@@ -0,0 +1,28 @@
1
+ # pcb-scene3d-viewer 1.3.3
2
+
3
+ Version 1.3.3 improves external STEP placement when source-authored anchors and
4
+ pre-oriented assemblies are present.
5
+
6
+ ## STEP placement fidelity
7
+
8
+ - An explicit `preserveSourceAnchor` transform keeps a validated source body
9
+ origin fixed without activating component-center recovery.
10
+ - Embedded STEP assemblies omit a duplicate quarter-turn only when their loaded
11
+ mesh envelope proves that the authored depth and height axes are already
12
+ exchanged.
13
+ - Board-space component yaw and the original external-model asset remain
14
+ unchanged.
15
+
16
+ ## Compatibility
17
+
18
+ - Detection uses source origin, projection metadata, mesh dimensions, and
19
+ bounded error thresholds; it does not inspect filenames, project identities,
20
+ component labels, or library names.
21
+ - Existing scene colors, board geometry, and public APIs remain unchanged.
22
+
23
+ ## Verification
24
+
25
+ - Repository-owned tests cover preserved source anchors and embedded-axis
26
+ normalization alongside the existing external-model placement suite.
27
+ - The complete package suite, formatting check, and npm package dry run are
28
+ required for release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pcb-scene3d-viewer",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
5
5
  "keywords": [
6
6
  "pcb",
@@ -39,6 +39,7 @@
39
39
  "docs/release-notes-v1.3.0.md",
40
40
  "docs/release-notes-v1.3.1.md",
41
41
  "docs/release-notes-v1.3.2.md",
42
+ "docs/release-notes-v1.3.3.md",
42
43
  "docs/model-format.md",
43
44
  "docs/testing.md",
44
45
  "spec",
@@ -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.