@preference-sl/pref-viewer 2.16.2 → 2.17.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 culture data.
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,77 @@ 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 or hides the floating 3D render settings menu. Defaults to visible. |
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
+ Camera selection through `options.camera` uses `null` for the interactive default camera. A named camera such as `"Inner"` is resolved from the model first, then the environment, and is locked by default. If that name is absent from both assets, the viewer activates its default camera with `locked = false` and reconnects canvas controls, even after a previously locked imported camera. A missing name does not retain the previous imported view. Omitting `options.camera` preserves the current selection.
278
+
279
+ 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.
280
+
281
+ | fitMarginFactor | Space available for the model | Margin at each opposing edge of the limiting axis |
282
+ | --- | --- | --- |
283
+ | 0 | 100% | 0% |
284
+ | 0.1 | 90% | 5% |
285
+ | 0.15 (default) | 85% | 7.5% |
286
+ | 0.5 | 50% | 25% |
287
+
288
+ 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.
289
+
290
+ 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
+
292
+ 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.
293
+
294
+ 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
+
296
+ 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.
297
+
298
+ 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.
299
+
204
300
  ## Public methods
205
301
 
206
302
  ### Loading and configuration
207
303
 
208
304
  | Method | Description |
209
305
  | --- | --- |
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. |
306
+ | `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
307
  | `loadModel(model)` | Queues a model update. |
212
308
  | `loadScene(scene)` | Queues a scene/environment update. |
213
309
  | `loadMaterials(materials)` | Queues a materials update. |
@@ -225,19 +321,19 @@ Storage descriptors are passed through `model.storage`, `scene.storage`, `materi
225
321
  | --- | --- |
226
322
  | `showModel()` / `hideModel()` | Shows or hides the loaded model. |
227
323
  | `showScene()` / `hideScene()` | Shows or hides the loaded scene/environment. |
228
- | `syncVisibility(name, isVisible)` | Synchronizes `model` or `scene` visibility with the corresponding reflected attribute. |
229
324
  | `zoomCenter()` | Centers the 2D drawing view. |
230
325
  | `zoomExtentsAll()` | Fits all 2D drawing content in view. |
231
326
  | `zoomIn()` / `zoomOut()` | Controls 2D drawing zoom. |
232
- | `focusModel()` | Moves focus to the current model interaction target when available. |
327
+
328
+ These zoom methods operate only in 2D mode. In 3D, use canvas navigation and the ViewCube to orient and refit the model. Switching modes retains the semantic selection snapshot.
233
329
 
234
330
  ### Semantic selection and turnable state
235
331
 
236
332
  | Method | Description |
237
333
  | --- | --- |
238
- | `setSelectableElements(elements)` | Defines and renders selectable SVG ids in yellow (`#FFFF00`, opacity `0.5`). Accepts strings or objects with `id`/`Id`. |
239
- | `setSelectedElements(elements)` | Defines and renders selected highlights in red (`#FF0000`, opacity `0.5`). Accepts strings or objects with `id`/`Id` and optional `highlights`/`Highlights`. |
240
- | `applySelectionState(state)` | Applies selectable and selected state to the semantic snapshot and 2D drawing in one call. Accepts `selectableIds`/`selectables` and `selectedItems`/`selectedIds`/`selected`; state supplied before a drawing is reapplied after load. |
334
+ | `setSelectableElements(elements)` | Defines and renders selectable 2D/3D ids in yellow (`#FFFF00`, opacity `0.5`). Accepts strings or objects with `id`/`Id`. |
335
+ | `setSelectedElements(elements)` | Defines and renders selected 2D/3D highlights in red (`#FF0000`, opacity `0.5`). Accepts strings or objects with `id`/`Id` and optional `highlights`/`Highlights`. |
336
+ | `applySelectionState(state)` | Applies selectable and selected state to the semantic snapshot and both renderers in one call. Accepts `selectableIds`/`selectables` and `selectedItems`/`selectedIds`/`selected`; state supplied before drawing or model load is reapplied after that renderer loads. |
241
337
  | `clearSelectedElements()` | Clears selected elements while preserving the selectable set. |
242
338
  | `clearSelectableElements()` | Clears selectable elements while preserving the selected set. |
243
339
  | `clearSelectionState()` | Clears all semantic selection state. |
@@ -250,27 +346,39 @@ Storage descriptors are passed through `model.storage`, `scene.storage`, `materi
250
346
  | `toggleTurnable(options)` | Toggles semantic turnable state. |
251
347
  | `getTurnableState()` | Returns `{ running, speed, direction }`. |
252
348
 
253
- Selection colors are rendered through disposable SVG clones; the loaded base SVG and shared definitions are not modified. Selectable clones are inserted before selected clones so red selection remains visible above yellow availability. Reapplying state is idempotent, and replacing the drawing removes dynamic nodes and prior visual layers before the current semantic snapshot is rendered again.
349
+ In 2D, selection colors are rendered through disposable SVG clones; the loaded base SVG and shared definitions are not modified. In 3D, model meshes use Babylon overlays without mutating shared materials; every mesh instance with the semantic id is updated independently. Selected ids and their highlights take red priority over yellow selectable state. Reapplying state is idempotent, and replacing a drawing or model reapplies the current semantic snapshot.
350
+
351
+ 3D model loading disables Babylon's automatic glTF mesh instancing so repeated geometry can have independent overlays while materials remain shared. Environment loading retains instancing. This can increase draw calls for models with many repeated parts. Clearing selection restores the original overlay properties.
352
+
353
+ 3D clicks report eligible crossed ids independently of the selectable set; the host decides which ids to select. Only a final `_primitiveN` suffix is removed, and hits are sorted by distance before duplicate ids are removed.
354
+
355
+ The 3D click threshold defaults to 6 CSS pixels and is defined by the internal `BabylonJSController.config.pointerPicking.clickMaxDistancePx` setting. Movement beyond the threshold at any point cancels the click, including drags returning to their starting point. Cancelled pointers and multi-pointer gestures emit no click. Hidden, disabled, non-pickable meshes, groups and environment geometry are excluded.
356
+
357
+ The turnable methods store and publish semantic state for host integration. They do not start a camera rotation or a render-loop animation. Hosts can read the snapshot through `getTurnableState()` or consume it in `pref-viewer-3d-turnable-changed`.
254
358
 
255
359
  ### Animation controls
256
360
 
257
361
  | Method | Description |
258
362
  | --- | --- |
259
363
  | `getAnimations()` | Returns available model animations with state, progress, loop, and affected node ids. |
260
- | `playAnimation(name, action)` | Controls a named animation. Supported actions are delegated to `BabylonJSAnimationController`. |
261
- | `setAnimationProgress(name, progress)` | Sets animation progress from `0` to `1`. |
262
- | `setAnimationLoop(name, loop)` | Enables or disables looping for a named animation. |
364
+ | `playAnimation(name, action)` | Controls a named opening animation with `open`, `close`, `pause`, `goToOpened` or `goToClosed`. Returns whether the animation and action were accepted. |
365
+ | `setAnimationProgress(name, progress)` | Seeks to normalized progress from `0` to `1` and pauses playback. Returns `false` when the animation is absent. |
366
+ | `setAnimationLoop(name, loop)` | Enables or disables ping-pong looping for a named animation. Returns `false` when the animation is absent. |
367
+
368
+ Opening animations are discovered from paired glTF animation groups named `animation_open_<name>` and `animation_close_<name>`. State values are `0` (paused), `1` (closed), `2` (opened), `3` (opening) and `4` (closing). Enable `options.render.showAnimationMenu` to create the animation controller. Disabling that setting releases the controller: `getAnimations()` returns an empty array and imperative animation actions return `false`. A controller that is enabled can still receive imperative actions while its controls are closed.
263
369
 
264
370
  ### Dialogs and downloads
265
371
 
266
372
  | Method | Description |
267
373
  | --- | --- |
268
- | `openDialog(title, content, footer)` | Opens a viewer dialog. |
374
+ | `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
375
  | `closeDialog()` | Closes the active viewer dialog. |
270
376
  | `downloadModelGLB()` / `downloadModelGLTF()` / `downloadModelUSDZ()` | Exports the current model. |
271
377
  | `downloadSceneGLB()` / `downloadSceneGLTF()` / `downloadSceneUSDZ()` | Exports the current scene/environment. |
272
378
  | `downloadModelAndSceneGLB()` / `downloadModelAndSceneGLTF()` / `downloadModelAndSceneUSDZ()` | Exports the combined model and scene. |
273
379
 
380
+ 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.
381
+
274
382
  ## Public properties
275
383
 
276
384
  ### Setters
@@ -279,7 +387,7 @@ These setters are convenience bridges for framework bindings and property-based
279
387
 
280
388
  | Property | Description |
281
389
  | --- | --- |
282
- | `config` | Forwards to `loadConfig(value)`. |
390
+ | `config` | Forwards an object or JSON string to `loadConfig(value)`, including boolean overlay flags with the same queue ordering and omission rules. |
283
391
  | `model` | Forwards to `loadModel(value)`. |
284
392
  | `scene` | Forwards to `loadScene(value)`. |
285
393
  | `materials` | Forwards to `loadMaterials(value)`. |
@@ -302,18 +410,18 @@ These setters are convenience bridges for framework bindings and property-based
302
410
 
303
411
  ## Events
304
412
 
305
- The component emits custom events so host applications can integrate loading indicators, error states, and viewer controls. Public event payloads do not include `source`. Only root-level events without an explicit 2D/3D scope include `detail.mode`, and that value is always the current `<pref-viewer>` mode (`"2d"` or `"3d"`).
413
+ The component emits custom events so host applications can integrate loading indicators, error states, and viewer controls. Root-dispatched lifecycle, selection-state and viewport events include `detail.timestamp` in Unix milliseconds. Generic root events also include `detail.mode`, which is always the current `<pref-viewer>` mode (`"2d"` or `"3d"`); scoped 2D/3D and legacy `drawing-*`/`scene-*` events omit it. `pref-viewer-element-click` is an exception: its detail remains exactly `{ ids: string[] }`, without mode or timestamp. Renderer events received directly from child components can have different metadata from their root forwarding.
306
414
 
307
415
  ### General Events
308
416
 
309
417
  | Event | Emitted by | Legacy alias | Description |
310
418
  | --- | --- | --- | --- |
311
- | `pref-viewer-initialized` | `<pref-viewer>` | None | The root component has been mounted and initialized. |
419
+ | `pref-viewer-initialized` | `<pref-viewer>` | None | The root component is connected and ready to process queued work. Detail includes `isInitialized`, `mode` and `timestamp`; it does not mean assets have finished loading. |
312
420
  | `pref-viewer-loading` | `<pref-viewer>` | None | A 2D or 3D load operation has started. Includes `detail.mode` with the current root mode. |
313
421
  | `pref-viewer-loaded` | `<pref-viewer>` | None | A 2D or 3D load operation completed. Includes `detail.mode` with the current root mode. |
314
422
  | `pref-viewer-error` | `<pref-viewer>` | None | A 2D or 3D operation failed. Includes `detail.mode` with the current root mode. |
315
- | `pref-viewer-selection-state-changed` | `<pref-viewer>` | None | The semantic selection state changed. |
316
- | `pref-viewer-element-click` | `<pref-viewer>` | None | A semantic SVG element was clicked or forwarded by the host. Geometry without its own id resolves the nearest id-bearing ancestor inside the drawing. `detail` is exactly `{ ids: string[] }`; clicks are emitted independently of the current selectable set. |
423
+ | `pref-viewer-selection-state-changed` | `<pref-viewer>` | None | The semantic selection state changed. Detail includes `selectableIds`, `selectedIds`, `selectedItems`, `mode` and `timestamp`. |
424
+ | `pref-viewer-element-click` | `<pref-viewer>` | None | A semantic 2D or 3D element was clicked or forwarded by the host. In 2D, geometry without its own id resolves the nearest id-bearing ancestor. In 3D, a primary click that moves no more than 6 CSS pixels reports every crossed visible, enabled, pickable model mesh, nearest first, from `mesh.id`; only a final `_primitiveN` suffix is removed and duplicate ids are omitted. Empty/background clicks do not emit. `detail` is exactly `{ ids: string[] }`, independently of the current selectable set. |
317
425
 
318
426
  ### 3D Events
319
427
 
@@ -325,8 +433,8 @@ The component emits custom events so host applications can integrate loading ind
325
433
  | `pref-viewer-3d-options-applying` | `<pref-viewer-3d>` | `scene-setting-options` | 3D viewer options are being applied. |
326
434
  | `pref-viewer-3d-options-applied` | `<pref-viewer-3d>` | `scene-set-options` | 3D viewer options were applied. |
327
435
  | `pref-viewer-3d-render-settings-change-requested` | `<pref-viewer-menu-3d>` | None | The 3D settings menu requested render-setting changes. |
328
- | `pref-viewer-3d-animation-changed` | Opening animation controller | None | A model animation state changed. |
329
- | `pref-viewer-3d-turnable-changed` | `<pref-viewer>` | None | The semantic turnable state changed. |
436
+ | `pref-viewer-3d-animation-changed` | `<pref-viewer-3d>` via its opening animation controller | None | An opening animation state, progress or loop changed. Detail is `{ name, state, progress, loop }`. The event bubbles across the shadow boundary. |
437
+ | `pref-viewer-3d-turnable-changed` | `<pref-viewer>` | None | The semantic turnable state changed. Detail includes `state: { running, speed, direction }` and `timestamp`. |
330
438
 
331
439
  ### 2D Events
332
440
 
@@ -339,7 +447,7 @@ The component emits custom events so host applications can integrate loading ind
339
447
 
340
448
  ### Internal Listeners
341
449
 
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.
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 layout dimensions for the render menu and is not a new public root event.
343
451
 
344
452
  Example:
345
453
 
@@ -389,6 +497,8 @@ viewer.setOptions({
389
497
 
390
498
  Render settings are persisted in `localStorage` under the `pref-viewer/render-settings` key when changed through the runtime settings flow. The menu is always synchronized from the effective controller settings after changes from the menu, `setOptions`, or `loadConfig`.
391
499
 
500
+ 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
+
392
502
  ## Runtime styles
393
503
 
394
504
  Runtime styles can be passed through the `styles` attribute, the `styles` property setter, `setStyles(styles)`, or `loadConfig({ styles })`.
@@ -403,6 +513,8 @@ viewer.setStyles({
403
513
  });
404
514
  ```
405
515
 
516
+ 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.
517
+
406
518
  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
519
 
408
520
  ## Localization
@@ -440,7 +552,53 @@ Start the local playground:
440
552
  npm start
441
553
  ```
442
554
 
443
- The playground is available from `playground/index.html` and serves the project through `http-server` after bundling the local source.
555
+ 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.
556
+
557
+ ### 3D selection implementation and maintenance
558
+
559
+ `src/babylonjs-selection-controller.js` encapsulates semantic picking helpers as public static methods of `BabylonJSSelectionController`: `normalizeMeshId`, `isPrimaryClickGesture`, `isPickableModelMesh`, `getModelMeshSet` and `collectPickedMeshIds`. The Babylon controller and tests call these methods through the class. Instance methods retain selection state and manage reversible overlays. These internal implementation helpers do not change the `<pref-viewer>` public API.
560
+
561
+ JavaScript classes, methods and named helper functions outside tests are documented with JSDoc, including parameters and return values where applicable. This also covers existing helpers and webpack configuration callbacks. Keep those comments and this README aligned with the current implementation. Edited code files use Windows CRLF line endings.
562
+
563
+ Shared calculations live in the static `RenderMath` and `MaterialUtils` classes. `I18n` owns the shared locale and translation subscribers, with private lookup helpers and public static methods. These modules export their classes only; viewer components and tests call their static methods directly. Former named function/constant exports and utility reexports from `BabylonJSController` have been removed. These helpers are internal modules, not additional package-root API exports. `BabylonJSController` encapsulates resize application, metadata path parsing and debug serialization as private methods, passing scene mesh snapshots explicitly to material diagnostics.
564
+
565
+ `BabylonJSAnimationOverlayController` owns one opening-highlight group: its utility scene, cached overlay bases, transformed presentation meshes and observers. It creates clones directly in the utility scene, hides geometry bases used only by instances, and releases only its own resources through an idempotent `dispose()`. These animation overlays remain separate from semantic 3D selection overlays. The temporary legacy storage module and opening-control callbacks retain their existing structure.
566
+
567
+ Focused regressions for this work live in `tests/babylonjs-selection-controller.test.js`, `tests/babylonjs-animation-overlay-controller.test.js`, `tests/encapsulated-utilities.test.js`, `tests/pref-viewer-menu-3d.test.js`, `tests/babylonjs-controller-wheel.test.js` and `tests/pref-viewer.test.js`. They cover picking and repeated model parts, reversible overlays, root state synchronization, animation resource ownership, localization, menu playback and diagnostics. Use `npm test -- --run --maxWorkers=2` for a bounded full-suite run; validate both builds and actual rendering when changing overlays.
568
+
569
+ ### Camera implementation and maintenance
570
+
571
+ - `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.
572
+ - `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
+ - `src/babylonjs-camera-zoom-controller.js`: independent `BabylonJSCameraZoomController` for model-only dynamic radius protection and authored-floor restoration. It works without cube resources.
574
+ - `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
+ - `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
+
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 visual projection, hover regions and modal stacking.
579
+
580
+ ### ViewCube SVG style maintenance
581
+
582
+ 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`.
583
+
584
+ | CSS variable | Default |
585
+ | --- | --- |
586
+ | `--cube-fill-color` | `#eef0f3` |
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.
444
602
 
445
603
  ## Package scripts
446
604
 
@@ -477,11 +635,15 @@ export function ProductViewer() {
477
635
  - The component is a Web Component, so it can be used from plain HTML, React, Vue, Angular, or any framework that supports custom elements.
478
636
  - Give the element an explicit width and height. Babylon.js needs a measurable canvas container.
479
637
  - Use object-based JavaScript APIs for complex payloads. Attributes must be valid JSON strings.
638
+ - Root load/configuration methods enqueue work and return `void` on accepted input, rather than a completion promise. Use the loading/loaded/error events for completion. Malformed JSON strings can throw synchronously.
639
+ - `loadConfig({ forceReload: true })` bypasses 3D asset cache metadata checks. A supplied `timestamp` that differs from the last supplied timestamp also forces the 3D reload; repeating the same value does not. These controls apply to the 3D configuration flow, not SVG cache invalidation.
480
640
  - Remote assets must be reachable by the browser and must satisfy the expected CORS policy.
481
641
  - glTF files with external resources are resolved and cached by the viewer when possible.
482
642
  - Avoid repeatedly replacing large assets if only visibility, camera, lighting, render, or material options changed.
483
643
  - Use `pref-viewer-initialized` only as a component-readiness signal. If a host generates model data asynchronously, wait for the host data-ready signal before calling `loadConfig()`.
484
644
  - For PrefWeb integration, consume the webpack UMD build (`bundle-umd.js`) and prefer development UMD builds while debugging source maps.
645
+ - Babylon's initial PBR BRDF preparation keeps demand rendering awake and waits at most two seconds, so a missing readiness signal does not indefinitely block a queued model load. `getDebugInfo()` provides camera, rendering and asset-readiness diagnostics.
646
+ - Release versioning and npm publication run in Bitbucket Pipelines: `develop` publishes the `beta` tag and `main` publishes `latest`. Do not run the release scripts locally.
485
647
 
486
648
  ## License
487
649
 
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;
@@ -88,6 +94,7 @@ export interface PrefViewerSelectionInput {
88
94
  }
89
95
 
90
96
  export interface PrefViewerElementClickDetail {
97
+ /** Semantic 2D ids, or distance-ordered, deduplicated 3D mesh ids with final _primitiveN removed. */
91
98
  ids: string[];
92
99
  }
93
100
 
@@ -250,6 +257,7 @@ export declare class PrefViewer extends HTMLElement {
250
257
  /** Convenience wrapper for `setOptions({ ibl })`. False when the payload is unusable. */
251
258
  setIBL(ibl: Record<string, unknown> | string): boolean | void;
252
259
  /** Runtime style overrides; currently `{ colorPrimary }`. False when unusable. */
260
+ /** Updates primary UI and animation-opening highlight color, including an active highlight. */
253
261
  setStyles(styles: Record<string, unknown> | string): boolean | void;
254
262
  /** 3D runtime diagnostics, or null before the 3D component exists. */
255
263
  getDebugInfo(): Record<string, unknown> | null;
@@ -264,7 +272,7 @@ export declare class PrefViewer extends HTMLElement {
264
272
  zoomIn(): void;
265
273
  zoomOut(): void;
266
274
 
267
- // ── Semantic selection ──
275
+ // ── Semantic selection synchronized to both 2D and 3D renderers ──
268
276
  setSelectableElements(elements?: PrefViewerElementDescriptor[]): PrefViewerSelectionState;
269
277
  setSelectedElements(elements?: Array<string | PrefViewerSelectionDescriptor>): PrefViewerSelectionState;
270
278
  applySelectionState(state?: PrefViewerSelectionInput): PrefViewerSelectionState;
@@ -350,6 +358,8 @@ export interface PrefViewerAttributes {
350
358
  "show-model"?: boolean | "true" | "false";
351
359
  "show-scene"?: boolean | "true" | "false";
352
360
  "show-render-menu"?: boolean | "true" | "false";
361
+ /** Enable the orientation cube in 3D (default: true); false releases its overlay resources. */
362
+ "show-view-cube"?: boolean | "true" | "false";
353
363
  }
354
364
 
355
365
  /** <pref-viewer mode="3d" style="display:block; width:100%; height:600px" /> */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.16.2",
3
+ "version": "2.17.0-beta.1",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {