@preference-sl/pref-viewer 2.17.0 → 2.17.2-beta.0
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 +63 -37
- package/package.json +1 -1
- package/src/babylonjs-camera-wheel-motion.js +54 -0
- package/src/babylonjs-camera-zoom-controller.js +4 -0
- package/src/babylonjs-controller.js +20 -7
- package/src/babylonjs-view-cube-controller.js +572 -213
- package/src/babylonjs-view-cube-renderer.js +546 -0
- package/src/styles.js +0 -88
- package/src/view-cube-orbit-math.js +199 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- Native custom elements: `<pref-viewer>`, `<pref-viewer-2d>`, `<pref-viewer-3d>`, `<pref-viewer-dialog>`, and `<pref-viewer-menu-3d>`.
|
|
8
8
|
- Babylon.js-based 3D visualization for glTF and GLB assets.
|
|
9
|
-
- Default-enabled ViewCube with 26 selectable orientations
|
|
9
|
+
- Default-enabled, draggable 3D ViewCube with 26 selectable orientations: click a face, edge or corner for a smooth, model-fitted view, or drag the cube to orbit the model. It follows the camera in real time, including its projection: the cube is orthographic for an orthographic viewer camera and perspective, with the same vertical FOV, for a perspective one.
|
|
10
10
|
- Model-only, geometry-dependent orbital zoom protection with near-plane sampling, independent of ViewCube visibility.
|
|
11
11
|
- Viewer dialogs above the render menu and ViewCube, with background pointer-input protection.
|
|
12
12
|
- Proportional mouse-wheel zoom for unlocked orbital cameras: increments decrease as the camera approaches its target, using Babylon's native input without an additional viewer zoom increment. Wheel sensitivity is currently an internal setting, not a public attribute or option.
|
|
@@ -88,8 +88,8 @@ viewer.setOptions({
|
|
|
88
88
|
intensity: 1,
|
|
89
89
|
shadows: true,
|
|
90
90
|
},
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
render: {
|
|
92
|
+
lightingTimeOfDay: 0.5,
|
|
93
93
|
},
|
|
94
94
|
});
|
|
95
95
|
```
|
|
@@ -239,13 +239,13 @@ Storage descriptors are passed through `model.storage`, `scene.storage`, `materi
|
|
|
239
239
|
|
|
240
240
|
## ViewCube
|
|
241
241
|
|
|
242
|
-
The ViewCube reflects the effective active camera orientation and offers **26 views: six faces, twelve edges and eight corners**. Click any region to change orientation smoothly and refit the model assetContainer. Every accepted click refits, even when selecting the current orientation: previous pan, framing and zoom are discarded. Environment geometry is excluded from this fit; only visible, enabled model meshes with valid geometry contribute to bounds. Hidden or disabled model geometry, including auxiliary walls/floors hidden by viewer options, does not enlarge the fit.
|
|
242
|
+
The ViewCube reflects the effective active camera orientation and offers **26 views: six faces, twelve edges and eight corners**. Click any region to change orientation smoothly and refit the model assetContainer, or drag the cube to orbit the model (see [Dragging the cube](#dragging-the-cube)). Every accepted click refits, even when selecting the current orientation: previous pan, framing and zoom are discarded. Environment geometry is excluded from this fit; only visible, enabled model meshes with valid geometry contribute to bounds. Hidden or disabled model geometry, including auxiliary walls/floors hidden by viewer options, does not enlarge the fit.
|
|
243
243
|
|
|
244
|
-
The active camera retains its identity and perspective or orthographic projection. Navigation supports Babylon ArcRotateCamera, FreeCamera and UniversalCamera, including supported parent transforms. Other camera classes, locked cameras and active XR tracked cameras are display-only. Selection does nothing when usable model bounds, a supported camera or positive viewport dimensions are unavailable.
|
|
244
|
+
The active camera retains its identity and perspective or orthographic projection. Navigation supports Babylon ArcRotateCamera, FreeCamera and UniversalCamera, including supported parent transforms. Other camera classes, locked cameras and active XR tracked cameras are display-only: the cube keeps following the camera, but hover, click and drag do nothing. Selection does nothing when usable model bounds, a supported camera or positive viewport dimensions are unavailable.
|
|
245
245
|
|
|
246
246
|
### Orientation and feedback
|
|
247
247
|
|
|
248
|
-
The inner view is the principal model view. English ViewCube labels
|
|
248
|
+
The inner view is the principal model view. English ViewCube face labels read `Inner` and `Outer`; Spanish labels are `Interior` and `Exterior`. Labels are drawn on the cube faces exactly as translated and follow culture changes. Face names use this world-axis convention:
|
|
249
249
|
|
|
250
250
|
| Direction | World axis | Reference |
|
|
251
251
|
| --- | --- | --- |
|
|
@@ -256,17 +256,37 @@ The inner view is the principal model view. English ViewCube labels use `Inner`
|
|
|
256
256
|
| Top / Superior | +Y | Looks down toward the model. |
|
|
257
257
|
| Bottom / Inferior | -Y | Looks up toward the model. |
|
|
258
258
|
|
|
259
|
-
The cube
|
|
259
|
+
The cube is drawn on its own small canvas, with a separate Babylon.js engine and virtual scene, in a PrefSketch-style look: six square labelled faces, thin bars on the twelve edges and small spheres on the eight corners; there are no chamfers. Every part of an edge or corner selects the same orientation. The cube follows the projection and FOV of the viewer camera: it is drawn with an orthographic camera when the viewer camera is orthographic, and with a perspective camera using the same vertical FOV when it is perspective (the cube keeps the same on-screen size either way, and an FOV change redraws it). Apart from that it follows only the direction of the camera, independent of model zoom. It lives in its own scene, so it adds nothing to the model scene, to model picking or to exports.
|
|
260
|
+
|
|
261
|
+
Hover paints the region under the pointer with `styles.colorPrimary` (`--pref-viewer-color-primary`). Any CSS colour syntax is accepted: hex, `rgb()`, `hsl()` or a named colour. The angularly nearest of the 26 views is shown as the current region, painted with the primary colour mixed 25% into the face fill; hover takes precedence until the pointer leaves. A primary-colour change is picked up within 250 ms, or immediately after a resize, a culture change or re-enabling the cube. The rest of the palette (face fill, borders, label text, edge bars and corner spheres) is a fixed light palette and is not configurable. There are no region tooltips.
|
|
262
|
+
|
|
263
|
+
Edges and corners are thin, so selection has a tolerance: a front-facing edge or corner whose centre is within 8 CSS pixels of the pointer is picked ahead of the face under it, corners before edges. On cubes smaller than 96 px the tolerance shrinks to a twelfth of the cube side so face centres stay selectable. Hover and click use the same pick, so the highlighted region is the one a click selects.
|
|
264
|
+
|
|
265
|
+
### Dragging the cube
|
|
266
|
+
|
|
267
|
+
Press on the cube and move the pointer more than 3 CSS pixels to drag it. Dragging orbits the viewer camera around its target at 0.012 radians per pixel, in the same direction as dragging the model with the mouse on the main canvas: the model follows the pointer, and the cube follows the camera. Elevation stops just short of the poles and the camera keeps world +Y as up. Dragging works with ArcRotateCamera, FreeCamera and UniversalCamera; authored alpha/beta limits of an ArcRotateCamera keep being enforced by Babylon on the next frame.
|
|
260
268
|
|
|
261
|
-
|
|
269
|
+
The whole cube canvas is the drag surface, including its transparent corners, and a press on it never starts a gesture on the main camera. A press that moves 3 pixels or less is a click. A drag never selects a region, even if it ends where it started, and it cancels a running click transition. Locked cameras, active XR tracked cameras and unsupported camera classes keep the cube display-only. The cursor is `grab` over the cube, `pointer` over a selectable region and `grabbing` while dragging.
|
|
262
270
|
|
|
263
|
-
|
|
271
|
+
### ViewCube accessibility
|
|
272
|
+
|
|
273
|
+
The cube canvas has `role="img"` and one localized accessible name (`View orientation` in English, `Orientación de vista` in Spanish). Individual regions have no accessible names. The cube is pointer-driven: the canvas is not a tab stop, does not take keyboard focus after pointer use and draws no focus ring. Keyboard operation of the cube is not provided.
|
|
264
274
|
|
|
265
275
|
### ViewCube layout and lifecycle
|
|
266
276
|
|
|
267
|
-
The cube
|
|
277
|
+
The cube is a square at the top right of the 3D view: **top 0px**, **right 0px** unless the host sets the inset below. Its side, in CSS pixels, is `min(clamp(22% of min(viewer width, viewer height), 96, 180), 40% of min(viewer width, viewer height))`, rounded to an integer. For example, a viewer whose smaller side is 300 px gets a 96 px cube, 600 px gets 132 px, 900 px or more gets the 180 px maximum, and a 200 px viewer gets 80 px (the 40% cap). A viewer without a usable size shows no cube.
|
|
278
|
+
|
|
279
|
+
Hosts can inset the cube from the right edge with the CSS custom property `--pref-viewer-view-cube-inset-right` (default `0px`). Set it on `<pref-viewer>` or on any ancestor; it inherits into the viewer:
|
|
280
|
+
|
|
281
|
+
```css
|
|
282
|
+
pref-viewer {
|
|
283
|
+
--pref-viewer-view-cube-inset-right: 24px;
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
When both overlays are enabled, the render-menu top equals the **measured side of the cube canvas** and updates when that size changes; the menu offset is measured, not a fixed value. Without the cube, the menu returns to its style-defined default top. The cube remains below the menu's layer, and viewer modals cover both. The internal `view-cube-layout` event carries `{ offset, height }` in CSS pixels: `offset` is the measured cube side (0 when there is no cube) and `height` is the scene viewport height.
|
|
268
288
|
|
|
269
|
-
Cube resources are created only while enabled in a visible 3D view. Disabling it or switching to 2D releases its overlay, listeners, observers and
|
|
289
|
+
Cube resources are created only while enabled in a visible 3D view. Disabling it or switching to 2D releases its overlay, listeners, observers, pending transition, engine and WebGL context. An active cube uses a second WebGL context next to the viewer's; it is repainted after a context restore and after a device pixel ratio change. Reactivation uses the current camera without reloading assets. Layout subscriptions connect before activation and are removed when inactive or disconnected.
|
|
270
290
|
|
|
271
291
|
Rapid selections retarget the transition from the displayed pose. Canvas pointer/wheel/key interaction, resize, projection change, camera/model replacement, locking, disabling or disposal cancels pending navigation; unlocking/reactivation does not replay it. Transitions use the existing demand-rendering scheduler and release their activity when finished without stopping unrelated model animations.
|
|
272
292
|
|
|
@@ -289,7 +309,9 @@ Increasing the factor makes the fitted model smaller. The other axis can have mo
|
|
|
289
309
|
|
|
290
310
|
The default orbital camera starts without `lowerBetaLimit` or `upperBetaLimit` and disables upside-down orbiting, whether or not the cube is enabled. ViewCube navigation keeps the camera up direction on world **+Y**, including top/bottom views and subsequent orbiting. Automatic loading preserves authored camera composition and navigation limits; an explicit unlocked cube action may relax incompatible orbit limits and adjust clipping/radius ranges to reach and fit the selected view. Locked cameras retain their view and limits.
|
|
291
311
|
|
|
292
|
-
Use the mouse wheel over the 3D canvas to zoom an unlocked orbital camera. Babylon's native
|
|
312
|
+
Use the mouse wheel over the 3D canvas to zoom an unlocked orbital camera. The default camera uses Babylon's native `zoomToMouseLocation`: pointing at an off-center detail shifts the target toward that region as you zoom in, with the ViewCube shown or hidden. Selecting any cube preset, including the current view, cancels pending wheel motion and recenters the target on the visible model's world-bounds center using the existing fit. This does not guarantee an exact surface point stays at the same screen pixel.
|
|
313
|
+
|
|
314
|
+
Babylon's native input applies a radius-proportional increment once, so the same wheel input moves less at a smaller radius. Input wakes idle rendering, which continues until native wheel inertia settles. Named imported orbital cameras retain their existing input settings, and Free/Universal wheel navigation remains unchanged. The internal orbital wheel sensitivity (`wheelDeltaPercentage`) is 0.015. Wheel sensitivity has no public attribute or config option.
|
|
293
315
|
|
|
294
316
|
The orbital minimum radius adapts to eligible triangle surfaces in the **current visible model assetContainer only**. Hidden, disabled, non-pickable, disposed or zero-visibility meshes do not contribute. Environment meshes are excluded from both hits and raycast traversal, and model replacement/removal cannot retain an obsolete geometry floor. Clear checked paths allow close inspection without a minimum based on overall model bounds.
|
|
295
317
|
|
|
@@ -447,7 +469,7 @@ The component emits custom events so host applications can integrate loading ind
|
|
|
447
469
|
|
|
448
470
|
### Internal Listeners
|
|
449
471
|
|
|
450
|
-
`<pref-viewer>` listens to `pref-viewer-2d-viewport-changed` from `<pref-viewer-2d>`, `pref-viewer-3d-loaded` and `pref-viewer-3d-error` from `<pref-viewer-3d>`, and `pref-viewer-3d-render-settings-change-requested` from `<pref-viewer-menu-3d>`. Internal `pref-viewer-2d-element-click` and `pref-viewer-3d-element-click` events are intercepted and forwarded through the single public `pref-viewer-element-click` contract. `BabylonJSController` listens to `pref-viewer-3d-animation-changed` from the nearest `<pref-viewer-3d>` host. The root also conditionally subscribes to the internal, non-bubbling `view-cube-layout` event while the cube is active in 3D. It carries measured
|
|
472
|
+
`<pref-viewer>` listens to `pref-viewer-2d-viewport-changed` from `<pref-viewer-2d>`, `pref-viewer-3d-loaded` and `pref-viewer-3d-error` from `<pref-viewer-3d>`, and `pref-viewer-3d-render-settings-change-requested` from `<pref-viewer-menu-3d>`. Internal `pref-viewer-2d-element-click` and `pref-viewer-3d-element-click` events are intercepted and forwarded through the single public `pref-viewer-element-click` contract. `BabylonJSController` listens to `pref-viewer-3d-animation-changed` from the nearest `<pref-viewer-3d>` host. The root also conditionally subscribes to the internal, non-bubbling `view-cube-layout` event while the cube is active in 3D. It carries `{ offset, height }` in CSS pixels, where `offset` is the measured side of the square cube and `height` is the scene viewport height, so the render menu can sit below the cube; it is not a new public root event.
|
|
451
473
|
|
|
452
474
|
Example:
|
|
453
475
|
|
|
@@ -499,6 +521,23 @@ Render settings are persisted in `localStorage` under the `pref-viewer/render-se
|
|
|
499
521
|
|
|
500
522
|
The showroom daylight background uses a shared linear HDR color calibrated with exposure and ACES tone mapping for the current light studio gray target, `#d9d9d9`, in a scene-less view. Both the static look and time-of-day cycle use this same daylight value; the cycle retains its dark night endpoint. Scene geometry can cover the background. The internal `BabylonJSController.config.look.showroom.backgroundDay` value is not a public background-color option and must be recalibrated if tone mapping or exposure changes.
|
|
501
523
|
|
|
524
|
+
## Environment map and scene lights
|
|
525
|
+
|
|
526
|
+
`options.ibl.url` accepts two formats, chosen by the URL's extension (query and hash ignored):
|
|
527
|
+
|
|
528
|
+
| Extension | Loaded as | Notes |
|
|
529
|
+
| --- | --- | --- |
|
|
530
|
+
| `.env` | Prefiltered Babylon environment (`CubeTexture.CreateFromPrefilteredData`) | No prefiltering in the browser; a 4K environment is typically under 1 MB. |
|
|
531
|
+
| anything else | Radiance HDR (`HDRCubeTexture`) | Prefiltered on load, which costs time and memory for large files. |
|
|
532
|
+
|
|
533
|
+
Either file is downloaded once, cached in IndexedDB, and reused across reloads.
|
|
534
|
+
|
|
535
|
+
Directional and spot lights that come with the scene glTF (`KHR_lights_punctual`) cast shadows when `render.shadowsEnabled` is `true` and `ibl.shadows` is off, alongside the built-in light's. Every scene and model mesh casts unless its node sets `extras.castShadows: false`, and receives unless it sets `extras.receiveShadows: false`. A light's shadow darkness comes from its node's `extras.shadowDarkness`, from `0` (black) to `1` (no shadow), and is `0.5` when absent:
|
|
536
|
+
|
|
537
|
+
```json
|
|
538
|
+
{ "name": "Sun", "rotation": [-0.5, 0, 0, 0.866], "extras": { "shadowDarkness": 0.15 }, "extensions": { "KHR_lights_punctual": { "light": 0 } } }
|
|
539
|
+
```
|
|
540
|
+
|
|
502
541
|
## Runtime styles
|
|
503
542
|
|
|
504
543
|
Runtime styles can be passed through the `styles` attribute, the `styles` property setter, `setStyles(styles)`, or `loadConfig({ styles })`.
|
|
@@ -513,7 +552,7 @@ viewer.setStyles({
|
|
|
513
552
|
});
|
|
514
553
|
```
|
|
515
554
|
|
|
516
|
-
The ViewCube uses this same primary color for its hover and
|
|
555
|
+
The ViewCube uses this same primary color for its hover feedback and, mixed into the face fill, for its current-orientation feedback. Changing `colorPrimary` through `styles` or `config` attributes, `setStyles`, or `loadConfig({ styles })` also updates animation-opening highlight color, including an opening currently highlighted, without reloading the model or changing whether highlighting is enabled. The render menu color control stays synchronized. Persisted primary styles are applied to highlights when the viewer connects. `options.render.highlightColor` can still set an explicit highlight color; a later primary-style update replaces it.
|
|
517
556
|
|
|
518
557
|
Only `colorPrimary` is currently supported. The value updates `PrefViewerStyleVariables.colorPrimary`, is exposed on the host as `--pref-viewer-color-primary`, and is persisted in `localStorage` under `pref-viewer/styles`. Persisted styles are restored when a new `<pref-viewer>` instance is created and connected.
|
|
519
558
|
|
|
@@ -568,37 +607,24 @@ Focused regressions for this work live in `tests/babylonjs-selection-controller.
|
|
|
568
607
|
|
|
569
608
|
### Camera implementation and maintenance
|
|
570
609
|
|
|
571
|
-
- `src/babylonjs-view-cube-controller.js`: dedicated `BabylonJSViewCubeController` for
|
|
610
|
+
- `src/babylonjs-view-cube-controller.js`: dedicated `BabylonJSViewCubeController` for pointer interaction (hover, click, drag), camera reading and writing, click transitions, size, colours and cleanup. It delegates model bounds and destination-fit calculations to BabylonJSCameraFraming and the drawing to the renderer. Cube-only helpers and constants remain private; region generation remains a public static utility. Its constructor JSDoc documents the host object and callbacks.
|
|
611
|
+
- `src/babylonjs-view-cube-renderer.js`: `BabylonJSViewCubeRenderer`, the cube's own Babylon.js engine and virtual scene with its 26 region meshes, picking and painting. It owns no render loop and no interaction logic; the controller renders it from the host loop.
|
|
612
|
+
- `src/view-cube-orbit-math.js`: pure helpers for orbit angles, the 26 view directions, drag deltas and the cube size rule, without Babylon or DOM dependencies.
|
|
572
613
|
- `src/babylonjs-camera-framing.js`: stateless `BabylonJSCameraFraming` with static `getModelBounds`, `calculateCameraFit` and `isCameraSupported` utilities. Bounds use only visible, enabled geometry in the supplied model container; fit calculations preserve camera state and projection, use the shared camera margin and return a destination for the caller to apply. Default-camera automatic fitting uses these same utilities, preserving the current orientation and applying the destination immediately.
|
|
573
614
|
- `src/babylonjs-camera-zoom-controller.js`: independent `BabylonJSCameraZoomController` for model-only dynamic radius protection and authored-floor restoration. It works without cube resources.
|
|
615
|
+
- `src/babylonjs-camera-wheel-motion.js`: internal boundary for inspecting and cancelling the coupled native wheel inertia during rendering, geometry protection and camera lifecycle changes. Babylon computes the zoom; this boundary isolates version-sensitive native input state.
|
|
574
616
|
- `src/babylonjs-controller.js`: owns the scene, active camera, containers, native wheel settings and render scheduler; coordinates both controllers.
|
|
575
|
-
- `src/styles.js`: exports `PrefViewerViewCubeStyles`, imported by the cube controller. The SVG stylesheet uses multiline CSS and scoped variables for the main visual properties; no JSDoc is required for this styles constant.
|
|
576
617
|
- `src/pref-viewer-3d.js` and `src/pref-viewer.js`: bridge lazy visibility, queued config, localization and measured menu layout. The root stores the layout callback in `#handlers` and removes its listener during teardown. Shared styles place dialogs above viewer overlays.
|
|
577
618
|
|
|
578
|
-
ViewCube class members are ordered as properties, constructor, static methods, private methods and public methods/getters. New classes and new/modified methods carry JSDoc describing their parameters, return values and current lifecycle/side effects. JavaScript `if` and `for` bodies always use braces, including single-statement bodies. Focused tests live in `tests/babylonjs-camera-framing.test.js`, `tests/babylonjs-view-cube-controller.test.js`, `tests/babylonjs-camera-zoom-controller.test.js`, `tests/babylonjs-controller-wheel.test.js` and `tests/pref-viewer.test.js`. Browser checks in the existing playground remain necessary for
|
|
619
|
+
ViewCube class members are ordered as properties, constructor, static methods, private methods and public methods/getters. New classes and new/modified methods carry JSDoc describing their parameters, return values and current lifecycle/side effects. JavaScript `if` and `for` bodies always use braces, including single-statement bodies. Focused tests live in `tests/babylonjs-camera-framing.test.js`, `tests/babylonjs-view-cube-controller.test.js`, `tests/babylonjs-view-cube-renderer.test.js`, `tests/view-cube-orbit-math.test.js`, `tests/babylonjs-camera-zoom-controller.test.js`, `tests/babylonjs-controller-wheel.test.js` and `tests/pref-viewer.test.js`. Browser checks in the existing playground remain necessary for cube rendering, hover regions, dragging, focus and cursor behavior, and modal stacking, because the tests run on a null engine.
|
|
579
620
|
|
|
580
|
-
### ViewCube
|
|
621
|
+
### ViewCube rendering maintenance
|
|
581
622
|
|
|
582
|
-
The
|
|
623
|
+
The cube's visual constants (the light palette, face label font and size, edge and corner sizes, lighting) are internal to `src/babylonjs-view-cube-renderer.js`. They are not properties supported by `setStyles` or `config.styles`; only `colorPrimary` reaches the cube, through the hover and current colours. The renderer module also contains a dark palette constant; it is not exported from the package and is not part of the public API.
|
|
583
624
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
| `--cube-border-color` | `#78838f` |
|
|
588
|
-
| `--cube-border-width` | `1.0px` in the SVG coordinate system; also used by the hover outline |
|
|
589
|
-
| `--cube-hover-color` | Shared primary color; used by the fill and opaque outline |
|
|
590
|
-
| `--cube-hover-opacity` | `0.50` fill opacity |
|
|
591
|
-
| `--cube-active-color` | Shared primary color |
|
|
592
|
-
| `--cube-active-opacity` | `0.15` fill opacity |
|
|
593
|
-
| `--cube-text-font-family` | `Arial, sans-serif` |
|
|
594
|
-
| `--cube-text-font-size` | `9px` |
|
|
595
|
-
| `--cube-text-color` | `#29323b` |
|
|
596
|
-
| `--cube-text-font-weight` | `600` |
|
|
597
|
-
| `--cube-text-letter-spacing` | `1.0px` |
|
|
598
|
-
|
|
599
|
-
Both feedback colors default to `var(--pref-viewer-color-primary, ...)`, preserving supported runtime primary-color updates. Text size and spacing apply in the SVG coordinate system and scale with its viewport. Active-view borders remain transparent; hover borders remain fully opaque regardless of fill opacity. Geometry and input behavior remain owned by the controller.
|
|
600
|
-
|
|
601
|
-
ViewCube listener registration and hover updates are private instance methods. Surface creation, region construction, hit-region lookup, extent interpolation and SVG projection are private static helpers; per-update projection values and interpolation progress are passed explicitly. The render menu also encapsulates hue-channel calculation and lighting playback as private methods. Playback uses one callback bound to the menu instance and cancels its scheduled frame on stop or disconnection.
|
|
625
|
+
Keep these rules when changing the renderer: the cube scene stays virtual and its engine stays out of `EngineStore.Instances` while it is alive (otherwise the viewer can create resources on the cube's WebGL context); Babylon input stays detached from the cube canvas (otherwise the cursor and keyboard focus are disturbed); and the engine keeps `loseContextOnDispose` so its WebGL context is released.
|
|
626
|
+
|
|
627
|
+
ViewCube listener registration, drag handling and hover updates are private instance methods. Region construction, colour parsing and extent interpolation are private static helpers; interpolation progress is passed explicitly. The render menu also encapsulates hue-channel calculation and lighting playback as private methods. Playback uses one callback bound to the menu instance and cancels its scheduled frame on stop or disconnection.
|
|
602
628
|
|
|
603
629
|
## Package scripts
|
|
604
630
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges native mouse-location wheel inertia to the viewer's render and navigation lifecycle.
|
|
3
|
+
* Babylon 9.11 keeps coupled pan/radius inertia on its wheel input, outside the public
|
|
4
|
+
* camera offsets. Keep that version-sensitive access here; Babylon still computes all zoom.
|
|
5
|
+
*/
|
|
6
|
+
export default class BabylonJSCameraWheelMotion {
|
|
7
|
+
/**
|
|
8
|
+
* Returns the active native mouse-location input, without creating or replacing inputs.
|
|
9
|
+
* @param {object|null|undefined} camera - Camera to inspect.
|
|
10
|
+
* @returns {object|null} Native input when its mouse-location mode is enabled.
|
|
11
|
+
*/
|
|
12
|
+
static #input(camera) {
|
|
13
|
+
const input = camera?.inputs?.attached?.mousewheel;
|
|
14
|
+
return input?.zoomToMouseLocation ? input : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detects the complete native inertia tail, including pan without a public radius offset.
|
|
19
|
+
* @param {object|null|undefined} camera - Camera to inspect.
|
|
20
|
+
* @returns {boolean} Whether native wheel motion is pending.
|
|
21
|
+
*/
|
|
22
|
+
static isMoving(camera) {
|
|
23
|
+
const input = this.#input(camera);
|
|
24
|
+
return Boolean(input && (Math.abs(input._zoomToMouseRadiusOffset || 0) > 0 || input._inertialPanning?.lengthSquared() > 0));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Stops coupled inward motion when geometry protection clamps the camera.
|
|
29
|
+
* @param {object|null|undefined} camera - Camera constrained by the geometry guard.
|
|
30
|
+
* @returns {void}
|
|
31
|
+
*/
|
|
32
|
+
static stopInward(camera) {
|
|
33
|
+
if (this.#input(camera)?._zoomToMouseRadiusOffset > 0) {
|
|
34
|
+
this.cancel(camera);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Cancels native wheel inertia and invalidates its old focal plane without altering orbit/pan inputs.
|
|
40
|
+
* @param {object|null|undefined} camera - Camera being reframed, deactivated or disposed.
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
static cancel(camera) {
|
|
44
|
+
const input = this.#input(camera);
|
|
45
|
+
if (!input) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (typeof input._zoomToMouseRadiusOffset === "number") {
|
|
49
|
+
input._zoomToMouseRadiusOffset = 0;
|
|
50
|
+
}
|
|
51
|
+
input._inertialPanning?.setAll(0);
|
|
52
|
+
input._hitPlane = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Camera } from "@babylonjs/core/Cameras/camera.js";
|
|
|
2
2
|
import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera.js";
|
|
3
3
|
import { Ray } from "@babylonjs/core/Culling/ray.js";
|
|
4
4
|
import { Vector3 } from "@babylonjs/core/Maths/math.vector.js";
|
|
5
|
+
import BabylonJSCameraWheelMotion from "./babylonjs-camera-wheel-motion.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Maintains a geometry-dependent orbital zoom floor without moving the camera target.
|
|
@@ -245,6 +246,9 @@ export default class BabylonJSCameraZoomController {
|
|
|
245
246
|
const length = Math.max(basis.distance, this.#previousDistance);
|
|
246
247
|
const minimum = Math.max(this.#originalLimit ?? 0, this.#minimumDistance(basis, length) / basis.scale);
|
|
247
248
|
camera.lowerRadiusLimit = minimum;
|
|
249
|
+
if (camera.radius <= minimum) {
|
|
250
|
+
BabylonJSCameraWheelMotion.stopInward(camera);
|
|
251
|
+
}
|
|
248
252
|
if (camera.radius < minimum) {
|
|
249
253
|
camera.radius = minimum;
|
|
250
254
|
if (camera.inertialRadiusOffset > 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArcRotateCamera, AssetContainer, BRDFTextureTools, Camera, Color3, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, ImageProcessingConfiguration, LoadAssetContainerAsync, Material, Matrix, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Texture, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
|
|
1
|
+
import { ArcRotateCamera, AssetContainer, BRDFTextureTools, Camera, Color3, Color4, CubeTexture, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, ImageProcessingConfiguration, LoadAssetContainerAsync, Material, Matrix, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Texture, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
|
|
2
2
|
import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
|
|
3
3
|
import "@babylonjs/loaders";
|
|
4
4
|
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
|
|
@@ -11,6 +11,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
11
11
|
import BabylonJSViewCubeController from "./babylonjs-view-cube-controller.js";
|
|
12
12
|
import BabylonJSCameraFraming from "./babylonjs-camera-framing.js";
|
|
13
13
|
import BabylonJSCameraZoomController from "./babylonjs-camera-zoom-controller.js";
|
|
14
|
+
import BabylonJSCameraWheelMotion from "./babylonjs-camera-wheel-motion.js";
|
|
14
15
|
import BabylonJSSelectionController from "./babylonjs-selection-controller.js";
|
|
15
16
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
16
17
|
import I18n from "./localization/i18n.js";
|
|
@@ -838,7 +839,7 @@ export default class BabylonJSController {
|
|
|
838
839
|
*/
|
|
839
840
|
#isArcRotateCameraInMotion(camera) {
|
|
840
841
|
const EPSILON = 0.00001;
|
|
841
|
-
return Math.abs(camera?.inertialAlphaOffset || 0) > EPSILON || Math.abs(camera?.inertialBetaOffset || 0) > EPSILON || Math.abs(camera?.inertialRadiusOffset || 0) > EPSILON || Math.abs(camera?.inertialPanningX || 0) > EPSILON || Math.abs(camera?.inertialPanningY || 0) > EPSILON;
|
|
842
|
+
return BabylonJSCameraWheelMotion.isMoving(camera) || Math.abs(camera?.inertialAlphaOffset || 0) > EPSILON || Math.abs(camera?.inertialBetaOffset || 0) > EPSILON || Math.abs(camera?.inertialRadiusOffset || 0) > EPSILON || Math.abs(camera?.inertialPanningX || 0) > EPSILON || Math.abs(camera?.inertialPanningY || 0) > EPSILON;
|
|
842
843
|
}
|
|
843
844
|
|
|
844
845
|
/**
|
|
@@ -1076,6 +1077,7 @@ export default class BabylonJSController {
|
|
|
1076
1077
|
this.#camera.lowerBetaLimit = null;
|
|
1077
1078
|
this.#camera.allowUpsideDown = false;
|
|
1078
1079
|
this.#camera.wheelDeltaPercentage = BabylonJSController.config.camera.wheelDeltaPercentage;
|
|
1080
|
+
this.#camera.zoomToMouseLocation = true;
|
|
1079
1081
|
this.#camera.lowerRadiusLimit = null;
|
|
1080
1082
|
this.#camera.upperRadiusLimit = 20;
|
|
1081
1083
|
this.#camera.metadata = { locked: false };
|
|
@@ -1332,7 +1334,7 @@ export default class BabylonJSController {
|
|
|
1332
1334
|
* @returns {Promise<boolean>} Returns true if the environment texture was changed, false if it was already up to date or failed to load.
|
|
1333
1335
|
* @description
|
|
1334
1336
|
* Lifecycle implemented here:
|
|
1335
|
-
* 1. If `options.ibl.cachedUrl` exists, create `HDRCubeTexture` from it.
|
|
1337
|
+
* 1. If `options.ibl.cachedUrl` exists, create `HDRCubeTexture` from it (a prefiltered `CubeTexture` when `options.ibl.url` is a `.env`).
|
|
1336
1338
|
* 2. Wait for readiness, clone it into `#hdrTexture` for reuse.
|
|
1337
1339
|
* 3. Call `options.ibl.consumeCachedUrl(true)` to revoke temporary object URLs and clear `cachedUrl`.
|
|
1338
1340
|
* 4. On following reloads, if `options.ibl.valid === true` and no `cachedUrl` is present, use `#hdrTexture.clone()`.
|
|
@@ -1346,8 +1348,13 @@ export default class BabylonJSController {
|
|
|
1346
1348
|
let hdrTexture = null;
|
|
1347
1349
|
if (this.#options.ibl?.cachedUrl) {
|
|
1348
1350
|
const hdrTextureURI = this.#options.ibl.cachedUrl;
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
+
if (/\.env$/i.test(String(this.#options.ibl.url ?? "").split(/[?#]/)[0])) {
|
|
1352
|
+
// The cached file is a blob: URL with no extension, so the loader is told it is a .env.
|
|
1353
|
+
hdrTexture = CubeTexture.CreateFromPrefilteredData(hdrTextureURI, this.#scene, ".env");
|
|
1354
|
+
} else {
|
|
1355
|
+
const envTextureSize = BabylonJSController.config.quality.environmentTextureSize || 1024;
|
|
1356
|
+
hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, envTextureSize, false, false, false, true, undefined, undefined, false, true, true);
|
|
1357
|
+
}
|
|
1351
1358
|
} else if (this.#hdrTexture && this.#options.ibl?.valid === true) {
|
|
1352
1359
|
hdrTexture = this.#hdrTexture.clone();
|
|
1353
1360
|
} else {
|
|
@@ -1584,7 +1591,7 @@ export default class BabylonJSController {
|
|
|
1584
1591
|
return generator.getLight()?.name === "PrefViewerDirLight";
|
|
1585
1592
|
});
|
|
1586
1593
|
|
|
1587
|
-
this.#containers.environment?.
|
|
1594
|
+
this.#containers.environment?.assetContainer?.lights.forEach((light) => {
|
|
1588
1595
|
// Only shadows for DirectionalLight and SpotLight types
|
|
1589
1596
|
if (!(light instanceof DirectionalLight || light instanceof SpotLight)) {
|
|
1590
1597
|
return;
|
|
@@ -1593,7 +1600,9 @@ export default class BabylonJSController {
|
|
|
1593
1600
|
const shadowGenerator = new ShadowGenerator(1024, light);
|
|
1594
1601
|
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
1595
1602
|
shadowGenerator.blurKernel = 16;
|
|
1596
|
-
|
|
1603
|
+
// KHR_lights_punctual parents the light to its glTF node, which carries the node's extras.
|
|
1604
|
+
const extrasShadowDarkness = light.parent?.metadata?.gltf?.extras?.shadowDarkness;
|
|
1605
|
+
shadowGenerator.darkness = typeof extrasShadowDarkness === "number" && Number.isFinite(extrasShadowDarkness) ? Math.min(Math.max(extrasShadowDarkness, 0), 1) : 0.5;
|
|
1597
1606
|
shadowGenerator.bias = 0.0005;
|
|
1598
1607
|
shadowGenerator.normalBias = 0.02;
|
|
1599
1608
|
shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
|
|
@@ -2656,6 +2665,8 @@ export default class BabylonJSController {
|
|
|
2656
2665
|
cameraState.setSuccess(true);
|
|
2657
2666
|
}
|
|
2658
2667
|
}
|
|
2668
|
+
BabylonJSCameraWheelMotion.cancel(this.#scene.activeCamera);
|
|
2669
|
+
BabylonJSCameraWheelMotion.cancel(camera);
|
|
2659
2670
|
this.#applyShowroomCameraFraming(camera);
|
|
2660
2671
|
this.#fitDefaultCameraToModel(camera);
|
|
2661
2672
|
this.#scene.activeCamera?.detachControl();
|
|
@@ -3458,6 +3469,8 @@ export default class BabylonJSController {
|
|
|
3458
3469
|
* @returns {Promise<void>}
|
|
3459
3470
|
*/
|
|
3460
3471
|
async disable() {
|
|
3472
|
+
BabylonJSCameraWheelMotion.cancel(this.#scene?.activeCamera);
|
|
3473
|
+
BabylonJSCameraWheelMotion.cancel(this.#camera);
|
|
3461
3474
|
this.#cameraZoom?.dispose();
|
|
3462
3475
|
this.#cameraZoom = null;
|
|
3463
3476
|
this.#viewCubeSuspended = true;
|