Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 111 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,6 @@ pnpm add @virtualdisplay.io/client
yarn add @virtualdisplay.io/client
```

## What's new in v3.1

- Simplified message handling with unified event system
- JSON Schema validation using AJV
- Cleaner, more maintainable codebase
- Improved TypeScript types
- Better error messages with specific error codes

## Quick start

### Simple product (no options)
Expand Down Expand Up @@ -365,27 +357,46 @@ client.setMapping(product.server3dMapping);
- Let your CMS handle filtering of unavailable combinations
- Version your mappings when 3D models are updated

## Example
## Examples

Check out our example implementations:

See the [color configurator example](./examples/color-configurator/) for a
complete working implementation that demonstrates:
- [Color configurator](./examples/color-configurator/) - Product configurator with attribute mapping
- [Camera controls](./examples/camera-controls/) - Interactive camera positioning demo
- [Snapshot demo](./examples/snapshot-demo/) - Capturing product images programmatically
- [Static model](./examples/static-model/) - Simple viewer without configuration

- Setting up the client
- Defining attribute mappings
- Binding UI controls to attributes
- Real-time state synchronization
Each example demonstrates different aspects of the API with complete working code.

## API reference

### ClientOptions

```typescript
interface ClientOptions {
parent: string | HTMLElement; // Container element or selector
license: string; // Your license key
model: string; // Model ID to load
debug?: boolean; // Enable debug logging (default: false)
language?: string; // Language for UI inside the iframe (default: 'nl', supported: 'nl', 'en', 'de')
parent: string | HTMLElement; // Where to embed the 3D viewer
license: string; // License key from your Virtualdisplay account
model: string; // Which 3D model to load
debug?: boolean; // Shows console logs for troubleshooting
language?: string; // UI language: 'nl', 'en', or 'de' (default: 'nl')

// UI elements (all visible by default)
ui?: {
arEnabled?: boolean;
fullscreenEnabled?: boolean;
loadingIndicatorEnabled?: boolean;
};

// Camera behavior
camera?: {
initialRotate?: number; // Starting view angle (-180 to 180, 0 = front)
initialTilt?: number; // Starting elevation (0 = top view, 90 = side view)
initialZoom?: number; // Starting distance (100 = default, may need adjustment)
minZoom?: number; // Minimum zoom level constraint
maxZoom?: number; // Maximum zoom level constraint
minTilt?: number; // Minimum tilt angle constraint
maxTilt?: number; // Maximum tilt angle constraint
};
}
```

Expand Down Expand Up @@ -423,45 +434,66 @@ if (colorAttr) {
}
```

#### `destroy(): void`
#### UI control methods

Clean up and remove the client connection. Always call this when unmounting your
component.
Control UI elements visibility dynamically through the client:

```typescript
// React example
useEffect(() => {
const client = new VirtualdisplayClient({ ... });
return () => client.destroy();
}, []);
client.viewer.setArEnabled(false);
client.viewer.setFullscreenEnabled(true);
client.viewer.setLoadingIndicatorEnabled(false);
client.viewer.hideAllUI();
client.viewer.showAllUI();
client.viewer.updateUIConfig({ arEnabled: false, fullscreenEnabled: true });
```

#### `camera: Camera`

Control the 3D viewer's camera position programmatically. The camera API provides
a fluent interface for chaining multiple operations.
Control the 3D viewer's camera position programmatically.

```typescript
// Single operation
client.camera.rotate(45).set();

// Chain multiple operations
client.camera.rotate(90).tilt(45).zoom(150).set();

// Reset to default position
client.camera.reset();
```

**Camera methods:**
#### `snapshot.take(filename: string): Photo`

Request a snapshot of the current 3D view. You must provide a filename with a valid image extension
(.png, .jpg, .jpeg, or .webp).

This works perfectly with the camera API - first position the model exactly how you want it, then capture:

```typescript
// Position the model for a perfect product shot
client.camera.rotate(45).tilt(60).zoom(120).set();

// Take a snapshot once positioned
const photo = client.snapshot.take('product-hero-shot.jpg');

// Register callback for when image is ready
photo.onDeveloped((photoData) => {
// photoData.filename - The filename you provided
// photoData.data - Base64 encoded image data

// Use the image
const img = document.createElement('img');
img.src = photoData.data;
document.body.appendChild(img);
});
```

The snapshot captures exactly what the user sees, including selected options and current camera angle.
The image format is determined by the file extension you provide.

#### `destroy(): void`

- `rotate(degrees: number)`: Rotate horizontally (-180° to 180°, 0° = front)
- `tilt(degrees: number)`: Tilt vertically (0° to 180°, 75° = default, 0° = top, 90° = side)
- `zoom(percentage: number)`: Zoom level (25% to 400%, 100% = default)
- `reset()`: Reset to default position (equivalent to `rotate(0).tilt(75).zoom(100).set()`)
- `set()`: Execute all chained commands
Clean up and remove the client connection.

**Note:** Camera commands are accumulated until `set()` is called, allowing efficient
batch operations. The `reset()` method is the only exception - it executes immediately.
```typescript
// When done with the viewer
client.destroy();
```

### Types

Expand Down Expand Up @@ -507,17 +539,50 @@ class Camera {
rotate(degrees: number): Camera; // Rotate horizontally
tilt(degrees: number): Camera; // Tilt vertically
zoom(percentage: number): Camera; // Set zoom level
reset(): void; // Reset to default (executes immediately)
reset(): void; // Reset to base position (executes immediately)
set(): void; // Execute chained commands
}

class Snapshot {
// Methods
take(): Promise<Photo>; // Capture current view
}

interface Photo {
dataUri: string; // Base64 data URI
blob: Blob; // Raw image blob
filename: string; // Suggested filename
}

class VirtualdisplayViewerService {
// Methods
setArEnabled(enabled: boolean): void;
setFullscreenEnabled(enabled: boolean): void;
setLoadingIndicatorEnabled(enabled: boolean): void;
updateUIConfig(config: Partial<UIConfig>): void;
hideAllUI(): void;
showAllUI(): void;
}

interface UIConfig {
arEnabled?: boolean;
fullscreenEnabled?: boolean;
loadingIndicatorEnabled?: boolean;
}
```

#### NodeSelector
#### Node manipulation

The client exports `NodeSelector` for direct node manipulation. This is intended
The client provides methods for direct node access. This is intended
for specialized tools like mapping editors and inspectors, not for typical
product configurators. For product configurators, use the attribute-based API.

```typescript
const node = client.getNode('nodeId'); // Get specific node
const nodes = client.getNodes(); // Get all nodes
const selector = client.getNodeSelector('nodeId'); // Get node selector for manipulation
```

##### Node visibility monitoring

For advanced use cases, you can monitor visibility changes on individual nodes:
Expand Down
10 changes: 4 additions & 6 deletions examples/camera-controls/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -394,13 +394,11 @@ <h4>Command log</h4>
};

window.resetCamera = function () {
// Use set functions to reset to defaults
setRotation(0);
setTilt(75);
setZoom(100);
// Use the proper reset command that resets to the model's base position
client.camera.reset();

// Override logs with single reset message
addLog('Camera reset to default position', 'reset');
// Log the reset
addLog('Camera reset to base position', 'reset');
};

window.animateToPosition = async function () {
Expand Down
1 change: 0 additions & 1 deletion examples/snapshot-demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,6 @@ <h4>Activity log</h4>
parent: '#viewer',
license: 'demo',
model: 'Felt_panel',
debug: true,
});

// UI Elements
Expand Down
9 changes: 5 additions & 4 deletions examples/ui-controls/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,11 @@ <h3>Batch controls</h3>
parent: '#viewer',
model: 'Felt_panel',
license: 'demo',
debug: true,
arEnabled,
fullscreenEnabled,
loadingIndicatorEnabled,
ui: {
arEnabled,
fullscreenEnabled,
loadingIndicatorEnabled,
},
});

// Make client available globally
Expand Down
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,26 @@
"@semantic-release/github": "^11.0.3",
"@semantic-release/npm": "^12.0.2",
"@semantic-release/release-notes-generator": "^14.0.3",
"@types/node": "^24.0.14",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.37.0",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"@virtualdisplay-io/shared-config": "^1.4.0",
"@vitest/coverage-v8": "^3.2.4",
"conventional-changelog-conventionalcommits": "9.0.0",
"conventional-changelog-conventionalcommits": "9.1.0",
"eslint": "^9.31.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-unicorn": "^59.0.1",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-unicorn": "^60.0.0",
"globals": "^16.3.0",
"jsdom": "^26.1.0",
"markdownlint-cli2": "^0.15.0",
"markdownlint-cli2": "^0.18.1",
"markdownlint-cli2-formatter-pretty": "^0.0.8",
"prettier": "^3.6.2",
"semantic-release": "^24.2.7",
"terser": "^5.43.1",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite": "^7.0.5",
"vite-bundle-visualizer": "^1.2.1",
"vite-plugin-dts": "^4.5.4",
"vitest": "^3.2.4"
Expand Down
Loading