pcb-scene3d-viewer 1.1.11 → 1.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pcb-scene3d-viewer",
3
- "version": "1.1.11",
3
+ "version": "1.1.19",
4
4
  "description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
5
5
  "keywords": [
6
6
  "pcb",
@@ -2,6 +2,8 @@
2
2
  * Resolves camera presets and basic rig state for the PCB 3D scene.
3
3
  */
4
4
  export class PcbScene3dCameraRig {
5
+ static #INSPECTION_PROJECTION_STATE_KEY = 'scene3dInspectionProjection'
6
+
5
7
  /**
6
8
  * Resolves the initial camera radius from the board size.
7
9
  * @param {{ board?: { widthMil?: number, heightMil?: number }, sourceFormat?: string }} sceneDescription
@@ -111,6 +113,15 @@ export class PcbScene3dCameraRig {
111
113
  camera?.lookAt?.(pose.target.x, pose.target.y, pose.target.z)
112
114
  controls?.update?.()
113
115
  camera?.updateProjectionMatrix?.()
116
+ if (normalizedPreset === 'top' || normalizedPreset === 'bottom') {
117
+ PcbScene3dCameraRig.#applyInspectionProjection(
118
+ camera,
119
+ controls,
120
+ pose
121
+ )
122
+ } else {
123
+ PcbScene3dCameraRig.#restoreProjection(camera, controls)
124
+ }
114
125
 
115
126
  return pose
116
127
  }
@@ -166,6 +177,174 @@ export class PcbScene3dCameraRig {
166
177
  }
167
178
  }
168
179
 
180
+ /**
181
+ * Replaces perspective projection with orthographic projection for
182
+ * top/bottom inspection views.
183
+ * @param {object} camera Three.js perspective camera.
184
+ * @param {object} controls OrbitControls-like instance.
185
+ * @param {object} pose Resolved camera pose.
186
+ * @returns {void}
187
+ */
188
+ static #applyInspectionProjection(camera, controls, pose) {
189
+ if (!camera || !pose?.target || !pose?.radius) {
190
+ return
191
+ }
192
+
193
+ const state = PcbScene3dCameraRig.#captureProjectionState(
194
+ camera,
195
+ controls
196
+ )
197
+ const radius = Math.max(Number(pose.radius || 0), 1)
198
+ const fovRadians = (Number(camera.fov || 38) * Math.PI) / 180
199
+
200
+ state.active = true
201
+ state.orthographicHeight = 2 * radius * Math.tan(fovRadians / 2)
202
+ PcbScene3dCameraRig.#applyOrthographicProjection(camera, state)
203
+ controls?.update?.()
204
+ }
205
+
206
+ /**
207
+ * Captures the first unmodified projection settings for a camera.
208
+ * @param {object} camera Three.js camera.
209
+ * @param {object} controls OrbitControls-like instance.
210
+ * @returns {object}
211
+ */
212
+ static #captureProjectionState(camera, controls) {
213
+ const userData = PcbScene3dCameraRig.#userData(camera)
214
+ const stateKey = PcbScene3dCameraRig.#INSPECTION_PROJECTION_STATE_KEY
215
+
216
+ if (!userData[stateKey]) {
217
+ userData[stateKey] = {
218
+ active: false,
219
+ orthographicHeight: 0,
220
+ originalUpdateProjectionMatrix:
221
+ camera.updateProjectionMatrix?.bind?.(camera) || null,
222
+ isPerspectiveCamera: camera.isPerspectiveCamera,
223
+ isOrthographicCamera: camera.isOrthographicCamera,
224
+ left: camera.left,
225
+ right: camera.right,
226
+ top: camera.top,
227
+ bottom: camera.bottom,
228
+ zoom: Number(camera?.zoom || 1),
229
+ near: Number(camera?.near || 0),
230
+ far: Number(camera?.far || 0),
231
+ maxDistance: Number(controls?.maxDistance || 0)
232
+ }
233
+ PcbScene3dCameraRig.#patchCameraProjectionUpdater(camera)
234
+ }
235
+
236
+ return userData[stateKey]
237
+ }
238
+
239
+ /**
240
+ * Patches projection updates so orthographic inspection survives resizes.
241
+ * @param {object} camera Three.js camera.
242
+ * @returns {void}
243
+ */
244
+ static #patchCameraProjectionUpdater(camera) {
245
+ const state =
246
+ camera.userData[
247
+ PcbScene3dCameraRig.#INSPECTION_PROJECTION_STATE_KEY
248
+ ]
249
+
250
+ if (!state?.originalUpdateProjectionMatrix) {
251
+ return
252
+ }
253
+
254
+ camera.updateProjectionMatrix = () => {
255
+ state.originalUpdateProjectionMatrix()
256
+ if (state.active) {
257
+ PcbScene3dCameraRig.#applyOrthographicProjection(camera, state)
258
+ }
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Applies an orthographic projection matrix to the existing camera.
264
+ * @param {object} camera Three.js camera.
265
+ * @param {object} state Captured camera state.
266
+ * @returns {void}
267
+ */
268
+ static #applyOrthographicProjection(camera, state) {
269
+ const zoom = Math.max(Number(camera.zoom || 1), 0.0001)
270
+ const height = Math.max(Number(state.orthographicHeight || 0) / zoom, 1)
271
+ const width = height * Math.max(Number(camera.aspect || 1), 0.0001)
272
+ const left = -width / 2
273
+ const right = width / 2
274
+ const top = height / 2
275
+ const bottom = -height / 2
276
+
277
+ camera.left = left
278
+ camera.right = right
279
+ camera.top = top
280
+ camera.bottom = bottom
281
+ camera.isPerspectiveCamera = false
282
+ camera.isOrthographicCamera = true
283
+ camera.projectionMatrix?.makeOrthographic?.(
284
+ left,
285
+ right,
286
+ top,
287
+ bottom,
288
+ camera.near,
289
+ camera.far
290
+ )
291
+ camera.projectionMatrixInverse
292
+ ?.copy?.(camera.projectionMatrix)
293
+ ?.invert?.()
294
+ }
295
+
296
+ /**
297
+ * Restores perspective settings after leaving top/bottom inspection views.
298
+ * @param {object} camera Three.js camera.
299
+ * @param {object} controls OrbitControls-like instance.
300
+ * @returns {void}
301
+ */
302
+ static #restoreProjection(camera, controls) {
303
+ const state =
304
+ camera?.userData?.[
305
+ PcbScene3dCameraRig.#INSPECTION_PROJECTION_STATE_KEY
306
+ ]
307
+
308
+ if (!state) {
309
+ return
310
+ }
311
+
312
+ if ('zoom' in camera) {
313
+ camera.zoom = state.zoom
314
+ }
315
+ if ('near' in camera && state.near > 0) {
316
+ camera.near = state.near
317
+ }
318
+ if ('far' in camera && state.far > 0) {
319
+ camera.far = state.far
320
+ }
321
+ camera.isPerspectiveCamera = state.isPerspectiveCamera
322
+ camera.isOrthographicCamera = state.isOrthographicCamera
323
+ camera.left = state.left
324
+ camera.right = state.right
325
+ camera.top = state.top
326
+ camera.bottom = state.bottom
327
+ if (controls && 'maxDistance' in controls && state.maxDistance > 0) {
328
+ controls.maxDistance = state.maxDistance
329
+ }
330
+ state.active = false
331
+ controls?.update?.()
332
+ camera.updateProjectionMatrix?.()
333
+ }
334
+
335
+ /**
336
+ * Ensures a mutable user-data object exists on the camera.
337
+ * @param {object} camera Three.js camera.
338
+ * @returns {object}
339
+ */
340
+ static #userData(camera) {
341
+ if (!camera.userData) {
342
+ camera.userData = {}
343
+ }
344
+
345
+ return camera.userData
346
+ }
347
+
169
348
  /**
170
349
  * Resolves screen-up for bottom views from the scene coordinate contract.
171
350
  * @param {{ coordinateSystem?: string }} sceneDescription Scene metadata.