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
- Quick start
- Constructor
- Lifecycle
- Camera — flyTo, setPosition, getState
- Variants — switch between pre-configured variants
- Animations — play/pause clips, control speed and looping
- Auto-spin — continuous rotation
- Hotspots — interactive markers
- Post-processing (fx) — bloom, tone mapping, color grading, depth of field, etc.
- Environment — HDRI lighting, background, shadows
- Objects — transform, visibility, materials, selection outline
- Materials — NEW full material control (color, metalness, SSS, clearcoat, transmission, etc.)
- Utilities — screenshot, settings, thumbnail
- Events — subscribe to viewer events
- URL parameters — pre-configure viewer without JavaScript
- TypeScript — type-safe usage
- React examples — basic setup, material controls, visibility
Installation
npm install smart3d-embed-apiOr 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
errorevent with codenot_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 + variantsCreating 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 objectfx.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 objectenv.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 yetEvents
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>
)
}