@preference-sl/pref-viewer 2.16.2 → 2.17.0-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 +137 -7
- package/index.d.ts +9 -0
- package/package.json +1 -1
- package/src/babylonjs-camera-framing.js +171 -0
- package/src/babylonjs-camera-zoom-controller.js +272 -0
- package/src/babylonjs-controller.js +312 -205
- package/src/babylonjs-view-cube-controller.js +718 -0
- package/src/localization/translations.js +18 -0
- package/src/pref-viewer-3d.js +29 -5
- package/src/pref-viewer-menu-3d.js +2 -2
- package/src/pref-viewer.js +99 -24
- package/src/styles.js +94 -3
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
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, synchronized camera feedback, smooth model framing and perspective/orthographic support.
|
|
10
|
+
- Model-only, geometry-dependent orbital zoom protection with near-plane sampling, independent of ViewCube visibility.
|
|
11
|
+
- Viewer dialogs above the render menu and ViewCube, with background pointer-input protection.
|
|
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.
|
|
9
13
|
- 2D SVG drawing support with pan and zoom controls.
|
|
10
14
|
- Unified `<pref-viewer>` wrapper that coordinates 2D, 3D, menu, dialog, localization, and loading lifecycle.
|
|
11
15
|
- Sequential task queue for deterministic config, model, scene, materials, drawing, options, and culture updates.
|
|
@@ -92,7 +96,9 @@ viewer.setOptions({
|
|
|
92
96
|
|
|
93
97
|
## Configuration model
|
|
94
98
|
|
|
95
|
-
The main `config` payload can include model, scene, materials, drawing, options, and
|
|
99
|
+
The main `config` payload can include model, scene, materials, drawing, options, culture, styles, and boolean `showRenderMenu` / `showViewCube` flags.
|
|
100
|
+
|
|
101
|
+
When a queued configuration is processed, explicit boolean flags update the corresponding `show-render-menu` and `show-view-cube` attributes before its assets load. Omitted or non-boolean flags preserve the current settings. Explicit processed flags override the corresponding current attributes; subsequent direct attribute changes remain effective. Reapplying an equal attribute value does not recreate the render menu. The same rules apply to `loadConfig(config)`, the `config` property setter and the JSON `config` attribute; flags follow existing task ordering, including during pending asset loads. These fields belong to the root config, alongside `culture` and `styles`.
|
|
96
102
|
|
|
97
103
|
```js
|
|
98
104
|
viewer.loadConfig({
|
|
@@ -141,10 +147,39 @@ viewer.loadConfig({
|
|
|
141
147
|
colorPrimary: "#ff6700",
|
|
142
148
|
},
|
|
143
149
|
culture: "en-EN",
|
|
150
|
+
showRenderMenu: true,
|
|
151
|
+
showViewCube: true,
|
|
144
152
|
forceReload: false,
|
|
145
153
|
});
|
|
146
154
|
```
|
|
147
155
|
|
|
156
|
+
### Overlay visibility
|
|
157
|
+
|
|
158
|
+
Use boolean values in config and string values in attributes:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
viewer.config = {
|
|
162
|
+
showRenderMenu: true,
|
|
163
|
+
showViewCube: false,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Change only the cube flag; the menu keeps its current setting.
|
|
167
|
+
viewer.loadConfig({ showViewCube: true });
|
|
168
|
+
|
|
169
|
+
// Direct attribute control remains available after processing config.
|
|
170
|
+
viewer.setAttribute("show-view-cube", "false");
|
|
171
|
+
viewer.setAttribute("show-render-menu", "true");
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```html
|
|
175
|
+
<pref-viewer
|
|
176
|
+
mode="3d"
|
|
177
|
+
config='{"showRenderMenu":true,"showViewCube":false}'
|
|
178
|
+
></pref-viewer>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Both overlays are enabled by default in 3D when their attributes are absent. Use explicit `"true"`/`"false"` strings for attributes: the menu accepts an absent attribute or literal `"true"` as enabled, whereas only literal `"false"` disables the cube. A config value such as `"false"` is a string and does not change visibility.
|
|
182
|
+
|
|
148
183
|
## Asset storage descriptors
|
|
149
184
|
|
|
150
185
|
Storage descriptors are passed through `model.storage`, `scene.storage`, `materials.storage`, or `drawing.storage`.
|
|
@@ -198,16 +233,75 @@ Storage descriptors are passed through `model.storage`, `scene.storage`, `materi
|
|
|
198
233
|
| `mode` | Viewer mode: `3d` or `2d`. Defaults to `3d`. |
|
|
199
234
|
| `show-model` | Shows or hides the current 3D model. Use `true` or `false`. |
|
|
200
235
|
| `show-scene` | Shows or hides the current 3D scene/environment. Use `true` or `false`. |
|
|
201
|
-
| `show-render-menu` | Shows
|
|
236
|
+
| `show-render-menu` | Shows the floating 3D render settings menu when absent or `"true"`; `"false"` hides it. Also configurable through boolean `config.showRenderMenu`. See [ViewCube layout](#viewcube-layout-and-lifecycle). |
|
|
237
|
+
| `show-view-cube` | Shows the orientation cube in 3D. Enabled by default; only literal `"false"` disables it. Also configurable through boolean `config.showViewCube`. Runtime toggles preserve loaded assets and camera state. See [ViewCube](#viewcube). |
|
|
202
238
|
| `styles` | JSON string with supported style overrides. Currently supports `colorPrimary`, applies it as `--pref-viewer-color-primary`, updates `PrefViewerStyleVariables.colorPrimary`, and persists it in `localStorage`. |
|
|
203
239
|
|
|
240
|
+
## ViewCube
|
|
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.
|
|
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.
|
|
245
|
+
|
|
246
|
+
### Orientation and feedback
|
|
247
|
+
|
|
248
|
+
The inner view is the principal model view. English ViewCube labels use `Inner` and `Outer` (displayed as `INNER` and `OUTER`); Spanish labels remain `Interior` and `Exterior`. Accessible names and combined edge/corner labels use the same localized terms. Face names use this world-axis convention:
|
|
249
|
+
|
|
250
|
+
| Direction | World axis | Reference |
|
|
251
|
+
| --- | --- | --- |
|
|
252
|
+
| Inner / Front | +Z | Looks toward the model from +Z. |
|
|
253
|
+
| Outer / Back | -Z | Looks toward the model from -Z. |
|
|
254
|
+
| Left | +X | Left relative to the inner view. |
|
|
255
|
+
| Right | -X | Right relative to the inner view. |
|
|
256
|
+
| Top / Superior | +Y | Looks down toward the model. |
|
|
257
|
+
| Bottom / Inferior | -Y | Looks up toward the model. |
|
|
258
|
+
|
|
259
|
+
The cube has sharp edges without chamfers. Edge regions appear as prism strips on both adjacent faces, with boundaries parallel to the edge; corner regions appear as small cubes. All visible surfaces belonging to an edge or corner select the same orientation.
|
|
260
|
+
|
|
261
|
+
Hover uses `styles.colorPrimary`, backed by `PrefViewerStyleVariables.colorPrimary`, with **50% fill opacity** and fully opaque borders around the complete clickable region. Face hover fills only its central clickable area, excluding edge/corner regions. The angularly nearest of the 26 views has **15% primary-color fill** and no highlighted border; hover takes precedence until the pointer leaves. Primary-color and culture changes update the existing overlay. There are no region tooltips; visible face labels use uppercase lettering and increased letter spacing for readability; accessible region names remain localized.
|
|
262
|
+
|
|
263
|
+
Cube perspective follows the active camera's projection and FOV. Mouse movement changes feedback without rebuilding SVG geometry; camera pose/projection changes update the projected geometry. The cube remains independent of model zoom and does not add Babylon scene meshes or appear in exports.
|
|
264
|
+
|
|
265
|
+
### ViewCube layout and lifecycle
|
|
266
|
+
|
|
267
|
+
The cube stays at **top 0px / right 0px**. When both overlays are enabled, the render-menu top equals the **measured cube SVG viewport height** and updates when that size changes. The maximum cube viewport is 100 × 100 CSS pixels and shrinks to fit smaller scene viewports; the menu offset is measured, not fixed to that 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.
|
|
268
|
+
|
|
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 pending transition. Reactivation uses the current camera without reloading assets. Layout subscriptions connect before activation and are removed when inactive or disconnected.
|
|
270
|
+
|
|
271
|
+
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
|
+
|
|
273
|
+
## 3D camera and wheel zoom
|
|
274
|
+
|
|
275
|
+
The default orbital camera starts on the **Inner (+Z)** side before the ViewCube is created, so the loading indicator does not first show Outer and then switch to Inner when model framing completes. The initial fit uses the same model bounds and projection-aware calculation as the ViewCube, with the shared framing factor (`BabylonJSController.config.camera.fitMarginFactor`) of 0.15. The resulting fit matches a cube destination for the same orientation, projection and canvas dimensions; the initial orientation is exact Inner, with no elevation, so selecting Inner immediately after loading preserves the view. The initial fit preserves its orbit angles; later model reloads preserve the user's current orientation. Explicitly selected cameras from the model or environment retain their authored view, which may differ from Inner.
|
|
276
|
+
|
|
277
|
+
The shared internal setting `BabylonJSController.config.camera.fitMarginFactor` accepts finite values from **0 to 0.5, inclusive**. It reserves a total fraction of the viewport along the limiting axis; the model's projected bounding envelope can occupy `1 - fitMarginFactor` of that axis. It is applied when automatic or ViewCube framing runs, rather than continuously changing the current zoom.
|
|
278
|
+
|
|
279
|
+
| fitMarginFactor | Space available for the model | Margin at each opposing edge of the limiting axis |
|
|
280
|
+
| --- | --- | --- |
|
|
281
|
+
| 0 | 100% | 0% |
|
|
282
|
+
| 0.1 | 90% | 5% |
|
|
283
|
+
| 0.15 (default) | 85% | 7.5% |
|
|
284
|
+
| 0.5 | 50% | 25% |
|
|
285
|
+
|
|
286
|
+
Increasing the factor makes the fitted model smaller. The other axis can have more free space because framing preserves aspect ratio. The calculation uses visible model world bounds; the actual silhouette may occupy less space, and near-plane safety can require extra clearance. Invalid values leave the existing camera framing unchanged. This setting replaces the internal `fitFactor` name and its previous semantics; it is not a property of the public viewer `config` object.
|
|
287
|
+
|
|
288
|
+
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.
|
|
289
|
+
|
|
290
|
+
Use the mouse wheel over the 3D canvas to zoom an unlocked orbital camera. 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. This applies to the default and selected unlocked orbital cameras independently of the cube; existing Free/Universal wheel navigation remains unchanged. The internal orbital wheel sensitivity (`wheelDeltaPercentage`) is 0.015. Wheel sensitivity has no public attribute or config option.
|
|
291
|
+
|
|
292
|
+
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.
|
|
293
|
+
|
|
294
|
+
For the default camera with a visible model, the near clipping plane moves closer to the eye as the radius decreases: `minZ` is capped at `max(0.00001, abs(radius) * 0.001)`, retaining any smaller existing positive value. The plane stays in front of the eye; `maxZ`, FOV and projection mode remain unchanged. Imported cameras retain their clipping configuration, and locks/XR/explicit cube transitions preserve their existing policy. Protection samples the updated projection and accounts for near-plane depth and a small safety margin. Perspective samples the center, edge midpoints and corners of the near-plane footprint; orthographic uses the center probe with near-depth clearance. Large inward steps and inertia are constrained at detected surfaces, while outward retreat remains possible. If a checked near-plane point is already inside a closed model mesh, recovery finds its outward triangle-surface exit even when the previous radius is equally short. Empty gaps within mesh bounds are not treated as solids; environment meshes are never queried during recovery. The geometry guard changes radius rather than moving or constraining the target and preserves projection mode; adaptive default-camera near depth is handled separately. Authored minimum-radius constraints remain respected and are restored when releasing a camera. Explicit cube transitions temporarily suspend the geometry floor so the requested model fit can complete; protection resumes afterward.
|
|
295
|
+
|
|
296
|
+
These are sampled ray checks, not continuous volume collision. Small surfaces between probes, open/non-manifold geometry and custom/deformed rigs are not guaranteed to be protected. Interior-point recovery assumes closed meshes. In extreme close-up or grazing-angle views, small parts between the samples may still show minor edge clipping; this is a known limitation of the sampled protection. It does not guarantee clipping-free inspection of every surface. Orthographic protection does not sample the complete near-plane area.
|
|
297
|
+
|
|
204
298
|
## Public methods
|
|
205
299
|
|
|
206
300
|
### Loading and configuration
|
|
207
301
|
|
|
208
302
|
| Method | Description |
|
|
209
303
|
| --- | --- |
|
|
210
|
-
| `loadConfig(config)` | Queues a full configuration update. Accepts an object or JSON string with model, scene, materials, drawing, options, culture, styles, and visibility descriptors. |
|
|
304
|
+
| `loadConfig(config)` | Queues a full configuration update. Accepts an object or JSON string with model, scene, materials, drawing, options, culture, styles, boolean showRenderMenu/showViewCube flags, and visibility descriptors. |
|
|
211
305
|
| `loadModel(model)` | Queues a model update. |
|
|
212
306
|
| `loadScene(scene)` | Queues a scene/environment update. |
|
|
213
307
|
| `loadMaterials(materials)` | Queues a materials update. |
|
|
@@ -265,12 +359,14 @@ Selection colors are rendered through disposable SVG clones; the loaded base SVG
|
|
|
265
359
|
|
|
266
360
|
| Method | Description |
|
|
267
361
|
| --- | --- |
|
|
268
|
-
| `openDialog(title, content, footer)` | Opens a viewer dialog. |
|
|
362
|
+
| `openDialog(title, content, footer)` | Opens a modal above viewer content, including the render menu and ViewCube. Its backdrop intercepts background pointer input. Returns the dialog element on success or `null` if opening fails. |
|
|
269
363
|
| `closeDialog()` | Closes the active viewer dialog. |
|
|
270
364
|
| `downloadModelGLB()` / `downloadModelGLTF()` / `downloadModelUSDZ()` | Exports the current model. |
|
|
271
365
|
| `downloadSceneGLB()` / `downloadSceneGLTF()` / `downloadSceneUSDZ()` | Exports the current scene/environment. |
|
|
272
366
|
| `downloadModelAndSceneGLB()` / `downloadModelAndSceneGLTF()` / `downloadModelAndSceneUSDZ()` | Exports the combined model and scene. |
|
|
273
367
|
|
|
368
|
+
All viewer dialogs share this priority, including the model-download dialog opened with **Ctrl+Alt+D** while the 3D canvas receives keyboard input. Existing backdrop dismissal remains available: a click over a covered control may close the dialog, but does not also activate that control. Closing the dialog restores access to the underlying controls.
|
|
369
|
+
|
|
274
370
|
## Public properties
|
|
275
371
|
|
|
276
372
|
### Setters
|
|
@@ -279,7 +375,7 @@ These setters are convenience bridges for framework bindings and property-based
|
|
|
279
375
|
|
|
280
376
|
| Property | Description |
|
|
281
377
|
| --- | --- |
|
|
282
|
-
| `config` | Forwards to `loadConfig(value)
|
|
378
|
+
| `config` | Forwards an object or JSON string to `loadConfig(value)`, including boolean overlay flags with the same queue ordering and omission rules. |
|
|
283
379
|
| `model` | Forwards to `loadModel(value)`. |
|
|
284
380
|
| `scene` | Forwards to `loadScene(value)`. |
|
|
285
381
|
| `materials` | Forwards to `loadMaterials(value)`. |
|
|
@@ -339,7 +435,7 @@ The component emits custom events so host applications can integrate loading ind
|
|
|
339
435
|
|
|
340
436
|
### Internal Listeners
|
|
341
437
|
|
|
342
|
-
`<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>`. `BabylonJSController` listens to `pref-viewer-3d-animation-changed` from the nearest `<pref-viewer-3d>` host.
|
|
438
|
+
`<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>`. `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 layout dimensions for the render menu and is not a new public root event.
|
|
343
439
|
|
|
344
440
|
Example:
|
|
345
441
|
|
|
@@ -403,6 +499,8 @@ viewer.setStyles({
|
|
|
403
499
|
});
|
|
404
500
|
```
|
|
405
501
|
|
|
502
|
+
The ViewCube uses this same primary color for its hover and active-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.
|
|
503
|
+
|
|
406
504
|
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.
|
|
407
505
|
|
|
408
506
|
## Localization
|
|
@@ -440,7 +538,39 @@ Start the local playground:
|
|
|
440
538
|
npm start
|
|
441
539
|
```
|
|
442
540
|
|
|
443
|
-
The playground is available from `playground/index.html` and serves the project through `http-server` after bundling the local source.
|
|
541
|
+
The playground is available from `playground/index.html` at `http://localhost:8080/` and serves the project through `http-server` after bundling the local source. Use it for manual ViewCube, wheel-zoom and modal checks with representative model/environment assets. The separate ViewCube test HTML/script and its generated fixture bundle have been removed; automated tests remain.
|
|
542
|
+
|
|
543
|
+
### Camera implementation and maintenance
|
|
544
|
+
|
|
545
|
+
- `src/babylonjs-view-cube-controller.js`: dedicated `BabylonJSViewCubeController` for SVG regions, orientation/projection feedback, transitions and cleanup. It delegates model bounds and destination-fit calculations to BabylonJSCameraFraming. Cube-only helpers and constants remain private; region generation remains a public static utility. Its constructor JSDoc documents the host object and callbacks.
|
|
546
|
+
- `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.
|
|
547
|
+
- `src/babylonjs-camera-zoom-controller.js`: independent `BabylonJSCameraZoomController` for model-only dynamic radius protection and authored-floor restoration. It works without cube resources.
|
|
548
|
+
- `src/babylonjs-controller.js`: owns the scene, active camera, containers, native wheel settings and render scheduler; coordinates both controllers.
|
|
549
|
+
- `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.
|
|
550
|
+
- `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.
|
|
551
|
+
|
|
552
|
+
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 visual projection, hover regions and modal stacking.
|
|
553
|
+
|
|
554
|
+
### ViewCube SVG style maintenance
|
|
555
|
+
|
|
556
|
+
The following variables are defined in the root rule of `PrefViewerViewCubeStyles` in `src/styles.js`. They centralize defaults for future stylesheet changes; they are not additional properties supported by `setStyles` or `config.styles`.
|
|
557
|
+
|
|
558
|
+
| CSS variable | Default |
|
|
559
|
+
| --- | --- |
|
|
560
|
+
| `--cube-fill-color` | `#eef0f3` |
|
|
561
|
+
| `--cube-border-color` | `#78838f` |
|
|
562
|
+
| `--cube-border-width` | `1.0px` in the SVG coordinate system; also used by the hover outline |
|
|
563
|
+
| `--cube-hover-color` | Shared primary color; used by the fill and opaque outline |
|
|
564
|
+
| `--cube-hover-opacity` | `0.50` fill opacity |
|
|
565
|
+
| `--cube-active-color` | Shared primary color |
|
|
566
|
+
| `--cube-active-opacity` | `0.15` fill opacity |
|
|
567
|
+
| `--cube-text-font-family` | `Arial, sans-serif` |
|
|
568
|
+
| `--cube-text-font-size` | `9px` |
|
|
569
|
+
| `--cube-text-color` | `#29323b` |
|
|
570
|
+
| `--cube-text-font-weight` | `600` |
|
|
571
|
+
| `--cube-text-letter-spacing` | `1.0px` |
|
|
572
|
+
|
|
573
|
+
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.
|
|
444
574
|
|
|
445
575
|
## Package scripts
|
|
446
576
|
|
package/index.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export interface PrefViewerAsset {
|
|
|
29
29
|
|
|
30
30
|
/** Payload for `loadConfig`. Pass the object; a JSON string is also accepted. */
|
|
31
31
|
export interface PrefViewerConfig {
|
|
32
|
+
/** Sets show-render-menu when supplied; omission preserves the current attribute. */
|
|
33
|
+
showRenderMenu?: boolean;
|
|
34
|
+
/** Sets show-view-cube when supplied; omission preserves the current attribute. */
|
|
35
|
+
showViewCube?: boolean;
|
|
36
|
+
culture?: string;
|
|
37
|
+
styles?: Record<string, unknown> | string;
|
|
32
38
|
model?: PrefViewerAsset;
|
|
33
39
|
scene?: PrefViewerAsset;
|
|
34
40
|
materials?: PrefViewerAsset;
|
|
@@ -250,6 +256,7 @@ export declare class PrefViewer extends HTMLElement {
|
|
|
250
256
|
/** Convenience wrapper for `setOptions({ ibl })`. False when the payload is unusable. */
|
|
251
257
|
setIBL(ibl: Record<string, unknown> | string): boolean | void;
|
|
252
258
|
/** Runtime style overrides; currently `{ colorPrimary }`. False when unusable. */
|
|
259
|
+
/** Updates primary UI and animation-opening highlight color, including an active highlight. */
|
|
253
260
|
setStyles(styles: Record<string, unknown> | string): boolean | void;
|
|
254
261
|
/** 3D runtime diagnostics, or null before the 3D component exists. */
|
|
255
262
|
getDebugInfo(): Record<string, unknown> | null;
|
|
@@ -350,6 +357,8 @@ export interface PrefViewerAttributes {
|
|
|
350
357
|
"show-model"?: boolean | "true" | "false";
|
|
351
358
|
"show-scene"?: boolean | "true" | "false";
|
|
352
359
|
"show-render-menu"?: boolean | "true" | "false";
|
|
360
|
+
/** Enable the orientation cube in 3D (default: true); false releases its overlay resources. */
|
|
361
|
+
"show-view-cube"?: boolean | "true" | "false";
|
|
353
362
|
}
|
|
354
363
|
|
|
355
364
|
/** <pref-viewer mode="3d" style="display:block; width:100%; height:600px" /> */
|
package/package.json
CHANGED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Matrix, Quaternion, Vector3 } from "@babylonjs/core/Maths/math.vector.js";
|
|
2
|
+
import BabylonJSController from "./babylonjs-controller.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Supported camera adapters; the host keeps ownership of their lifecycle.
|
|
6
|
+
* @typedef {import("@babylonjs/core").ArcRotateCamera|import("@babylonjs/core").FreeCamera|import("@babylonjs/core").UniversalCamera} FramingCamera
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Model-only world bounds used for framing.
|
|
11
|
+
* @typedef {Object} CameraFramingBounds
|
|
12
|
+
* @property {Vector3} center - World-space bounding-box center.
|
|
13
|
+
* @property {number} radius - Half the bounding-box diagonal.
|
|
14
|
+
* @property {Vector3[]} corners - Eight world-space bounding-box corners.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Destination framing shared by the camera adapters.
|
|
19
|
+
* @typedef {Object} CameraFramingFit
|
|
20
|
+
* @property {Vector3} target - Model center in world space.
|
|
21
|
+
* @property {number} distance - Destination camera-to-target world distance.
|
|
22
|
+
* @property {Quaternion} rotation - Destination world orientation.
|
|
23
|
+
* @property {number} halfWidth - Orthographic horizontal half-extent.
|
|
24
|
+
* @property {number} halfHeight - Orthographic vertical half-extent.
|
|
25
|
+
* @property {number} radius - Model bounds radius used for clipping adjustments.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* BabylonJSCameraFraming - Calculates model-only world bounds and projection-aware camera fits.
|
|
30
|
+
*
|
|
31
|
+
* Stateless utilities shared by camera navigation controllers. Destination calculations preserve
|
|
32
|
+
* the camera and its projection; callers apply the returned pose and orthographic extents.
|
|
33
|
+
* Reads the shared camera margin at call time and does not own assets or rendering resources.
|
|
34
|
+
* @class
|
|
35
|
+
*/
|
|
36
|
+
export default class BabylonJSCameraFraming {
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Checks that all three vector components are finite numbers.
|
|
40
|
+
* @private
|
|
41
|
+
* @static
|
|
42
|
+
* @param {Vector3|null|undefined} v - Candidate vector.
|
|
43
|
+
* @returns {boolean|null|undefined} True for usable coordinates; otherwise false or the absent input.
|
|
44
|
+
*/
|
|
45
|
+
static #finiteVector(v) {
|
|
46
|
+
return v && [v.x, v.y, v.z].every(Number.isFinite);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Computes model-only world bounds from visible, enabled meshes with valid geometry.
|
|
51
|
+
* Updates mesh world matrices; never includes an environment container implicitly.
|
|
52
|
+
* @public
|
|
53
|
+
* @static
|
|
54
|
+
* @param {import("@babylonjs/core").AssetContainer|null|undefined} container - Model asset container.
|
|
55
|
+
* @returns {CameraFramingBounds|null} Bounds and eight corners, or null for missing/degenerate geometry.
|
|
56
|
+
*/
|
|
57
|
+
static getModelBounds(container) {
|
|
58
|
+
let min = null;
|
|
59
|
+
let max = null;
|
|
60
|
+
for (const mesh of container?.meshes ?? []) {
|
|
61
|
+
if ((mesh?.getTotalVertices?.() ?? 0) <= 0 || mesh.isDisposed?.() || mesh.isEnabled?.() === false || mesh.isVisible === false || mesh.visibility <= 0) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
mesh.computeWorldMatrix(true);
|
|
65
|
+
const box = mesh.getBoundingInfo?.()?.boundingBox;
|
|
66
|
+
if (!BabylonJSCameraFraming.#finiteVector(box?.minimumWorld) || !BabylonJSCameraFraming.#finiteVector(box?.maximumWorld)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
min = min ? Vector3.Minimize(min, box.minimumWorld) : box.minimumWorld.clone();
|
|
70
|
+
max = max ? Vector3.Maximize(max, box.maximumWorld) : box.maximumWorld.clone();
|
|
71
|
+
}
|
|
72
|
+
if (!min || !max) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const radius = Vector3.Distance(min, max) / 2;
|
|
76
|
+
if (!Number.isFinite(radius) || radius <= 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const corners = [];
|
|
80
|
+
for (const x of [min.x, max.x]) {
|
|
81
|
+
for (const y of [min.y, max.y]) {
|
|
82
|
+
for (const z of [min.z, max.z]) {
|
|
83
|
+
corners.push(new Vector3(x, y, z));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { center: Vector3.Center(min, max), radius, corners };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Checks whether the camera has a supported navigation adapter.
|
|
92
|
+
* @public
|
|
93
|
+
* @static
|
|
94
|
+
* @param {import("@babylonjs/core").Camera|null|undefined} camera - Camera to inspect.
|
|
95
|
+
* @returns {boolean} Whether it is an ArcRotateCamera, FreeCamera or UniversalCamera.
|
|
96
|
+
*/
|
|
97
|
+
static isCameraSupported(camera) {
|
|
98
|
+
return ["ArcRotateCamera", "FreeCamera", "UniversalCamera"].includes(camera?.getClassName?.());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds a destination view basis using world +Y as up and the scene handedness.
|
|
103
|
+
* Applies a minimal horizontal offset at the poles to avoid a singular look-at matrix.
|
|
104
|
+
* @private
|
|
105
|
+
* @static
|
|
106
|
+
* @param {Vector3} direction - World-space direction from the target towards the camera.
|
|
107
|
+
* @param {boolean} rightHanded - Whether the scene uses right-handed coordinates.
|
|
108
|
+
* @returns {{view: Matrix, rotation: Quaternion}} View matrix and world orientation.
|
|
109
|
+
*/
|
|
110
|
+
static #viewBasis(direction, rightHanded) {
|
|
111
|
+
// A tiny pole offset avoids a singular look-at while keeping world +Y as camera up.
|
|
112
|
+
const eye = direction.clone();
|
|
113
|
+
if (eye.x * eye.x + eye.z * eye.z < 1e-12) {
|
|
114
|
+
eye.z = 0.0001;
|
|
115
|
+
}
|
|
116
|
+
const view = (rightHanded ? Matrix.LookAtRH : Matrix.LookAtLH)(eye.normalize(), Vector3.Zero(), Vector3.Up());
|
|
117
|
+
const rotation = Quaternion.Identity();
|
|
118
|
+
view.clone().invert().decompose(undefined, rotation);
|
|
119
|
+
return { view, rotation };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Calculates destination framing from model corners without modifying the camera.
|
|
124
|
+
* Uses BabylonJSController.config.camera.fitMarginFactor as the total free viewport fraction.
|
|
125
|
+
* A factor of 0.1 leaves 90% available, reserving 5% at each opposing edge of the limiting axis.
|
|
126
|
+
* Preserves projection, including horizontal-fixed FOV. The factor must be finite and in [0, 0.5].
|
|
127
|
+
* Fits projected world-box corners; minimum near clearance can increase the requested inset.
|
|
128
|
+
* @public
|
|
129
|
+
* @static
|
|
130
|
+
* @param {FramingCamera} camera - Active supported camera.
|
|
131
|
+
* @param {CameraFramingBounds|null} bounds - Model-only world bounds.
|
|
132
|
+
* @param {Vector3} direction - Selected preset direction towards the observer.
|
|
133
|
+
* @param {{width: number, height: number}} viewport - Scene viewport dimensions in pixels.
|
|
134
|
+
* @returns {CameraFramingFit|null} Destination fit, or null when navigation inputs are unusable.
|
|
135
|
+
*/
|
|
136
|
+
static calculateCameraFit(camera, bounds, direction, viewport) {
|
|
137
|
+
if (!bounds || !BabylonJSCameraFraming.isCameraSupported(camera) || !(viewport?.width > 0 && viewport?.height > 0)) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const aspect = viewport.width / viewport.height;
|
|
141
|
+
const basis = BabylonJSCameraFraming.#viewBasis(direction, camera.getScene().useRightHandedSystem);
|
|
142
|
+
// Transform normals: discard the look-at matrix translation for offsets about the model center.
|
|
143
|
+
const points = bounds.corners.map((p) => Vector3.TransformNormal(p.subtract(bounds.center), basis.view));
|
|
144
|
+
const marginFactor = BabylonJSController.config.camera.fitMarginFactor;
|
|
145
|
+
if (!Number.isFinite(marginFactor) || marginFactor < 0 || marginFactor > 0.5) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const available = 1 - marginFactor;
|
|
149
|
+
let distance = camera.mode === 1 ? bounds.radius * 1.01 : 0;
|
|
150
|
+
let halfWidth = 0;
|
|
151
|
+
let halfHeight = 0;
|
|
152
|
+
const tan = Math.tan(camera.fov / 2);
|
|
153
|
+
const tanX = camera.fovMode === 1 ? tan : tan * aspect;
|
|
154
|
+
const tanY = camera.fovMode === 1 ? tan / aspect : tan;
|
|
155
|
+
if (camera.mode !== 1 && (!(tanX > 0) || !(tanY > 0) || !Number.isFinite(tanX + tanY))) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
for (const p of points) {
|
|
159
|
+
halfWidth = Math.max(halfWidth, Math.abs(p.x) / available);
|
|
160
|
+
halfHeight = Math.max(halfHeight, Math.abs(p.y) / available);
|
|
161
|
+
// p.z is positive towards the observer in RH, negative in LH.
|
|
162
|
+
const towardObserver = camera.getScene().useRightHandedSystem ? p.z : -p.z;
|
|
163
|
+
if (camera.mode !== 1) {
|
|
164
|
+
distance = Math.max(distance, towardObserver + Math.max(bounds.radius * 0.001, 0.00001), towardObserver + Math.abs(p.x) / (tanX * available), towardObserver + Math.abs(p.y) / (tanY * available));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
halfHeight = Math.max(halfHeight, halfWidth / aspect, bounds.radius * 0.001);
|
|
168
|
+
halfWidth = halfHeight * aspect;
|
|
169
|
+
return { target: bounds.center.clone(), distance, rotation: basis.rotation, halfWidth, halfHeight, radius: bounds.radius };
|
|
170
|
+
}
|
|
171
|
+
}
|