customforge 0.1.0-alpha.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +79 -24
  2. package/LICENSES/NunitoSans-OFL.txt +93 -0
  3. package/LICENSES/plain-mug-CC-BY-4.0.txt +11 -0
  4. package/README.md +592 -290
  5. package/README.zh-CN.md +589 -290
  6. package/dist/NunitoSans-Variable.ttf +0 -0
  7. package/dist/ProductCustomizer-4ytA68eg.js +2350 -0
  8. package/dist/ProductCustomizer-4ytA68eg.js.map +1 -0
  9. package/dist/bridge/TextureBridge.d.ts +1 -1
  10. package/dist/core/api.d.ts +94 -0
  11. package/dist/core/api.d.ts.map +1 -0
  12. package/dist/core/config.d.ts +3 -12
  13. package/dist/core/config.d.ts.map +1 -1
  14. package/dist/core/design.d.ts.map +1 -1
  15. package/dist/core/types.d.ts +217 -8
  16. package/dist/core/types.d.ts.map +1 -1
  17. package/dist/core/uv.d.ts +8 -0
  18. package/dist/core/uv.d.ts.map +1 -0
  19. package/dist/cup_decal_small_margins.glb +0 -0
  20. package/dist/customizer/ProductCustomizer.d.ts +174 -7
  21. package/dist/customizer/ProductCustomizer.d.ts.map +1 -1
  22. package/dist/editor/DesignEditor.d.ts +224 -14
  23. package/dist/editor/DesignEditor.d.ts.map +1 -1
  24. package/dist/editor/DesignHistory.d.ts +27 -0
  25. package/dist/editor/DesignHistory.d.ts.map +1 -0
  26. package/dist/editor/uvBounds.d.ts +15 -0
  27. package/dist/editor/uvBounds.d.ts.map +1 -0
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -1108
  31. package/dist/index.js.map +1 -1
  32. package/dist/style.css +2123 -11
  33. package/dist/viewer/ProductViewer.d.ts +37 -6
  34. package/dist/viewer/ProductViewer.d.ts.map +1 -1
  35. package/dist/viewer/uvLayout.d.ts +11 -0
  36. package/dist/viewer/uvLayout.d.ts.map +1 -0
  37. package/dist/workbench/CustomForgeWorkbench.d.ts +205 -0
  38. package/dist/workbench/CustomForgeWorkbench.d.ts.map +1 -0
  39. package/dist/workbench/assets/catalog.d.ts +8 -0
  40. package/dist/workbench/assets/catalog.d.ts.map +1 -0
  41. package/dist/workbench/config.d.ts +96 -0
  42. package/dist/workbench/config.d.ts.map +1 -0
  43. package/dist/workbench/icons.d.ts +4 -0
  44. package/dist/workbench/icons.d.ts.map +1 -0
  45. package/dist/workbench/index.d.ts +25 -0
  46. package/dist/workbench/index.d.ts.map +1 -0
  47. package/dist/workbench/template.d.ts +3 -0
  48. package/dist/workbench/template.d.ts.map +1 -0
  49. package/dist/workbench/types.d.ts +542 -0
  50. package/dist/workbench/types.d.ts.map +1 -0
  51. package/dist/workbench.js +3052 -0
  52. package/dist/workbench.js.map +1 -0
  53. package/package.json +77 -71
package/README.md CHANGED
@@ -1,64 +1,79 @@
1
- <div align="center">
2
-
3
- # CustomForge
4
-
5
- **Turn a 2D texture canvas into a live 3D product preview**
6
-
7
- A lightweight, framework-agnostic prototype for building browser-based product customization experiences with Fabric.js and Three.js
8
-
9
- <p>
10
- <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white">
11
- <img alt="Vite" src="https://img.shields.io/badge/Vite-8-646CFF?logo=vite&logoColor=white">
12
- <img alt="Three.js" src="https://img.shields.io/badge/Three.js-r185-111111?logo=threedotjs&logoColor=white">
13
- <img alt="Fabric.js" src="https://img.shields.io/badge/Fabric.js-7-DB4D6D">
14
- <img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-2F6F9F">
15
- </p>
16
-
17
- <p>
18
- <strong>English</strong> |
19
- <a href="./README.zh-CN.md">中文简体</a>
20
- </p>
21
-
22
- </div>
23
-
24
- ---
25
-
26
- ## What It Does
27
-
28
- CustomForge connects a familiar 2D design surface to a UV-mapped 3D model:
29
-
30
- - Add and edit text on a 2D texture canvas
31
- - Upload, move, scale, and rotate images
32
- - Preview every canvas change on a 3D product in real time
33
- - Load remote GLB/GLTF models and optional base textures
34
- - Select the customizable surface by mesh name
35
- - Rotate and zoom the 3D preview with OrbitControls
36
- - Export the composed texture as a PNG
37
- - Save and restore editable objects through versioned Design JSON
38
-
39
- The included workbench starts with a procedural cup, so the project works immediately without external assets
40
-
41
- ## Repository Development
42
-
43
- Requirements: Node.js 22+ and pnpm 11+
44
-
45
- ```bash
46
- pnpm install
47
- pnpm dev
48
- ```
49
-
50
- Open the URL printed by Vite
51
-
52
- The built-in demo appears with a UV workspace on the left and a live 3D preview on the right
53
-
54
- ## npm Alpha Package
55
-
56
- CustomForge is available from the npm Registry under the `alpha` dist-tag and the API and Design JSON Schema may change between alpha versions
57
-
58
- Install the current public alpha with:
1
+ <div align="center">
2
+
3
+ <img src="./src/workbench/assets/CustomForgeLogo.png" alt="CustomForge Logo" width="136">
4
+
5
+ <h1>CustomForge</h1>
6
+
7
+ <hr>
8
+
9
+ <p><strong>Turn a 2D texture canvas into a live 3D product preview</strong></p>
10
+
11
+ <p>A lightweight, framework-agnostic toolkit for building browser-based product customization experiences with Fabric.js and Three.js</p>
12
+
13
+ <p>
14
+ <strong>English</strong>
15
+ <span> · </span>
16
+ <a href="./README.zh-CN.md">简体中文</a>
17
+ </p>
18
+
19
+ <p>
20
+ <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-strict-2563EB?style=flat-square&logo=typescript&logoColor=white&labelColor=1F2937">
21
+ <img alt="Vite" src="https://img.shields.io/badge/Vite-8-7C3AED?style=flat-square&logo=vite&logoColor=white&labelColor=1F2937">
22
+ <img alt="Three.js" src="https://img.shields.io/badge/Three.js-r185-27272A?style=flat-square&logo=threedotjs&logoColor=white&labelColor=1F2937">
23
+ <img alt="Fabric.js" src="https://img.shields.io/badge/Fabric.js-7-BE185D?style=flat-square&labelColor=1F2937">
24
+ <img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-0F766E?style=flat-square&logo=apache&logoColor=white&labelColor=1F2937">
25
+ </p>
26
+
27
+ </div>
28
+
29
+ ---
30
+
31
+ ## Demo
32
+
33
+ ![CustomForge product customization workflow](./.github/assets/demo.gif)
34
+
35
+ ## What It Does
36
+
37
+ CustomForge connects a familiar 2D design surface to a UV-mapped 3D model:
38
+
39
+ - Add and edit text on a 2D texture canvas
40
+ - Format selected text through an Office-style contextual toolbar
41
+ - Upload, move, scale, and rotate images
42
+ - Preview every canvas change on a 3D product in real time
43
+ - Load remote GLB/GLTF models and optional base textures
44
+ - Select the customizable surface by mesh name
45
+ - Display the target mesh UV printable region and boundary as a non-exported editor guide
46
+ - Rotate and zoom the 3D preview with OrbitControls
47
+ - Export the composed texture as a PNG
48
+ - Save and restore editable objects through versioned Design JSON
49
+ - Organize objects through names, visibility, locking, and layer order
50
+ - Undo and redo design changes through snapshot history
51
+ - Use configurable typography, background, and decorative asset Dialogs
52
+ - Adapt the default Workbench with branding, labels, theme tokens, and icons
53
+
54
+ The included workbench starts with the bundled `cup_decal_small_margins.glb`, so the project works immediately without fetching an external model
55
+
56
+ ## Repository Development
57
+
58
+ Requirements: Node.js 22+ and pnpm 11+
59
+
60
+ ```bash
61
+ pnpm install
62
+ pnpm dev
63
+ ```
64
+
65
+ Open the URL printed by Vite
66
+
67
+ The built-in demo appears with a UV workspace on the left and a live 3D preview on the right
68
+
69
+ ## npm Package
70
+
71
+ CustomForge `0.1.0` is the first non-prerelease version published to the npm Registry. Public APIs follow semantic versioning, and Design JSON version 1 documents remain readable throughout the `0.1.x` line
72
+
73
+ Install the current release with:
59
74
 
60
75
  ```powershell
61
- pnpm add customforge@alpha
76
+ pnpm add customforge
62
77
  ```
63
78
 
64
79
  Fabric.js and Three.js are installed automatically as transitive dependencies
@@ -76,240 +91,527 @@ Consumer projects can explicitly acknowledge this browser-only choice in `packag
76
91
  }
77
92
  }
78
93
  ```
79
-
80
- Package page: [npmjs.com/package/customforge](https://www.npmjs.com/package/customforge)
81
-
82
- For repository-independent verification before publishing, build and create the local package from the repository root:
83
-
84
- ```powershell
85
- pnpm pack:local
86
- ```
87
-
88
- The command builds JavaScript, TypeScript declarations, and core styles, checks the npm file list, and creates:
89
-
90
- ```text
91
- customforge-0.1.0-alpha.1.tgz
94
+
95
+ Package page: [npmjs.com/package/customforge](https://www.npmjs.com/package/customforge)
96
+
97
+ Browser baseline: Chrome and Edge 111+, Firefox 113+, and Safari 16.4+. CustomForge requires ES2022 modules, Canvas 2D, WebGL, `ResizeObserver`, native `dialog`, container queries, and `color-mix()` support
98
+
99
+ For repository-independent verification before publishing, build and create the local package from the repository root:
100
+
101
+ ```powershell
102
+ pnpm pack:local
103
+ ```
104
+
105
+ The command builds JavaScript, TypeScript declarations, and public styles, checks the npm file list, and creates:
106
+
107
+ ```text
108
+ customforge-0.1.0.tgz
92
109
  ```
93
110
 
94
111
  Install that local package in an independent Vite TypeScript project:
95
112
 
96
113
  ```powershell
97
- pnpm add D:\projects\3DRendering\core_code\customforge-0.1.0-alpha.1.tgz
114
+ pnpm add D:\projects\3DRendering\core_code\customforge-0.1.0.tgz
115
+ ```
116
+
117
+ The checked-in `examples/npm-consumer` project imports CustomForge only through this `.tgz`. Run `pnpm install --ignore-workspace` in that directory so pnpm installs it independently from the parent workspace
118
+
119
+ ## Load Your Product
120
+
121
+ Choose **Load product** in the demo, then upload a self-contained `.glb` file or
122
+ provide a remote GLB / GLTF URL. Local `.gltf` files are not accepted because
123
+ they can depend on separate binary and texture files
124
+
125
+ The following fields are available under **Advanced options**:
126
+
127
+ | Setting | Purpose |
128
+ | --- | --- |
129
+ | Base artwork URL | Optional image placed underneath editable objects and included in the composed texture |
130
+ | Printable mesh name | Name of the mesh that receives the live canvas texture; defaults to `PrintArea` |
131
+ | Flip texture vertically | Enable only when the design appears vertically inverted on a model |
132
+
133
+ UV coordinates are expected to be stored in the 3D model
134
+
135
+ The optional texture image is the visual base layer, not a replacement for model UV data. Without one, the design canvas and printable overlay remain transparent
136
+
137
+ After each product load, the editor reads the target mesh UV coordinates and displays a subtle printable-region fill with its outer boundary above the design canvas. Internal triangulation stays hidden, and the guide is not written into the live texture, Design JSON, or exported PNG
138
+
139
+ > [!IMPORTANT]
140
+ > Remote models, textures, decals, and fonts must be served with CORS headers that allow the app origin
141
+ >
142
+ > A blocked or canvas-tainting resource may fail to load and can prevent PNG export
143
+
144
+ ## Basic Usage
145
+
146
+ Use the `ProductCustomizerApi` core when the consuming application owns the complete UI. It can be mounted into any two DOM containers and does not require the Workbench DOM structure or CSS classes:
147
+
148
+ CustomForge is browser-only and must be initialized after its DOM containers are available
149
+
150
+ The editor and viewer require two different containers; call `destroy()` when the page, host component, or instance is no longer in use to release Fabric, WebGL, observer, and event resources
151
+
152
+ ```html
153
+ <div id="texture-editor"></div>
154
+ <div id="product-viewer"></div>
155
+ ```
156
+
157
+ ```ts
158
+ import { createCustomizer } from 'customforge'
159
+ import 'customforge/style.css'
160
+
161
+ const customizer = await createCustomizer({
162
+ editor: '#texture-editor',
163
+ viewer: '#product-viewer',
164
+ editorWidth: 1024,
165
+ editorHeight: 512,
166
+ product: {
167
+ modelUrl: 'https://example.com/product.glb',
168
+ textureUrl: 'https://example.com/base-texture.png',
169
+ surfaceMesh: 'PrintArea',
170
+ textureFlipY: false,
171
+ },
172
+ appearance: {
173
+ editor: {
174
+ controlSize: 7,
175
+ objectBorder: '#0057b8',
176
+ uvBoundary: '#d92d20',
177
+ },
178
+ viewer: { backgroundColor: '#f4f4f5' },
179
+ },
180
+ })
181
+
182
+ customizer.addText({
183
+ text: 'Hello world',
184
+ x: 120,
185
+ y: 180,
186
+ fontSize: 64,
187
+ fontWeight: 'bold',
188
+ textAlign: 'center',
189
+ color: '#172126',
190
+ })
191
+
192
+ await customizer.addImage({
193
+ src: 'https://example.com/logo.png',
194
+ x: 720,
195
+ y: 240,
196
+ width: 220,
197
+ })
198
+
199
+ window.addEventListener('beforeunload', () => customizer.destroy(), {
200
+ once: true,
201
+ })
202
+ ```
203
+
204
+ The public package uses the same root and style imports as the local `.tgz`; pin an exact version when reproducible installs are required
205
+
206
+ ## Ready-made Workbench
207
+
208
+ Use the optional Workbench entry when a complete default interface is preferable to building controls from scratch
209
+
210
+ The host element must have an explicit height so the design surface, collapsible layers panel, and 3D preview can measure their available space
211
+
212
+ ```html
213
+ <div id="customforge-workbench" style="height: 720px"></div>
214
+ ```
215
+
216
+ ```ts
217
+ import { createWorkbench } from 'customforge/workbench'
218
+ import 'customforge/style.css'
219
+
220
+ const workbench = await createWorkbench({
221
+ container: '#customforge-workbench',
222
+ className: 'store-customizer',
223
+ product: {
224
+ modelUrl: 'https://example.com/product.glb',
225
+ surfaceMesh: 'PrintArea',
226
+ },
227
+ features: {
228
+ presetBackgrounds: true,
229
+ presetElements: true,
230
+ },
231
+ layout: {
232
+ header: true,
233
+ layers: true,
234
+ },
235
+ branding: {
236
+ logoUrl: '/brand/logo.png',
237
+ title: 'Studio',
238
+ subtitle: 'Product personalization',
239
+ },
240
+ labels: {
241
+ addText: 'Typography',
242
+ addImage: 'Artwork',
243
+ productDialogTitle: 'Choose a product model',
244
+ loadingProduct: 'Loading product...',
245
+ },
246
+ fontFamilies: [
247
+ { value: 'Arial', label: 'Arial' },
248
+ { value: 'Georgia', label: 'Georgia' },
249
+ ],
250
+ formatError: (error) =>
251
+ error instanceof Error ? error.message : 'The operation failed',
252
+ theme: {
253
+ accent: '#0057b8',
254
+ accentHover: '#003f87',
255
+ accentContrast: '#ffffff',
256
+ },
257
+ assets: {
258
+ backgrounds: [
259
+ { id: 'floral', name: 'Floral', url: '/presets/floral.png' },
260
+ ],
261
+ elements: [
262
+ { id: 'flower', name: 'Flower', url: '/presets/flower.png' },
263
+ ],
264
+ },
265
+ })
266
+ ```
267
+
268
+ The bundled CustomForge logo is used by default and can be replaced or hidden through `branding`
269
+
270
+ `labels` covers every fixed Workbench label, Dialog message, placeholder, validation prompt, status, color name, download filename, and accessibility name. `fontFamilies`, `textPresets`, `assets`, and `branding` control the visible names generated from configurable data, while `formatError` converts model, image, UV, and Design JSON failures into user-facing copy
271
+
272
+ CustomForge provides default English copy but does not own application locale state. Build the options from the host application's i18n system and create the Workbench with the active language pack. This keeps locale routing and fallback behavior in the consuming application instead of coupling the library to a specific i18n framework
273
+
274
+ Use the exported `WorkbenchLabels` type when authoring a complete language pack; `WorkbenchOptions.labels` remains partial so applications only overriding a few terms are not forced to repeat the defaults
275
+
276
+ `theme` maps to scoped CSS variables, and `icons` can disable built-in icons or replace individual semantic icons with image URLs
277
+
278
+ `appearance` configures Fabric selection controls, UV helper colors, and the WebGL clear color that normal Workbench CSS cannot style reliably
279
+
280
+ The default UI font stack prefers the rounded `Nunito Sans` family and falls back to system sans-serif fonts
281
+
282
+ CustomForge bundles the font asset and does not fetch third-party font services at runtime
283
+
284
+ Text and image commands open focused Dialogs instead of immediately mutating the canvas
285
+
286
+ - The text Dialog includes an input, color control, and replaceable typography presets
287
+ - Selecting one or more text objects reveals contextual controls inside the existing single-row toolbar for font family, size, bold, italic, underline, alignment, text color, highlight, line height, and letter spacing without shifting the canvas
288
+ - The contextual toolbar supports mixed multi-selection values and an explicit command for entering on-canvas text editing
289
+ - The image Dialog includes local upload, background presets, and decorative element presets
290
+ - A design background replaces the previous design background, fills the current product's UV printable bounds, starts locked at the bottom layer, and persists in Design JSON
291
+ - Selecting an ordinary image reveals a command that converts it into the design background, fits it to the current UV printable bounds, and moves it to the bottom layer
292
+
293
+ Feature and layout switches can also be changed after initialization
294
+
295
+ ```ts
296
+ workbench.setFeature('addText', true)
297
+ workbench.setLayout('header', true)
298
+ workbench.setTheme({ accent: '#0057b8' })
299
+ ```
300
+
301
+ ### Extend the Workbench with application actions
302
+
303
+ Register application-owned buttons through `extensions` instead of querying or rewriting Workbench DOM. Each action receives the public Workbench and headless customizer APIs, a current state snapshot, the current selection, and the target layer when applicable:
304
+
305
+ ```ts
306
+ const workbench = await createWorkbench({
307
+ container: '#customforge-workbench',
308
+ className: 'store-customizer',
309
+ extensions: [
310
+ {
311
+ id: 'store.export-png',
312
+ placement: 'globalActions',
313
+ label: t('actions.exportPng'),
314
+ variant: 'primary',
315
+ className: 'store-export-command',
316
+ onClick: async ({ customizer, workbench, signal }) => {
317
+ const blob = await customizer.getTextureBlob()
318
+ if (!signal.aborted) {
319
+ workbench.setStatus(`${blob.size} bytes`)
320
+ }
321
+ },
322
+ },
323
+ {
324
+ id: 'store.straighten',
325
+ placement: 'selectionToolbar',
326
+ label: t('actions.straighten'),
327
+ visible: ({ selection }) => selection.length === 1,
328
+ disabled: ({ selection }) => selection.some(({ locked }) => locked),
329
+ onClick: ({ customizer, selection }) => {
330
+ selection.forEach(({ id }) => {
331
+ customizer.updateObjectTransform(id, { rotation: 0 })
332
+ })
333
+ },
334
+ },
335
+ ],
336
+ })
337
+ ```
338
+
339
+ | Placement | Intended use | Additional context |
340
+ | --- | --- | --- |
341
+ | `globalActions` | Product-, export-, or workflow-level commands | Standard state and selection |
342
+ | `editorToolbar` | Design commands that do not require a selection | Standard state and selection |
343
+ | `selectionToolbar` | Commands for the active selection | Hidden automatically without a selection |
344
+ | `layerActions` | Compact commands repeated for each layer | `layer` contains that row's object |
345
+
346
+ `visible` and `disabled` accept booleans or state predicates. Predicates refresh automatically after CustomForge state, selection, history, and view events; call `workbench.refreshExtensions()` when they also depend on host application state. Promise-returning actions enter a disabled loading state and rejected actions are passed through `formatError` to the existing status region.
347
+
348
+ Use `iconUrl`, `showLabel`, `variant`, `order`, and `className` to control presentation. Extension labels are application data, so pass the active i18n string. Scope deeper CSS with the Workbench `className` and the extension's own `className`:
349
+
350
+ ```css
351
+ .store-customizer .store-export-command {
352
+ text-transform: uppercase;
353
+ }
354
+ ```
355
+
356
+ Extensions can also be managed after initialization. The cleanup function only removes the registration that created it:
357
+
358
+ ```ts
359
+ const unregister = workbench.registerExtension(action)
360
+ const actions = workbench.getExtensions()
361
+ workbench.removeExtension('store.export-png')
362
+ unregister()
363
+ ```
364
+
365
+ `workbench.getFeatures()`, `workbench.getLayout()`, `workbench.getTheme()`, and `workbench.getExtensions()` return independent snapshots. `className` is applied only to that Workbench root, so application CSS can target one instance without relying on a global selector. For a fully custom interface, use `createCustomizer()` instead of restyling or querying Workbench internals.
366
+
367
+ | Feature switch | Controls |
368
+ | --- | --- |
369
+ | `addText`, `addImage`, `deleteSelection` | Object Dialogs and deletion |
370
+ | `textFormatting` | Contextual text formatting toolbar |
371
+ | `undoRedo` | Undo and redo buttons plus Workbench keyboard shortcuts |
372
+ | `saveDesign`, `loadDesign` | Design JSON actions |
373
+ | `loadRemoteProduct`, `resetView` | Product loading and preview actions |
374
+ | `reorderObjects`, `toggleObjectVisibility`, `lockObjects`, `renameObjects` | Layer management |
375
+ | `presetBackgrounds`, `presetElements` | Image Dialog preset tabs |
376
+
377
+ | Layout switch | Region |
378
+ | --- | --- |
379
+ | `header` | Brand and global product actions |
380
+ | `editorHeader`, `viewerHeader` | Workspace panel headings |
381
+ | `toolbar` | Design tool controls |
382
+ | `layers` | Collapsible object layer panel and toolbar entry |
383
+ | `status` | Runtime status inside the 3D preview |
384
+
385
+ All feature and layout values default to `true`
386
+
387
+ Feature switches only control the built-in Workbench controls and do not remove methods from `workbench.customizer`
388
+
389
+ Theme values are intended for coherent product-level styling rather than per-button color configuration
390
+
391
+ ```ts
392
+ const workbench = await createWorkbench({
393
+ container: '#customforge-workbench',
394
+ icons: {
395
+ enabled: true,
396
+ sources: {
397
+ addText: '/icons/typography.svg',
398
+ loadProduct: '/icons/upload.svg',
399
+ },
400
+ },
401
+ textPresets: [
402
+ {
403
+ id: 'brand-display',
404
+ name: 'Brand display',
405
+ fontFamily: 'Arial',
406
+ fontSize: 72,
407
+ width: 460,
408
+ color: '#17191c',
409
+ },
410
+ ],
411
+ })
412
+ ```
413
+
414
+ ## Design JSON
415
+
416
+ `saveDesign()` returns a Library-owned, JSON-safe document instead of exposing Fabric.js serialization. `loadDesign()` validates unknown input and restores the editable object stack asynchronously:
417
+
418
+ ```ts
419
+ const design = customizer.saveDesign()
420
+ localStorage.setItem('customforge-design', JSON.stringify(design))
421
+
422
+ const savedDesign = localStorage.getItem('customforge-design')
423
+ if (savedDesign) {
424
+ await customizer.loadDesign(JSON.parse(savedDesign))
425
+ }
426
+ ```
427
+
428
+ The current Schema version is `1`. It stores the logical canvas size and the text and image objects in back-to-front render order, including stable object IDs, names, visibility, locking, image roles, center-based transforms, and rich text formatting. Existing version 1 documents without the optional formatting fields continue to load with the original bold, centered defaults
429
+
430
+ Design JSON intentionally excludes the product model, target mesh, and base texture. A document can only be loaded into an editor with exactly the same logical width and height
431
+
432
+ An image with `role: 'background'` is a design object rather than the product base texture. Only one is allowed, it must be the first object, and older version 1 documents without `role` continue to load as ordinary image elements
433
+
434
+ Loading is transactional: the current design remains unchanged unless the document validates and every referenced image loads successfully. Blob URL images are converted to Data URLs when added; remote image URLs remain URLs and must continue to satisfy browser CORS requirements when restored
435
+
436
+ Design JSON version 1 remains readable throughout the `0.1.x` release line. New optional fields may be added without invalidating existing version 1 documents
437
+
438
+ ## Undo and Redo
439
+
440
+ History is enabled in both the headless core and Workbench and defaults to 50 undo steps
441
+
442
+ Set `historyLimit` in `createCustomizer` or `createWorkbench` options to change the retained undo depth
443
+
444
+ ```ts
445
+ if (customizer.canUndo()) {
446
+ await customizer.undo()
447
+ }
448
+
449
+ await customizer.redo()
450
+ customizer.clearHistory()
451
+
452
+ const stop = customizer.on('historychange', ({ canUndo, canRedo }) => {
453
+ console.log({ canUndo, canRedo })
454
+ })
455
+ ```
456
+
457
+ History covers object creation, deletion, canvas transforms, text content and formatting edits, image-to-background conversion, layer order, names, visibility, locking, and Design JSON loading
458
+
459
+ Workbench also supports `Ctrl` or `Cmd` + `Z`, `Ctrl` or `Cmd` + `Shift` + `Z`, and `Ctrl` + `Y` while focus is outside form fields
460
+
461
+ Successful product replacement keeps the current design but starts a new history baseline
462
+
463
+ ## Instance API
464
+
465
+ | Method | Description |
466
+ | --- | --- |
467
+ | `addText(options)` | Add and select editable text, constrained to the canvas; `fontSize` defaults to `22` |
468
+ | `addImage(options)` | Load and select an element or replace the design background |
469
+ | `getState()` | Return product, canvas, printable bounds, objects, selection, history, and 3D view in one snapshot |
470
+ | `getProduct()` | Return the resolved current product configuration |
471
+ | `getCanvasSize()` | Return the logical texture dimensions |
472
+ | `getPrintableBounds()` | Return the UV printable bounds in logical canvas coordinates |
473
+ | `getObjects()` | Return the current objects in back-to-front layer order |
474
+ | `getSelectedObjectIds()` | Return stable IDs for the current selection |
475
+ | `selectObject(id)` | Select a visible object by stable ID |
476
+ | `selectObjects(ids)` | Select multiple visible objects by stable ID |
477
+ | `clearSelection()` | Clear the current canvas selection without changing the design |
478
+ | `removeObject(id)` | Remove an object by stable ID |
479
+ | `moveObject(id, index)` | Move an object to a zero-based layer index |
480
+ | `setImageAsBackground(id)` | Replace the design background with an existing image and fit it to the current UV printable bounds |
481
+ | `renameObject(id, name)` | Change the object name shown in layer tools |
482
+ | `setObjectVisibility(id, visible)` | Include or exclude an object from rendering |
483
+ | `setObjectLocked(id, locked)` | Lock or unlock canvas transformations |
484
+ | `updateObjectTransform(id, options)` | Update center position, scale, rotation, and flips by stable ID |
485
+ | `updateText(id, options)` | Update text content and formatting by stable ID |
486
+ | `editText(id)` | Enter on-canvas editing for an unlocked text object |
487
+ | `deleteSelected()` | Remove the active object or selection |
488
+ | `saveDesign()` | Return the current versioned Design JSON document |
489
+ | `loadDesign(value)` | Validate and transactionally restore Design JSON |
490
+ | `canUndo()`, `canRedo()` | Query the current history directions |
491
+ | `undo()`, `redo()` | Restore the previous or next design snapshot |
492
+ | `clearHistory()` | Make the current design the new history baseline |
493
+ | `loadProduct(product)` | Replace the model, base texture, and target mesh |
494
+ | `getTextureDataUrl()` | Return the composed texture as a PNG Data URL |
495
+ | `getTextureBlob()` | Return the composed texture as a PNG Blob |
496
+ | `exportTexture(filename?)` | Download the composed texture as PNG |
497
+ | `getViewState()`, `setViewState(state)` | Read or restore the 3D camera position and target |
498
+ | `resetView()` | Restore the default 3D camera position |
499
+ | `on(event, listener)` | Subscribe to instance events; returns an unsubscribe function |
500
+ | `destroy()` | Release DOM events, Fabric state, and WebGL resources |
501
+
502
+ Available events are `ready`, `change`, `selectionchange`, `historychange`, `viewchange`, `status`, and `error`
503
+
504
+ The headless contract is exported as `ProductCustomizerApi`; the default UI contract is exported as `CustomForgeWorkbenchApi`. The concrete classes remain available, but consumers can depend on these interfaces without coupling application code to their implementation.
505
+
506
+ `ProductCustomizer` does not expose its Fabric.js editor or Three.js viewer instances; consumers interact through the facade API
507
+
508
+ Imports from `core`, `editor`, `viewer`, `bridge`, or any other undeclared package subpath are unsupported
509
+
510
+ ## Architecture
511
+
512
+ ```mermaid
513
+ flowchart LR
514
+ A[Text and images] --> B[DesignEditor]
515
+ C[Base texture] --> B
516
+ B --> D[HTML Canvas]
517
+ D --> E[TextureBridge]
518
+ E --> F[Three.js CanvasTexture]
519
+ G[GLB / GLTF model] --> H[ProductViewer]
520
+ F --> H
521
+ H --> I[Printable mesh]
98
522
  ```
99
-
100
- The checked-in `examples/npm-consumer` project imports CustomForge only through this `.tgz`. Run `pnpm install --ignore-workspace` in that directory so pnpm installs it independently from the parent workspace
101
-
102
- ## Load Your Product
103
-
104
- Choose **Load remote** in the demo and provide:
105
-
106
- | Field | Purpose |
107
- | --- | --- |
108
- | GLB / GLTF URL | URL of the UV-mapped 3D product model |
109
- | Base texture URL | Optional image shown underneath editable objects |
110
- | Customizable mesh | Name of the mesh that receives the live canvas texture |
111
- | Flip texture vertically | Corrects texture orientation when required by the model |
112
-
113
- UV coordinates are expected to be stored in the 3D model
114
-
115
- The optional texture image is the visual base layer, not a replacement for model UV data
116
-
117
- > [!IMPORTANT]
118
- > Remote models, textures, decals, and fonts must be served with CORS headers that allow the app origin
119
- >
120
- > A blocked or canvas-tainting resource may fail to load and can prevent PNG export
121
-
122
- ## Basic Usage
123
-
124
- The framework-independent Library entry can be mounted into any two DOM containers:
125
-
126
- CustomForge is browser-only and must be initialized after its DOM containers are available
127
-
128
- The editor and viewer require two different containers; call `destroy()` when the page, host component, or instance is no longer in use to release Fabric, WebGL, observer, and event resources
129
-
130
- ```html
131
- <div id="texture-editor"></div>
132
- <div id="product-viewer"></div>
133
- ```
134
-
135
- ```ts
136
- import { createCustomizer } from 'customforge'
137
- import 'customforge/style.css'
138
-
139
- const customizer = await createCustomizer({
140
- editor: '#texture-editor',
141
- viewer: '#product-viewer',
142
- editorWidth: 1024,
143
- editorHeight: 512,
144
- product: {
145
- modelUrl: 'https://example.com/product.glb',
146
- textureUrl: 'https://example.com/base-texture.png',
147
- surfaceMesh: 'PrintArea',
148
- textureFlipY: false,
149
- },
150
- })
151
-
152
- customizer.addText({
153
- text: 'Hello world',
154
- x: 120,
155
- y: 180,
156
- fontSize: 64,
157
- color: '#172126',
158
- })
159
-
160
- await customizer.addImage({
161
- src: 'https://example.com/logo.png',
162
- x: 720,
163
- y: 240,
164
- width: 220,
165
- })
166
-
167
- window.addEventListener('beforeunload', () => customizer.destroy(), {
168
- once: true,
169
- })
170
- ```
171
-
172
- The public package uses the same root and style imports as the local `.tgz`; pin an exact alpha version when reproducible installs are required
173
-
174
- ## Design JSON
175
-
176
- `saveDesign()` returns a Library-owned, JSON-safe document instead of exposing Fabric.js serialization. `loadDesign()` validates unknown input and restores the editable object stack asynchronously:
177
-
178
- ```ts
179
- const design = customizer.saveDesign()
180
- localStorage.setItem('customforge-design', JSON.stringify(design))
181
-
182
- const savedDesign = localStorage.getItem('customforge-design')
183
- if (savedDesign) {
184
- await customizer.loadDesign(JSON.parse(savedDesign))
185
- }
186
- ```
187
-
188
- The current Schema version is `1`. It stores the logical canvas size and the text and image objects in back-to-front render order, including stable object IDs and center-based transforms
189
-
190
- Design JSON intentionally excludes the product model, target mesh, and base texture. A document can only be loaded into an editor with exactly the same logical width and height
191
-
192
- Loading is transactional: the current design remains unchanged unless the document validates and every referenced image loads successfully. Blob URL images are converted to Data URLs when added; remote image URLs remain URLs and must continue to satisfy browser CORS requirements when restored
193
-
194
- The Schema is still an alpha contract and may change in later alpha versions
195
-
196
- ## Instance API
197
-
198
- | Method | Description |
199
- | --- | --- |
200
- | `addText(options)` | Add and select editable text, constrained to the canvas |
201
- | `addImage(options)` | Load and select an image, constrained to the canvas |
202
- | `deleteSelected()` | Remove the active object or selection |
203
- | `saveDesign()` | Return the current versioned Design JSON document |
204
- | `loadDesign(value)` | Validate and transactionally restore Design JSON |
205
- | `loadProduct(product)` | Replace the model, base texture, and target mesh |
206
- | `exportTexture(filename?)` | Download the composed texture as PNG |
207
- | `resetView()` | Restore the default 3D camera position |
208
- | `on(event, listener)` | Subscribe to instance events; returns an unsubscribe function |
209
- | `destroy()` | Release DOM events, Fabric state, and WebGL resources |
210
-
211
- Available events are `ready`, `change`, `selectionchange`, `status`, and `error`
212
-
213
- The initial alpha stability boundary is limited to the methods above, `createCustomizer`, `ProductCustomizer`, and the configuration, event, and Design JSON types exported from the package root
214
-
215
- `ProductCustomizer` does not expose its Fabric.js editor or Three.js viewer instances; consumers interact through the facade API
216
-
217
- Imports from `core`, `editor`, `viewer`, `bridge`, or any other undeclared package subpath are unsupported
218
-
219
- ## Architecture
220
-
221
- ```mermaid
222
- flowchart LR
223
- A[Text and images] --> B[DesignEditor]
224
- C[Base texture] --> B
225
- B --> D[HTML Canvas]
226
- D --> E[TextureBridge]
227
- E --> F[Three.js CanvasTexture]
228
- G[GLB / GLTF model] --> H[ProductViewer]
229
- F --> H
230
- H --> I[Customizable mesh]
231
- ```
232
-
233
- ```text
234
- ProductCustomizer
235
- |-- DesignEditor Fabric.js rendering and object interaction
236
- |-- ProductViewer Three.js scene, model, material, and camera
237
- `-- TextureBridge Canvas-to-material synchronization
238
- ```
239
-
240
- `ProductCustomizer` coordinates the modules while keeping Fabric.js and Three.js details behind a small instance API
241
-
242
- The demo UI uses Vanilla TypeScript; the core does not depend on Vue, React, or another UI framework
243
-
244
- ## Model Contract
245
-
246
- The current prototype expects:
247
-
248
- - A GLB or GLTF model with valid UV coordinates
249
- - An uncompressed model that can be loaded by the standard Three.js `GLTFLoader`
250
- - A named mesh for the customizable surface, such as `PrintArea`
251
- - The target texture in the first material slot of that mesh
252
- - Remote asset URLs accessible under the browser's CORS policy
253
-
254
- The built-in demo follows the same `PrintArea` mesh convention
255
-
256
- ## Project Structure
257
-
258
- ```text
259
- src/
260
- |-- bridge/ Canvas and Three.js texture synchronization
261
- |-- core/ Public types, configuration, and DOM helpers
262
- |-- customizer/ Public instance orchestration
263
- |-- demo/ Runnable workbench UI
264
- |-- editor/ Fabric.js design surface
265
- |-- style.css Public Library style entry
266
- |-- styles/ Library core styles
267
- |-- viewer/ Three.js product preview
268
- `-- index.ts Framework-independent source entry
269
-
270
- examples/
271
- `-- npm-consumer/ Independent local .tgz consumer
272
-
273
- scripts/
274
- `-- verify-package.mjs npm file and artifact boundary checks
275
- ```
276
-
277
- ## Development Commands
278
-
279
- ```bash
280
- pnpm check # TypeScript project check
281
- pnpm test # Unit tests
282
- pnpm build # Type-check and production build
283
- pnpm build:lib # Build ESM, declarations, and core styles
284
- pnpm verify:package # Check dist and the npm file list
285
- pnpm pack:local # Build, verify, and create the local .tgz
286
- pnpm release:check # Run checks, tests, package build, verification, and local pack
287
- pnpm preview # Preview the production build
288
- ```
289
-
290
- ## Current Scope
291
-
292
- This is an early technical prototype
293
-
294
- The public API and design document format are not stable yet
295
-
296
- The current milestone intentionally focuses on one texture and one customizable mesh
297
-
298
- Public npm releases use the `alpha` dist-tag until the API and Design JSON contract are ready for a more stable channel
299
-
300
- Multi-surface products, undo/redo, and framework adapters are not implemented yet. Undo/redo is the next planned milestone and will build on the Design JSON snapshot contract
301
-
302
- ## Technology
303
-
304
- - [Fabric.js](https://fabricjs.com/) for the 2D editing surface
305
- - [Three.js](https://threejs.org/) for model loading and real-time 3D rendering
306
- - [Vite](https://vite.dev/) for development and application builds
307
- - [TypeScript](https://www.typescriptlang.org/) with strict checking enabled
308
-
309
- ## License
310
-
311
- CustomForge is licensed under the Apache License 2.0
312
-
313
- See [LICENSE](./LICENSE) for the full license terms
314
-
315
- Third-party dependencies and assets remain subject to their respective licenses
523
+
524
+ ```text
525
+ ProductCustomizer
526
+ |-- DesignEditor Fabric.js rendering and object interaction
527
+ |-- ProductViewer Three.js scene, model, material, and camera
528
+ `-- TextureBridge Canvas-to-material synchronization
529
+ ```
530
+
531
+ `ProductCustomizer` coordinates the modules while keeping Fabric.js and Three.js details behind a small instance API
532
+
533
+ The optional Workbench and demo use Vanilla TypeScript; the core does not depend on Vue, React, or another UI framework
534
+
535
+ ## Model Contract
536
+
537
+ The current model contract expects:
538
+
539
+ - A GLB or GLTF model with valid UV coordinates
540
+ - An uncompressed model that can be loaded by the standard Three.js `GLTFLoader`
541
+ - A named mesh for the customizable surface, such as `PrintArea`
542
+ - The target texture in the first material slot of that mesh
543
+ - Remote asset URLs accessible under the browser's CORS policy
544
+
545
+ The bundled default model separates the complete cup into `MugBody` and the UV-mapped printable overlay into `PrintArea`. CustomForge preserves the body material and applies the live design texture only to `PrintArea`
546
+
547
+ ## Project Structure
548
+
549
+ ```text
550
+ src/
551
+ |-- bridge/ Canvas and Three.js texture synchronization
552
+ |-- core/ Public types, configuration, and DOM helpers
553
+ |-- customizer/ Public instance orchestration
554
+ |-- demo/ Runnable workbench UI
555
+ |-- editor/ Fabric.js design surface and snapshot history
556
+ |-- style.css Public Library style entry
557
+ |-- styles/ Core and Workbench styles
558
+ |-- viewer/ Three.js product preview
559
+ |-- workbench/ Configurable UI, Dialogs, icons, and preset assets
560
+ `-- index.ts Framework-independent source entry
561
+
562
+ examples/
563
+ |-- api-contract-consumer/ Public API, custom UI, and browser smoke consumer
564
+ `-- npm-consumer/ Independent local .tgz consumer
565
+
566
+ scripts/
567
+ |-- run-browser-smoke.mjs Headless browser contract gate
568
+ |-- verify-consumers.mjs Independent tarball consumer gate
569
+ |-- verify-package.mjs npm file and artifact boundary checks
570
+ `-- verify-release.mjs Version, changelog, tag, and example checks
571
+ ```
572
+
573
+ ## Development Commands
574
+
575
+ ```bash
576
+ pnpm check # TypeScript project check
577
+ pnpm test # Unit tests
578
+ pnpm build # Type-check and production build
579
+ pnpm build:lib # Build core and Workbench ESM, declarations, and styles
580
+ pnpm verify:release # Check stable version, changelog, tag, and consumer paths
581
+ pnpm verify:package # Check dist and the npm file list
582
+ pnpm verify:consumers # Install the local .tgz into and build both consumers
583
+ pnpm test:browser # Run the packaged API consumer in headless Chrome
584
+ pnpm pack:local # Build, verify, and create the local .tgz through prepack
585
+ pnpm release:check # Run the complete stable release gate
586
+ pnpm preview # Preview the production build
587
+ ```
588
+
589
+ See the [release guide](https://github.com/songshanliu/CustomForge/blob/main/RELEASING.md) for npm Trusted Publishing setup, tag rules, the release sequence, and failure handling
590
+
591
+ ## Current Scope
592
+
593
+ Version `0.1.0` is the first non-prerelease package. Because the project remains on the `0.x` line, breaking public API changes may be introduced only in a future minor release and will be documented in the changelog
594
+
595
+ The current release intentionally focuses on one logical design canvas mapped to one named Mesh and its first material slot. Design JSON version 1 documents remain readable across `0.1.x`
596
+
597
+ Multi-surface products, advanced alignment tools, and framework adapters are not implemented yet
598
+
599
+ ## Technology
600
+
601
+ - [Fabric.js](https://fabricjs.com/) for the 2D editing surface
602
+ - [Three.js](https://threejs.org/) for model loading and real-time 3D rendering
603
+ - [Lucide](https://lucide.dev/) for bundled Workbench controls
604
+ - [Vite](https://vite.dev/) for development and application builds
605
+ - [TypeScript](https://www.typescriptlang.org/) with strict checking enabled
606
+
607
+ ## License
608
+
609
+ CustomForge is licensed under the Apache License 2.0
610
+
611
+ See [LICENSE](./LICENSE) for the full license terms
612
+
613
+ Third-party dependencies and assets remain subject to their respective licenses
614
+
615
+ The bundled Nunito Sans font is licensed under the SIL Open Font License 1.1, available in [NunitoSans-OFL.txt](./LICENSES/NunitoSans-OFL.txt)
616
+
617
+ The bundled Plain Mug model by LightSwitch is licensed under CC BY 4.0; attribution and source details are available in [plain-mug-CC-BY-4.0.txt](./LICENSES/plain-mug-CC-BY-4.0.txt)