Workbench
The Workbench is the complete CustomForge interface with a design toolbar, layer management, dialogs, status feedback, and a 3D viewer
Use it when you want a ready-to-embed product customizer without building every control yourself
Basic setup
import { createWorkbench } from 'customforge/workbench'
import 'customforge/style.css'
const workbench = await createWorkbench({
container: '#workbench',
product: {
modelUrl: 'https://cdn.example.com/product.glb',
surfaceMesh: 'PrintArea',
},
})The mount container must have an explicit or inherited height
Configure the interface
All configuration groups are optional and unspecified values keep their defaults
const workbench = await createWorkbench({
container: '#workbench',
className: 'acme-customizer',
historyLimit: 50,
features: {
loadRemoteProduct: false,
saveDesign: false,
},
layout: {
editorHeader: false,
viewerHeader: false,
},
branding: {
logoUrl: '/brand/logo.png',
logoAlt: 'Acme',
title: 'Acme Studio',
subtitle: 'Design your product',
},
labels: {
addText: 'Typography',
addImage: 'Artwork',
productDialogTitle: 'Choose a product model',
loadingProduct: 'Loading product...',
},
fontFamilies: [
{ value: 'Arial', label: 'Arial' },
{ value: 'Georgia', label: 'Georgia' },
],
formatError: (error) =>
error instanceof Error ? error.message : 'The operation failed',
theme: {
accent: '#155eef',
accentHover: '#004eeb',
accentContrast: '#ffffff',
controlRadius: '6px',
},
appearance: {
editor: {
controlSize: 7,
objectBorder: '#155eef',
uvBoundary: '#d92d20',
},
viewer: { backgroundColor: '#f4f4f5' },
},
})Styling the default UI
theme controls the supported Workbench design tokens. className adds one or more classes to this instance's root, so application CSS can add scoped layout or component rules without changing another Workbench
.acme-customizer {
--cfw-stage: #f7f7f8;
min-height: 680px;
}Theme, feature, and layout values can be read or updated after initialization
const currentTheme = workbench.getTheme()
workbench.setTheme({ accent: '#0057b8', accentHover: '#003f87' })
const features = workbench.getFeatures()
const layout = workbench.getLayout()appearance configures drawing surfaces that CSS cannot reliably style: Fabric selection controls, UV helper colors, and the WebGL clear color. For complete control over the DOM and component design, use createCustomizer() and ProductCustomizerApi instead of depending on Workbench internals
Extension buttons
Use extensions to place application commands in stable Workbench locations. This keeps integrations on the public API and avoids querying or moving internal DOM nodes
import type { WorkbenchExtensionButton } from 'customforge/workbench'
const actions: WorkbenchExtensionButton[] = [
{
id: 'acme.export-png',
placement: 'globalActions',
label: i18n.t('customizer.exportPng'),
variant: 'primary',
className: 'acme-export-command',
order: 10,
onClick: async ({ customizer, workbench, signal }) => {
const blob = await customizer.getTextureBlob()
if (!signal.aborted) {
workbench.setStatus(`PNG ready (${blob.size} bytes)`)
}
},
},
{
id: 'acme.straighten',
placement: 'selectionToolbar',
label: i18n.t('customizer.straighten'),
visible: ({ selection }) => selection.length === 1,
disabled: ({ selection }) => selection.some(({ locked }) => locked),
onClick: ({ customizer, selection }) => {
selection.forEach(({ id }) => {
customizer.updateObjectTransform(id, { rotation: 0 })
})
},
},
{
id: 'acme.center-layer',
placement: 'layerActions',
label: i18n.t('customizer.centerLayer'),
iconUrl: '/icons/center.svg',
onClick: ({ customizer, layer, state }) => {
if (!layer) return
customizer.updateObjectTransform(layer.id, {
x: state.printableBounds.left + state.printableBounds.width / 2,
y: state.printableBounds.top + state.printableBounds.height / 2,
})
},
},
]
const workbench = await createWorkbench({
container: '#workbench',
extensions: actions,
})| Placement | Intended use | Behavior |
|---|---|---|
globalActions | Product-, export-, or workflow-level commands | Rendered in the global header action group |
editorToolbar | Commands that do not require a selection | Rendered with the main design tools |
selectionToolbar | Commands for selected objects | Hidden automatically while the selection is empty |
layerActions | Compact per-layer commands | Repeated for each layer with that object in layer |
Every state predicate and click handler receives workbench, customizer, the latest state, and the current selection. layerActions also receives the row's layer. Click handlers additionally receive the original event, the button anchor for positioning application popovers, and a signal aborted when the Workbench is destroyed
visible and disabled accept either booleans or predicate functions. They are recomputed automatically after core state, selection, history, and view events. Call refreshExtensions() if a predicate also reads state owned only by the host application
Promise-returning handlers automatically put their button into a disabled loading state. Rejected handlers and predicate errors are passed through formatError and shown in the existing status region. The same extension cannot run twice concurrently; layer actions track pending state independently for each layer
Presentation and localization
labelis always used for the accessible name and tooltip, and should come from the host i18n systemiconUrlaccepts a normal, Data, or Blob URL; the host owns the Blob URL lifetimeshowLabeldefaults totrue, except icon-basedlayerActionsdefault to a compact icon-only buttonvariantacceptsprimary,secondary,plain, ordangerordersorts buttons within one placement in ascending orderclassNameadds one or more classes to that button for instance-scoped CSS
.acme-customizer .acme-export-command {
min-width: 148px;
text-transform: uppercase;
}Keep layer labels short or use an icon-only action because the layer panel is intentionally compact. Custom CSS should be scoped through the Workbench className and extension className, not internal DOM queries
Runtime registration
Extensions can be added and removed without rebuilding the Workbench or losing editor state
const unregister = workbench.registerExtension({
id: 'acme.approve',
placement: 'globalActions',
label: i18n.t('customizer.approve'),
onClick: () => submitApproval(workbench.customizer.saveDesign()),
})
const registered = workbench.getExtensions()
workbench.refreshExtensions()
workbench.removeExtension('acme.approve')
unregister()IDs are unique within one Workbench. The cleanup function removes the extension only while that exact registration is still active, so it is safe to call after an explicit removal
Localization
CustomForge provides default English copy but leaves locale state and switching to the host application
labelscovers every fixed label, dialog message, placeholder, validation prompt, status, color name, download filename, and accessibility namebranding,fontFamilies,textPresets, andassetsprovide visible names generated from application dataformatErrorconverts model, image, UV, and Design JSON failures into user-facing copy
Build these options from the active locale in the application's i18n system. Use the exported WorkbenchLabels type to validate a complete language pack; WorkbenchOptions.labels remains partial for applications that only need to replace a few terms
Feature switches
Feature switches control the built-in Workbench controls, not the underlying workbench.customizer methods
| Feature | Controls |
|---|---|
addText, addImage, deleteSelection | Object creation and deletion |
textFormatting | Contextual text formatting controls |
undoRedo | History buttons and Workbench keyboard shortcuts |
saveDesign, loadDesign | Design JSON actions |
loadRemoteProduct, resetView | Product loading and preview actions |
reorderObjects, toggleObjectVisibility, lockObjects, renameObjects | Layer management |
presetBackgrounds, presetElements | Preset tabs in the image dialog |
Every switch defaults to true
You can update one switch after initialization
workbench.setFeature('addText', false)
workbench.setFeature('loadRemoteProduct', true)The product dialog accepts a self-contained local .glb file or a remote GLB / GLTF URL. Base artwork, printable mesh name, and vertical texture flipping remain available under Advanced options. PNG export is not shown in the built-in header; call workbench.customizer.exportTexture() from an application-owned action when needed
Contextual text formatting
Selecting one or more text objects reveals an Office-style formatting group inside the existing single-row toolbar. It provides font family, size, bold, italic, underline, alignment, text color, highlight, line height, and letter spacing without pushing the canvas down
Mixed values are represented when multiple text objects have different formatting. The edit-text command enters on-canvas editing for an unlocked text object
Layout switches
| Layout key | Region |
|---|---|
header | Brand and global product actions |
editorHeader | 2D editor title bar |
viewerHeader | 3D viewer title bar |
toolbar | Design command toolbar |
layers | Object layer panel |
status | Status and object count bar |
Every region is visible by default
workbench.setLayout('header', false)
workbench.setLayout('layers', true)Preset assets
Add application-owned backgrounds and decorative elements to the image dialog
const workbench = await createWorkbench({
container: '#workbench',
assets: {
backgrounds: [
{
id: 'summer-blue',
name: 'Summer blue',
url: '/assets/backgrounds/summer-blue.png',
},
],
elements: [
{
id: 'brand-mark',
name: 'Brand mark',
url: '/assets/elements/brand-mark.png',
thumbnailUrl: '/assets/thumbnails/brand-mark.webp',
},
],
},
})Backgrounds fill the canvas, replace the previous design background, and stay locked at the bottom layer
Elements are added as normal editable image objects
IDs must be unique inside each asset collection and all remote URLs must satisfy CORS requirements
Custom text presets
const workbench = await createWorkbench({
container: '#workbench',
textPresets: [
{
id: 'brand-display',
name: 'Brand display',
previewText: 'CustomForge',
fontFamily: 'Arial',
fontSize: 22,
width: 220,
color: '#17191c',
},
],
})Passing an empty array hides the preset area while keeping manual text creation available
The consumer page must load any font family used by a preset or restored Design JSON
Use the underlying customizer
The Workbench exposes the stable ProductCustomizerApi through workbench.customizer
workbench.customizer.addText({ text: 'Limited edition' })
const design = workbench.customizer.saveDesign()
await workbench.customizer.undo()You can also display application-specific status feedback
workbench.setStatus('Design synced', 'ready')Workbench API
createWorkbench() returns CustomForgeWorkbenchApi
| Method or property | Purpose |
|---|---|
customizer | Full headless ProductCustomizerApi |
element | Root element for scoped integration and styling |
getFeatures(), setFeature(name, enabled) | Read or update built-in feature controls |
getLayout(), setLayout(name, visible) | Read or update built-in layout regions |
getTheme(), setTheme(values) | Read or merge instance theme tokens |
getExtensions() | Return independent configuration snapshots for registered extension buttons |
registerExtension(extension) | Validate, register, and render one extension button |
removeExtension(id) | Remove an extension button by stable ID |
refreshExtensions() | Recompute extension visibility and disabled predicates |
setStatus(message, mode?) | Set built-in preview status feedback |
destroy() | Release the UI and underlying core resources |
Cleanup
Call workbench.destroy() when the interface is removed
This also destroys the underlying customizer and releases its WebGL and editor resources
