npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@preference-sl/pref-viewer

v2.17.0

Published

Web Component to preview GLTF models with Babylon.js

Readme

@preference-sl/pref-viewer

@preference-sl/pref-viewer is a framework-agnostic Web Component for embedding an interactive 2D/3D product viewer in web applications. It renders glTF/GLB models with Babylon.js, supports SVG drawings, localized UI controls, runtime render settings, asset caching, and export flows for GLB, glTF ZIP, and USDZ.

Features

  • Native custom elements: <pref-viewer>, <pref-viewer-2d>, <pref-viewer-3d>, <pref-viewer-dialog>, and <pref-viewer-menu-3d>.
  • Babylon.js-based 3D visualization for glTF and GLB assets.
  • Default-enabled ViewCube with 26 selectable orientations, synchronized camera feedback, smooth model framing and perspective/orthographic support.
  • Model-only, geometry-dependent orbital zoom protection with near-plane sampling, independent of ViewCube visibility.
  • Viewer dialogs above the render menu and ViewCube, with background pointer-input protection.
  • 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.
  • 2D SVG drawing support with pan and zoom controls.
  • Unified <pref-viewer> wrapper that coordinates 2D, 3D, menu, dialog, localization, and loading lifecycle.
  • Sequential task queue for deterministic config, model, scene, materials, drawing, options, and culture updates.
  • Direct URL, base64/data URI, and IndexedDB-backed asset loading.
  • File cache validation using size and timestamp metadata.
  • Runtime render settings for anti-aliasing, ambient occlusion, IBL, shadows, lighting time of day, and hover highlight.
  • Localized UI copy for en-EN and es-ES.
  • Export helpers for model, scene, or combined model + scene downloads.
  • Type declarations for TypeScript and JSX usage.

Installation

npm install @preference-sl/pref-viewer

Basic usage

Import the package once in your application entry point. The import registers the custom elements globally.

import "@preference-sl/pref-viewer";

Then use the element in HTML:

<pref-viewer
  style="display: block; width: 100%; height: 600px;"
  mode="3d"
  culture="en-EN"
  config='{
    "model": {
      "storage": {
        "url": "https://example.com/assets/model.glb"
      },
      "visible": true
    },
    "scene": {
      "storage": {
        "url": "https://example.com/assets/scene.gltf"
      },
      "visible": true
    }
  }'
></pref-viewer>

JavaScript API usage

You can configure the viewer declaratively with attributes or imperatively with JavaScript. Prefer the JavaScript API when passing large objects, dynamic values, or user-driven updates.

const viewer = document.querySelector("pref-viewer");

viewer.loadConfig({
  model: {
    storage: {
      url: "https://example.com/assets/model.glb",
    },
    visible: true,
  },
  scene: {
    storage: {
      url: "https://example.com/assets/scene.gltf",
    },
    visible: true,
  },
  culture: "en-EN",
});

viewer.setOptions({
  camera: "MainCamera",
  ibl: {
    url: "https://example.com/assets/environment.hdr",
    intensity: 1,
    shadows: true,
  },
  lighting: {
    timeOfDay: 0.5,
  },
});

Configuration model

The main config payload can include model, scene, materials, drawing, options, culture, styles, and boolean showRenderMenu / showViewCube flags.

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.

viewer.loadConfig({
  model: {
    storage: {
      url: "https://example.com/model.glb",
    },
    visible: true,
  },
  scene: {
    storage: {
      url: "https://example.com/scene.gltf",
    },
    visible: true,
  },
  materials: {
    storage: {
      url: "https://example.com/materials.glb",
    },
  },
  drawing: {
    storage: {
      url: "https://example.com/drawing.svg",
    },
  },
  options: {
    camera: "MainCamera",
    ibl: {
      url: "https://example.com/environment.hdr",
      intensity: 1,
      shadows: true,
    },
    render: {
      ambientOcclusionEnabled: true,
      antiAliasingEnabled: true,
      iblEnabled: true,
      shadowsEnabled: false,
      showAnimationMenu: true,
      showLightingTimeOfDay: true,
      lightingTimeOfDay: 0.5,
      highlightEnabled: true,
      highlightColor: "#ff6700",
    },
  },
  styles: {
    colorPrimary: "#ff6700",
  },
  culture: "en-EN",
  showRenderMenu: true,
  showViewCube: true,
  forceReload: false,
});

Overlay visibility

Use boolean values in config and string values in attributes:

viewer.config = {
  showRenderMenu: true,
  showViewCube: false,
};

// Change only the cube flag; the menu keeps its current setting.
viewer.loadConfig({ showViewCube: true });

// Direct attribute control remains available after processing config.
viewer.setAttribute("show-view-cube", "false");
viewer.setAttribute("show-render-menu", "true");
<pref-viewer
  mode="3d"
  config='{"showRenderMenu":true,"showViewCube":false}'
></pref-viewer>

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.

Asset storage descriptors

Storage descriptors are passed through model.storage, scene.storage, materials.storage, or drawing.storage.

Remote URL

{
  storage: {
    url: "https://example.com/assets/model.glb"
  }
}

IndexedDB-backed asset

{
  storage: {
    db: "PrefConfiguratorDB",
    table: "gltfModels",
    id: "product-model-id",
    url: "https://example.com/assets/fallback-model.glb"
  }
}

Base64 or data URI

{
  storage: {
    url: "data:model/gltf-binary;base64,BASE64_CONTENT"
  }
}

Attributes

<pref-viewer> observes the following attributes directly. When they change, the host component queues the corresponding load/configuration action or updates its own UI state.

| Attribute | Description | | --- | --- | | config | JSON string with the full viewer configuration. Forwards to loadConfig(value). | | model | JSON string with a model payload. Forwards to loadModel(value). | | scene | JSON string with a scene/environment payload. Forwards to loadScene(value). | | materials | JSON string with a materials payload. Forwards to loadMaterials(value). | | drawing | JSON string with a 2D drawing payload. Forwards to loadDrawing(value). | | options | JSON string with viewer options. Forwards to setOptions(value). Use this for runtime render changes such as highlight, animation menu, IBL, shadows, and lighting time of day. | | culture | UI locale. Supported values: en-EN, es-ES. Persists the resolved locale in localStorage. | | mode | Viewer mode: 3d or 2d. Defaults to 3d. | | show-model | Shows or hides the current 3D model. Use true or false. | | show-scene | Shows or hides the current 3D scene/environment. Use true or false. | | 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. | | 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. | | 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. |

ViewCube

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.

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.

Orientation and feedback

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:

| Direction | World axis | Reference | | --- | --- | --- | | Inner / Front | +Z | Looks toward the model from +Z. | | Outer / Back | -Z | Looks toward the model from -Z. | | Left | +X | Left relative to the inner view. | | Right | -X | Right relative to the inner view. | | Top / Superior | +Y | Looks down toward the model. | | Bottom / Inferior | -Y | Looks up toward the model. |

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.

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.

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.

ViewCube layout and lifecycle

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.

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.

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.

3D camera and wheel zoom

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.

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.

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.

| fitMarginFactor | Space available for the model | Margin at each opposing edge of the limiting axis | | --- | --- | --- | | 0 | 100% | 0% | | 0.1 | 90% | 5% | | 0.15 (default) | 85% | 7.5% | | 0.5 | 50% | 25% |

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.

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.

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.

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.

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.

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.

Public methods

Loading and configuration

| Method | Description | | --- | --- | | 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. | | loadModel(model) | Queues a model update. | | loadScene(scene) | Queues a scene/environment update. | | loadMaterials(materials) | Queues a materials update. | | loadDrawing(drawing) | Queues a 2D drawing update. | | setOptions(options) | Queues camera, material, IBL, lighting, animation-menu, highlight, and render option updates. | | setStyles(styles) | Queues supported runtime style overrides. Currently supports { colorPrimary }, persists it in localStorage, updates PrefViewerStyleVariables.colorPrimary, and applies --pref-viewer-color-primary. | | setIBL(ibl) | Convenience wrapper for setOptions({ ibl }). | | setCulture(culture) | Changes the active UI locale and persists it in localStorage. | | setMode(mode) | Switches between 2d and 3d. | | getDebugInfo() | Returns a 3D runtime diagnostic snapshot when the internal 3D component and controller are available. |

Visibility and navigation

| Method | Description | | --- | --- | | showModel() / hideModel() | Shows or hides the loaded model. | | showScene() / hideScene() | Shows or hides the loaded scene/environment. | | zoomCenter() | Centers the 2D drawing view. | | zoomExtentsAll() | Fits all 2D drawing content in view. | | zoomIn() / zoomOut() | Controls 2D drawing zoom. |

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.

Semantic selection and turnable state

| Method | Description | | --- | --- | | setSelectableElements(elements) | Defines and renders selectable 2D/3D ids in yellow (#FFFF00, opacity 0.5). Accepts strings or objects with id/Id. | | 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. | | 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. | | clearSelectedElements() | Clears selected elements while preserving the selectable set. | | clearSelectableElements() | Clears selectable elements while preserving the selected set. | | clearSelectionState() | Clears all semantic selection state. | | getSelectionState() | Returns { selectableIds, selectedIds, selectedItems }. | | notifyElementClick(element) | Emits pref-viewer-element-click with exactly { ids: string[] } from a string, descriptor, collection, or { ids } input. This replaces the provisional detail.element payload. | | appendSvgNode(id, svgId, svg) | Adds a host-owned SVG node to the interactive 2D drawing. The node's identified geometry participates in hit-testing and selection. Returns false for a duplicate svgId or absent node. | | removeAppendedSvgNodes() | Removes every SVG node previously added through appendSvgNode and reconciles the current visual selection layers. | | startTurnable(options) | Starts semantic turnable state and emits pref-viewer-3d-turnable-changed. | | stopTurnable() | Stops semantic turnable state. | | toggleTurnable(options) | Toggles semantic turnable state. | | getTurnableState() | Returns { running, speed, direction }. |

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.

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.

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.

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.

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.

Animation controls

| Method | Description | | --- | --- | | getAnimations() | Returns available model animations with state, progress, loop, and affected node ids. | | playAnimation(name, action) | Controls a named opening animation with open, close, pause, goToOpened or goToClosed. Returns whether the animation and action were accepted. | | setAnimationProgress(name, progress) | Seeks to normalized progress from 0 to 1 and pauses playback. Returns false when the animation is absent. | | setAnimationLoop(name, loop) | Enables or disables ping-pong looping for a named animation. Returns false when the animation is absent. |

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.

Dialogs and downloads

| Method | Description | | --- | --- | | 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. | | closeDialog() | Closes the active viewer dialog. | | downloadModelGLB() / downloadModelGLTF() / downloadModelUSDZ() | Exports the current model. | | downloadSceneGLB() / downloadSceneGLTF() / downloadSceneUSDZ() | Exports the current scene/environment. | | downloadModelAndSceneGLB() / downloadModelAndSceneGLTF() / downloadModelAndSceneUSDZ() | Exports the combined model and scene. |

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.

Public properties

Setters

These setters are convenience bridges for framework bindings and property-based integration.

| Property | Description | | --- | --- | | config | Forwards an object or JSON string to loadConfig(value), including boolean overlay flags with the same queue ordering and omission rules. | | model | Forwards to loadModel(value). | | scene | Forwards to loadScene(value). | | materials | Forwards to loadMaterials(value). | | drawing | Forwards to loadDrawing(value). | | options | Forwards to setOptions(value). | | styles | Forwards to setStyles(value). | | culture | Forwards to setCulture(value). | | mode | Forwards to setMode(value). |

Getters

| Property | Description | | --- | --- | | culture | Current resolved locale. | | isInitialized | Whether the root viewer has completed initialization. | | isLoaded | Whether the root viewer considers the current load complete. | | isLoading | Whether a load operation is currently in progress. | | isMode2D | true when the current mode is 2d. | | isMode3D | true when the current mode is 3d. |

Events

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.

General Events

| Event | Emitted by | Legacy alias | Description | | --- | --- | --- | --- | | 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. | | pref-viewer-loading | <pref-viewer> | None | A 2D or 3D load operation has started. Includes detail.mode with the current root mode. | | pref-viewer-loaded | <pref-viewer> | None | A 2D or 3D load operation completed. Includes detail.mode with the current root mode. | | pref-viewer-error | <pref-viewer> | None | A 2D or 3D operation failed. Includes detail.mode with the current root mode. | | pref-viewer-selection-state-changed | <pref-viewer> | None | The semantic selection state changed. Detail includes selectableIds, selectedIds, selectedItems, mode and timestamp. | | 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. |

3D Events

| Event | Emitted by | Legacy alias | Description | | --- | --- | --- | --- | | pref-viewer-3d-loading | <pref-viewer>, <pref-viewer-3d> | scene-loading | A 3D scene/model/material/options operation has started. | | pref-viewer-3d-loaded | <pref-viewer>, <pref-viewer-3d> | scene-loaded | A 3D scene/model/material/options operation completed. Includes operation details when available. | | pref-viewer-3d-error | <pref-viewer> | scene-error | A 3D operation failed. | | pref-viewer-3d-options-applying | <pref-viewer-3d> | scene-setting-options | 3D viewer options are being applied. | | pref-viewer-3d-options-applied | <pref-viewer-3d> | scene-set-options | 3D viewer options were applied. | | pref-viewer-3d-render-settings-change-requested | <pref-viewer-menu-3d> | None | The 3D settings menu requested render-setting changes. | | 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. | | pref-viewer-3d-turnable-changed | <pref-viewer> | None | The semantic turnable state changed. Detail includes state: { running, speed, direction } and timestamp. |

2D Events

| Event | Emitted by | Legacy alias | Description | | --- | --- | --- | --- | | pref-viewer-2d-loading | <pref-viewer>, <pref-viewer-2d> | drawing-loading | A 2D drawing load started. | | pref-viewer-2d-loaded | <pref-viewer>, <pref-viewer-2d> | drawing-loaded | A 2D drawing load completed. | | pref-viewer-2d-error | <pref-viewer>, <pref-viewer-2d> | drawing-error | A 2D drawing operation failed. | | pref-viewer-2d-viewport-changed | <pref-viewer>, <pref-viewer-2d> | None | The 2D drawing pan/zoom viewport changed. detail contains numeric x, y, scale and boolean moved, scaled, maximized, minimized; it does not contain centered or any private desde2d, mode, or source marker. |

Internal Listeners

<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.

Example:

const viewer = document.querySelector("pref-viewer");

viewer.addEventListener("pref-viewer-3d-loaded", (event) => {
  console.log("Viewer 3D loaded", event.detail);
});

viewer.addEventListener("pref-viewer-3d-error", (event) => {
  console.error("Viewer 3D failed", event.detail);
});

Render settings

Render settings can be passed through setOptions, loadConfig({ options: { render } }), property setters, or the built-in 3D menu.

viewer.setOptions({
  render: {
    antiAliasingEnabled: true,
    ambientOcclusionEnabled: true,
    iblEnabled: true,
    shadowsEnabled: false,
    showAnimationMenu: true,
    highlightEnabled: true,
    highlightColor: "#ff6700",
    showLightingTimeOfDay: true,
    lightingTimeOfDay: 0.5,
  },
});

| Setting | Type | Behavior | | --- | --- | --- | | antiAliasingEnabled | boolean | Enables/disables antialiasing. Changes are persisted and can trigger a render-settings reload. | | ambientOcclusionEnabled | boolean | Enables/disables SSAO. Changes are persisted and can trigger a render-settings reload. | | iblEnabled | boolean | Enables/disables image-based lighting. Changes are persisted and can trigger a render-settings reload. | | shadowsEnabled | boolean | Enables/disables shadows. Changes are persisted and can trigger a render-settings reload. | | showAnimationMenu | boolean/"false" | Enables the animation control menu when model animations are available. Applies immediately, persists, and refreshes menu availability. | | highlightEnabled | boolean/"false" | Enables/disables hover highlight for animation openings. Applies immediately and persists. | | highlightColor | string | Hex color used by hover highlight. Applies immediately and persists. | | showLightingTimeOfDay | boolean/"false" | Shows or hides the time-of-day slider. When set to false without an explicit lightingTimeOfDay, the viewer resets lightingTimeOfDay to the default 0.5. | | lightingTimeOfDay | number | Normalized time of day from 0 to 1; 0.5 is the default noon/day value. Applies immediately and persists unless persist: false is used by transient playback. |

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.

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.

Runtime styles

Runtime styles can be passed through the styles attribute, the styles property setter, setStyles(styles), or loadConfig({ styles }).

<pref-viewer styles='{"colorPrimary":"#0055cc"}'></pref-viewer>
viewer.setStyles({
  colorPrimary: "#0055cc",
});

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.

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.

Localization

The default locale is en-EN. The viewer currently includes translations for:

  • en-EN
  • es-ES
<pref-viewer culture="es-ES"></pref-viewer>
viewer.setCulture("en-EN");

Development

Install dependencies:

npm install

Run tests:

npm test

Start the local playground:

npm start

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.

3D selection implementation and maintenance

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.

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.

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.

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.

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.

Camera implementation and maintenance

  • 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.
  • 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.
  • src/babylonjs-camera-zoom-controller.js: independent BabylonJSCameraZoomController for model-only dynamic radius protection and authored-floor restoration. It works without cube resources.
  • src/babylonjs-controller.js: owns the scene, active camera, containers, native wheel settings and render scheduler; coordinates both controllers.
  • 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.
  • 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.

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.

ViewCube SVG style maintenance

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.

| CSS variable | Default | | --- | --- | | --cube-fill-color | #eef0f3 | | --cube-border-color | #78838f | | --cube-border-width | 1.0px in the SVG coordinate system; also used by the hover outline | | --cube-hover-color | Shared primary color; used by the fill and opaque outline | | --cube-hover-opacity | 0.50 fill opacity | | --cube-active-color | Shared primary color | | --cube-active-opacity | 0.15 fill opacity | | --cube-text-font-family | Arial, sans-serif | | --cube-text-font-size | 9px | | --cube-text-color | #29323b | | --cube-text-font-weight | 600 | | --cube-text-letter-spacing | 1.0px |

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.

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.

Package scripts

| Script | Description | | --- | --- | | npm run build | Bundles src/index.js into dist/bundle.js with esbuild. This is the standard ESM playground/runtime build. | | npm run build:prefweb | Creates a production webpack UMD build for PrefWeb consumption. It limits output to a single chunk and emits dist/bundle-umd.js. | | npm run build:prefweb:dev | Creates a development webpack UMD build with external source maps for PrefWeb/browser debugging. Source map module names point back to project source paths. | | npm start | Builds the standard bundle and starts a local HTTP server on port 8080. | | npm test | Runs the Vitest test suite. | | npm run release | Creates a standard-version release. | | npm run release:beta | Creates a beta prerelease with standard-version. |

TypeScript and JSX

The package ships index.d.ts, including JSX support for <pref-viewer>.

export function ProductViewer() {
  return (
    <pref-viewer
      model={JSON.stringify({
        storage: {
          url: "https://example.com/assets/model.glb",
        },
      })}
    />
  );
}

Notes for integrators

  • The component is a Web Component, so it can be used from plain HTML, React, Vue, Angular, or any framework that supports custom elements.
  • Give the element an explicit width and height. Babylon.js needs a measurable canvas container.
  • Use object-based JavaScript APIs for complex payloads. Attributes must be valid JSON strings.
  • 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.
  • 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.
  • Remote assets must be reachable by the browser and must satisfy the expected CORS policy.
  • glTF files with external resources are resolved and cached by the viewer when possible.
  • Avoid repeatedly replacing large assets if only visibility, camera, lighting, render, or material options changed.
  • 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().
  • For PrefWeb integration, consume the webpack UMD build (bundle-umd.js) and prefer development UMD builds while debugging source maps.
  • 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.
  • 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.

License

Proprietary. Copyright (c) 2026 Preference S.L. All rights reserved.

This package is published for authorized Preference S.L. customers and partners. Use, distribution, and access are governed by the applicable commercial agreement with Preference S.L. See LICENSE.md.