Skip to content

Core Library

Use the core Library when your application needs complete control over layout, controls, dialogs, localization, and visual design

createCustomizer() returns the stable ProductCustomizerApi contract. It does not render a toolbar, panel, dialog, or status UI, and it does not expose Fabric.js or Three.js instances

Mount two surfaces

The editor and viewer require different DOM elements with stable dimensions

html
<div class="customizer-layout">
  <div id="texture-editor"></div>
  <div id="product-viewer"></div>
</div>
css
.customizer-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  min-height: 640px;
}

#texture-editor,
#product-viewer {
  min-width: 0;
  min-height: 0;
}

Create an instance

ts
import { createCustomizer } from 'customforge'
import 'customforge/style.css'

const customizer = await createCustomizer({
  editor: '#texture-editor',
  viewer: '#product-viewer',
  editorWidth: 1024,
  editorHeight: 512,
  historyLimit: 50,
  product: {
    modelUrl: 'https://cdn.example.com/product.glb',
    textureUrl: 'https://cdn.example.com/base-texture.png',
    surfaceMesh: 'PrintArea',
    textureFlipY: false,
  },
  appearance: {
    editor: {
      selectionBorder: '#0057b8',
      controlBorder: '#0057b8',
      controlSize: 7,
      uvBoundary: '#d92d20',
    },
    viewer: { backgroundColor: '#f4f4f5' },
  },
})

Omit product to use the built-in demonstration cup

The editor dimensions are logical texture dimensions and must match when a saved design is loaded again

Add objects

ts
const text = customizer.addText({
  text: 'Hello world',
  name: 'Headline',
  x: 120,
  y: 180,
  width: 480,
  fontFamily: 'Arial',
  fontSize: 64,
  color: '#172126',
})

const image = await customizer.addImage({
  src: 'https://cdn.example.com/logo.png',
  name: 'Logo',
  x: 720,
  y: 240,
  width: 220,
})

Both methods return serializable object snapshots with stable IDs, so a custom UI can update its own selection or layer state immediately

Text positions use the initial top-left corner, while image positions use the image center

Objects are automatically scaled or moved to remain fully inside the editor canvas

When fontSize is omitted, new text uses 22px. AddTextOptions also accepts fontWeight, fontStyle, underline, textAlign, lineHeight, charSpacing, and backgroundColor

Format existing text

Use the stable object ID to update text content or formatting without reaching into Fabric.js

ts
const [textId] = customizer.getSelectedObjectIds()

if (textId) {
  customizer.updateText(textId, {
    fontSize: 28,
    fontWeight: 'bold',
    fontStyle: 'italic',
    underline: true,
    textAlign: 'center',
    lineHeight: 1.2,
    charSpacing: 40,
    backgroundColor: '#fff2a8',
  })

  customizer.editText(textId)
}

Pass backgroundColor: null to restore a transparent text background. The consumer page must load custom fonts before using or restoring them

Object and layer API

MethodPurpose
getObjects()Return objects from back to front
getSelectedObjectIds()Return stable IDs in the active selection
selectObject(id)Select one visible object
selectObjects(ids)Select several visible objects; an empty array clears the selection
clearSelection()Clear the active selection without changing the design
removeObject(id)Remove one object by ID
moveObject(id, index)Move an object to a zero-based layer index
renameObject(id, name)Change the application-facing layer name
setObjectVisibility(id, visible)Include or exclude an object from rendering
setObjectLocked(id, locked)Enable or prevent canvas transforms
updateObjectTransform(id, options)Update center position, scale, rotation, and flips
updateText(id, options)Update text content and formatting by stable ID
editText(id)Enter on-canvas editing for an unlocked text object
deleteSelected()Delete the active object or selection

These methods are the intended boundary for a custom layer panel

State and geometry

Use snapshots instead of reading Workbench DOM or rendering-library objects

ts
const state = customizer.getState()
const product = customizer.getProduct()
const canvas = customizer.getCanvasSize()
const printableBounds = customizer.getPrintableBounds()

customizer.selectObjects(state.objects.slice(-2).map(({ id }) => id))
const firstObject = state.objects[0]
if (firstObject) {
  customizer.updateObjectTransform(firstObject.id, {
    x: printableBounds.left + printableBounds.width / 2,
    y: printableBounds.top + printableBounds.height / 2,
    rotation: 12,
  })
}

All coordinates use the configured logical canvas, not the CSS display size. Returned state, product, geometry, objects, and view values are independent snapshots

Save and restore designs

ts
const design = customizer.saveDesign()
localStorage.setItem('product-design', JSON.stringify(design))

const saved = localStorage.getItem('product-design')
if (saved) {
  await customizer.loadDesign(JSON.parse(saved))
}

The current schema version is 1

Design JSON contains the logical canvas size, editable objects, and rich text formatting, but excludes the product model, target Mesh, and product base texture

Formatting fields remain optional in schema version 1. Existing documents without them continue to load with the original bold, centered text defaults

Loading is transactional, so an invalid document or failed image load does not replace the current design

Remote image URLs must remain available and CORS-enabled when a saved design is restored

History

ts
if (customizer.canUndo()) {
  await customizer.undo()
}

if (customizer.canRedo()) {
  await customizer.redo()
}

customizer.clearHistory()

Subscribe to history state to update your own controls

ts
const stopHistory = customizer.on(
  'historychange',
  ({ canUndo, canRedo }) => {
    undoButton.disabled = !canUndo
    redoButton.disabled = !canRedo
  },
)

Events

EventPayload purpose
readyProduct model and texture completed loading
changeDesign content or layer state changed
selectionchangeSelected object IDs changed
historychangeUndo or redo availability changed
viewchange3D camera position or target changed
statusA loading or runtime status message changed
errorModel, texture, or image processing failed

Every on() call returns an unsubscribe function

ts
const stopErrors = customizer.on('error', ({ error }) => {
  console.error(error)
})

stopErrors()
stopHistory()

Product and output controls

ts
await customizer.loadProduct({
  modelUrl: 'https://cdn.example.com/another-product.glb',
  surfaceMesh: 'PrintArea',
})

const pngUrl = customizer.getTextureDataUrl()
const pngBlob = await customizer.getTextureBlob()
customizer.exportTexture('product-design.png')

const view = customizer.getViewState()
customizer.setViewState(view)
customizer.resetView()

A successful product change preserves the current design objects and starts a new history baseline

The Data URL, Blob, and downloaded PNG use the logical canvas resolution and omit selection controls and UV helper graphics. Remote images require CORS permission or browser canvas security rules will reject output

API boundary

Use the exported ProductCustomizerApi type for application services and components. The concrete ProductCustomizer class is available, but custom UI code should depend only on the interface and events documented here

The core intentionally does not expose Fabric.js canvas objects, Three.js scenes, or Workbench DOM. This keeps custom controls and styling independent from internal dependency upgrades

Cleanup

ts
customizer.destroy()

Call destroy() exactly once when the instance is no longer needed to release event listeners, Fabric state, observers, and WebGL resources