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

smart3d-embed-api

v1.0.7

Published

Embed API SDK for the Smart3D 3D viewer

Readme

smart3d-embed-api

JavaScript / TypeScript SDK for embedding and controlling the Smart3D 3D viewer in your own web application.

The viewer runs in an <iframe>. This SDK wraps the postMessage protocol between your page and the iframe in a clean, fully-typed, promise-based API.

Contents


Installation

npm install smart3d-embed-api

Or via CDN (no bundler required):

<script src="https://unpkg.com/smart3d-embed-api/viewer-embed-sdk.iife.js"></script>
<!-- exposes window.ViewerEmbed -->

Quick start

The model must have Unlisted or Public visibility to be embeddable. Models set to Draft or Private will fire an error event with code not_embeddable. All newly created models default to Unlisted, so embedding works out of the box unless the visibility was changed.

<iframe
  id="viewer"
  src="https://s3d.brand3d.com/e/YOUR_MODEL_ID"
  width="800" height="600"
  allow="fullscreen"
></iframe>
import { ViewerEmbed } from 'smart3d-embed-api';

const iframe = document.getElementById('viewer') as HTMLIFrameElement;
const viewer = new ViewerEmbed(iframe);

// Handle load errors (e.g. model is private)
viewer.onError(({ code, message }) => {
  console.error('Viewer error:', code, message);
  // code: 'not_embeddable' | 'not_found' | 'load_failed'
});

const { modelName, clips } = await viewer.ready();
console.log('Loaded:', modelName, '| clips:', clips);

// fly the camera in
await viewer.camera.flyTo({ posX: 0, posY: 1.5, posZ: 4, tgtX: 0, tgtY: 0.5, tgtZ: 0 });

Constructor

new ViewerEmbed(iframe: HTMLIFrameElement, options?: ViewerEmbedOptions)

| Option | Type | Default | Description | |--------|------|---------|-------------| | targetOrigin | string | '*' | Passed to postMessage. Set to your viewer's exact origin for security (e.g. 'https://app.s3d.brand3d.com'). | | commandTimeout | number | 10000 | Milliseconds before a command that expects a response rejects with a timeout error. |

Call viewer.destroy() when you remove the iframe to clean up message listeners and pending timers.


Lifecycle

viewer.ready(): Promise<ReadyPayload>

Resolves once the scene has finished loading. Always await this before sending commands.

const { modelId, modelName, clips } = await viewer.ready();

| Field | Type | Description | |-------|------|-------------| | modelId | string | Unique ID of the loaded model | | modelName | string | Display name | | clips | string[] | Animation clip names available in the model | | settings | unknown | Full scene settings snapshot |

viewer.destroy(): void

Removes the message event listener and rejects all pending commands. Call this when tearing down the iframe.


Camera

viewer.camera.flyTo(opts)           // smooth animated move — returns Promise<void>
viewer.camera.setPosition(opts)     // instant teleport
viewer.camera.setFov(fov)           // set field of view in degrees
viewer.camera.setBounds(opts)       // constrain orbit angles / zoom
viewer.camera.reset()               // return to default position
viewer.camera.getState()            // returns Promise<CameraStatePayload>

flyTo / setPosition options

{
  posX: number; posY: number; posZ: number;  // camera position
  tgtX: number; tgtY: number; tgtZ: number;  // look-at target
  duration?: number;  // flyTo only — animation duration in ms (default 1200)
}

setBounds options (all optional)

{
  polarMin?: number;    // min vertical angle in radians
  polarMax?: number;    // max vertical angle in radians
  azimuthMin?: number;  // min horizontal angle in radians
  azimuthMax?: number;  // max horizontal angle in radians
  zoomMin?: number;     // min zoom distance
  zoomMax?: number;     // max zoom distance
}

getState response

{ posX, posY, posZ, tgtX, tgtY, tgtZ, fov }

Variants

Switch between pre-configured model variants (materials, colors, configurations). NEW: Programmatically create and manage variants with full control over materials, visibility, transforms, and transitions.

Switching Variants

await viewer.variants.switch('setId', 'variantId')
await viewer.variants.switchByName('Color', 'Red')
await viewer.variants.reset()
const sets   = await viewer.variants.getState()     // active state
const all    = await viewer.variants.getAvailable()  // all sets + variants

Creating Variants ⭐ NEW

// Create a variant set
const { setId } = await viewer.variants.createSet('Paint Colors', 'Pearl White')

// Create a variant in the set
const { variantId } = await viewer.variants.create('Crimson Red', setId, undefined, {
  mode: 'shader',
  durationSec: 0.5,
})

// Apply materials
await viewer.variants.addMaterials(setId, variantId, {
  'body_paint': {
    color: '#cc0000',
    metalness: 0.8,
    roughness: 0.3,
  },
})

// Control visibility
await viewer.variants.addVisibility(setId, variantId, {
  'spoiler': true,
  'bumper_sport': false,
})

// Set transforms (position, rotation, scale)
await viewer.variants.addTransforms(setId, variantId, {
  'doors': {
    rotationDeg: { x: 0, y: 90, z: 0 },
  },
})

// Update variant settings
await viewer.variants.update(setId, variantId, {
  name: 'Crimson Red Sport',
  transitionSettings: { mode: 'snapshot', durationSec: 1.0 },
})

// Capture current scene state as variant
await viewer.variants.captureSnapshot(setId, variantId)

// Delete variant or set
await viewer.variants.delete(setId, variantId)
await viewer.variants.deleteSet(setId)

Material Properties in Variants

When adding materials to a variant, you can use any property from MaterialSettings:

  • Basic: color, emissive, metalness, roughness, opacity, ior, transparent
  • Clearcoat: clearcoat, clearcoatRoughness
  • Transmission: transmission, thickness, attenuationColor, specularIntensity
  • SSS: sssEnabled, sssIntensity, sssThickness, sssDistortion, sssAmbient, sssAttenuation, sssPower, sssScale
  • Surface: normalMap, bumpMap, displacementMap, normalScale, bumpScale
  • Edge Falloff: edgeFalloffEnabled, edgeFalloffColor, edgeFalloffIntensity, edgeFalloffWidth
  • Environment: aoMapIntensity, envMapIntensity

Transition Settings

Control how smoothly variants animate when switching:

interface VariantTransitionSettings {
  mode: 'shader' | 'snapshot';  // GPU fade vs. full state blend
  durationSec: number;           // Animation duration (e.g., 0.3 - 2.0)
}
  • 'shader' — Fast GPU cross-fade (best for color/material changes)
  • 'snapshot' — Full state blend (best for complex reconfigurations)

VariantSetInfo

{
  id:              string;
  name:            string;
  activeVariantId: string;
  variants: Array<{ id: string; name: string }>;
}

Complete Example

// Build a car customizer with color and trim options
async function setupCustomizer() {
  // Create color set
  const { setId: colorSetId } = await viewer.variants.createSet('Paint Colors')
  
  const colors = [
    { name: 'Pearl White', hex: '#ffffff' },
    { name: 'Midnight Black', hex: '#0a0a0a' },
    { name: 'Crimson Red', hex: '#cc0000' },
  ]
  
  for (const { name, hex } of colors) {
    const { variantId } = await viewer.variants.create(name, colorSetId)
    await viewer.variants.addMaterials(colorSetId, variantId, {
      'body': { color: hex },
    })
  }
  
  // Create trim set
  const { setId: trimSetId } = await viewer.variants.createSet('Trims')
  
  const { variantId: sportId } = await viewer.variants.create('Sport', trimSetId)
  await viewer.variants.addVisibility(trimSetId, sportId, {
    'sport_bumper': true,
    'standard_bumper': false,
  })
  await viewer.variants.addMaterials(trimSetId, sportId, {
    'wheels': { color: '#1a1a1a', metalness: 0.95 },
  })
}

// Listen for variant changes
viewer.onVariantChanged(({ setName, variantName }) => {
  console.log(`Switched to ${variantName} in ${setName}`)
  updateUIButtons()
})

See EMBED_API_VARIANTS_GUIDE.md for comprehensive examples and workflows.


Animations

await viewer.animations.play('Walk', { loop: 'loop', speed: 1 })
await viewer.animations.pause('Walk')
await viewer.animations.stop('Walk')
await viewer.animations.setSpeed('Walk', 1.5)
await viewer.animations.setLoopMode('Walk', 'pingpong')

const clips  = await viewer.animations.getClips()   // string[]
const tracks = await viewer.animations.getState()   // AnimationTrackSettings[]

play options

{
  loop?:  'loop' | 'once' | 'pingpong';  // default: 'loop'
  speed?: number;                         // default: 1
}

AnimationTrackSettings

{ name: string; playing: boolean; loop: AnimationLoopMode; speed: number }

Auto-spin

Continuously rotate the model around the Y axis.

viewer.autoSpin.enable({ speed: 30, inactivityDelay: 2000 })
viewer.autoSpin.disable()
viewer.autoSpin.setSpeed(60)   // degrees per second
const state = await viewer.autoSpin.getState()

enable options (all optional)

| Option | Type | Description | |--------|------|-------------| | speed | number | Degrees per second. 0 = no spin. Default: 30 | | inactivityDelay | number | ms of user inactivity before spin resumes after interaction |

getState response

{ enabled: boolean; spinning: boolean; speed: number; inactivityDelay: number }

Hotspots

Control interactive hotspot markers inside the scene.

// Visibility
viewer.hotspots.setVisibility('hotspot-1', false)
viewer.hotspots.setAllVisibility(true)

// UI rendering mode
viewer.hotspots.setUiMode('custom')   // 'native' | 'custom' | 'hidden'

// Programmatic trigger
viewer.hotspots.triggerAction('hotspot-1', 0)

// CRUD (for dynamically created hotspots)
const { id } = await viewer.hotspots.create({
  title: 'Engine',
  description: 'V8 twin-turbo',
  meshName: 'engine_mesh',
  localPosition: { x: 0.1, y: 0.4, z: 0 },
})
viewer.hotspots.update(id, { title: 'Updated title' })
viewer.hotspots.remove(id)
viewer.hotspots.clearCustom()        // remove all programmatically-created hotspots

const all = await viewer.hotspots.getAll()

UI modes

| Mode | Behaviour | |------|-----------| | native | Default glass chip + popup rendered by the viewer | | custom | Anchors are invisible; use hotspot:screenPositions event to render your own UI | | hidden | No UI rendered; screen positions still emit and actions still fire |

create payload (all optional except positioning)

{
  id?:            string;    // auto-generated if omitted
  title?:         string;
  description?:   string;
  meshName?:      string;    // attach to a named mesh
  localPosition?: { x, y, z };
  images?:        string[];  // URLs
  actions?:       HotspotAction[];
  uiMode?:        HotspotUiMode;
}

Post-processing (fx)

Adjust rendering effects at runtime.

viewer.fx.set({ bloomEnabled: true, bloomInt: 0.4, exposure: 1.1 })
viewer.fx.reset()
const state = await viewer.fx.getState()   // full FxSettings object

fx.set accepts any partial subset of FxSettings — only the fields you pass are changed. See the FxSettings type for the full list of available properties covering tone mapping, color grading, SSAO, HBAO, SSGI, SSR, bloom, chromatic aberration, depth of field, vignette, and antialiasing.


Environment

Control the scene's HDRI lighting and background.

viewer.env.set({ rotY: Math.PI / 4, showGrid: false, bgColor: '#1a1a2e' })
viewer.env.reset()
const state = await viewer.env.getState()   // full EnvSettings object

env.set accepts any partial subset of EnvSettings. Notable fields:

| Field | Type | Description | |-------|------|-------------| | rotY | number | HDRI rotation in radians | | envIntensity | number | IBL lighting intensity | | bgIntensity | number | Background brightness | | showHdriBackground | boolean | Show HDRI as background vs solid colour | | bgColor | string | Solid background hex colour (e.g. '#ffffff') | | showGrid | boolean | Ground grid visibility | | shadowMethod | 'pcss' \| 'accumulative' \| 'vsm' | Shadow technique | | fogEnabled | boolean | Scene fog |


Objects

Move, rotate, and scale individual objects in the scene.

viewer.objects.setTransform('wheel_fl', {
  position:    { x: 0, y: 0, z: 0 },
  rotationDeg: { x: 0, y: 45, z: 0 },
  scale:       { x: 1, y: 1, z: 1 },
})
viewer.objects.resetTransform('wheel_fl')
const list = await viewer.objects.getAll()  // { key, name }[]

The key values come from objects.getAll() or the scene:objectsChanged event.

Mesh Visibility

Toggle visibility of individual meshes in the scene.

// Hide a mesh
viewer.objects.setMeshVisibility('wheel_front_left', false)

// Show a mesh
viewer.objects.setMeshVisibility('wheel_front_left', true)

// Check current visibility state
const isVisible = await viewer.objects.getMeshVisibility('wheel_front_left')

Selection Outline (Hidden Mesh Support)

Show selection outlines that are visible even when the mesh is hidden. This is useful for highlighting parts without rendering them.

// Show outline for a mesh (works even if mesh is hidden)
viewer.objects.selectOutline('steering_wheel', true)

// Clear the outline
viewer.objects.selectOutline('steering_wheel', false)

Behavior:

  • When mesh is hidden and outline is selected: Only the outline is visible
  • When mesh is visible and outline is selected: Both mesh and outline are rendered
  • The outline uses the scene's edge-detection post-processing effect

Materials

Modify material properties on individual meshes. Control color, metalness, roughness, subsurface scattering, clearcoat, transmission, and more.

// Set material properties on a mesh
viewer.materials.setByMesh('chassis', {
  color: '#ff0000',
  metalness: 0.8,
  roughness: 0.2,
})

// Get all material properties
const state = await viewer.materials.getByMesh('chassis')

// Reset materials to original/baseline
viewer.materials.resetByMesh('chassis')

Supported Material Properties

| Category | Property | Type | Range | Default | |----------|----------|------|-------|---------| | Basic | color | hex string | '#000000' - '#ffffff' | '#ffffff' | | | emissive | hex string | any hex | '#000000' | | | emissiveIntensity | number | 0 - 2 | 0 | | | metalness | number | 0 - 1 | 0 | | | roughness | number | 0 - 1 | 0.5 | | | opacity | number | 0 - 1 | 1 | | | transparent | boolean | true/false | false | | | ior | number | 1 - 2+ | 1.5 | | | alphaTest | number | 0 - 1 | 0 | | Clearcoat | clearcoat | number | 0 - 1 | 0 | | | clearcoatRoughness | number | 0 - 1 | 0 | | | clearcoatMap | URL or null | texture URL | null | | | clearcoatNormalMap | URL or null | texture URL | null | | Transmission | transmission | number | 0 - 1 | 0 | | | thickness | number | 0 - 1+ | 0.5 | | | attenuationDistance | number | any | 0 | | | attenuationColor | hex string | any hex | '#ffffff' | | | specularIntensity | number | 0 - 2 | 1 | | | specularColor | hex string | any hex | '#ffffff' | | Subsurface Scattering | sssEnabled | boolean | true/false | false | | | sssIntensity | number | 0 - 1+ | 0 | | | sssThickness | number | 0 - 1+ | 0 | | | sssThicknessColor | hex string | any hex | '#ffffff' | | | sssDistortion | number | 0 - 1 | 0.1 | | | sssAmbient | number | 0 - 1 | 0 | | | sssAttenuation | number | 0 - 1 | 0.1 | | | sssPower | number | 1 - 4 | 2 | | | sssScale | number | 5 - 50 | 10 | | Normal / Displacement | normalMap | URL or null | texture URL | null | | | normalScale | {x, y} | 0 - 2 | {x: 1, y: 1} | | | bumpMap | URL or null | texture URL | null | | | bumpScale | number | 0 - 2 | 1 | | | displacementMap | URL or null | texture URL | null | | | displacementScale | number | 0 - 2 | 1 | | | displacementBias | number | -1 - 1 | 0 | | Edge Falloff | edgeFalloffEnabled | boolean | true/false | false | | | edgeFalloffColor | hex string | any hex | '#8fdcff' | | | edgeFalloffIntensity | number | 0 - 2 | 1 | | | edgeFalloffWidth | number | 0 - 1 | 0.24 | | | edgeFalloffPower | number | 1 - 4 | 2.2 | | | edgeFalloffInteriorOpacity | number | 0 - 1 | 0.15 | | Environment | aoMapIntensity | number | 0 - 2 | 1 | | | envMapIntensity | number | 0 - 2 | 1 |

Material Examples

Metallic material:

viewer.materials.setByMesh('part', {
  color: '#c0c0c0',
  metalness: 0.9,
  roughness: 0.1,
})

Glass/Transparent material:

viewer.materials.setByMesh('window', {
  transmission: 0.95,
  thickness: 0.5,
  opacity: 0.9,
  color: '#ffffff',
})

Skin with subsurface scattering:

viewer.materials.setByMesh('face', {
  sssEnabled: true,
  sssIntensity: 0.5,
  sssThickness: 0.3,
  sssThicknessColor: '#f4a460',
  metalness: 0,
  roughness: 0.6,
})

Clearcoat (car paint):

viewer.materials.setByMesh('body', {
  color: '#ff0000',
  metalness: 0.8,
  roughness: 0.3,
  clearcoat: 1.0,
  clearcoatRoughness: 0.05,
})

Finding Mesh Names

Use objects.getAll() to discover available mesh names:

const meshes = await viewer.objects.getAll()
meshes.forEach(mesh => {
  console.log(`Mesh: ${mesh.name} (key: ${mesh.key})`)
})

Material Updates vs Full Reset

Material updates are partial — only properties you specify are changed:

// Only changes color; other properties unchanged
viewer.materials.setByMesh('part', { color: '#ff0000' })

// Reset all materials to baseline
viewer.materials.resetByMesh('part')

Utilities

// Capture a PNG screenshot of the current view
const { dataUrl } = await viewer.screenshot()
// dataUrl is a base64 PNG: "data:image/png;base64,..."

// Get the full scene settings snapshot
const settings = await viewer.getSettings()

// Get the model's latest thumbnail URL
const { thumbnailUrl } = await viewer.getThumbnail()
// thumbnailUrl: string | null — null if no thumbnail has been generated yet

Events

Subscribe to viewer events using on / off / once, or the typed shorthand helpers.

// Generic
viewer.on('camera:moved', (state) => console.log(state))
viewer.once('variant:changed', (e) => console.log(e.variantName))
viewer.off('camera:moved', myHandler)

// Typed shorthands
viewer.onReady(({ modelName }) => console.log('Ready:', modelName))
viewer.onError(({ code, message }) => console.error(code, message))
viewer.onCameraMoved((state) => { /* CameraStatePayload */ })
viewer.onVariantChanged(({ setName, variantName }) => { })
viewer.onHotspotClick(({ hotspotId, screenX, screenY }) => { })
viewer.onHotspotScreenPositions(({ positions }) => { })
viewer.onAutoSpinStarted(({ speed }) => { })
viewer.onAutoSpinPaused(() => { })
viewer.onAnimationStarted(({ clipName }) => { })
viewer.onAnimationEnded(({ clipName }) => { })
viewer.onModelSaved(({ modelId, modelName, savedAt }) => { })
viewer.onModelOptimized(({ modelId, modelName, deliveryUrl, optimizationMetrics }) => { })

Full event reference

| Event | Payload | Description | |-------|---------|-------------| | ready | ReadyPayload | Scene finished loading | | error | { code, message } | Load error (not_embeddable, not_found, load_failed) — not_embeddable fires when the model's visibility is draft or private | | camera:moved | CameraStatePayload | Camera position changed | | camera:interactionStart | — | User began dragging / zooming | | camera:interactionEnd | — | User released | | camera:flyStart | — | Animated fly-to started | | camera:flyEnd | — | Animated fly-to completed | | autospin:started | { speed } | Auto-spin activated | | autospin:paused | — | Spin paused by user interaction | | autospin:resumed | — | Spin resumed after inactivity | | autospin:stopped | — | Auto-spin disabled | | variant:changed | { setId, setName, variantId, variantName } | Active variant switched | | variant:conflictDetected | — | Variant conflict between sets | | animation:started | { clipName } | Clip began playing | | animation:ended | { clipName } | Clip finished | | animation:looped | { clipName } | Clip completed one loop iteration | | animation:paused | { clipName } | Clip paused | | hotspot:click | { hotspotId, title, description, isCustom, screenX, screenY } | Hotspot tapped | | hotspot:enter | { hotspotId, screenX, screenY } | Pointer entered hotspot | | hotspot:leave | { hotspotId, screenX, screenY } | Pointer left hotspot | | hotspot:actionTriggered | { hotspotId, actionIndex, action } | Hotspot action executed | | hotspot:screenPositions | { positions: HotspotScreenPosition[] } | Per-frame screen coords for all hotspots | | scene:objectsChanged | — | Scene objects list changed | | model:saved | { modelId, modelName, savedAt } | Editor saved the model to the server (savedAt is ISO 8601) | | model:optimized | { modelId, modelName, deliveryUrl, optimizationMetrics } | Cloud optimization pipeline completed successfully |

HotspotScreenPosition

{ id: string; screenX: number; screenY: number; behindCamera: boolean }

Use behindCamera to hide hotspot UI elements when the anchor is on the back side of the model.


URL parameters

You can pre-configure the viewer by appending query parameters to the embed src URL — no JavaScript needed.

<iframe src="https://s3d.brand3d.com/e/MODEL_ID?variant=colorSet:red&spin=1&spin.speed=20"></iframe>

| Parameter | Example | Description | |-----------|---------|-------------| | variant | setId:variantId | Activate a specific variant on load | | anim | Walk:play | Start an animation clip (play, pause, stop) | | spin | 1 | Enable auto-spin with defaults | | spin.speed | 30 | Auto-spin speed in degrees/second | | spin.delay | 2000 | Inactivity delay in ms before spin resumes | | camPosX/Y/Z | 0, 2, 5 | Initial camera position | | camTgtX/Y/Z | 0, 0, 0 | Initial camera target | | fx.* | fx.exposure=1.2 | Any FxSettings field | | env.* | env.rotY=1.57 | Any EnvSettings field | | hotspots | custom | Initial hotspot UI mode (native, custom, hidden) | | bg | transparent | Render with a transparent canvas background | | skipIntro | 1 | Skip the initial fly-in animation. Loads straight at the saved final camera pose, or at the camera.* coords if provided. |

Transparent background

Set bg=transparent to composite the 3D model over your page with no background colour. Shadows are still rendered via a shadow-only ground plane that floats naturally over whatever is behind the iframe. TAA and accumulative shadows are automatically disabled in this mode (they require opaque frame accumulation).

<iframe
  src="https://s3d.brand3d.com/e/MODEL_ID?bg=transparent"
  style="background: transparent; border: none;"
  allowtransparency="true"
></iframe>

Note: Set style="background: transparent" on the <iframe> element itself — without it some browsers render a white box around the frame even when the viewer canvas is transparent.


Custom hotspot UI example

Use 'custom' mode to render your own hotspot tooltips positioned in screen space:

viewer.hotspots.setUiMode('custom')

viewer.on('hotspot:screenPositions', ({ positions }) => {
  for (const { id, screenX, screenY, behindCamera } of positions) {
    const el = document.getElementById(`hs-${id}`)
    if (!el) continue
    el.style.display = behindCamera ? 'none' : 'block'
    el.style.left = `${screenX}px`
    el.style.top  = `${screenY}px`
  }
})

viewer.on('hotspot:click', ({ hotspotId, title, description }) => {
  showTooltip(hotspotId, title, description)
})

TypeScript

All methods and event payloads are fully typed. Import types directly if needed:

import type { 
  FxSettings, 
  EnvSettings, 
  ObjectTransformState,
  MaterialSettings,
  MeshVisibilityState,
} from 'smart3d-embed-api/embed/embedPublicTypes';
import type { 
  CameraStatePayload, 
  VariantSetInfo, 
  EmbedEventType,
  MaterialSetByMeshPayload,
  ObjectSetMeshVisibilityPayload,
} from 'smart3d-embed-api/embed/embedProtocol';

Use MaterialSettings type for full type safety when configuring materials:

import type { MaterialSettings } from 'smart3d-embed-api/embed/embedPublicTypes';

const materialConfig: Partial<MaterialSettings> = {
  color: '#ff0000',
  metalness: 0.8,
  roughness: 0.2,
};

viewer.materials.setByMesh('part', materialConfig);

React Examples

Basic setup

import { useEffect, useRef } from 'react'
import { ViewerEmbed } from 'smart3d-embed-api'

export function Viewer3D({ modelId }: { modelId: string }) {
  const iframeRef = useRef<HTMLIFrameElement>(null)

  useEffect(() => {
    if (!iframeRef.current) return
    const viewer = new ViewerEmbed(iframeRef.current, {
      targetOrigin: 'https://s3d.brand3d.com',
    })

    viewer.ready().then(({ modelName }) => {
      console.log('Loaded:', modelName)
      viewer.autoSpin.enable({ speed: 20 })
    })

    return () => viewer.destroy()
  }, [])

  return (
    <iframe
      ref={iframeRef}
      src={`https://s3d.brand3d.com/e/${modelId}`}
      style={{ width: '100%', aspectRatio: '16/9', border: 'none' }}
      allow="fullscreen"
    />
  )
}

Material controls example

import { useEffect, useRef, useState } from 'react'
import { ViewerEmbed } from 'smart3d-embed-api'
import type { MaterialSettings } from 'smart3d-embed-api/embed/embedPublicTypes'

export function MaterialConfigurator({ modelId }: { modelId: string }) {
  const iframeRef = useRef<HTMLIFrameElement>(null)
  const viewerRef = useRef<ViewerEmbed | null>(null)
  const [meshes, setMeshes] = useState<string[]>([])
  const [selectedMesh, setSelectedMesh] = useState<string>('')
  const [color, setColor] = useState<string>('#ffffff')

  useEffect(() => {
    if (!iframeRef.current) return
    const viewer = new ViewerEmbed(iframeRef.current)
    viewerRef.current = viewer

    viewer.ready().then(async () => {
      const allMeshes = await viewer.objects.getAll()
      setMeshes(allMeshes.map(m => m.name))
      if (allMeshes.length > 0) {
        setSelectedMesh(allMeshes[0].name)
      }
    })

    return () => viewer.destroy()
  }, [modelId])

  const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newColor = e.target.value
    setColor(newColor)
    if (viewerRef.current && selectedMesh) {
      viewerRef.current.materials.setByMesh(selectedMesh, { color: newColor })
    }
  }

  const applyPreset = (preset: Partial<MaterialSettings>) => {
    if (viewerRef.current && selectedMesh) {
      viewerRef.current.materials.setByMesh(selectedMesh, preset)
    }
  }

  return (
    <div style={{ display: 'flex', gap: '20px' }}>
      <iframe
        ref={iframeRef}
        src={`https://s3d.brand3d.com/e/${modelId}`}
        style={{ flex: 1, aspectRatio: '16/9', border: 'none' }}
        allow="fullscreen"
      />
      <div style={{ width: '300px', padding: '20px', background: '#f5f5f5' }}>
        <h3>Material Editor</h3>
        <div>
          <label>Mesh:</label>
          <select value={selectedMesh} onChange={e => setSelectedMesh(e.target.value)}>
            {meshes.map(m => <option key={m} value={m}>{m}</option>)}
          </select>
        </div>
        <div>
          <label>Color:</label>
          <input type="color" value={color} onChange={handleColorChange} />
        </div>
        <div style={{ marginTop: '20px' }}>
          <button onClick={() => applyPreset({ metalness: 0.9, roughness: 0.1 })}>
            Metallic
          </button>
          <button onClick={() => applyPreset({ transmission: 0.95, thickness: 0.5 })}>
            Glass
          </button>
          <button onClick={() => applyPreset({ metalness: 0, roughness: 0.8 })}>
            Plastic
          </button>
        </div>
      </div>
    </div>
  )
}

Mesh visibility and selection example

import { useEffect, useRef } from 'react'
import { ViewerEmbed } from 'smart3d-embed-api'

export function PartVisibility({ modelId }: { modelId: string }) {
  const iframeRef = useRef<HTMLIFrameElement>(null)
  const viewerRef = useRef<ViewerEmbed | null>(null)

  useEffect(() => {
    if (!iframeRef.current) return
    const viewer = new ViewerEmbed(iframeRef.current)
    viewerRef.current = viewer
    return () => viewer.destroy()
  }, [modelId])

  const togglePartVisibility = async (partName: string) => {
    if (!viewerRef.current) return
    const isVisible = await viewerRef.current.objects.getMeshVisibility(partName)
    viewerRef.current.objects.setMeshVisibility(partName, !isVisible)
  }

  const highlightPart = (partName: string) => {
    if (!viewerRef.current) return
    viewerRef.current.objects.selectOutline(partName, true)
  }

  const clearHighlight = (partName: string) => {
    if (!viewerRef.current) return
    viewerRef.current.objects.selectOutline(partName, false)
  }

  return (
    <div>
      <iframe
        ref={iframeRef}
        src={`https://s3d.brand3d.com/e/${modelId}`}
        style={{ width: '100%', aspectRatio: '16/9', border: 'none' }}
        allow="fullscreen"
      />
      <div style={{ padding: '20px' }}>
        <button onClick={() => togglePartVisibility('wheel_fl')}>Toggle Front Left Wheel</button>
        <button onClick={() => highlightPart('door')} onMouseLeave={() => clearHighlight('door')}>
          Highlight Door
        </button>
      </div>
    </div>
  )
}