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
<div class="customizer-layout">
<div id="texture-editor"></div>
<div id="product-viewer"></div>
</div>.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
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
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
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
| Method | Purpose |
|---|---|
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
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
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
if (customizer.canUndo()) {
await customizer.undo()
}
if (customizer.canRedo()) {
await customizer.redo()
}
customizer.clearHistory()Subscribe to history state to update your own controls
const stopHistory = customizer.on(
'historychange',
({ canUndo, canRedo }) => {
undoButton.disabled = !canUndo
redoButton.disabled = !canRedo
},
)Events
| Event | Payload purpose |
|---|---|
ready | Product model and texture completed loading |
change | Design content or layer state changed |
selectionchange | Selected object IDs changed |
historychange | Undo or redo availability changed |
viewchange | 3D camera position or target changed |
status | A loading or runtime status message changed |
error | Model, texture, or image processing failed |
Every on() call returns an unsubscribe function
const stopErrors = customizer.on('error', ({ error }) => {
console.error(error)
})
stopErrors()
stopHistory()Product and output controls
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
customizer.destroy()Call destroy() exactly once when the instance is no longer needed to release event listeners, Fabric state, observers, and WebGL resources
