printo-engine
v0.1.1
Published
Framework-agnostic Three.js engine for 3D product personalization and printable-surface customization
Maintainers
Readme
printo-engine
Framework-agnostic TypeScript engine for 3D product personalization: it composes a 2D design (images + text) into a texture and applies it to a printable surface of a Three.js GLB product.
This package has no runtime dependencies and no UI framework. Three.js is a peer dependency.
Install
npm install printo-engine threeRequirements: browser support for WebGL, ES modules, Canvas 2D, and ResizeObserver. Types are shipped for both the main entry and the printo-engine/types subpath.
Quick Start
import { createPrinto, type ProductConfig } from 'printo-engine';
const mug: ProductConfig = {
id: 'ceramic-mug',
name: 'Ceramic Coffee Mug',
model: '/models/mug.glb',
defaultBaseColor: '#FFFFFF',
availableColors: [
{ name: 'Ceramic White', hex: '#FFFFFF' },
{ name: 'Matte Charcoal', hex: '#26292E' },
],
material: { roughness: 0.15, metalness: 0.04, clearcoat: 0.4 },
materialOverrides: [
{ materialName: 'Coffee', color: '#3D1E0C', roughness: 0.2, clearcoat: 0.6 },
],
printableSurfaces: [
{
id: 'mug-outer-wrap',
name: 'Full Body Cylindrical Wrap',
strategy: 'uv',
meshName: 'Coffee-Mug',
materialName: 'Logo',
uvBounds: { uMin: 0.09, uMax: 0.91, vMin: 0.14, vMax: 0.77 },
printableWidthMm: 198,
printableHeightMm: 80,
textureResolution: { width: 2048, height: 1024 },
},
],
camera: {
initialPosition: [0, 2, 17],
target: [0, 0.5, 0],
minDistance: 7,
maxDistance: 32,
fov: 42,
},
};
const configurator = createPrinto({
container: document.getElementById('viewer')!,
products: [mug],
initialProductId: 'ceramic-mug',
onLoadingCompleted: () => console.log('ready'),
});
await configurator.init();
// Compose artwork into the design document and push it to the product texture.
await configurator.addImage(
'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><rect width="200" height="200" fill="#111827"/></svg>'),
{ x: 0.5, y: 0.45, scale: 0.6, rotation: 0 },
);
configurator.addText('YOUR BRAND', {
fontFamily: 'Arial, sans-serif',
fontSize: 56,
fontWeight: '700',
color: '#FFFFFF',
x: 0.5,
y: 0.72,
});
configurator.setProductColor('#26292E');
configurator.downloadSnapshot('my-product.png');Use from a CDN (no build step)
printo-engine is ESM-first and can run directly in the browser from a CDN such as jsDelivr using ES module import maps. Three.js and its addon subpaths are remapped to CDN URLs, then the engine is imported as a plain module:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/examples/jsm/controls/OrbitControls.js": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js",
"three/examples/jsm/loaders/GLTFLoader.js": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/GLTFLoader.js"
}
}
</script>
<script type="module">
import { createPrinto } from 'https://cdn.jsdelivr.net/npm/printo-engine/dist/index.js';
// createPrinto(...) and init() as in the Quick Start above.
</script>A complete standalone single-file example — product switcher, base colors, auto-rotate, reset design, snapshot download — is in the repository at example/index.html. It loads its product GLB files from the hosted Printo demo, so it needs no local assets. Serve the page over HTTP (ES modules do not load from file://).
Notes:
- The versionless
…/npm/printo-engine/dist/index.jsURL resolves to thelatestdist-tag. Pin a version (e.g.[email protected]/dist/index.js) for reproducible deployments. - Three.js above is
0.185.1; the engine's peer range is>=0.160.0 <1.0.0. - Types (
printo-engine/types) are TypeScript-only and are consumed vianpm install, not from the CDN page.
How it works
Design document Surface texture Rendered product
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ image elements │ ──▶ │ composited canvas │ ──▶ │ UV material texture │
│ text elements │ │ (textureResolution)│ │ applied via GLB │
│ productColor │ └────────────────────┘ │ materialName/mesh │
└────────────────────┘ └────────────────────┘- The design document is a plain data structure (
{ productColor, elements }) with no Three.js objects. It is the source of truth for the design. - The compositor draws the design onto an offscreen canvas matching each surface's
textureResolution. - The viewer maps that canvas onto the product. The artwork follows the model's real geometry because it is driven by the product's UVs — not by anything overlaid on the rendered frame.
Mapping
Only UV mapping is implemented and shipped.
- A printable surface with
strategy: 'uv'is exactly one texture mapped onto the surface's material. - Optional
uvBoundsclips the artwork to a sub-rectangle of the texture (used for e.g. a wrapped band on a mug). - Without
uvBounds, the surface uses the full texture plane (used for a garment like a T-shirt). - The material is located by
materialNamefirst (preferred), falling back tomeshName.
No projection (DecalGeometry) and no "hybrid" mapping engine are included. The API does not pretend otherwise: the strategy union is 'uv' only.
Design coordinates
Element x/y are normalized texture-plane coordinates (0–1) anchored at the center, not screen pixels.
Note: where a centered
x: 0.5lands on the model depends on how the model was UV-unwrapped (whether the seam sits at the front or the back of the printable material). Verify your art placement on the actual model, or rotate the camera/model so the intended printable face is visible. The engine applies the product camera exactly as configured; it does not auto-frame to the viewport aspect ratio.
Product Configuration
| Field | Description |
|---|---|
| id | Unique identifier |
| name | Display name |
| model | Path to GLB/glTF model (browser-served, CORS-permissive) |
| defaultBaseColor | Hex color of the non-printable, non-overridden materials |
| availableColors | Array of { name, hex } selectable colors |
| material | PBR base: roughness, metalness, optional clearcoat |
| materialOverrides | Per-material { materialName, color?, roughness?, metalness?, clearcoat? } |
| printableSurfaces | Array of UV printable surface definitions |
| camera | initialPosition, target, minDistance, maxDistance, fov |
Printable surface (UVPrintableSurface)
| Field | Description |
|---|---|
| id | Unique surface id |
| name | Display name |
| strategy | 'uv' (always) |
| meshName | Mesh whose material carries the texture |
| materialName | Material name to texture (preferred over meshName) |
| uvBounds? | Optional { uMin, uMax, vMin, vMax } clip region |
| printableWidthMm / printableHeightMm | Physical design area (informational / future print export) |
| textureResolution | { width, height } of the composited texture |
Rendering & materials
- Every mesh material is replaced by a
THREE.MeshPhysicalMaterialbuilt from the product'smaterialproperties while preserving the sourcenormalMap,aoMap, andnormalScale. - The printable surface's material receives the composited canvas texture.
materialOverridesfix specific materials to a given color/PBR profile.- Recoloring via
setProductColorrecolors only the non-printable body materials. The printable texture's background is re-composed in the product color, so embedded artwork is never double-tinted.
API
createPrinto(options)
Creates a PrintoConfigurator. Options: container, products, initialProductId, and optional event callbacks (onProductChanged, onDesignChanged, onLoadingStarted, onLoadingCompleted, onError).
Configurator methods
| Method | Description |
|---|---|
| init() | Load and render the initial product. Await it before adding design content. |
| getCurrentProduct() / getAvailableProducts() / getCurrentDesign() | Read current state. getCurrentDesign returns a deep copy of elements. |
| setProduct(id) | Switch products (discards stale in-flight loads). |
| setProductColor(hex) | Recolor non-printable body materials and re-compose the texture background. |
| addImage(dataUrl, options?) | Add/replace the image element (unshifts it so it renders behind text). |
| updateImage(updates) | Update the image (optionally swap src, which re-preloads before drawing). |
| removeImage() | Remove the image element. |
| addText(text, options?) / updateText(id, options) / removeText(id) | Manage text elements. |
| updateElement(id, updates) / removeElement(id) | Manage any element generically. |
| setArtworkTransform(id, transform) | Apply x/y/scale/rotation. |
| resetDesign() | Clear elements and reset to the product default color. |
| resetCamera() / setAutoRotate(bool) / resize() | Camera helpers. (Auto-resize happens via ResizeObserver.) |
| takeSnapshot() / downloadSnapshot(filename?) | Capture the rendered scene as PNG. |
| dispose() | Dispose resources and listeners. |
Events
configurator.on('productChanged', (e) => e.product);
configurator.on('designChanged', (e) => e.design); // also fires on mutations now
configurator.on('loadingStarted', (e) => e.product);
configurator.on('loadingCompleted', (e) => e.product);
configurator.on('error', (e) => { e.message; e.originalError; });Asset licensing
Models and textures are not bundled in this package. Your GLB assets are yours; select models whose licensing permits your use, and keep an asset manifest with source URLs and license terms.
Live demo
The Printo demo (live at https://fenomen-alex.github.io/printo/) is a static Vite + TypeScript application that uses this engine for mug and T-shirt customization.
