customforge 0.1.0-alpha.2 → 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 (47) hide show
  1. package/CHANGELOG.md +85 -56
  2. package/LICENSES/NunitoSans-OFL.txt +93 -93
  3. package/LICENSES/plain-mug-CC-BY-4.0.txt +11 -0
  4. package/README.md +592 -461
  5. package/README.zh-CN.md +587 -459
  6. package/dist/{ProductCustomizer-rMRyWe7L.js → ProductCustomizer-4ytA68eg.js} +869 -143
  7. package/dist/ProductCustomizer-4ytA68eg.js.map +1 -0
  8. package/dist/bridge/TextureBridge.d.ts +1 -1
  9. package/dist/core/api.d.ts +94 -0
  10. package/dist/core/api.d.ts.map +1 -0
  11. package/dist/core/config.d.ts +3 -12
  12. package/dist/core/config.d.ts.map +1 -1
  13. package/dist/core/design.d.ts.map +1 -1
  14. package/dist/core/types.d.ts +183 -5
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/core/uv.d.ts +8 -0
  17. package/dist/core/uv.d.ts.map +1 -0
  18. package/dist/cup_decal_small_margins.glb +0 -0
  19. package/dist/customizer/ProductCustomizer.d.ts +98 -9
  20. package/dist/customizer/ProductCustomizer.d.ts.map +1 -1
  21. package/dist/editor/DesignEditor.d.ts +118 -10
  22. package/dist/editor/DesignEditor.d.ts.map +1 -1
  23. package/dist/editor/uvBounds.d.ts +15 -0
  24. package/dist/editor/uvBounds.d.ts.map +1 -0
  25. package/dist/index.d.ts +4 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/style.css +2120 -1327
  30. package/dist/viewer/ProductViewer.d.ts +37 -6
  31. package/dist/viewer/ProductViewer.d.ts.map +1 -1
  32. package/dist/viewer/uvLayout.d.ts +11 -0
  33. package/dist/viewer/uvLayout.d.ts.map +1 -0
  34. package/dist/workbench/CustomForgeWorkbench.d.ts +85 -8
  35. package/dist/workbench/CustomForgeWorkbench.d.ts.map +1 -1
  36. package/dist/workbench/config.d.ts +42 -1
  37. package/dist/workbench/config.d.ts.map +1 -1
  38. package/dist/workbench/icons.d.ts.map +1 -1
  39. package/dist/workbench/index.d.ts +3 -3
  40. package/dist/workbench/index.d.ts.map +1 -1
  41. package/dist/workbench/template.d.ts.map +1 -1
  42. package/dist/workbench/types.d.ts +261 -13
  43. package/dist/workbench/types.d.ts.map +1 -1
  44. package/dist/workbench.js +1248 -116
  45. package/dist/workbench.js.map +1 -1
  46. package/package.json +27 -20
  47. package/dist/ProductCustomizer-rMRyWe7L.js.map +0 -1
package/README.zh-CN.md CHANGED
@@ -23,462 +23,590 @@
23
23
  <img alt="Fabric.js" src="https://img.shields.io/badge/Fabric.js-7-BE185D?style=flat-square&labelColor=1F2937">
24
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
25
  </p>
26
-
27
- </div>
28
-
29
- ---
30
-
31
- ## 项目能力
32
-
33
- CustomForge 将常见的二维设计界面与带 UV 的三维模型连接起来:
34
-
35
- - 在二维纹理画布上添加和编辑文字
36
- - 上传、移动、缩放和旋转图片
37
- - 将画布的每次变化实时呈现在三维产品上
38
- - 加载远程 GLB/GLTF 模型和可选的基础纹理
39
- - 通过 Mesh 名称指定可定制表面
40
- - 使用 OrbitControls 旋转和缩放三维预览
41
- - 将合成后的纹理导出为 PNG
42
- - 通过版本化 Design JSON 保存和恢复可编辑对象
43
- - 通过名称、显隐、锁定和顺序管理对象图层
44
- - 使用设计快照撤销和重做修改
45
- - 使用可配置的文字、背景和装饰素材 Dialog
46
- - 通过品牌、文案、主题变量和图标适配默认 Workbench
47
-
48
- 工作台内置了一个程序生成的杯子,无需准备外部资源即可直接运行
49
-
50
- ## 仓库开发
51
-
52
- 环境要求:Node.js 22+ 和 pnpm 11+
53
-
54
- ```bash
55
- pnpm install
56
- pnpm dev
57
- ```
58
-
59
- 打开 Vite 输出的地址
60
-
61
- 内置演示的左侧是 UV 编辑区,右侧是实时三维预览
62
-
63
- ## npm Alpha 制品
64
-
65
- CustomForge 已通过 `alpha` dist-tag 发布到 npm Registry,不同 alpha 版本之间的 API 和 Design JSON Schema 可能发生变化
66
-
67
- 使用以下命令安装当前公开 alpha:
68
-
69
- ```powershell
70
- pnpm add customforge@alpha
71
- ```
72
-
73
- Fabric.js 和 Three.js 会作为传递依赖自动安装
74
-
75
- pnpm 可能提示可选的原生 `canvas` 构建脚本已被忽略,CustomForge 运行在浏览器中,不使用 Node 原生 canvas,因此不需要执行 `pnpm approve-builds`
76
-
77
- 消费项目可以在 `package.json` 中明确记录这一浏览器端选择:
78
-
79
- ```json
80
- {
81
- "pnpm": {
82
- "ignoredBuiltDependencies": ["canvas"]
83
- }
84
- }
85
- ```
86
-
87
- 包页面:[npmjs.com/package/customforge](https://www.npmjs.com/package/customforge)
88
-
89
- 发布前如需进行独立于仓库源码的验证,可在仓库根目录构建并生成本地包:
90
-
91
- ```powershell
92
- pnpm pack:local
93
- ```
94
-
95
- 命令依次生成 JavaScriptTypeScript 声明和公开样式,检查 npm 文件清单,并创建:
96
-
97
- ```text
98
- customforge-0.1.0-alpha.2.tgz
99
- ```
100
-
101
- 在独立 Vite TypeScript 项目中安装该本地制品:
102
-
103
- ```powershell
104
- pnpm add D:\projects\3DRendering\core_code\customforge-0.1.0-alpha.2.tgz
105
- ```
106
-
107
- 仓库中的 `examples/npm-consumer` 提供了一个只通过该 `.tgz` 导入的消费示例。在该目录中使用 `pnpm install --ignore-workspace`,确保 pnpm 将其作为独立于父级 workspace 的项目安装
108
-
109
- ## 加载你的产品
110
-
111
- 在演示页面中选择 **Load product**,然后填写:
112
-
113
- | 字段 | 用途 |
114
- | --- | --- |
115
- | GLB / GLTF URL | UV 的三维产品模型地址 |
116
- | Base texture URL | 显示在可编辑对象下方的可选基础纹理 |
117
- | Customizable mesh | 接收实时画布纹理的 Mesh 名称 |
118
- | Flip texture vertically | 在模型需要时修正纹理的垂直方向 |
119
-
120
- UV 坐标应当保存在三维模型中
121
-
122
- 可选的纹理图片只是二维编辑器的基础图层,不能替代模型中的 UV 数据
123
-
124
- > [!IMPORTANT]
125
- > 远程模型、纹理、贴图和字体必须提供允许当前页面来源访问的 CORS 响应头
126
- >
127
- > 被浏览器阻止或污染 Canvas 的资源可能无法加载,也会导致 PNG 导出失败
128
-
129
- ## 基本用法
130
-
131
- 与框架无关的 Library 入口可以挂载到任意两个 DOM 容器中:
132
-
133
- CustomForge 仅支持浏览器环境,应在 DOM 挂载容器可用后创建实例
134
-
135
- 编辑器和查看器必须使用两个不同的容器;页面卸载、组件卸载或不再使用实例时必须调用 `destroy()` 释放 Fabric、WebGL、观察器和事件资源
136
-
137
- ```html
138
- <div id="texture-editor"></div>
139
- <div id="product-viewer"></div>
140
- ```
141
-
142
- ```ts
143
- import { createCustomizer } from 'customforge'
144
- import 'customforge/style.css'
145
-
146
- const customizer = await createCustomizer({
147
- editor: '#texture-editor',
148
- viewer: '#product-viewer',
149
- editorWidth: 1024,
150
- editorHeight: 512,
151
- product: {
152
- modelUrl: 'https://example.com/product.glb',
153
- textureUrl: 'https://example.com/base-texture.png',
154
- surfaceMesh: 'PrintArea',
155
- textureFlipY: false,
156
- },
157
- })
158
-
159
- customizer.addText({
160
- text: 'Hello world',
161
- x: 120,
162
- y: 180,
163
- fontSize: 64,
164
- color: '#172126',
165
- })
166
-
167
- await customizer.addImage({
168
- src: 'https://example.com/logo.png',
169
- x: 720,
170
- y: 240,
171
- width: 220,
172
- })
173
-
174
- window.addEventListener('beforeunload', () => customizer.destroy(), {
175
- once: true,
176
- })
177
- ```
178
-
179
- 公开包与本地 `.tgz` 使用相同的根入口和样式入口;需要可重复安装时应固定具体 alpha 版本
180
-
181
- ## 开箱即用的 Workbench
182
-
183
- 不希望从零编写控制界面时,可以使用可选的 Workbench 入口
184
-
185
- 挂载元素必须设置明确高度,设计画布、图层面板和三维区域才能正确计算可用空间
186
-
187
- ```html
188
- <div id="customforge-workbench" style="height: 720px"></div>
189
- ```
190
-
191
- ```ts
192
- import { createWorkbench } from 'customforge/workbench'
193
- import 'customforge/style.css'
194
-
195
- const workbench = await createWorkbench({
196
- container: '#customforge-workbench',
197
- product: {
198
- modelUrl: 'https://example.com/product.glb',
199
- surfaceMesh: 'PrintArea',
200
- },
201
- features: {
202
- presetBackgrounds: true,
203
- presetElements: true,
204
- },
205
- layout: {
206
- header: true,
207
- layers: true,
208
- },
209
- branding: {
210
- logoUrl: '/brand/logo.png',
211
- title: '定制工作室',
212
- subtitle: '产品个性化设计',
213
- },
214
- labels: {
215
- addText: '文字设计',
216
- addImage: '图片素材',
217
- },
218
- theme: {
219
- accent: '#0057b8',
220
- accentHover: '#003f87',
221
- accentContrast: '#ffffff',
222
- },
223
- assets: {
224
- backgrounds: [
225
- { id: 'floral', name: '花卉背景', url: '/presets/floral.png' },
226
- ],
227
- elements: [
228
- { id: 'flower', name: '小花', url: '/presets/flower.png' },
229
- ],
230
- },
231
- })
232
- ```
233
-
234
- 默认使用包内的 CustomForge Logo,可以通过 `branding` 替换或隐藏
235
-
236
- `labels` 用于替换 Workbench 可见文案,`theme` 映射到限定作用域的 CSS 变量,`icons` 可以关闭内置图标或用图片地址替换指定语义图标
237
-
238
- 默认界面字体栈优先使用圆润的 `Nunito Sans`,不可用时回退到系统无衬线字体
239
-
240
- CustomForge 会随制品提供该字体资源,不会在运行时请求第三方字体服务
241
-
242
- 文字和图片命令现在会打开专用 Dialog,不会在点击工具栏按钮后立即修改画布
243
-
244
- - 文字 Dialog 提供文本输入、颜色选择和可替换的排版预设
245
- - 图片 Dialog 提供本地上传、预设背景和装饰元素
246
- - 设计背景会替换已有设计背景、铺满画布、默认锁定在最底层并保存到 Design JSON
247
-
248
- 功能和布局开关也可以在初始化后动态调整
249
-
250
- ```ts
251
- workbench.setFeature('addText', true)
252
- workbench.setLayout('header', true)
253
- ```
254
-
255
- | 功能开关 | 控制内容 |
256
- | --- | --- |
257
- | `addText`、`addImage`、`deleteSelection` | 对象 Dialog 和删除操作 |
258
- | `undoRedo` | 撤销、重做按钮和 Workbench 键盘快捷键 |
259
- | `saveDesign`、`loadDesign` | Design JSON 操作 |
260
- | `loadRemoteProduct`、`exportTexture`、`resetView` | 产品和输出操作 |
261
- | `reorderObjects`、`toggleObjectVisibility`、`lockObjects`、`renameObjects` | 图层管理 |
262
- | `presetBackgrounds`、`presetElements` | 图片 Dialog 中的预设素材页签 |
263
-
264
- | 布局开关 | 控制区域 |
265
- | --- | --- |
266
- | `header` | 品牌和全局产品操作 |
267
- | `editorHeader`、`viewerHeader` | 工作区面板标题栏 |
268
- | `toolbar` | 设计工具栏 |
269
- | `layers` | 对象图层面板 |
270
- | `status` | 运行状态和对象数量 |
271
-
272
- 所有功能和布局开关默认均为 `true`
273
-
274
- 功能开关只控制 Workbench 自带控件,不会移除 `workbench.customizer` 上的底层方法
275
-
276
- 主题配置用于统一的产品级视觉调整,不提供难以维护的逐按钮颜色配置
277
-
278
- ```ts
279
- const workbench = await createWorkbench({
280
- container: '#customforge-workbench',
281
- icons: {
282
- enabled: true,
283
- sources: {
284
- addText: '/icons/typography.svg',
285
- exportTexture: null,
286
- },
287
- },
288
- textPresets: [
289
- {
290
- id: 'brand-display',
291
- name: '品牌展示',
292
- fontFamily: 'Arial',
293
- fontSize: 72,
294
- width: 460,
295
- color: '#17191c',
296
- },
297
- ],
298
- })
299
- ```
300
-
301
- ## Design JSON
302
-
303
- `saveDesign()` 返回由 Library 自身定义、可安全写入 JSON 的文档,不暴露 Fabric.js 序列化格式。`loadDesign()` 校验未知输入,并异步恢复可编辑对象栈:
304
-
305
- ```ts
306
- const design = customizer.saveDesign()
307
- localStorage.setItem('customforge-design', JSON.stringify(design))
308
-
309
- const savedDesign = localStorage.getItem('customforge-design')
310
- if (savedDesign) {
311
- await customizer.loadDesign(JSON.parse(savedDesign))
312
- }
313
- ```
314
-
315
- 当前 Schema 版本为 `1`,文档保存逻辑画布尺寸,并按从后到前的渲染顺序保存文字和图片对象,包括稳定对象 ID、名称、显隐、锁定状态、图片用途与基于中心点的变换
316
-
317
- Design JSON 有意排除产品模型、目标 Mesh 和基础纹理。文档只能加载到逻辑宽高完全相同的编辑器中
318
-
319
- 带有 `role: 'background'` 的图片是设计对象,不是产品基础纹理,只允许存在一个并且必须位于对象数组首位,旧版 v1 文档缺少 `role` 时继续按普通图片元素加载
320
-
321
- 加载具有事务性:只有文档校验通过且全部引用图片成功加载后,当前设计才会被替换。Blob URL 图片会在添加时转换为 Data URL;远程图片仍保留 URL,恢复时必须继续满足浏览器 CORS 要求
322
-
323
- Schema 目前仍属于 alpha 契约,后续 alpha 版本可能调整
324
-
325
- ## 撤销与重做
326
-
327
- 无界面核心和 Workbench 都支持历史记录,默认保留 50 个撤销步骤
328
-
329
- 可以在 `createCustomizer` 或 `createWorkbench` 配置中通过 `historyLimit` 调整保留的撤销深度
330
-
331
- ```ts
332
- if (customizer.canUndo()) {
333
- await customizer.undo()
334
- }
335
-
336
- await customizer.redo()
337
- customizer.clearHistory()
338
-
339
- const stop = customizer.on('historychange', ({ canUndo, canRedo }) => {
340
- console.log({ canUndo, canRedo })
341
- })
342
- ```
343
-
344
- 历史记录覆盖对象添加、删除、画布变换、文字编辑、图层排序、名称、显隐、锁定和 Design JSON 加载
345
-
346
- Workbench 在焦点不位于表单控件时支持 `Ctrl` 或 `Cmd` + `Z`、`Ctrl` 或 `Cmd` + `Shift` + `Z` 和 `Ctrl` + `Y`
347
-
348
- 成功更换产品后会保留当前设计对象,但以当前设计重新开始历史记录
349
-
350
- ## 实例 API
351
-
352
- | 方法 | 说明 |
353
- | --- | --- |
354
- | `addText(options)` | 添加并选中文字,自动约束在画布内 |
355
- | `addImage(options)` | 加载普通图片元素或替换设计背景 |
356
- | `getObjects()` | 按从后到前的图层顺序返回当前对象 |
357
- | `getSelectedObjectIds()` | 返回当前选区中的稳定对象 ID |
358
- | `selectObject(id)` | 根据稳定 ID 选中可见对象 |
359
- | `removeObject(id)` | 根据稳定 ID 删除对象 |
360
- | `moveObject(id, index)` | 将对象移动到从 0 开始的图层索引 |
361
- | `renameObject(id, name)` | 修改图层工具中显示的对象名称 |
362
- | `setObjectVisibility(id, visible)` | 设置对象是否参与渲染 |
363
- | `setObjectLocked(id, locked)` | 锁定或解锁画布变换 |
364
- | `deleteSelected()` | 删除当前对象或选区 |
365
- | `saveDesign()` | 返回当前版本化 Design JSON 文档 |
366
- | `loadDesign(value)` | 校验并以事务方式恢复 Design JSON |
367
- | `canUndo()`、`canRedo()` | 查询当前可用的历史方向 |
368
- | `undo()`、`redo()` | 恢复前一个或后一个设计快照 |
369
- | `clearHistory()` | 将当前设计设为新的历史起点 |
370
- | `loadProduct(product)` | 更换模型、基础纹理和目标 Mesh |
371
- | `exportTexture(filename?)` | 将合成纹理下载为 PNG |
372
- | `resetView()` | 恢复默认三维相机位置 |
373
- | `on(event, listener)` | 订阅实例事件,并返回取消订阅函数 |
374
- | `destroy()` | 释放 DOM 事件、Fabric 状态和 WebGL 资源 |
375
-
376
- 当前事件包括 `ready`、`change`、`selectionchange`、`historychange`、`status` `error`
377
-
378
- 首个 alpha 的稳定候选边界只包括上表方法、`createCustomizer`、`ProductCustomizer`、`customforge/workbench` 入口,以及从已声明入口导出的配置、事件、Workbench Design JSON 类型
379
-
380
- `ProductCustomizer` 不公开 Fabric.js 编辑器和 Three.js 查看器实例,使用方只通过门面 API 操作定制器
381
-
382
- 不支持从 `core`、`editor`、`viewer`、`bridge` 或其他未声明的包子路径导入模块
383
-
384
- ## 架构
385
-
386
- ```mermaid
387
- flowchart LR
388
- A[文字和图片] --> B[DesignEditor]
389
- C[基础纹理] --> B
390
- B --> D[HTML Canvas]
391
- D --> E[TextureBridge]
392
- E --> F[Three.js CanvasTexture]
393
- G[GLB / GLTF 模型] --> H[ProductViewer]
394
- F --> H
395
- H --> I[可定制 Mesh]
396
- ```
397
-
398
- ```text
399
- ProductCustomizer
400
- |-- DesignEditor Fabric.js 渲染和对象交互
401
- |-- ProductViewer Three.js 场景、模型、材质和相机
402
- `-- TextureBridge Canvas 到材质的同步
403
- ```
404
-
405
- `ProductCustomizer` 负责协调各模块,并将 Fabric.js 和 Three.js 的实现细节隐藏在小型实例 API 后面
406
-
407
- 可选 Workbench 和演示界面使用原生 TypeScript,核心不依赖 Vue、React 或其他 UI 框架
408
-
409
- ## 模型约定
410
-
411
- 当前原型要求:
412
-
413
- - GLB 或 GLTF 模型包含有效的 UV 坐标
414
- - 模型未经压缩,能够由标准 Three.js `GLTFLoader` 直接加载
415
- - 模型具有一个用于定制表面的命名 Mesh,例如 `PrintArea`
416
- - 目标纹理位于该 Mesh 的第一个材质槽中
417
- - 浏览器可以在 CORS 策略下访问远程资源地址
418
-
419
- 内置演示同样遵循 `PrintArea` Mesh 命名约定
420
-
421
- ## 项目结构
422
-
423
- ```text
424
- src/
425
- |-- bridge/ Canvas Three.js 纹理同步
426
- |-- core/ 公共类型、配置和 DOM 工具
427
- |-- customizer/ 公共实例编排
428
- |-- demo/ 可运行的工作台界面
429
- |-- editor/ Fabric.js 设计画布和快照历史
430
- |-- style.css Library 公开样式入口
431
- |-- styles/ 核心和 Workbench 样式
432
- |-- viewer/ Three.js 产品预览
433
- |-- workbench/ 可配置界面、Dialog、图标和预设素材
434
- `-- index.ts 与框架无关的源码入口
435
-
436
- examples/
437
- `-- npm-consumer/ 本地 .tgz 独立消费示例
438
-
439
- scripts/
440
- `-- verify-package.mjs npm 文件和制品边界检查
441
- ```
442
-
443
- ## 开发命令
444
-
445
- ```bash
446
- pnpm check # TypeScript 项目检查
447
- pnpm test # 单元测试
448
- pnpm build # 类型检查和生产构建
449
- pnpm build:lib # 构建核心与 Workbench ESM、类型声明和样式
450
- pnpm verify:package # 检查 dist 和 npm 文件清单
451
- pnpm pack:local # 构建、检查并生成本地 .tgz
452
- pnpm release:check # 执行检查、测试、制品构建、校验和本地打包
453
- pnpm preview # 预览生产构建
454
- ```
455
-
456
- ## 当前范围
457
-
458
- 这是一个早期技术原型
459
-
460
- 公共 API 和设计文档格式尚未稳定
461
-
462
- 当前里程碑有意聚焦于一张纹理和一个可定制 Mesh
463
-
464
- API Design JSON 契约进入更稳定阶段前,公开 npm 版本统一使用 `alpha` dist-tag
465
-
466
- 多定制面、高级对齐工具和框架适配器仍未实现
467
-
468
- ## 技术栈
469
-
470
- - [Fabric.js](https://fabricjs.com/):二维编辑画布
471
- - [Three.js](https://threejs.org/):模型加载和实时三维渲染
472
- - [Lucide](https://lucide.dev/):打包到 Workbench 中的界面图标
473
- - [Vite](https://vite.dev/):开发环境和应用构建
474
- - [TypeScript](https://www.typescriptlang.org/):启用严格类型检查
475
-
476
- ## 开源协议
477
-
478
- CustomForge 依据 Apache License 2.0 开源
479
-
480
- 完整协议内容请参阅 [LICENSE](./LICENSE)
481
-
482
- 第三方依赖和资产仍遵循各自的许可条款
483
-
484
- 随包提供的 Nunito Sans 字体遵循 SIL Open Font License 1.1,完整条款见 [NunitoSans-OFL.txt](./LICENSES/NunitoSans-OFL.txt)
26
+
27
+ </div>
28
+
29
+ ---
30
+
31
+ ## 操作演示
32
+
33
+ ![CustomForge 商品定制操作演示](./.github/assets/demo.gif)
34
+
35
+ ## 项目能力
36
+
37
+ CustomForge 将常见的二维设计界面与带 UV 的三维模型连接起来:
38
+
39
+ - 在二维纹理画布上添加和编辑文字
40
+ - 通过 Office 风格的上下文格式栏调整选中文字
41
+ - 上传、移动、缩放和旋转图片
42
+ - 将画布的每次变化实时呈现在三维产品上
43
+ - 上传本地 GLB 模型,或加载远程 GLB/GLTF 模型和可选的基础纹理
44
+ - 通过 Mesh 名称指定可定制表面
45
+ - 将目标 Mesh 的 UV 可打印区域和外边界显示为不会导出的编辑辅助层
46
+ - 使用 OrbitControls 旋转和缩放三维预览
47
+ - 将合成后的纹理导出为 PNG
48
+ - 通过版本化 Design JSON 保存和恢复可编辑对象
49
+ - 通过名称、显隐、锁定和顺序管理对象图层
50
+ - 使用设计快照撤销和重做修改
51
+ - 使用可配置的文字、背景和装饰素材 Dialog
52
+ - 通过品牌、文案、主题变量和图标适配默认 Workbench
53
+
54
+ 工作台默认加载随包提供的 `cup_decal_small_margins.glb`,无需请求外部模型即可直接运行
55
+
56
+ ## 仓库开发
57
+
58
+ 环境要求:Node.js 22+ 和 pnpm 11+
59
+
60
+ ```bash
61
+ pnpm install
62
+ pnpm dev
63
+ ```
64
+
65
+ 打开 Vite 输出的地址
66
+
67
+ 内置演示的左侧是 UV 编辑区,右侧是实时三维预览
68
+
69
+ ## npm 制品
70
+
71
+ CustomForge `0.1.0` 是发布到 npm Registry 的首个非预发布版本。公共 API 遵循语义化版本规则,Design JSON version 1 文档在整个 `0.1.x` 版本线内保持可读取兼容
72
+
73
+ 使用以下命令安装当前版本:
74
+
75
+ ```powershell
76
+ pnpm add customforge
77
+ ```
78
+
79
+ Fabric.js 和 Three.js 会作为传递依赖自动安装
80
+
81
+ pnpm 可能提示可选的原生 `canvas` 构建脚本已被忽略,CustomForge 运行在浏览器中,不使用 Node 原生 canvas,因此不需要执行 `pnpm approve-builds`
82
+
83
+ 消费项目可以在 `package.json` 中明确记录这一浏览器端选择:
84
+
85
+ ```json
86
+ {
87
+ "pnpm": {
88
+ "ignoredBuiltDependencies": ["canvas"]
89
+ }
90
+ }
91
+ ```
92
+
93
+ 包页面:[npmjs.com/package/customforge](https://www.npmjs.com/package/customforge)
94
+
95
+ 浏览器基线:Chrome 和 Edge 111+、Firefox 113+、Safari 16.4+。CustomForge 需要 ES2022 模块、Canvas 2DWebGL、`ResizeObserver`、原生 `dialog`、容器查询和 `color-mix()` 支持
96
+
97
+ 发布前如需进行独立于仓库源码的验证,可在仓库根目录构建并生成本地包:
98
+
99
+ ```powershell
100
+ pnpm pack:local
101
+ ```
102
+
103
+ 命令依次生成 JavaScript、TypeScript 声明和公开样式,检查 npm 文件清单,并创建:
104
+
105
+ ```text
106
+ customforge-0.1.0.tgz
107
+ ```
108
+
109
+ 在独立 Vite TypeScript 项目中安装该本地制品:
110
+
111
+ ```powershell
112
+ pnpm add D:\projects\3DRendering\core_code\customforge-0.1.0.tgz
113
+ ```
114
+
115
+ 仓库中的 `examples/npm-consumer` 提供了一个只通过该 `.tgz` 导入的消费示例。在该目录中使用 `pnpm install --ignore-workspace`,确保 pnpm 将其作为独立于父级 workspace 的项目安装
116
+
117
+ ## 加载你的产品
118
+
119
+ 在演示页面中选择 **Load product**,然后上传一个资源完整的本地 `.glb` 文件,或填写远程 GLB / GLTF 地址。本地 `.gltf` 文件可能依赖独立的二进制文件和纹理,因此不支持直接上传
120
+
121
+ 以下设置位于默认收起的 **Advanced options** 中:
122
+
123
+ | 设置 | 用途 |
124
+ | --- | --- |
125
+ | Base artwork URL | 放在可编辑对象下方,并包含在合成纹理中的可选图片 |
126
+ | Printable mesh name | 接收实时画布纹理的 Mesh 名称,默认为 `PrintArea` |
127
+ | Flip texture vertically | 仅在设计内容显示为上下颠倒时启用 |
128
+
129
+ UV 坐标应当保存在三维模型中
130
+
131
+ 可选的纹理图片只是二维编辑器的基础图层,不能替代模型中的 UV 数据;未提供时,设计画布和可打印贴花层保持透明
132
+
133
+ 每次加载产品后,编辑器会读取目标 Mesh UV 坐标,并在设计画布上方显示轻微的可打印区域底纹及其外边界。内部三角剖分保持隐藏,该辅助层不会写入实时纹理、Design JSON 或导出的 PNG
134
+
135
+ > [!IMPORTANT]
136
+ > 远程模型、纹理、贴图和字体必须提供允许当前页面来源访问的 CORS 响应头
137
+ >
138
+ > 被浏览器阻止或污染 Canvas 的资源可能无法加载,也会导致 PNG 导出失败
139
+
140
+ ## 基本用法
141
+
142
+ 消费方需要完全掌控界面时,应使用 `ProductCustomizerApi` 核心入口。它可以挂载到任意两个 DOM 容器中,不依赖 Workbench 的 DOM 结构或 CSS 类:
143
+
144
+ CustomForge 仅支持浏览器环境,应在 DOM 挂载容器可用后创建实例
145
+
146
+ 编辑器和查看器必须使用两个不同的容器;页面卸载、组件卸载或不再使用实例时必须调用 `destroy()` 释放 Fabric、WebGL、观察器和事件资源
147
+
148
+ ```html
149
+ <div id="texture-editor"></div>
150
+ <div id="product-viewer"></div>
151
+ ```
152
+
153
+ ```ts
154
+ import { createCustomizer } from 'customforge'
155
+ import 'customforge/style.css'
156
+
157
+ const customizer = await createCustomizer({
158
+ editor: '#texture-editor',
159
+ viewer: '#product-viewer',
160
+ editorWidth: 1024,
161
+ editorHeight: 512,
162
+ product: {
163
+ modelUrl: 'https://example.com/product.glb',
164
+ textureUrl: 'https://example.com/base-texture.png',
165
+ surfaceMesh: 'PrintArea',
166
+ textureFlipY: false,
167
+ },
168
+ appearance: {
169
+ editor: {
170
+ controlSize: 7,
171
+ objectBorder: '#0057b8',
172
+ uvBoundary: '#d92d20',
173
+ },
174
+ viewer: { backgroundColor: '#f4f4f5' },
175
+ },
176
+ })
177
+
178
+ customizer.addText({
179
+ text: 'Hello world',
180
+ x: 120,
181
+ y: 180,
182
+ fontSize: 64,
183
+ fontWeight: 'bold',
184
+ textAlign: 'center',
185
+ color: '#172126',
186
+ })
187
+
188
+ await customizer.addImage({
189
+ src: 'https://example.com/logo.png',
190
+ x: 720,
191
+ y: 240,
192
+ width: 220,
193
+ })
194
+
195
+ window.addEventListener('beforeunload', () => customizer.destroy(), {
196
+ once: true,
197
+ })
198
+ ```
199
+
200
+ 公开包与本地 `.tgz` 使用相同的根入口和样式入口;需要可重复安装时应固定具体版本
201
+
202
+ ## 开箱即用的 Workbench
203
+
204
+ 不希望从零编写控制界面时,可以使用可选的 Workbench 入口
205
+
206
+ 挂载元素必须设置明确高度,设计画布、图层面板和三维区域才能正确计算可用空间
207
+
208
+ ```html
209
+ <div id="customforge-workbench" style="height: 720px"></div>
210
+ ```
211
+
212
+ ```ts
213
+ import { createWorkbench } from 'customforge/workbench'
214
+ import 'customforge/style.css'
215
+
216
+ const workbench = await createWorkbench({
217
+ container: '#customforge-workbench',
218
+ className: 'store-customizer',
219
+ product: {
220
+ modelUrl: 'https://example.com/product.glb',
221
+ surfaceMesh: 'PrintArea',
222
+ },
223
+ features: {
224
+ presetBackgrounds: true,
225
+ presetElements: true,
226
+ },
227
+ layout: {
228
+ header: true,
229
+ layers: true,
230
+ },
231
+ branding: {
232
+ logoUrl: '/brand/logo.png',
233
+ title: '定制工作室',
234
+ subtitle: '产品个性化设计',
235
+ },
236
+ labels: {
237
+ addText: '文字设计',
238
+ addImage: '图片素材',
239
+ productDialogTitle: '选择商品模型',
240
+ loadingProduct: '正在加载商品……',
241
+ },
242
+ fontFamilies: [
243
+ { value: 'Arial', label: '无衬线字体' },
244
+ { value: 'Georgia', label: '衬线字体' },
245
+ ],
246
+ formatError: () => '资源加载失败,请检查文件或网络连接',
247
+ theme: {
248
+ accent: '#0057b8',
249
+ accentHover: '#003f87',
250
+ accentContrast: '#ffffff',
251
+ },
252
+ assets: {
253
+ backgrounds: [
254
+ { id: 'floral', name: '花卉背景', url: '/presets/floral.png' },
255
+ ],
256
+ elements: [
257
+ { id: 'flower', name: '小花', url: '/presets/flower.png' },
258
+ ],
259
+ },
260
+ })
261
+ ```
262
+
263
+ 默认使用包内的 CustomForge Logo,可以通过 `branding` 替换或隐藏
264
+
265
+ `labels` 覆盖 Workbench 中所有固定标签、Dialog 文案、占位符、校验提示、状态、颜色名称、下载文件名和无障碍名称。由配置数据生成的可见名称分别通过 `fontFamilies`、`textPresets`、`assets` 和 `branding` 传入,模型、图片、UV 与 Design JSON 异常则通过 `formatError` 转换为面向用户的提示
266
+
267
+ CustomForge 提供默认英文文案,但不接管应用的当前语言状态。宿主项目应从自身 i18n 系统生成配置,并使用当前语言包创建 Workbench,从而避免与 Vue I18n、React Intl 或其他国际化方案耦合
268
+
269
+ 编写完整语言包时可以使用导出的 `WorkbenchLabels` 类型进行字段完整性检查;`WorkbenchOptions.labels` 仍保持为可选字段集合,少量改词时无需重复全部默认值
270
+
271
+ `theme` 映射到限定作用域的 CSS 变量,`icons` 可以关闭内置图标或用图片地址替换指定语义图标
272
+
273
+ `appearance` 用于配置普通 Workbench CSS 无法可靠控制的 Fabric 选择控件、UV 辅助颜色和 WebGL 清屏颜色
274
+
275
+ 默认界面字体栈优先使用圆润的 `Nunito Sans`,不可用时回退到系统无衬线字体
276
+
277
+ CustomForge 会随制品提供该字体资源,不会在运行时请求第三方字体服务
278
+
279
+ 文字和图片命令现在会打开专用 Dialog,不会在点击工具栏按钮后立即修改画布
280
+
281
+ - 文字 Dialog 提供文本输入、颜色选择和可替换的排版预设
282
+ - 选中一个或多个文字对象时,现有单行工具栏会在不推动画布的情况下显示上下文格式控件,用于调整字体、字号、粗体、斜体、下划线、对齐、文字色、高亮色、行高和字距
283
+ - 上下文格式栏支持多选混合状态,并提供明确的画布内文字编辑入口
284
+ - 图片 Dialog 提供本地上传、预设背景和装饰元素
285
+ - 设计背景会替换已有设计背景、铺满当前商品的 UV 可打印边界、默认锁定在最底层并保存到 Design JSON
286
+ - 选中普通图片后可以将其转换为设计背景,自动适配当前 UV 可打印边界并移动到最底层
287
+
288
+ 功能和布局开关也可以在初始化后动态调整
289
+
290
+ ```ts
291
+ workbench.setFeature('addText', true)
292
+ workbench.setLayout('header', true)
293
+ workbench.setTheme({ accent: '#0057b8' })
294
+ ```
295
+
296
+ ### 向 Workbench 扩展业务按钮
297
+
298
+ 业务按钮应通过 `extensions` 注册,不要查询或改写 Workbench 内部 DOM。每个操作都会收到公开的 Workbench 与无界面 customizer 接口、最新状态快照、当前选区,并在需要时收到对应图层:
299
+
300
+ ```ts
301
+ const workbench = await createWorkbench({
302
+ container: '#customforge-workbench',
303
+ className: 'store-customizer',
304
+ extensions: [
305
+ {
306
+ id: 'store.export-png',
307
+ placement: 'globalActions',
308
+ label: t('actions.exportPng'),
309
+ variant: 'primary',
310
+ className: 'store-export-command',
311
+ onClick: async ({ customizer, workbench, signal }) => {
312
+ const blob = await customizer.getTextureBlob()
313
+ if (!signal.aborted) {
314
+ workbench.setStatus(`${blob.size} bytes`)
315
+ }
316
+ },
317
+ },
318
+ {
319
+ id: 'store.straighten',
320
+ placement: 'selectionToolbar',
321
+ label: t('actions.straighten'),
322
+ visible: ({ selection }) => selection.length === 1,
323
+ disabled: ({ selection }) => selection.some(({ locked }) => locked),
324
+ onClick: ({ customizer, selection }) => {
325
+ selection.forEach(({ id }) => {
326
+ customizer.updateObjectTransform(id, { rotation: 0 })
327
+ })
328
+ },
329
+ },
330
+ ],
331
+ })
332
+ ```
333
+
334
+ | 挂载位置 | 适用场景 | 额外上下文 |
335
+ | --- | --- | --- |
336
+ | `globalActions` | 商品、导出或流程级命令 | 标准状态与选区 |
337
+ | `editorToolbar` | 不要求选中对象的设计命令 | 标准状态与选区 |
338
+ | `selectionToolbar` | 作用于当前选区的命令 | 没有选区时自动隐藏 |
339
+ | `layerActions` | 在每个图层重复出现的紧凑命令 | `layer` 为当前行对象 |
340
+
341
+ `visible` 和 `disabled` 可以是布尔值或状态判断函数。核心状态、选区、历史和视角事件发生后会自动重新计算;如果条件还依赖宿主应用状态,应调用 `workbench.refreshExtensions()`。返回 Promise 的操作会自动进入 loading 并禁用防重复点击;被拒绝的 Promise 会通过 `formatError` 转换后显示到现有状态区域。
342
+
343
+ 可以通过 `iconUrl`、`showLabel`、`variant`、`order` 和 `className` 控制表现。扩展按钮文案属于业务数据,应传入当前语言对应的字符串。更深层的 CSS 定制应同时使用 Workbench 的 `className` 和扩展自身的 `className` 限定作用域:
344
+
345
+ ```css
346
+ .store-customizer .store-export-command {
347
+ text-transform: uppercase;
348
+ }
349
+ ```
350
+
351
+ 初始化后也可以管理扩展。注册方法返回的清理函数只会移除由该次注册创建的项目:
352
+
353
+ ```ts
354
+ const unregister = workbench.registerExtension(action)
355
+ const actions = workbench.getExtensions()
356
+ workbench.removeExtension('store.export-png')
357
+ unregister()
358
+ ```
359
+
360
+ `workbench.getFeatures()`、`workbench.getLayout()`、`workbench.getTheme()` `workbench.getExtensions()` 返回互不影响的配置快照。`className` 只添加到当前 Workbench 根元素,宿主 CSS 可以精确作用于某个实例,而不需要依赖全局选择器。需要完全自定义界面时,应使用 `createCustomizer()`,不要查询或改写 Workbench 内部 DOM。
361
+
362
+ | 功能开关 | 控制内容 |
363
+ | --- | --- |
364
+ | `addText`、`addImage`、`deleteSelection` | 对象 Dialog 和删除操作 |
365
+ | `textFormatting` | 上下文文字格式栏 |
366
+ | `undoRedo` | 撤销、重做按钮和 Workbench 键盘快捷键 |
367
+ | `saveDesign`、`loadDesign` | Design JSON 操作 |
368
+ | `loadRemoteProduct`、`resetView` | 产品加载和预览操作 |
369
+ | `reorderObjects`、`toggleObjectVisibility`、`lockObjects`、`renameObjects` | 图层管理 |
370
+ | `presetBackgrounds`、`presetElements` | 图片 Dialog 中的预设素材页签 |
371
+
372
+ | 布局开关 | 控制区域 |
373
+ | --- | --- |
374
+ | `header` | 品牌和全局产品操作 |
375
+ | `editorHeader`、`viewerHeader` | 工作区面板标题栏 |
376
+ | `toolbar` | 设计工具栏 |
377
+ | `layers` | 对象图层面板 |
378
+ | `status` | 运行状态和对象数量 |
379
+
380
+ 所有功能和布局开关默认均为 `true`
381
+
382
+ 功能开关只控制 Workbench 自带控件,不会移除 `workbench.customizer` 上的底层方法。Workbench 顶栏不再提供 PNG 导出入口,仍可通过 `workbench.customizer.exportTexture()` 使用底层导出能力
383
+
384
+ 主题配置用于统一的产品级视觉调整,不提供难以维护的逐按钮颜色配置
385
+
386
+ ```ts
387
+ const workbench = await createWorkbench({
388
+ container: '#customforge-workbench',
389
+ icons: {
390
+ enabled: true,
391
+ sources: {
392
+ addText: '/icons/typography.svg',
393
+ loadProduct: '/icons/upload.svg',
394
+ },
395
+ },
396
+ textPresets: [
397
+ {
398
+ id: 'brand-display',
399
+ name: '品牌展示',
400
+ fontFamily: 'Arial',
401
+ fontSize: 72,
402
+ width: 460,
403
+ color: '#17191c',
404
+ },
405
+ ],
406
+ })
407
+ ```
408
+
409
+ ## Design JSON
410
+
411
+ `saveDesign()` 返回由 Library 自身定义、可安全写入 JSON 的文档,不暴露 Fabric.js 序列化格式。`loadDesign()` 校验未知输入,并异步恢复可编辑对象栈:
412
+
413
+ ```ts
414
+ const design = customizer.saveDesign()
415
+ localStorage.setItem('customforge-design', JSON.stringify(design))
416
+
417
+ const savedDesign = localStorage.getItem('customforge-design')
418
+ if (savedDesign) {
419
+ await customizer.loadDesign(JSON.parse(savedDesign))
420
+ }
421
+ ```
422
+
423
+ 当前 Schema 版本为 `1`,文档保存逻辑画布尺寸,并按从后到前的渲染顺序保存文字和图片对象,包括稳定对象 ID、名称、显隐、锁定状态、图片用途、基于中心点的变换和丰富的文字排版属性。缺少可选排版字段的旧版 version 1 文档仍按原有的粗体、居中默认值加载
424
+
425
+ Design JSON 有意排除产品模型、目标 Mesh 和基础纹理。文档只能加载到逻辑宽高完全相同的编辑器中
426
+
427
+ 带有 `role: 'background'` 的图片是设计对象,不是产品基础纹理,只允许存在一个并且必须位于对象数组首位,旧版 v1 文档缺少 `role` 时继续按普通图片元素加载
428
+
429
+ 加载具有事务性:只有文档校验通过且全部引用图片成功加载后,当前设计才会被替换。Blob URL 图片会在添加时转换为 Data URL;远程图片仍保留 URL,恢复时必须继续满足浏览器 CORS 要求
430
+
431
+ Design JSON version 1 在整个 `0.1.x` 版本线内保持可读取兼容。后续可以增加可选字段,但不会让现有 version 1 文档失效
432
+
433
+ ## 撤销与重做
434
+
435
+ 无界面核心和 Workbench 都支持历史记录,默认保留 50 个撤销步骤
436
+
437
+ 可以在 `createCustomizer` `createWorkbench` 配置中通过 `historyLimit` 调整保留的撤销深度
438
+
439
+ ```ts
440
+ if (customizer.canUndo()) {
441
+ await customizer.undo()
442
+ }
443
+
444
+ await customizer.redo()
445
+ customizer.clearHistory()
446
+
447
+ const stop = customizer.on('historychange', ({ canUndo, canRedo }) => {
448
+ console.log({ canUndo, canRedo })
449
+ })
450
+ ```
451
+
452
+ 历史记录覆盖对象添加、删除、画布变换、文字内容与格式编辑、图片转背景、图层排序、名称、显隐、锁定和 Design JSON 加载
453
+
454
+ Workbench 在焦点不位于表单控件时支持 `Ctrl` 或 `Cmd` + `Z`、`Ctrl` 或 `Cmd` + `Shift` + `Z` 和 `Ctrl` + `Y`
455
+
456
+ 成功更换产品后会保留当前设计对象,但以当前设计重新开始历史记录
457
+
458
+ ## 实例 API
459
+
460
+ | 方法 | 说明 |
461
+ | --- | --- |
462
+ | `addText(options)` | 添加并选中文字并自动约束在画布内;`fontSize` 默认为 `22` |
463
+ | `addImage(options)` | 加载普通图片元素或替换设计背景 |
464
+ | `getState()` | 一次返回商品、画布、可打印边界、对象、选区、历史和三维视角快照 |
465
+ | `getProduct()` | 返回补全默认值后的当前商品配置 |
466
+ | `getCanvasSize()` | 返回逻辑纹理尺寸 |
467
+ | `getPrintableBounds()` | 返回逻辑画布坐标中的 UV 可打印边界 |
468
+ | `getObjects()` | 按从后到前的图层顺序返回当前对象 |
469
+ | `getSelectedObjectIds()` | 返回当前选区中的稳定对象 ID |
470
+ | `selectObject(id)` | 根据稳定 ID 选中可见对象 |
471
+ | `selectObjects(ids)` | 根据稳定 ID 多选可见对象 |
472
+ | `clearSelection()` | 清除当前画布选区且不修改设计内容 |
473
+ | `removeObject(id)` | 根据稳定 ID 删除对象 |
474
+ | `moveObject(id, index)` | 将对象移动到从 0 开始的图层索引 |
475
+ | `setImageAsBackground(id)` | 使用现有图片替换设计背景并自动适配当前 UV 可打印边界 |
476
+ | `renameObject(id, name)` | 修改图层工具中显示的对象名称 |
477
+ | `setObjectVisibility(id, visible)` | 设置对象是否参与渲染 |
478
+ | `setObjectLocked(id, locked)` | 锁定或解锁画布变换 |
479
+ | `updateObjectTransform(id, options)` | 根据稳定 ID 修改中心位置、缩放、旋转和翻转 |
480
+ | `updateText(id, options)` | 按稳定 ID 修改文字内容和排版样式 |
481
+ | `editText(id)` | 让未锁定文字进入画布内编辑状态 |
482
+ | `deleteSelected()` | 删除当前对象或选区 |
483
+ | `saveDesign()` | 返回当前版本化 Design JSON 文档 |
484
+ | `loadDesign(value)` | 校验并以事务方式恢复 Design JSON |
485
+ | `canUndo()`、`canRedo()` | 查询当前可用的历史方向 |
486
+ | `undo()`、`redo()` | 恢复前一个或后一个设计快照 |
487
+ | `clearHistory()` | 将当前设计设为新的历史起点 |
488
+ | `loadProduct(product)` | 更换模型、基础纹理和目标 Mesh |
489
+ | `getTextureDataUrl()` | 以 PNG Data URL 返回合成纹理 |
490
+ | `getTextureBlob()` | 以 PNG Blob 返回合成纹理 |
491
+ | `exportTexture(filename?)` | 将合成纹理下载为 PNG |
492
+ | `getViewState()`、`setViewState(state)` | 读取或恢复三维相机位置和观察目标点 |
493
+ | `resetView()` | 恢复默认三维相机位置 |
494
+ | `on(event, listener)` | 订阅实例事件,并返回取消订阅函数 |
495
+ | `destroy()` | 释放 DOM 事件、Fabric 状态和 WebGL 资源 |
496
+
497
+ 当前事件包括 `ready`、`change`、`selectionchange`、`historychange`、`viewchange`、`status` 和 `error`
498
+
499
+ 无界面能力契约导出为 `ProductCustomizerApi`,默认界面能力契约导出为 `CustomForgeWorkbenchApi`。具体类仍然保留,但消费方可以只依赖接口,避免业务代码与实现细节耦合。
500
+
501
+ `ProductCustomizer` 不公开 Fabric.js 编辑器和 Three.js 查看器实例,使用方只通过门面 API 操作定制器
502
+
503
+ 不支持从 `core`、`editor`、`viewer`、`bridge` 或其他未声明的包子路径导入模块
504
+
505
+ ## 架构
506
+
507
+ ```mermaid
508
+ flowchart LR
509
+ A[文字和图片] --> B[DesignEditor]
510
+ C[基础纹理] --> B
511
+ B --> D[HTML Canvas]
512
+ D --> E[TextureBridge]
513
+ E --> F[Three.js CanvasTexture]
514
+ G[GLB / GLTF 模型] --> H[ProductViewer]
515
+ F --> H
516
+ H --> I[可打印 Mesh]
517
+ ```
518
+
519
+ ```text
520
+ ProductCustomizer
521
+ |-- DesignEditor Fabric.js 渲染和对象交互
522
+ |-- ProductViewer Three.js 场景、模型、材质和相机
523
+ `-- TextureBridge Canvas 到材质的同步
524
+ ```
525
+
526
+ `ProductCustomizer` 负责协调各模块,并将 Fabric.js 和 Three.js 的实现细节隐藏在小型实例 API 后面
527
+
528
+ 可选 Workbench 和演示界面使用原生 TypeScript,核心不依赖 Vue、React 或其他 UI 框架
529
+
530
+ ## 模型约定
531
+
532
+ 当前模型约定要求:
533
+
534
+ - GLB 或 GLTF 模型包含有效的 UV 坐标
535
+ - 模型未经压缩,能够由标准 Three.js `GLTFLoader` 直接加载
536
+ - 模型具有一个用于定制表面的命名 Mesh,例如 `PrintArea`
537
+ - 目标纹理位于该 Mesh 的第一个材质槽中
538
+ - 浏览器可以在 CORS 策略下访问远程资源地址
539
+
540
+ 随包提供的默认模型将完整杯体保存为 `MugBody`,并将带 UV 的可打印贴花层保存为 `PrintArea`。CustomForge 会保留杯体材质,只把实时设计纹理应用到 `PrintArea`
541
+
542
+ ## 项目结构
543
+
544
+ ```text
545
+ src/
546
+ |-- bridge/ Canvas 与 Three.js 纹理同步
547
+ |-- core/ 公共类型、配置和 DOM 工具
548
+ |-- customizer/ 公共实例编排
549
+ |-- demo/ 可运行的工作台界面
550
+ |-- editor/ Fabric.js 设计画布和快照历史
551
+ |-- style.css Library 公开样式入口
552
+ |-- styles/ 核心和 Workbench 样式
553
+ |-- viewer/ Three.js 产品预览
554
+ |-- workbench/ 可配置界面、Dialog、图标和预设素材
555
+ `-- index.ts 与框架无关的源码入口
556
+
557
+ examples/
558
+ |-- api-contract-consumer/ 公共 API、自定义界面与浏览器 smoke 消费示例
559
+ `-- npm-consumer/ 本地 .tgz 独立消费示例
560
+
561
+ scripts/
562
+ |-- run-browser-smoke.mjs 无头浏览器契约门禁
563
+ |-- verify-consumers.mjs 独立 tarball 消费门禁
564
+ |-- verify-package.mjs npm 文件和制品边界检查
565
+ `-- verify-release.mjs 版本、变更日志、标签和示例检查
566
+ ```
567
+
568
+ ## 开发命令
569
+
570
+ ```bash
571
+ pnpm check # TypeScript 项目检查
572
+ pnpm test # 单元测试
573
+ pnpm build # 类型检查和生产构建
574
+ pnpm build:lib # 构建核心与 Workbench ESM、类型声明和样式
575
+ pnpm verify:release # 检查稳定版本、变更日志、标签和消费路径
576
+ pnpm verify:package # 检查 dist 和 npm 文件清单
577
+ pnpm verify:consumers # 在两个独立消费项目中安装并构建本地 .tgz
578
+ pnpm test:browser # 在无头 Chrome 中运行打包后的 API 消费示例
579
+ pnpm pack:local # 通过 prepack 构建、检查并生成本地 .tgz
580
+ pnpm release:check # 执行完整的稳定版发布门禁
581
+ pnpm preview # 预览生产构建
582
+ ```
583
+
584
+ npm Trusted Publishing 配置、标签规则、发布步骤和失败处理参见[发布指南](https://github.com/songshanliu/CustomForge/blob/main/RELEASING.md)
585
+
586
+ ## 当前范围
587
+
588
+ `0.1.0` 是首个非预发布制品。项目仍处于 `0.x` 版本线,公共 API 的破坏性调整只会进入后续次版本,并记录在变更日志中
589
+
590
+ 当前版本聚焦于一张逻辑设计画布映射到一个命名 Mesh 的第一个材质槽。Design JSON version 1 文档在 `0.1.x` 内保持可读取兼容
591
+
592
+ 多定制面、高级对齐工具和框架适配器仍未实现
593
+
594
+ ## 技术栈
595
+
596
+ - [Fabric.js](https://fabricjs.com/):二维编辑画布
597
+ - [Three.js](https://threejs.org/):模型加载和实时三维渲染
598
+ - [Lucide](https://lucide.dev/):打包到 Workbench 中的界面图标
599
+ - [Vite](https://vite.dev/):开发环境和应用构建
600
+ - [TypeScript](https://www.typescriptlang.org/):启用严格类型检查
601
+
602
+ ## 开源协议
603
+
604
+ CustomForge 依据 Apache License 2.0 开源
605
+
606
+ 完整协议内容请参阅 [LICENSE](./LICENSE)
607
+
608
+ 第三方依赖和资产仍遵循各自的许可条款
609
+
610
+ 随包提供的 Nunito Sans 字体遵循 SIL Open Font License 1.1,完整条款见 [NunitoSans-OFL.txt](./LICENSES/NunitoSans-OFL.txt)
611
+
612
+ 随包提供的 Plain Mug 模型由 LightSwitch 创作并采用 CC BY 4.0,署名和来源信息见 [plain-mug-CC-BY-4.0.txt](./LICENSES/plain-mug-CC-BY-4.0.txt)