pcb-scene3d-viewer 1.1.0 → 1.1.2

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.
@@ -0,0 +1,167 @@
1
+ import { PcbScene3dCutoutCircleDetector } from './PcbScene3dCutoutCircleDetector.mjs'
2
+
3
+ /**
4
+ * Resolves analytic overlap checks for sampled circular drill cutouts.
5
+ */
6
+ export class PcbScene3dCircularCutoutOverlap {
7
+ /**
8
+ * Returns true when one triangle overlaps a circular cutout.
9
+ * @param {{ x: number, y: number }[]} triangle
10
+ * @param {{ points?: { x: number, y: number }[], centerX?: number, centerY?: number, radius?: number }} cutout
11
+ * @param {number} [epsilon]
12
+ * @returns {boolean}
13
+ */
14
+ static overlapsTriangle(triangle, cutout, epsilon = 0.001) {
15
+ const radius = Number(cutout?.radius || 0) + Number(epsilon || 0)
16
+ if (!Number.isFinite(radius) || radius <= 0) {
17
+ return false
18
+ }
19
+
20
+ const center = {
21
+ x: Number(cutout?.centerX || 0),
22
+ y: Number(cutout?.centerY || 0)
23
+ }
24
+ const radiusSquared = radius * radius
25
+
26
+ for (const point of triangle) {
27
+ if (
28
+ PcbScene3dCutoutCircleDetector.distanceSquared(point, cutout) <=
29
+ radiusSquared
30
+ ) {
31
+ return true
32
+ }
33
+ }
34
+
35
+ if (
36
+ PcbScene3dCircularCutoutOverlap.#isPointInsideOrOnTriangle(
37
+ center,
38
+ triangle,
39
+ epsilon
40
+ )
41
+ ) {
42
+ return true
43
+ }
44
+
45
+ for (let index = 0; index < triangle.length; index += 1) {
46
+ const current = triangle[index]
47
+ const next = triangle[(index + 1) % triangle.length]
48
+
49
+ if (
50
+ PcbScene3dCircularCutoutOverlap.#distanceToSegmentSquared(
51
+ center,
52
+ current,
53
+ next,
54
+ epsilon
55
+ ) <= radiusSquared
56
+ ) {
57
+ return true
58
+ }
59
+ }
60
+
61
+ return PcbScene3dCircularCutoutOverlap.#hasSampledBoundaryOverlap(
62
+ triangle,
63
+ cutout,
64
+ epsilon
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Returns true when a sampled cutout boundary point lies in the triangle.
70
+ * @param {{ x: number, y: number }[]} triangle
71
+ * @param {{ points?: { x: number, y: number }[] }} cutout
72
+ * @param {number} epsilon
73
+ * @returns {boolean}
74
+ */
75
+ static #hasSampledBoundaryOverlap(triangle, cutout, epsilon) {
76
+ const boundaryPoints = Array.isArray(cutout?.points)
77
+ ? cutout.points
78
+ : []
79
+ const step = Math.max(1, Math.floor(boundaryPoints.length / 8))
80
+
81
+ for (let index = 0; index < boundaryPoints.length; index += step) {
82
+ if (
83
+ PcbScene3dCircularCutoutOverlap.#isPointInsideOrOnTriangle(
84
+ boundaryPoints[index],
85
+ triangle,
86
+ epsilon
87
+ )
88
+ ) {
89
+ return true
90
+ }
91
+ }
92
+
93
+ return false
94
+ }
95
+
96
+ /**
97
+ * Returns true when a point is inside or on one triangle.
98
+ * @param {{ x: number, y: number }} point
99
+ * @param {{ x: number, y: number }[]} triangle
100
+ * @param {number} epsilon
101
+ * @returns {boolean}
102
+ */
103
+ static #isPointInsideOrOnTriangle(point, triangle, epsilon) {
104
+ let hasNegative = false
105
+ let hasPositive = false
106
+
107
+ for (let index = 0; index < triangle.length; index += 1) {
108
+ const current = triangle[index]
109
+ const next = triangle[(index + 1) % triangle.length]
110
+ const sign = PcbScene3dCircularCutoutOverlap.#cross(
111
+ point,
112
+ current,
113
+ next
114
+ )
115
+
116
+ hasNegative = hasNegative || sign < -epsilon
117
+ hasPositive = hasPositive || sign > epsilon
118
+ }
119
+
120
+ return !(hasNegative && hasPositive)
121
+ }
122
+
123
+ /**
124
+ * Resolves the squared distance from one point to a finite segment.
125
+ * @param {{ x: number, y: number }} point
126
+ * @param {{ x: number, y: number }} start
127
+ * @param {{ x: number, y: number }} end
128
+ * @param {number} epsilon
129
+ * @returns {number}
130
+ */
131
+ static #distanceToSegmentSquared(point, start, end, epsilon) {
132
+ const dx = end.x - start.x
133
+ const dy = end.y - start.y
134
+ const lengthSquared = dx * dx + dy * dy
135
+
136
+ if (lengthSquared <= epsilon) {
137
+ return (point.x - start.x) ** 2 + (point.y - start.y) ** 2
138
+ }
139
+
140
+ const ratio = Math.max(
141
+ 0,
142
+ Math.min(
143
+ 1,
144
+ ((point.x - start.x) * dx + (point.y - start.y) * dy) /
145
+ lengthSquared
146
+ )
147
+ )
148
+ const projectedX = start.x + ratio * dx
149
+ const projectedY = start.y + ratio * dy
150
+
151
+ return (point.x - projectedX) ** 2 + (point.y - projectedY) ** 2
152
+ }
153
+
154
+ /**
155
+ * Resolves the signed area for three points.
156
+ * @param {{ x: number, y: number }} first
157
+ * @param {{ x: number, y: number }} second
158
+ * @param {{ x: number, y: number }} third
159
+ * @returns {number}
160
+ */
161
+ static #cross(first, second, third) {
162
+ return (
163
+ (second.x - first.x) * (third.y - first.y) -
164
+ (second.y - first.y) * (third.x - first.x)
165
+ )
166
+ }
167
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Normalizes and applies live 3D component transform adjustments.
3
+ */
4
+ export class PcbScene3dComponentAdjustment {
5
+ /**
6
+ * Finds transform adjustment targets beneath one scene object.
7
+ * @param {any} rootObject Root object.
8
+ * @returns {any[]}
9
+ */
10
+ static findTargets(rootObject) {
11
+ if (!rootObject) {
12
+ return []
13
+ }
14
+
15
+ const targets = rootObject?.userData?.scene3dAdjustmentTarget
16
+ ? [rootObject]
17
+ : []
18
+ ;(rootObject?.children || []).forEach((child) => {
19
+ targets.push(...PcbScene3dComponentAdjustment.findTargets(child))
20
+ })
21
+
22
+ return targets
23
+ }
24
+
25
+ /**
26
+ * Applies one live transform to a target relative to its original baseline.
27
+ * @param {any} THREE Three.js namespace.
28
+ * @param {any} target Target object.
29
+ * @param {{ scale?: { x?: number, y?: number, z?: number }, rotationDeg?: { x?: number, y?: number, z?: number }, offsetMil?: { x?: number, y?: number, z?: number } }} adjustment Adjustment.
30
+ * @returns {void}
31
+ */
32
+ static applyToTarget(THREE, target, adjustment) {
33
+ const current = PcbScene3dComponentAdjustment.normalize(adjustment)
34
+ const baseline = PcbScene3dComponentAdjustment.normalize(
35
+ target?.userData?.scene3dAdjustmentBaseline ||
36
+ PcbScene3dComponentAdjustment.neutral()
37
+ )
38
+ const delta = {
39
+ scale: {
40
+ x: PcbScene3dComponentAdjustment.#scaleRatio(
41
+ current.scale.x,
42
+ baseline.scale.x
43
+ ),
44
+ y: PcbScene3dComponentAdjustment.#scaleRatio(
45
+ current.scale.y,
46
+ baseline.scale.y
47
+ ),
48
+ z: PcbScene3dComponentAdjustment.#scaleRatio(
49
+ current.scale.z,
50
+ baseline.scale.z
51
+ )
52
+ },
53
+ rotationDeg: {
54
+ x: current.rotationDeg.x - baseline.rotationDeg.x,
55
+ y: current.rotationDeg.y - baseline.rotationDeg.y,
56
+ z: current.rotationDeg.z - baseline.rotationDeg.z
57
+ },
58
+ offsetMil: {
59
+ x: current.offsetMil.x - baseline.offsetMil.x,
60
+ y: current.offsetMil.y - baseline.offsetMil.y,
61
+ z: current.offsetMil.z - baseline.offsetMil.z
62
+ }
63
+ }
64
+
65
+ target?.position?.set?.(
66
+ delta.offsetMil.x,
67
+ delta.offsetMil.y,
68
+ delta.offsetMil.z
69
+ )
70
+ target?.scale?.set?.(delta.scale.x, delta.scale.y, delta.scale.z)
71
+ PcbScene3dComponentAdjustment.#applyRotation(THREE, target, delta)
72
+ }
73
+
74
+ /**
75
+ * Normalizes one adjustment object.
76
+ * @param {{ scale?: { x?: number, y?: number, z?: number }, rotationDeg?: { x?: number, y?: number, z?: number }, offsetMil?: { x?: number, y?: number, z?: number }, dxMil?: number, dyMil?: number, dzMil?: number } | null | undefined} adjustment Raw adjustment.
77
+ * @returns {{ scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }}
78
+ */
79
+ static normalize(adjustment) {
80
+ const scale = adjustment?.scale || {}
81
+ const rotationDeg = adjustment?.rotationDeg || {}
82
+ const offsetMil = adjustment?.offsetMil || {}
83
+
84
+ return {
85
+ scale: {
86
+ x: PcbScene3dComponentAdjustment.#numberOr(scale.x, 1),
87
+ y: PcbScene3dComponentAdjustment.#numberOr(scale.y, 1),
88
+ z: PcbScene3dComponentAdjustment.#numberOr(scale.z, 1)
89
+ },
90
+ rotationDeg: {
91
+ x: PcbScene3dComponentAdjustment.#numberOr(rotationDeg.x, 0),
92
+ y: PcbScene3dComponentAdjustment.#numberOr(rotationDeg.y, 0),
93
+ z: PcbScene3dComponentAdjustment.#numberOr(rotationDeg.z, 0)
94
+ },
95
+ offsetMil: {
96
+ x: PcbScene3dComponentAdjustment.#numberOr(
97
+ offsetMil.x ?? adjustment?.dxMil,
98
+ 0
99
+ ),
100
+ y: PcbScene3dComponentAdjustment.#numberOr(
101
+ offsetMil.y ?? adjustment?.dyMil,
102
+ 0
103
+ ),
104
+ z: PcbScene3dComponentAdjustment.#numberOr(
105
+ offsetMil.z ?? adjustment?.dzMil,
106
+ 0
107
+ )
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Returns a neutral transform adjustment.
114
+ * @returns {{ scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }}
115
+ */
116
+ static neutral() {
117
+ return {
118
+ scale: { x: 1, y: 1, z: 1 },
119
+ rotationDeg: { x: 0, y: 0, z: 0 },
120
+ offsetMil: { x: 0, y: 0, z: 0 }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Applies KiCad-style model-local rotation order to an adjustment target.
126
+ * @param {any} THREE Three.js namespace.
127
+ * @param {any} target Target object.
128
+ * @param {{ rotationDeg: { x: number, y: number, z: number } }} adjustment Rotation adjustment.
129
+ * @returns {void}
130
+ */
131
+ static #applyRotation(THREE, target, adjustment) {
132
+ const x = (-Number(adjustment.rotationDeg.x || 0) * Math.PI) / 180
133
+ const y = (-Number(adjustment.rotationDeg.y || 0) * Math.PI) / 180
134
+ const z = (-Number(adjustment.rotationDeg.z || 0) * Math.PI) / 180
135
+
136
+ if (THREE?.Matrix4 && target?.quaternion?.setFromRotationMatrix) {
137
+ const rotationMatrix = new THREE.Matrix4()
138
+ .makeRotationZ(z)
139
+ .multiply(new THREE.Matrix4().makeRotationY(y))
140
+ .multiply(new THREE.Matrix4().makeRotationX(x))
141
+ target.quaternion.setFromRotationMatrix(rotationMatrix)
142
+ return
143
+ }
144
+
145
+ if (!target?.rotation) {
146
+ return
147
+ }
148
+
149
+ target.rotation.x = x
150
+ target.rotation.y = y
151
+ target.rotation.z = z
152
+ }
153
+
154
+ /**
155
+ * Returns a finite number or a fallback.
156
+ * @param {unknown} value Source value.
157
+ * @param {number} fallback Fallback value.
158
+ * @returns {number}
159
+ */
160
+ static #numberOr(value, fallback) {
161
+ const numericValue = Number(value)
162
+ return Number.isFinite(numericValue) ? numericValue : fallback
163
+ }
164
+
165
+ /**
166
+ * Computes a safe scale ratio.
167
+ * @param {number} value Current value.
168
+ * @param {number} baseline Baseline value.
169
+ * @returns {number}
170
+ */
171
+ static #scaleRatio(value, baseline) {
172
+ return baseline === 0 ? value : value / baseline
173
+ }
174
+ }
@@ -0,0 +1,95 @@
1
+ import { PcbScene3dComponentAdjustment } from './PcbScene3dComponentAdjustment.mjs'
2
+
3
+ /**
4
+ * Tracks live transform adjustments and applies them to registered targets.
5
+ */
6
+ export class PcbScene3dComponentAdjustmentRegistry {
7
+ /** @type {() => any} */
8
+ #resolveThree
9
+ /** @type {Map<string, Set<any>>} */
10
+ #targets
11
+ /** @type {Map<string, { scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }>} */
12
+ #adjustments
13
+
14
+ /**
15
+ * @param {() => any} resolveThree Three.js namespace resolver.
16
+ */
17
+ constructor(resolveThree) {
18
+ this.#resolveThree = resolveThree
19
+ this.#targets = new Map()
20
+ this.#adjustments = new Map()
21
+ }
22
+
23
+ /** @returns {void} */
24
+ clear() {
25
+ this.#targets.clear()
26
+ this.#adjustments.clear()
27
+ }
28
+
29
+ /**
30
+ * Applies a live model-local adjustment to one component.
31
+ * @param {string} designator Component designator.
32
+ * @param {{ scale?: { x?: number, y?: number, z?: number }, rotationDeg?: { x?: number, y?: number, z?: number }, offsetMil?: { x?: number, y?: number, z?: number } }} adjustment Transform adjustment.
33
+ * @returns {boolean}
34
+ */
35
+ set(designator, adjustment) {
36
+ const normalizedDesignator = String(designator || '').trim()
37
+ if (!normalizedDesignator) {
38
+ return false
39
+ }
40
+
41
+ const normalizedAdjustment =
42
+ PcbScene3dComponentAdjustment.normalize(adjustment)
43
+ this.#adjustments.set(normalizedDesignator, normalizedAdjustment)
44
+ this.#apply(normalizedDesignator, normalizedAdjustment)
45
+ return true
46
+ }
47
+
48
+ /**
49
+ * Registers one model-local transform adjustment target.
50
+ * @param {string | undefined} designator Component designator.
51
+ * @param {any} target Target object.
52
+ * @returns {void}
53
+ */
54
+ register(designator, target) {
55
+ const normalizedDesignator = String(designator || '').trim()
56
+ if (!normalizedDesignator || !target) {
57
+ return
58
+ }
59
+
60
+ if (!this.#targets.has(normalizedDesignator)) {
61
+ this.#targets.set(normalizedDesignator, new Set())
62
+ }
63
+
64
+ this.#targets.get(normalizedDesignator)?.add(target)
65
+ const pendingAdjustment = this.#adjustments.get(normalizedDesignator)
66
+ if (pendingAdjustment) {
67
+ PcbScene3dComponentAdjustment.applyToTarget(
68
+ this.#resolveThree(),
69
+ target,
70
+ pendingAdjustment
71
+ )
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Applies one adjustment to all known targets for a component.
77
+ * @param {string} designator Component designator.
78
+ * @param {{ scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }} adjustment Adjustment.
79
+ * @returns {void}
80
+ */
81
+ #apply(designator, adjustment) {
82
+ const targets = this.#targets.get(designator)
83
+ if (!targets) {
84
+ return
85
+ }
86
+
87
+ targets.forEach((target) => {
88
+ PcbScene3dComponentAdjustment.applyToTarget(
89
+ this.#resolveThree(),
90
+ target,
91
+ adjustment
92
+ )
93
+ })
94
+ }
95
+ }