Skip to content

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

ts
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

ts
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

css
.acme-customizer {
  --cfw-stage: #f7f7f8;
  min-height: 680px;
}

Theme, feature, and layout values can be read or updated after initialization

ts
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

ts
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,
})
PlacementIntended useBehavior
globalActionsProduct-, export-, or workflow-level commandsRendered in the global header action group
editorToolbarCommands that do not require a selectionRendered with the main design tools
selectionToolbarCommands for selected objectsHidden automatically while the selection is empty
layerActionsCompact per-layer commandsRepeated 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

  • label is always used for the accessible name and tooltip, and should come from the host i18n system
  • iconUrl accepts a normal, Data, or Blob URL; the host owns the Blob URL lifetime
  • showLabel defaults to true, except icon-based layerActions default to a compact icon-only button
  • variant accepts primary, secondary, plain, or danger
  • order sorts buttons within one placement in ascending order
  • className adds one or more classes to that button for instance-scoped CSS
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

ts
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

  • labels covers every fixed label, dialog message, placeholder, validation prompt, status, color name, download filename, and accessibility name
  • branding, fontFamilies, textPresets, and assets provide visible names generated from application data
  • formatError converts 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

FeatureControls
addText, addImage, deleteSelectionObject creation and deletion
textFormattingContextual text formatting controls
undoRedoHistory buttons and Workbench keyboard shortcuts
saveDesign, loadDesignDesign JSON actions
loadRemoteProduct, resetViewProduct loading and preview actions
reorderObjects, toggleObjectVisibility, lockObjects, renameObjectsLayer management
presetBackgrounds, presetElementsPreset tabs in the image dialog

Every switch defaults to true

You can update one switch after initialization

ts
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 keyRegion
headerBrand and global product actions
editorHeader2D editor title bar
viewerHeader3D viewer title bar
toolbarDesign command toolbar
layersObject layer panel
statusStatus and object count bar

Every region is visible by default

ts
workbench.setLayout('header', false)
workbench.setLayout('layers', true)

Preset assets

Add application-owned backgrounds and decorative elements to the image dialog

ts
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

ts
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

ts
workbench.customizer.addText({ text: 'Limited edition' })

const design = workbench.customizer.saveDesign()
await workbench.customizer.undo()

You can also display application-specific status feedback

ts
workbench.setStatus('Design synced', 'ready')

Workbench API

createWorkbench() returns CustomForgeWorkbenchApi

Method or propertyPurpose
customizerFull headless ProductCustomizerApi
elementRoot 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