customforge 0.1.0-alpha.2 → 0.1.1

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 +91 -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-BnjY3v7d.js} +898 -156
  7. package/dist/ProductCustomizer-BnjY3v7d.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/dist/workbench.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as resolveElement, t as ProductCustomizer } from "./ProductCustomizer-rMRyWe7L.js";
1
+ import { n as resolveElement, t as ProductCustomizer } from "./ProductCustomizer-BnjY3v7d.js";
2
2
  //#endregion
3
3
  //#region src/workbench/assets/catalog.ts
4
4
  /**
@@ -19,13 +19,13 @@ var builtInAssetLibrary = {
19
19
  //#region src/workbench/config.ts
20
20
  var defaultFeatures = {
21
21
  addText: true,
22
+ textFormatting: true,
22
23
  addImage: true,
23
24
  undoRedo: true,
24
25
  deleteSelection: true,
25
26
  saveDesign: true,
26
27
  loadDesign: true,
27
28
  loadRemoteProduct: true,
28
- exportTexture: true,
29
29
  resetView: true,
30
30
  reorderObjects: true,
31
31
  toggleObjectVisibility: true,
@@ -46,17 +46,35 @@ var defaultLabels = {
46
46
  workbench: "Product customization studio",
47
47
  editorTitle: "Design surface",
48
48
  viewerTitle: "Product preview",
49
+ editorMode: "2D",
50
+ viewerMode: "3D",
51
+ editorCanvas: "UV texture editor",
52
+ viewerCanvas: "Interactive 3D product preview",
49
53
  layers: "Layers",
50
54
  noObjects: "No design objects",
51
55
  addText: "Text",
56
+ textFormatting: "Text formatting",
57
+ moreTextOptions: "More text options",
58
+ editText: "Edit text",
59
+ fontFamily: "Font",
60
+ fontSize: "Font size",
61
+ bold: "Bold",
62
+ italic: "Italic",
63
+ underline: "Underline",
64
+ alignLeft: "Align left",
65
+ alignCenter: "Align center",
66
+ alignRight: "Align right",
67
+ textBackground: "Text highlight",
68
+ lineHeight: "Line height",
69
+ letterSpacing: "Letter spacing",
52
70
  addImage: "Image",
71
+ setImageAsBackground: "Set as background",
53
72
  undo: "Undo",
54
73
  redo: "Redo",
55
74
  saveDesign: "Save design JSON",
56
75
  loadDesign: "Open design JSON",
57
76
  deleteSelection: "Delete selected object",
58
77
  loadProduct: "Load product",
59
- exportTexture: "Export PNG",
60
78
  resetView: "Reset 3D view",
61
79
  textObject: "Text",
62
80
  imageObject: "Image",
@@ -72,8 +90,25 @@ var defaultLabels = {
72
90
  textDialogTitle: "Add text",
73
91
  textInputLabel: "Text",
74
92
  textInputPlaceholder: "Make it yours",
93
+ defaultText: "Edit this text",
94
+ textRequired: "Enter text",
75
95
  textPresets: "Style",
76
96
  textColor: "Color",
97
+ colorBlack: "Black",
98
+ colorSlate: "Slate",
99
+ colorGray: "Gray",
100
+ colorWhite: "White",
101
+ colorRed: "Red",
102
+ colorOrange: "Orange",
103
+ colorYellow: "Yellow",
104
+ colorGreen: "Green",
105
+ colorTeal: "Teal",
106
+ colorCyan: "Cyan",
107
+ colorBlue: "Blue",
108
+ colorPurple: "Purple",
109
+ colorPink: "Pink",
110
+ colorBrown: "Brown",
111
+ mixedValue: "—",
77
112
  addTextConfirm: "Add text",
78
113
  imageDialogEyebrow: "Artwork",
79
114
  imageDialogTitle: "Add image",
@@ -87,43 +122,69 @@ var defaultLabels = {
87
122
  close: "Close",
88
123
  cancel: "Cancel",
89
124
  productDialogEyebrow: "Product source",
90
- productDialogTitle: "Load remote product",
125
+ productDialogTitle: "Load product",
126
+ productSourceMethod: "Model source",
127
+ productFileSource: "Upload GLB",
128
+ productUrlSource: "From URL",
129
+ modelFile: "GLB file",
130
+ chooseModelFile: "Choose GLB file",
131
+ noModelFileSelected: "No file selected",
132
+ invalidModelFile: "Choose a .glb file",
133
+ modelFileRequired: "Choose a GLB file",
91
134
  modelUrl: "GLB or GLTF URL",
92
- textureUrl: "Base texture URL",
93
- surfaceMesh: "Customizable mesh",
135
+ modelUrlPlaceholder: "https://example.com/product.glb",
136
+ modelUrlRequired: "Enter a model URL",
137
+ invalidModelUrl: "Enter a valid model URL",
138
+ advancedProductOptions: "Advanced options",
139
+ textureUrl: "Base artwork URL (optional)",
140
+ textureUrlPlaceholder: "https://example.com/artwork.png",
141
+ invalidTextureUrl: "Enter a valid artwork URL",
142
+ textureUrlHint: "Placed beneath all editable objects",
143
+ surfaceMesh: "Printable mesh name",
144
+ surfaceMeshRequired: "Enter a printable mesh name",
145
+ surfaceMeshHint: "The model mesh that receives the design; defaults to PrintArea",
94
146
  flipTexture: "Flip texture vertically",
147
+ flipTextureHint: "Only enable this when the design appears upside down",
95
148
  useDemo: "Use built-in demo",
149
+ addingImage: "Adding image",
150
+ loadingDesign: "Loading design",
151
+ loadingProduct: "Loading product...",
152
+ undoing: "Undoing",
153
+ redoing: "Redoing",
96
154
  starting: "Starting",
97
155
  productReady: "Product ready",
98
156
  designSaved: "Design saved",
157
+ textAdded: "Text added",
158
+ imageAdded: "Image added",
99
159
  designLoaded: "Design loaded",
100
160
  undoComplete: "Undo complete",
101
161
  redoComplete: "Redo complete",
102
162
  objectCountOne: "{count} object",
103
- objectCountMany: "{count} objects"
163
+ objectCountMany: "{count} objects",
164
+ designFilename: "customforge-design.json"
104
165
  };
105
166
  var defaultTheme = {
106
- ink: "#24272b",
107
- muted: "#747b84",
108
- border: "#e1e4e8",
167
+ ink: "#18181b",
168
+ muted: "#71717a",
169
+ border: "#e4e4e7",
109
170
  surface: "#ffffff",
110
- surfaceMuted: "#f5f6f8",
111
- stage: "#f1f3f5",
112
- accent: "#0875c9",
113
- accentHover: "#075fa3",
171
+ surfaceMuted: "#fafafa",
172
+ stage: "#f4f4f5",
173
+ accent: "#268a4b",
174
+ accentHover: "#1f7640",
114
175
  accentContrast: "#ffffff",
115
176
  danger: "#b42318",
116
177
  fontFamily: "\"Nunito Sans\", \"Avenir Next\", \"Segoe UI Variable\", \"Segoe UI\", ui-sans-serif, system-ui, sans-serif",
117
- controlRadius: "7px"
178
+ controlRadius: "4px"
118
179
  };
119
180
  var defaultTextPresets = [
120
181
  {
121
182
  id: "modern-bold",
122
183
  name: "Modern bold",
123
- previewText: "MAKE IT YOURS",
184
+ previewText: "CustomForge",
124
185
  fontFamily: "Arial",
125
- fontSize: 68,
126
- width: 460,
186
+ fontSize: 22,
187
+ width: 220,
127
188
  color: "#17191c"
128
189
  },
129
190
  {
@@ -154,6 +215,22 @@ var defaultTextPresets = [
154
215
  color: "#7a3e2f"
155
216
  }
156
217
  ];
218
+ var defaultFontFamilies = [
219
+ "Arial",
220
+ "Georgia",
221
+ "Trebuchet MS",
222
+ "Verdana",
223
+ "Times New Roman",
224
+ "Courier New",
225
+ "Brush Script MT",
226
+ "Nunito Sans"
227
+ ].map((fontFamily) => ({
228
+ value: fontFamily,
229
+ label: fontFamily
230
+ }));
231
+ function defaultErrorFormatter(error) {
232
+ return error instanceof Error ? error.message : String(error);
233
+ }
157
234
  function positiveInteger(value, fallback, name) {
158
235
  const resolved = value ?? fallback;
159
236
  if (!Number.isInteger(resolved) || resolved < 1) throw new RangeError(`${name} must be a positive integer`);
@@ -193,6 +270,89 @@ function uniqueTextPresets(presets) {
193
270
  };
194
271
  });
195
272
  }
273
+ function normalizeClassNames(value) {
274
+ return [...new Set(value?.trim().split(/\s+/).filter(Boolean) ?? [])];
275
+ }
276
+ var extensionPlacements = /* @__PURE__ */ new Set([
277
+ "globalActions",
278
+ "editorToolbar",
279
+ "selectionToolbar",
280
+ "layerActions"
281
+ ]);
282
+ var extensionVariants = /* @__PURE__ */ new Set([
283
+ "primary",
284
+ "secondary",
285
+ "plain",
286
+ "danger"
287
+ ]);
288
+ function normalizeExtensionPredicate(value, fallback, field) {
289
+ if (value === void 0) return fallback;
290
+ if (typeof value !== "boolean" && typeof value !== "function") throw new TypeError(`Extension ${field} must be a boolean or function`);
291
+ return value;
292
+ }
293
+ /**
294
+ * 校验并补全单个 Workbench 扩展按钮配置
295
+ *
296
+ * @param extension 外部扩展按钮配置
297
+ * @returns 可直接渲染的内部配置
298
+ */
299
+ function normalizeWorkbenchExtension(extension) {
300
+ const id = extension.id.trim();
301
+ const label = extension.label.trim();
302
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) throw new TypeError("Extension id must start with an alphanumeric character and contain only letters, numbers, dots, underscores, or hyphens");
303
+ if (!extensionPlacements.has(extension.placement)) throw new TypeError(`Extension ${id} has an invalid placement`);
304
+ if (!label) throw new TypeError(`Extension ${id} requires a label`);
305
+ if (typeof extension.onClick !== "function") throw new TypeError(`Extension ${id} requires an onClick function`);
306
+ const iconUrl = extension.iconUrl?.trim() || void 0;
307
+ const showLabel = extension.showLabel ?? (extension.placement !== "layerActions" || iconUrl === void 0);
308
+ if (!showLabel && !iconUrl) throw new TypeError(`Extension ${id} must provide iconUrl when showLabel is false`);
309
+ const variant = extension.variant ?? (extension.placement === "globalActions" ? "secondary" : "plain");
310
+ if (!extensionVariants.has(variant)) throw new TypeError(`Extension ${id} has an invalid variant`);
311
+ const order = extension.order ?? 0;
312
+ if (!Number.isFinite(order)) throw new TypeError(`Extension ${id} order must be a finite number`);
313
+ return {
314
+ id,
315
+ placement: extension.placement,
316
+ label,
317
+ iconUrl,
318
+ showLabel,
319
+ variant,
320
+ order,
321
+ classNames: normalizeClassNames(extension.className),
322
+ visible: normalizeExtensionPredicate(extension.visible, true, "visible"),
323
+ disabled: normalizeExtensionPredicate(extension.disabled, false, "disabled"),
324
+ onClick: extension.onClick
325
+ };
326
+ }
327
+ /**
328
+ * 校验 Workbench 扩展按钮集合并拒绝重复 ID
329
+ *
330
+ * @param extensions 外部扩展按钮集合
331
+ * @returns 保持声明顺序的完整内部配置
332
+ */
333
+ function normalizeWorkbenchExtensions(extensions) {
334
+ const ids = /* @__PURE__ */ new Set();
335
+ return extensions.map((extension) => {
336
+ const normalized = normalizeWorkbenchExtension(extension);
337
+ if (ids.has(normalized.id)) throw new TypeError(`Extension id is duplicated: ${normalized.id}`);
338
+ ids.add(normalized.id);
339
+ return normalized;
340
+ });
341
+ }
342
+ function uniqueFontFamilies(fonts) {
343
+ const values = /* @__PURE__ */ new Set();
344
+ return fonts.map((font) => {
345
+ const value = font.value.trim();
346
+ const label = font.label.trim();
347
+ if (!value || !label) throw new TypeError("Font families require value and label");
348
+ if (values.has(value)) throw new TypeError(`Font family value is duplicated: ${value}`);
349
+ values.add(value);
350
+ return {
351
+ value,
352
+ label
353
+ };
354
+ });
355
+ }
196
356
  function normalizeBranding(branding) {
197
357
  return {
198
358
  logoUrl: branding?.logoUrl?.trim() || "data:image/png;base64,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",
@@ -212,10 +372,12 @@ function normalizeBranding(branding) {
212
372
  */
213
373
  function normalizeWorkbenchOptions(options) {
214
374
  return {
375
+ classNames: normalizeClassNames(options.className),
215
376
  editorWidth: positiveInteger(options.editorWidth, 1024, "editorWidth"),
216
377
  editorHeight: positiveInteger(options.editorHeight, 512, "editorHeight"),
217
378
  historyLimit: positiveInteger(options.historyLimit, 50, "historyLimit"),
218
379
  product: options.product,
380
+ appearance: options.appearance,
219
381
  features: {
220
382
  ...defaultFeatures,
221
383
  ...options.features
@@ -238,8 +400,11 @@ function normalizeWorkbenchOptions(options) {
238
400
  sources: { ...options.icons?.sources }
239
401
  },
240
402
  textPresets: uniqueTextPresets(options.textPresets ?? defaultTextPresets),
403
+ fontFamilies: uniqueFontFamilies(options.fontFamilies ?? defaultFontFamilies),
241
404
  backgrounds: uniqueAssets(options.assets?.backgrounds ?? builtInAssetLibrary.backgrounds, "Background"),
242
- elements: uniqueAssets(options.assets?.elements ?? builtInAssetLibrary.elements, "Element")
405
+ elements: uniqueAssets(options.assets?.elements ?? builtInAssetLibrary.elements, "Element"),
406
+ extensions: normalizeWorkbenchExtensions(options.extensions ?? []),
407
+ formatError: options.formatError ?? defaultErrorFormatter
243
408
  };
244
409
  }
245
410
  //#endregion
@@ -393,17 +558,47 @@ var ArrowDown = [["path", { d: "M12 5v14" }], ["path", { d: "m19 12-7 7-7-7" }]]
393
558
  */
394
559
  var ArrowUp = [["path", { d: "m5 12 7-7 7 7" }], ["path", { d: "M12 19V5" }]];
395
560
  //#endregion
396
- //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/download.mjs
561
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/bold.mjs
397
562
  /**
398
563
  * @license lucide v1.34.0 - ISC
399
564
  *
400
565
  * This source code is licensed under the ISC license.
401
566
  * See the LICENSE file in the root directory of this source tree.
402
567
  */
403
- var Download = [
404
- ["path", { d: "M12 15V3" }],
405
- ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
406
- ["path", { d: "m7 10 5 5 5-5" }]
568
+ var Bold = [["path", { d: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" }]];
569
+ //#endregion
570
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/chevron-down.mjs
571
+ /**
572
+ * @license lucide v1.34.0 - ISC
573
+ *
574
+ * This source code is licensed under the ISC license.
575
+ * See the LICENSE file in the root directory of this source tree.
576
+ */
577
+ var ChevronDown = [["path", { d: "m6 9 6 6 6-6" }]];
578
+ //#endregion
579
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/ellipsis.mjs
580
+ /**
581
+ * @license lucide v1.34.0 - ISC
582
+ *
583
+ * This source code is licensed under the ISC license.
584
+ * See the LICENSE file in the root directory of this source tree.
585
+ */
586
+ var Ellipsis = [
587
+ ["circle", {
588
+ cx: "12",
589
+ cy: "12",
590
+ r: "1"
591
+ }],
592
+ ["circle", {
593
+ cx: "19",
594
+ cy: "12",
595
+ r: "1"
596
+ }],
597
+ ["circle", {
598
+ cx: "5",
599
+ cy: "12",
600
+ r: "1"
601
+ }]
407
602
  ];
408
603
  //#endregion
409
604
  //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/eye-off.mjs
@@ -442,6 +637,15 @@ var Eye = [["path", { d: "M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.87
442
637
  */
443
638
  var FolderOpen = [["path", { d: "m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" }]];
444
639
  //#endregion
640
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/highlighter.mjs
641
+ /**
642
+ * @license lucide v1.34.0 - ISC
643
+ *
644
+ * This source code is licensed under the ISC license.
645
+ * See the LICENSE file in the root directory of this source tree.
646
+ */
647
+ var Highlighter = [["path", { d: "m9 11-6 6v3h9l3-3" }], ["path", { d: "m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" }]];
648
+ //#endregion
445
649
  //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/image-plus.mjs
446
650
  /**
447
651
  * @license lucide v1.34.0 - ISC
@@ -485,6 +689,34 @@ var Image = [
485
689
  ["path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" }]
486
690
  ];
487
691
  //#endregion
692
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/italic.mjs
693
+ /**
694
+ * @license lucide v1.34.0 - ISC
695
+ *
696
+ * This source code is licensed under the ISC license.
697
+ * See the LICENSE file in the root directory of this source tree.
698
+ */
699
+ var Italic = [
700
+ ["line", {
701
+ x1: "19",
702
+ x2: "10",
703
+ y1: "4",
704
+ y2: "4"
705
+ }],
706
+ ["line", {
707
+ x1: "14",
708
+ x2: "5",
709
+ y1: "20",
710
+ y2: "20"
711
+ }],
712
+ ["line", {
713
+ x1: "15",
714
+ x2: "9",
715
+ y1: "4",
716
+ y2: "20"
717
+ }]
718
+ ];
719
+ //#endregion
488
720
  //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/layers.mjs
489
721
  /**
490
722
  * @license lucide v1.34.0 - ISC
@@ -548,6 +780,41 @@ var Lock = [["rect", {
548
780
  ry: "2"
549
781
  }], ["path", { d: "M7 11V7a5 5 0 0 1 10 0v4" }]];
550
782
  //#endregion
783
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/palette.mjs
784
+ /**
785
+ * @license lucide v1.34.0 - ISC
786
+ *
787
+ * This source code is licensed under the ISC license.
788
+ * See the LICENSE file in the root directory of this source tree.
789
+ */
790
+ var Palette = [
791
+ ["path", { d: "M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" }],
792
+ ["circle", {
793
+ cx: "13.5",
794
+ cy: "6.5",
795
+ r: ".5",
796
+ fill: "currentColor"
797
+ }],
798
+ ["circle", {
799
+ cx: "17.5",
800
+ cy: "10.5",
801
+ r: ".5",
802
+ fill: "currentColor"
803
+ }],
804
+ ["circle", {
805
+ cx: "6.5",
806
+ cy: "12.5",
807
+ r: ".5",
808
+ fill: "currentColor"
809
+ }],
810
+ ["circle", {
811
+ cx: "8.5",
812
+ cy: "7.5",
813
+ r: ".5",
814
+ fill: "currentColor"
815
+ }]
816
+ ];
817
+ //#endregion
551
818
  //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/redo-2.mjs
552
819
  /**
553
820
  * @license lucide v1.34.0 - ISC
@@ -566,6 +833,25 @@ var Redo2 = [["path", { d: "m15 14 5-5-5-5" }], ["path", { d: "M20 9H9.5A5.5 5.5
566
833
  */
567
834
  var RotateCcw = [["path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }], ["path", { d: "M3 3v5h5" }]];
568
835
  //#endregion
836
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/rows-3.mjs
837
+ /**
838
+ * @license lucide v1.34.0 - ISC
839
+ *
840
+ * This source code is licensed under the ISC license.
841
+ * See the LICENSE file in the root directory of this source tree.
842
+ */
843
+ var Rows3 = [
844
+ ["rect", {
845
+ width: "18",
846
+ height: "18",
847
+ x: "3",
848
+ y: "3",
849
+ rx: "2"
850
+ }],
851
+ ["path", { d: "M21 9H3" }],
852
+ ["path", { d: "M21 15H3" }]
853
+ ];
854
+ //#endregion
569
855
  //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/save.mjs
570
856
  /**
571
857
  * @license lucide v1.34.0 - ISC
@@ -579,6 +865,81 @@ var Save = [
579
865
  ["path", { d: "M7 3v4a1 1 0 0 0 1 1h7" }]
580
866
  ];
581
867
  //#endregion
868
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/stretch-horizontal.mjs
869
+ /**
870
+ * @license lucide v1.34.0 - ISC
871
+ *
872
+ * This source code is licensed under the ISC license.
873
+ * See the LICENSE file in the root directory of this source tree.
874
+ */
875
+ var StretchHorizontal = [["rect", {
876
+ width: "20",
877
+ height: "6",
878
+ x: "2",
879
+ y: "4",
880
+ rx: "2"
881
+ }], ["rect", {
882
+ width: "20",
883
+ height: "6",
884
+ x: "2",
885
+ y: "14",
886
+ rx: "2"
887
+ }]];
888
+ //#endregion
889
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/text-align-center.mjs
890
+ /**
891
+ * @license lucide v1.34.0 - ISC
892
+ *
893
+ * This source code is licensed under the ISC license.
894
+ * See the LICENSE file in the root directory of this source tree.
895
+ */
896
+ var TextAlignCenter = [
897
+ ["path", { d: "M21 5H3" }],
898
+ ["path", { d: "M17 12H7" }],
899
+ ["path", { d: "M19 19H5" }]
900
+ ];
901
+ //#endregion
902
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/text-align-end.mjs
903
+ /**
904
+ * @license lucide v1.34.0 - ISC
905
+ *
906
+ * This source code is licensed under the ISC license.
907
+ * See the LICENSE file in the root directory of this source tree.
908
+ */
909
+ var TextAlignEnd = [
910
+ ["path", { d: "M21 5H3" }],
911
+ ["path", { d: "M21 12H9" }],
912
+ ["path", { d: "M21 19H7" }]
913
+ ];
914
+ //#endregion
915
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/text-align-start.mjs
916
+ /**
917
+ * @license lucide v1.34.0 - ISC
918
+ *
919
+ * This source code is licensed under the ISC license.
920
+ * See the LICENSE file in the root directory of this source tree.
921
+ */
922
+ var TextAlignStart = [
923
+ ["path", { d: "M21 5H3" }],
924
+ ["path", { d: "M15 12H3" }],
925
+ ["path", { d: "M17 19H3" }]
926
+ ];
927
+ //#endregion
928
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/text-cursor-input.mjs
929
+ /**
930
+ * @license lucide v1.34.0 - ISC
931
+ *
932
+ * This source code is licensed under the ISC license.
933
+ * See the LICENSE file in the root directory of this source tree.
934
+ */
935
+ var TextCursorInput = [
936
+ ["path", { d: "M12 20h-1a2 2 0 0 1-2-2 2 2 0 0 1-2 2H6" }],
937
+ ["path", { d: "M13 8h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7" }],
938
+ ["path", { d: "M5 16H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1" }],
939
+ ["path", { d: "M6 4h1a2 2 0 0 1 2 2 2 2 0 0 1 2-2h1" }],
940
+ ["path", { d: "M9 6v12" }]
941
+ ];
942
+ //#endregion
582
943
  //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/trash-2.mjs
583
944
  /**
584
945
  * @license lucide v1.34.0 - ISC
@@ -607,6 +968,20 @@ var Type = [
607
968
  ["path", { d: "M9 20h6" }]
608
969
  ];
609
970
  //#endregion
971
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/underline.mjs
972
+ /**
973
+ * @license lucide v1.34.0 - ISC
974
+ *
975
+ * This source code is licensed under the ISC license.
976
+ * See the LICENSE file in the root directory of this source tree.
977
+ */
978
+ var Underline = [["path", { d: "M6 4v6a6 6 0 0 0 12 0V4" }], ["line", {
979
+ x1: "4",
980
+ x2: "20",
981
+ y1: "20",
982
+ y2: "20"
983
+ }]];
984
+ //#endregion
610
985
  //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/undo-2.mjs
611
986
  /**
612
987
  * @license lucide v1.34.0 - ISC
@@ -680,24 +1055,36 @@ function renderWorkbenchIcons(root, configuration) {
680
1055
  createIcons({
681
1056
  nameAttr: "data-customforge-icon",
682
1057
  icons: {
1058
+ AlignCenter: TextAlignCenter,
1059
+ AlignLeft: TextAlignStart,
1060
+ AlignRight: TextAlignEnd,
683
1061
  ArrowDown,
684
1062
  ArrowUp,
685
- Download,
1063
+ Bold,
1064
+ ChevronDown,
1065
+ Ellipsis,
686
1066
  Eye,
687
1067
  EyeOff,
688
1068
  FolderOpen,
689
1069
  Image,
690
1070
  ImagePlus,
1071
+ Highlighter,
1072
+ Italic,
691
1073
  Layers3: Layers,
692
1074
  Link2,
693
1075
  Lock,
694
1076
  LockOpen,
1077
+ Palette,
695
1078
  Redo2,
1079
+ Rows3,
696
1080
  RotateCcw,
697
1081
  Save,
1082
+ StretchHorizontal,
1083
+ TextCursorInput,
698
1084
  Trash2,
699
1085
  Type,
700
1086
  Undo2,
1087
+ Underline,
701
1088
  Upload,
702
1089
  X
703
1090
  },
@@ -751,14 +1138,11 @@ function createWorkbenchElement() {
751
1138
  </span>
752
1139
  </div>
753
1140
  <div class="customforge-workbench__global-actions">
1141
+ <div class="customforge-workbench__extension-slot" data-extension-slot="globalActions"></div>
754
1142
  <button class="customforge-workbench__button customforge-workbench__button--secondary" data-action="load-remote" data-feature="loadRemoteProduct" data-control-label="loadProduct" type="button">
755
- <i data-customforge-icon="link-2" data-icon-slot="loadProduct"></i>
1143
+ <i data-customforge-icon="upload" data-icon-slot="loadProduct"></i>
756
1144
  <span data-label="loadProduct"></span>
757
1145
  </button>
758
- <button class="customforge-workbench__button customforge-workbench__button--primary" data-action="export-texture" data-feature="exportTexture" data-control-label="exportTexture" type="button">
759
- <i data-customforge-icon="download" data-icon-slot="exportTexture"></i>
760
- <span data-label="exportTexture"></span>
761
- </button>
762
1146
  </div>
763
1147
  </header>
764
1148
 
@@ -766,10 +1150,10 @@ function createWorkbenchElement() {
766
1150
  <section class="customforge-workbench__panel customforge-workbench__panel--editor" data-region-label="editorTitle">
767
1151
  <header class="customforge-workbench__panelbar" data-layout="editorHeader">
768
1152
  <div class="customforge-workbench__panel-title">
769
- <span class="customforge-workbench__panel-kicker">2D</span>
1153
+ <span class="customforge-workbench__panel-kicker" data-label="editorMode"></span>
770
1154
  <h1 data-label="editorTitle"></h1>
771
1155
  </div>
772
- <span class="customforge-workbench__resolution" data-role="resolution">1024 x 512</span>
1156
+ <span class="customforge-workbench__resolution" data-role="resolution"></span>
773
1157
  </header>
774
1158
 
775
1159
  <div class="customforge-workbench__toolbar" data-layout="toolbar" data-region-label="workbench" role="toolbar">
@@ -794,6 +1178,7 @@ function createWorkbenchElement() {
794
1178
  <span data-label="addImage"></span>
795
1179
  </button>
796
1180
  </div>
1181
+ <div class="customforge-workbench__extension-slot" data-extension-slot="editorToolbar"></div>
797
1182
  <span class="customforge-workbench__separator" data-tool-group="document-separator" aria-hidden="true"></span>
798
1183
  <div class="customforge-workbench__tool-group" data-tool-group="document">
799
1184
  <button class="customforge-workbench__icon-button" data-action="save-design" data-feature="saveDesign" data-control-label="saveDesign" type="button">
@@ -810,6 +1195,98 @@ function createWorkbenchElement() {
810
1195
  <i data-customforge-icon="trash-2" data-icon-slot="deleteSelection"></i>
811
1196
  <span class="customforge-workbench__icon-label" data-label="deleteSelection"></span>
812
1197
  </button>
1198
+ <div class="customforge-workbench__extension-slot" data-extension-slot="selectionToolbar"></div>
1199
+
1200
+ <span class="customforge-workbench__separator" data-tool-group="layers-separator" aria-hidden="true"></span>
1201
+ <button class="customforge-workbench__tool-button customforge-workbench__layers-toggle" data-action="toggle-layers" data-control-label="layers" data-role="layers-toggle" type="button" aria-expanded="false">
1202
+ <i data-customforge-icon="layers-3" data-icon-slot="layers"></i>
1203
+ <span data-label="layers"></span>
1204
+ <span class="customforge-workbench__layer-count" data-role="layer-count" aria-hidden="true">0</span>
1205
+ </button>
1206
+
1207
+ <div class="customforge-workbench__text-toolbar" data-role="text-toolbar" data-region-label="textFormatting" role="group" hidden>
1208
+ <span class="customforge-workbench__format-separator" aria-hidden="true"></span>
1209
+ <select class="customforge-workbench__font-select" data-role="text-font-family" data-control-label="fontFamily"></select>
1210
+ <label class="customforge-workbench__format-number" data-control-label="fontSize">
1211
+ <span class="customforge-workbench__icon-label" data-label="fontSize"></span>
1212
+ <input data-role="text-font-size" data-control-label="fontSize" type="number" min="8" max="360" step="1" inputmode="numeric">
1213
+ </label>
1214
+ <span class="customforge-workbench__format-separator" aria-hidden="true"></span>
1215
+ <div class="customforge-workbench__format-group" role="group">
1216
+ <button class="customforge-workbench__format-button" data-text-style="bold" data-control-label="bold" type="button" aria-pressed="false">
1217
+ <i data-customforge-icon="bold" data-icon-slot="bold"></i>
1218
+ <span class="customforge-workbench__icon-label" data-label="bold"></span>
1219
+ </button>
1220
+ <button class="customforge-workbench__format-button" data-text-style="italic" data-control-label="italic" type="button" aria-pressed="false">
1221
+ <i data-customforge-icon="italic" data-icon-slot="italic"></i>
1222
+ <span class="customforge-workbench__icon-label" data-label="italic"></span>
1223
+ </button>
1224
+ <button class="customforge-workbench__format-button" data-text-style="underline" data-control-label="underline" type="button" aria-pressed="false">
1225
+ <i data-customforge-icon="underline" data-icon-slot="underline"></i>
1226
+ <span class="customforge-workbench__icon-label" data-label="underline"></span>
1227
+ </button>
1228
+ </div>
1229
+ <details class="customforge-workbench__text-options" data-role="text-options">
1230
+ <summary class="customforge-workbench__format-button" data-control-label="moreTextOptions">
1231
+ <i data-customforge-icon="ellipsis" data-icon-slot="moreTextOptions"></i>
1232
+ <span class="customforge-workbench__icon-label" data-label="moreTextOptions"></span>
1233
+ </summary>
1234
+ <div class="customforge-workbench__text-options-panel">
1235
+ <div class="customforge-workbench__text-options-row">
1236
+ <button class="customforge-workbench__format-button" data-action="edit-text" data-control-label="editText" type="button">
1237
+ <i data-customforge-icon="text-cursor-input" data-icon-slot="editText"></i>
1238
+ <span class="customforge-workbench__icon-label" data-label="editText"></span>
1239
+ </button>
1240
+ <span class="customforge-workbench__format-separator" aria-hidden="true"></span>
1241
+ <div class="customforge-workbench__format-group" role="group">
1242
+ <button class="customforge-workbench__format-button" data-text-align="left" data-control-label="alignLeft" type="button" aria-pressed="false">
1243
+ <i data-customforge-icon="align-left" data-icon-slot="alignLeft"></i>
1244
+ <span class="customforge-workbench__icon-label" data-label="alignLeft"></span>
1245
+ </button>
1246
+ <button class="customforge-workbench__format-button" data-text-align="center" data-control-label="alignCenter" type="button" aria-pressed="false">
1247
+ <i data-customforge-icon="align-center" data-icon-slot="alignCenter"></i>
1248
+ <span class="customforge-workbench__icon-label" data-label="alignCenter"></span>
1249
+ </button>
1250
+ <button class="customforge-workbench__format-button" data-text-align="right" data-control-label="alignRight" type="button" aria-pressed="false">
1251
+ <i data-customforge-icon="align-right" data-icon-slot="alignRight"></i>
1252
+ <span class="customforge-workbench__icon-label" data-label="alignRight"></span>
1253
+ </button>
1254
+ </div>
1255
+ <span class="customforge-workbench__format-separator" aria-hidden="true"></span>
1256
+ <label class="customforge-workbench__format-color" data-control-label="textColor">
1257
+ <i data-customforge-icon="palette" data-icon-slot="textColor"></i>
1258
+ <input data-role="text-format-color" data-control-label="textColor" type="color" value="#172126">
1259
+ </label>
1260
+ <div class="customforge-workbench__format-group" role="group">
1261
+ <button class="customforge-workbench__format-button" data-action="toggle-text-background" data-control-label="textBackground" type="button" aria-pressed="false">
1262
+ <i data-customforge-icon="highlighter" data-icon-slot="textBackground"></i>
1263
+ <span class="customforge-workbench__icon-label" data-label="textBackground"></span>
1264
+ </button>
1265
+ <input class="customforge-workbench__background-color" data-role="text-background-color" data-control-label="textBackground" type="color" value="#fff2a8">
1266
+ </div>
1267
+ </div>
1268
+ <div class="customforge-workbench__text-color-palette" data-role="text-color-palette" data-region-label="textColor" role="group"></div>
1269
+ <div class="customforge-workbench__text-options-row customforge-workbench__text-options-row--metrics">
1270
+ <label class="customforge-workbench__format-metric" data-control-label="lineHeight">
1271
+ <i data-customforge-icon="rows-3" data-icon-slot="lineHeight"></i>
1272
+ <input data-role="text-line-height" data-control-label="lineHeight" type="number" min="0.5" max="5" step="0.1" inputmode="decimal">
1273
+ </label>
1274
+ <label class="customforge-workbench__format-metric" data-control-label="letterSpacing">
1275
+ <i data-customforge-icon="stretch-horizontal" data-icon-slot="letterSpacing"></i>
1276
+ <input data-role="text-letter-spacing" data-control-label="letterSpacing" type="number" min="-500" max="2000" step="10" inputmode="numeric">
1277
+ </label>
1278
+ </div>
1279
+ </div>
1280
+ </details>
1281
+ </div>
1282
+
1283
+ <div class="customforge-workbench__image-toolbar" data-role="image-toolbar" data-region-label="setImageAsBackground" role="group" hidden>
1284
+ <span class="customforge-workbench__format-separator" aria-hidden="true"></span>
1285
+ <button class="customforge-workbench__tool-button" data-action="set-image-as-background" data-control-label="setImageAsBackground" type="button">
1286
+ <i data-customforge-icon="stretch-horizontal" data-icon-slot="setImageAsBackground"></i>
1287
+ <span data-label="setImageAsBackground"></span>
1288
+ </button>
1289
+ </div>
813
1290
  </div>
814
1291
 
815
1292
  <div class="customforge-workbench__editor-body">
@@ -820,7 +1297,6 @@ function createWorkbenchElement() {
820
1297
  <i data-customforge-icon="layers-3" data-icon-slot="layers"></i>
821
1298
  <span data-label="layers"></span>
822
1299
  </span>
823
- <span data-role="layer-count">0</span>
824
1300
  </header>
825
1301
  <ol class="customforge-workbench__layer-list" data-role="layer-list"></ol>
826
1302
  </aside>
@@ -830,7 +1306,7 @@ function createWorkbenchElement() {
830
1306
  <section class="customforge-workbench__panel customforge-workbench__panel--viewer" data-region-label="viewerTitle">
831
1307
  <header class="customforge-workbench__panelbar" data-layout="viewerHeader">
832
1308
  <div class="customforge-workbench__panel-title">
833
- <span class="customforge-workbench__panel-kicker">3D</span>
1309
+ <span class="customforge-workbench__panel-kicker" data-label="viewerMode"></span>
834
1310
  <h2 data-label="viewerTitle"></h2>
835
1311
  </div>
836
1312
  <button class="customforge-workbench__icon-button" data-action="reset-view" data-feature="resetView" data-control-label="resetView" type="button">
@@ -838,16 +1314,16 @@ function createWorkbenchElement() {
838
1314
  <span class="customforge-workbench__icon-label" data-label="resetView"></span>
839
1315
  </button>
840
1316
  </header>
841
- <div class="customforge-workbench__viewer-stage" data-role="viewer-host"></div>
1317
+ <div class="customforge-workbench__viewer-stage">
1318
+ <div class="customforge-workbench__viewer-host" data-role="viewer-host"></div>
1319
+ <div class="customforge-workbench__status" data-layout="status">
1320
+ <span class="customforge-workbench__status-indicator" aria-hidden="true"></span>
1321
+ <span data-role="status-label" role="status" data-label="starting"></span>
1322
+ </div>
1323
+ </div>
842
1324
  </section>
843
1325
  </section>
844
1326
 
845
- <footer class="customforge-workbench__statusbar" data-layout="status">
846
- <span class="customforge-workbench__status-indicator" aria-hidden="true"></span>
847
- <span data-role="status-label" role="status" data-label="starting"></span>
848
- <span class="customforge-workbench__object-count" data-role="object-count"></span>
849
- </footer>
850
-
851
1327
  <input data-role="image-input" type="file" accept="image/png,image/jpeg,image/webp" hidden>
852
1328
  <input data-role="design-input" type="file" accept="application/json,.json" hidden>
853
1329
 
@@ -936,29 +1412,61 @@ function createWorkbenchElement() {
936
1412
  <span class="customforge-workbench__icon-label" data-label="close"></span>
937
1413
  </button>
938
1414
  </header>
939
- <div class="customforge-workbench__dialog-body customforge-workbench__form-fields">
940
- <label class="customforge-workbench__field">
941
- <span data-label="modelUrl"></span>
942
- <div class="customforge-workbench__input-shell">
943
- <i data-customforge-icon="link-2" data-icon-slot="loadProduct"></i>
944
- <input data-role="model-url" name="modelUrl" type="url" placeholder="https://example.com/product.glb" required>
945
- </div>
946
- </label>
947
- <label class="customforge-workbench__field">
948
- <span data-label="textureUrl"></span>
949
- <div class="customforge-workbench__input-shell">
950
- <i data-customforge-icon="image-plus" data-icon-slot="addImage"></i>
951
- <input data-role="texture-url" name="textureUrl" type="url" placeholder="https://example.com/texture.png">
1415
+ <div class="customforge-workbench__dialog-body customforge-workbench__product-form">
1416
+ <div class="customforge-workbench__product-source-switch" data-region-label="productSourceMethod" role="group">
1417
+ <button data-product-source="file" data-label="productFileSource" type="button" aria-pressed="true"></button>
1418
+ <button data-product-source="url" data-label="productUrlSource" type="button" aria-pressed="false"></button>
1419
+ </div>
1420
+
1421
+ <section data-product-source-panel="file">
1422
+ <label class="customforge-workbench__field">
1423
+ <span data-label="modelFile"></span>
1424
+ <span class="customforge-workbench__product-file-picker">
1425
+ <input data-role="model-file" name="modelFile" type="file" accept=".glb,model/gltf-binary" required>
1426
+ <span class="customforge-workbench__product-file-command" data-label="chooseModelFile"></span>
1427
+ <small data-role="model-file-name" data-label="noModelFileSelected"></small>
1428
+ </span>
1429
+ </label>
1430
+ </section>
1431
+
1432
+ <section data-product-source-panel="url" hidden>
1433
+ <label class="customforge-workbench__field">
1434
+ <span data-label="modelUrl"></span>
1435
+ <div class="customforge-workbench__input-shell">
1436
+ <i data-customforge-icon="link-2" data-icon-slot="loadProduct"></i>
1437
+ <input data-role="model-url" name="modelUrl" type="url" data-placeholder-label="modelUrlPlaceholder" disabled>
1438
+ </div>
1439
+ </label>
1440
+ </section>
1441
+
1442
+ <details class="customforge-workbench__product-options">
1443
+ <summary>
1444
+ <span data-label="advancedProductOptions"></span>
1445
+ <i data-customforge-icon="chevron-down"></i>
1446
+ </summary>
1447
+ <div class="customforge-workbench__product-options-fields">
1448
+ <label class="customforge-workbench__field">
1449
+ <span data-label="textureUrl"></span>
1450
+ <div class="customforge-workbench__input-shell">
1451
+ <i data-customforge-icon="image-plus" data-icon-slot="addImage"></i>
1452
+ <input data-role="texture-url" name="textureUrl" type="url" data-placeholder-label="textureUrlPlaceholder">
1453
+ </div>
1454
+ <small data-label="textureUrlHint"></small>
1455
+ </label>
1456
+ <label class="customforge-workbench__field">
1457
+ <span data-label="surfaceMesh"></span>
1458
+ <input data-role="mesh-name" name="meshName" type="text" value="PrintArea" required>
1459
+ <small data-label="surfaceMeshHint"></small>
1460
+ </label>
1461
+ <label class="customforge-workbench__check-field">
1462
+ <input data-role="flip-texture" name="flipTexture" type="checkbox">
1463
+ <span>
1464
+ <strong data-label="flipTexture"></strong>
1465
+ <small data-label="flipTextureHint"></small>
1466
+ </span>
1467
+ </label>
952
1468
  </div>
953
- </label>
954
- <label class="customforge-workbench__field">
955
- <span data-label="surfaceMesh"></span>
956
- <input data-role="mesh-name" name="meshName" type="text" value="PrintArea" required>
957
- </label>
958
- <label class="customforge-workbench__check-field">
959
- <input data-role="flip-texture" name="flipTexture" type="checkbox">
960
- <span data-label="flipTexture"></span>
961
- </label>
1469
+ </details>
962
1470
  </div>
963
1471
  <footer class="customforge-workbench__dialog-actions">
964
1472
  <button class="customforge-workbench__button customforge-workbench__button--secondary" data-action="use-demo" data-label="useDemo" type="button"></button>
@@ -975,9 +1483,69 @@ function createWorkbenchElement() {
975
1483
  //#endregion
976
1484
  //#region src/workbench/CustomForgeWorkbench.ts
977
1485
  var DIALOG_CLOSE_DELAY_MS = 180;
978
- function errorMessage(error) {
979
- return error instanceof Error ? error.message : String(error);
980
- }
1486
+ var EXTENSION_SLOTS = [
1487
+ "globalActions",
1488
+ "editorToolbar",
1489
+ "selectionToolbar"
1490
+ ];
1491
+ var TEXT_COLOR_PRESETS = [
1492
+ {
1493
+ label: "colorBlack",
1494
+ value: "#17191c"
1495
+ },
1496
+ {
1497
+ label: "colorSlate",
1498
+ value: "#475467"
1499
+ },
1500
+ {
1501
+ label: "colorGray",
1502
+ value: "#98a2b3"
1503
+ },
1504
+ {
1505
+ label: "colorWhite",
1506
+ value: "#ffffff"
1507
+ },
1508
+ {
1509
+ label: "colorRed",
1510
+ value: "#d92d20"
1511
+ },
1512
+ {
1513
+ label: "colorOrange",
1514
+ value: "#f79009"
1515
+ },
1516
+ {
1517
+ label: "colorYellow",
1518
+ value: "#fdb022"
1519
+ },
1520
+ {
1521
+ label: "colorGreen",
1522
+ value: "#12b76a"
1523
+ },
1524
+ {
1525
+ label: "colorTeal",
1526
+ value: "#0e9384"
1527
+ },
1528
+ {
1529
+ label: "colorCyan",
1530
+ value: "#06aed4"
1531
+ },
1532
+ {
1533
+ label: "colorBlue",
1534
+ value: "#0875c9"
1535
+ },
1536
+ {
1537
+ label: "colorPurple",
1538
+ value: "#7f56d9"
1539
+ },
1540
+ {
1541
+ label: "colorPink",
1542
+ value: "#c11574"
1543
+ },
1544
+ {
1545
+ label: "colorBrown",
1546
+ value: "#854a0e"
1547
+ }
1548
+ ];
981
1549
  function requiredElement(root, selector) {
982
1550
  const element = root.querySelector(selector);
983
1551
  if (!element) throw new Error(`Workbench element was not found: ${selector}`);
@@ -1063,32 +1631,51 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1063
1631
  unsubscribe = [];
1064
1632
  features;
1065
1633
  layout;
1634
+ theme;
1066
1635
  labels;
1067
1636
  icons;
1068
1637
  textPresets;
1638
+ fontFamilies;
1069
1639
  backgrounds;
1070
1640
  elements;
1641
+ extensions = /* @__PURE__ */ new Map();
1642
+ pendingExtensions = /* @__PURE__ */ new Map();
1643
+ formatError;
1071
1644
  layerList;
1072
1645
  deleteButton;
1073
1646
  undoButton;
1074
1647
  redoButton;
1648
+ layersToggle;
1075
1649
  statusLabel;
1076
- objectCountLabel;
1077
1650
  imageInput;
1078
1651
  designInput;
1079
1652
  loadDesignButton;
1080
1653
  textDialog;
1081
1654
  textForm;
1655
+ textToolbar;
1656
+ imageToolbar;
1657
+ textOptions;
1658
+ textFontFamily;
1659
+ textFontSize;
1660
+ textColor;
1661
+ textBackgroundColor;
1662
+ textLineHeight;
1663
+ textLetterSpacing;
1082
1664
  imageDialog;
1083
1665
  imageForm;
1084
1666
  imageSubmitButton;
1085
1667
  productDialog;
1086
1668
  remoteForm;
1087
1669
  submitProductButton;
1670
+ modelFileInput;
1671
+ modelUrlInput;
1088
1672
  selectedObjectIds = /* @__PURE__ */ new Set();
1089
1673
  selectedTextPresetId;
1090
1674
  selectedImage;
1091
1675
  activeImageTab = "upload";
1676
+ productSourceMode = "file";
1677
+ activeProductObjectUrl;
1678
+ layersExpanded = false;
1092
1679
  layerSignature = "";
1093
1680
  draggedObjectId;
1094
1681
  destroyed = false;
@@ -1098,36 +1685,59 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1098
1685
  this.customizer = customizer;
1099
1686
  this.features = options.features;
1100
1687
  this.layout = options.layout;
1688
+ this.theme = options.theme;
1101
1689
  this.labels = options.labels;
1102
1690
  this.icons = options.icons;
1103
1691
  this.textPresets = options.textPresets;
1692
+ this.fontFamilies = options.fontFamilies;
1104
1693
  this.backgrounds = options.backgrounds;
1105
1694
  this.elements = options.elements;
1695
+ options.extensions.forEach((extension) => {
1696
+ this.extensions.set(extension.id, extension);
1697
+ });
1698
+ this.formatError = options.formatError;
1106
1699
  this.selectedTextPresetId = this.textPresets[0]?.id;
1107
1700
  this.layerList = requiredElement(element, "[data-role=\"layer-list\"]");
1108
1701
  this.deleteButton = this.action("delete-selection");
1109
1702
  this.undoButton = this.action("undo");
1110
1703
  this.redoButton = this.action("redo");
1704
+ this.layersToggle = this.action("toggle-layers");
1111
1705
  this.statusLabel = requiredElement(element, "[data-role=\"status-label\"]");
1112
- this.objectCountLabel = requiredElement(element, "[data-role=\"object-count\"]");
1113
1706
  this.imageInput = requiredElement(element, "[data-role=\"image-input\"]");
1114
1707
  this.designInput = requiredElement(element, "[data-role=\"design-input\"]");
1115
1708
  this.loadDesignButton = this.action("load-design");
1116
1709
  this.textDialog = requiredElement(element, "[data-role=\"text-dialog\"]");
1117
1710
  this.textForm = requiredElement(element, "[data-role=\"text-form\"]");
1711
+ this.textToolbar = requiredElement(element, "[data-role=\"text-toolbar\"]");
1712
+ this.imageToolbar = requiredElement(element, "[data-role=\"image-toolbar\"]");
1713
+ this.textOptions = requiredElement(element, "[data-role=\"text-options\"]");
1714
+ this.textFontFamily = requiredElement(element, "[data-role=\"text-font-family\"]");
1715
+ this.textFontSize = requiredElement(element, "[data-role=\"text-font-size\"]");
1716
+ this.textColor = requiredElement(element, "[data-role=\"text-format-color\"]");
1717
+ this.textBackgroundColor = requiredElement(element, "[data-role=\"text-background-color\"]");
1718
+ this.textLineHeight = requiredElement(element, "[data-role=\"text-line-height\"]");
1719
+ this.textLetterSpacing = requiredElement(element, "[data-role=\"text-letter-spacing\"]");
1118
1720
  this.imageDialog = requiredElement(element, "[data-role=\"image-dialog\"]");
1119
1721
  this.imageForm = requiredElement(element, "[data-role=\"image-form\"]");
1120
1722
  this.imageSubmitButton = this.action("submit-image");
1121
1723
  this.productDialog = requiredElement(element, "[data-role=\"product-dialog\"]");
1122
1724
  this.remoteForm = requiredElement(element, "[data-role=\"remote-form\"]");
1123
1725
  this.submitProductButton = this.action("submit-product");
1726
+ this.modelFileInput = requiredElement(element, "[data-role=\"model-file\"]");
1727
+ this.modelUrlInput = requiredElement(element, "[data-role=\"model-url\"]");
1728
+ this.renderTextFontOptions();
1124
1729
  this.renderTextPresets();
1730
+ this.renderTextColorPresets();
1125
1731
  this.renderAssetPresets();
1732
+ this.renderExtensionSlots();
1126
1733
  this.bindEvents();
1127
1734
  this.applyVisibility();
1128
1735
  this.setImageTab("upload");
1736
+ this.setProductSourceMode("file");
1129
1737
  this.updateHistoryButtons();
1130
1738
  this.renderLayers(true);
1739
+ this.updateTextToolbar();
1740
+ this.updateImageToolbar();
1131
1741
  this.setStatus(this.labels.productReady);
1132
1742
  }
1133
1743
  /**
@@ -1141,6 +1751,7 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1141
1751
  const host = resolveElement(options.container, "Workbench");
1142
1752
  const normalized = normalizeWorkbenchOptions(options);
1143
1753
  const element = createWorkbenchElement();
1754
+ if (normalized.classNames.length > 0) element.classList.add(...normalized.classNames);
1144
1755
  applyLabels(element, normalized.labels);
1145
1756
  applyBranding(element, normalized.branding);
1146
1757
  applyTheme(element, normalized.theme);
@@ -1154,7 +1765,14 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1154
1765
  editorWidth: normalized.editorWidth,
1155
1766
  editorHeight: normalized.editorHeight,
1156
1767
  historyLimit: normalized.historyLimit,
1157
- product: normalized.product
1768
+ product: normalized.product,
1769
+ appearance: normalized.appearance,
1770
+ editorAriaLabel: normalized.labels.editorCanvas,
1771
+ viewerAriaLabel: normalized.labels.viewerCanvas,
1772
+ defaultText: normalized.labels.defaultText,
1773
+ textObjectName: normalized.labels.textObject,
1774
+ imageObjectName: normalized.labels.imageObject,
1775
+ backgroundObjectName: normalized.labels.backgroundObject
1158
1776
  });
1159
1777
  return new CustomForgeWorkbench(host, element, customizer, normalized);
1160
1778
  } catch (error) {
@@ -1162,6 +1780,10 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1162
1780
  throw error;
1163
1781
  }
1164
1782
  }
1783
+ /** 返回当前默认功能控件开关的独立快照 */
1784
+ getFeatures() {
1785
+ return { ...this.features };
1786
+ }
1165
1787
  /**
1166
1788
  * 在运行时显示或隐藏某项内置功能控件
1167
1789
  *
@@ -1178,6 +1800,12 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1178
1800
  if (feature === "presetBackgrounds" && this.activeImageTab === "backgrounds" || feature === "presetElements" && this.activeImageTab === "elements") this.setImageTab("upload");
1179
1801
  this.applyVisibility();
1180
1802
  this.renderLayers(true);
1803
+ this.updateTextToolbar();
1804
+ this.updateImageToolbar();
1805
+ }
1806
+ /** 返回当前默认布局区域开关的独立快照 */
1807
+ getLayout() {
1808
+ return { ...this.layout };
1181
1809
  }
1182
1810
  /**
1183
1811
  * 在运行时显示或隐藏 Workbench 布局区域
@@ -1188,15 +1816,107 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1188
1816
  setLayout(section, visible) {
1189
1817
  this.layout[section] = visible;
1190
1818
  this.applyVisibility();
1819
+ this.updateTextToolbar();
1820
+ this.updateImageToolbar();
1821
+ }
1822
+ /** 返回当前完整主题的独立快照 */
1823
+ getTheme() {
1824
+ return { ...this.theme };
1191
1825
  }
1192
1826
  /**
1193
- * 更新底部状态栏内容
1827
+ * 合并并立即应用当前 Workbench 实例的主题变量
1828
+ *
1829
+ * @param theme 要覆盖的主题字段
1830
+ * @returns 合并后的完整主题独立快照
1831
+ */
1832
+ setTheme(theme) {
1833
+ this.theme = {
1834
+ ...this.theme,
1835
+ ...theme
1836
+ };
1837
+ applyTheme(this.element, this.theme);
1838
+ this.refreshExtensions();
1839
+ return this.getTheme();
1840
+ }
1841
+ /** 返回当前已注册扩展按钮的独立配置快照 */
1842
+ getExtensions() {
1843
+ return [...this.extensions.values()].map(({ classNames, ...extension }) => ({
1844
+ ...extension,
1845
+ className: classNames.length > 0 ? classNames.join(" ") : void 0
1846
+ }));
1847
+ }
1848
+ /**
1849
+ * 注册扩展按钮并立即渲染到声明位置
1850
+ *
1851
+ * @param extension 扩展按钮配置
1852
+ * @returns 仅在该注册仍为当前同 ID 项时将其移除的清理函数
1853
+ * @throws ID、位置、文案、图标或回调配置无效以及 ID 重复时抛出错误
1854
+ */
1855
+ registerExtension(extension) {
1856
+ const normalized = normalizeWorkbenchExtension(extension);
1857
+ if (this.extensions.has(normalized.id)) throw new TypeError(`Extension id is duplicated: ${normalized.id}`);
1858
+ this.extensions.set(normalized.id, normalized);
1859
+ this.renderRegisteredExtensions();
1860
+ return () => {
1861
+ if (this.extensions.get(normalized.id) === normalized) this.removeExtension(normalized.id);
1862
+ };
1863
+ }
1864
+ /** 按稳定 ID 移除扩展按钮并返回是否存在该注册 */
1865
+ removeExtension(id) {
1866
+ const normalizedId = id.trim();
1867
+ const extension = this.extensions.get(normalizedId);
1868
+ if (!extension) return false;
1869
+ this.extensions.delete(normalizedId);
1870
+ for (const [key, pendingExtension] of this.pendingExtensions) if (pendingExtension === extension) this.pendingExtensions.delete(key);
1871
+ this.renderRegisteredExtensions();
1872
+ return true;
1873
+ }
1874
+ /** 使用最新核心状态重新计算所有扩展按钮 */
1875
+ refreshExtensions() {
1876
+ if (this.destroyed) return;
1877
+ const state = this.customizer.getState();
1878
+ const objectsById = new Map(state.objects.map((object) => [object.id, object]));
1879
+ const selection = state.selectedObjectIds.map((id) => objectsById.get(id)).filter((object) => object !== void 0);
1880
+ this.element.querySelectorAll("[data-extension-id]").forEach((button) => {
1881
+ const extensionId = button.dataset.extensionId;
1882
+ const extension = extensionId ? this.extensions.get(extensionId) : void 0;
1883
+ if (!extension) {
1884
+ button.remove();
1885
+ return;
1886
+ }
1887
+ const layerId = button.dataset.extensionLayerId;
1888
+ const layer = layerId ? objectsById.get(layerId) : void 0;
1889
+ const context = {
1890
+ workbench: this,
1891
+ customizer: this.customizer,
1892
+ state,
1893
+ selection,
1894
+ layer
1895
+ };
1896
+ const visible = (extension.placement !== "selectionToolbar" || selection.length > 0) && (extension.placement !== "layerActions" || layer !== void 0) && this.resolveExtensionPredicate(extension.visible, context, false);
1897
+ const executionKey = this.extensionExecutionKey(extension.id, layer?.id);
1898
+ const pending = this.pendingExtensions.get(executionKey) === extension;
1899
+ button.hidden = !visible;
1900
+ button.disabled = !visible || pending || this.resolveExtensionPredicate(extension.disabled, context, true);
1901
+ button.dataset.loading = String(pending);
1902
+ button.setAttribute("aria-busy", String(pending));
1903
+ });
1904
+ EXTENSION_SLOTS.forEach((placement) => {
1905
+ const slot = this.extensionSlot(placement);
1906
+ slot.hidden = ![...slot.querySelectorAll("button")].some((button) => !button.hidden);
1907
+ });
1908
+ this.updateLayerActionWidths();
1909
+ this.updateRegionVisibility();
1910
+ }
1911
+ /**
1912
+ * 更新三维预览区域内的运行状态
1194
1913
  *
1195
1914
  * @param message 状态文字
1196
1915
  * @param mode 状态样式,默认为 ready
1197
1916
  */
1198
1917
  setStatus(message, mode = "ready") {
1199
1918
  this.statusLabel.textContent = message;
1919
+ this.statusLabel.title = message;
1200
1920
  this.element.dataset.status = mode;
1201
1921
  }
1202
1922
  /** 释放 DOM 事件、核心实例和 Workbench 挂载内容 */
@@ -1207,8 +1927,10 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1207
1927
  this.dialogCloseTimers.forEach((timer) => window.clearTimeout(timer));
1208
1928
  this.dialogCloseTimers.clear();
1209
1929
  this.abortController.abort();
1930
+ this.pendingExtensions.clear();
1210
1931
  this.unsubscribe.splice(0).forEach((stop) => stop());
1211
1932
  this.customizer.destroy();
1933
+ this.replaceActiveProductObjectUrl();
1212
1934
  this.host.replaceChildren();
1213
1935
  }
1214
1936
  bindEvents() {
@@ -1216,25 +1938,74 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1216
1938
  this.unsubscribe.push(this.customizer.on("change", ({ objectCount }) => {
1217
1939
  this.updateObjectCount(objectCount);
1218
1940
  this.renderLayers();
1941
+ this.updateTextToolbar();
1942
+ this.updateImageToolbar();
1943
+ this.refreshExtensions();
1219
1944
  }), this.customizer.on("selectionchange", ({ objectIds }) => {
1220
1945
  this.selectedObjectIds = new Set(objectIds);
1221
1946
  this.deleteButton.disabled = objectIds.length === 0;
1222
1947
  this.renderLayers();
1223
- }), this.customizer.on("historychange", () => this.updateHistoryButtons()), this.customizer.on("status", ({ message }) => this.setStatus(this.localizeStatus(message))), this.customizer.on("error", ({ error }) => this.setStatus(error.message, "error")));
1948
+ this.updateTextToolbar();
1949
+ this.updateImageToolbar();
1950
+ this.refreshExtensions();
1951
+ }), this.customizer.on("historychange", () => {
1952
+ this.updateHistoryButtons();
1953
+ this.refreshExtensions();
1954
+ }), this.customizer.on("viewchange", () => this.refreshExtensions()), this.customizer.on("status", ({ message }) => {
1955
+ const localized = this.localizeStatus(message);
1956
+ if (localized) this.setStatus(localized);
1957
+ }), this.customizer.on("error", ({ error }) => this.setStatus(this.formatError(error), "error")));
1224
1958
  this.element.addEventListener("click", (event) => this.handleRootClick(event), { signal });
1225
1959
  this.element.addEventListener("keydown", (event) => {
1226
1960
  this.handleImageTabShortcut(event);
1227
1961
  this.handleHistoryShortcut(event);
1228
1962
  }, { signal });
1963
+ document.addEventListener("pointerdown", (event) => {
1964
+ const target = event.target;
1965
+ if (target instanceof Node && !this.textOptions.contains(target)) this.textOptions.open = false;
1966
+ }, { signal });
1967
+ document.addEventListener("keydown", (event) => {
1968
+ if (event.key === "Escape") this.textOptions.open = false;
1969
+ }, { signal });
1229
1970
  this.textForm.addEventListener("submit", (event) => this.addTextFromDialog(event), { signal });
1230
1971
  this.imageForm.addEventListener("submit", (event) => void this.addImageFromDialog(event), { signal });
1231
- this.remoteForm.addEventListener("submit", (event) => void this.loadRemoteProduct(event), { signal });
1972
+ this.remoteForm.addEventListener("submit", (event) => void this.loadProductFromDialog(event), { signal });
1232
1973
  requiredElement(this.element, "[data-role=\"text-value\"]").addEventListener("input", (event) => {
1233
1974
  event.currentTarget.setCustomValidity("");
1234
1975
  this.updateTextPreviews();
1235
1976
  }, { signal });
1236
1977
  requiredElement(this.element, "[data-role=\"text-color\"]").addEventListener("input", () => this.updateTextPreviews(), { signal });
1978
+ this.textFontFamily.addEventListener("change", () => {
1979
+ if (this.textFontFamily.value) this.updateSelectedText({ fontFamily: this.textFontFamily.value });
1980
+ }, { signal });
1981
+ this.textFontSize.addEventListener("change", () => {
1982
+ const value = this.textFontSize.valueAsNumber;
1983
+ if (Number.isFinite(value)) this.updateSelectedText({ fontSize: value });
1984
+ }, { signal });
1985
+ const updateTextColor = () => {
1986
+ this.updateSelectedText({ color: this.textColor.value });
1987
+ };
1988
+ this.textColor.addEventListener("input", updateTextColor, { signal });
1989
+ this.textColor.addEventListener("change", updateTextColor, { signal });
1990
+ const updateTextBackgroundColor = () => {
1991
+ this.updateSelectedText({ backgroundColor: this.textBackgroundColor.value });
1992
+ };
1993
+ this.textBackgroundColor.addEventListener("input", updateTextBackgroundColor, { signal });
1994
+ this.textBackgroundColor.addEventListener("change", updateTextBackgroundColor, { signal });
1995
+ this.textLineHeight.addEventListener("change", () => {
1996
+ const value = this.textLineHeight.valueAsNumber;
1997
+ if (Number.isFinite(value)) this.updateSelectedText({ lineHeight: value });
1998
+ }, { signal });
1999
+ this.textLetterSpacing.addEventListener("change", () => {
2000
+ const value = this.textLetterSpacing.valueAsNumber;
2001
+ if (Number.isFinite(value)) this.updateSelectedText({ charSpacing: value });
2002
+ }, { signal });
1237
2003
  this.imageInput.addEventListener("change", () => this.selectUploadedImage(), { signal });
2004
+ this.modelFileInput.addEventListener("change", () => this.updateSelectedProductFile(), { signal });
2005
+ this.modelUrlInput.addEventListener("input", () => this.modelUrlInput.setCustomValidity(""), { signal });
2006
+ for (const selector of ["[data-role=\"texture-url\"]", "[data-role=\"mesh-name\"]"]) requiredElement(this.element, selector).addEventListener("input", (event) => {
2007
+ event.currentTarget.setCustomValidity("");
2008
+ }, { signal });
1238
2009
  this.designInput.addEventListener("change", () => void this.loadSelectedDesign(), { signal });
1239
2010
  this.imageDialog.addEventListener("close", () => this.resetImageSelection(), { signal });
1240
2011
  for (const dialog of [
@@ -1260,6 +2031,27 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1260
2031
  }
1261
2032
  handleRootClick(event) {
1262
2033
  const target = event.target;
2034
+ const extension = target.closest("[data-extension-id]");
2035
+ if (extension && !extension.disabled && !extension.hidden) {
2036
+ this.runExtension(extension, event);
2037
+ return;
2038
+ }
2039
+ const textColor = target.closest("[data-text-color]");
2040
+ if (textColor?.dataset.textColor) {
2041
+ this.setColorControl(this.textColor, textColor.dataset.textColor);
2042
+ this.updateSelectedText({ color: textColor.dataset.textColor });
2043
+ return;
2044
+ }
2045
+ const textStyle = target.closest("[data-text-style]");
2046
+ if (textStyle?.dataset.textStyle) {
2047
+ this.toggleSelectedTextStyle(textStyle.dataset.textStyle);
2048
+ return;
2049
+ }
2050
+ const textAlign = target.closest("[data-text-align]");
2051
+ if (textAlign?.dataset.textAlign) {
2052
+ this.updateSelectedText({ textAlign: textAlign.dataset.textAlign });
2053
+ return;
2054
+ }
1263
2055
  const layerAction = target.closest("[data-layer-action]");
1264
2056
  if (layerAction) {
1265
2057
  this.handleLayerAction(layerAction);
@@ -1280,13 +2072,29 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1280
2072
  this.setImageTab(tab.dataset.imageTab);
1281
2073
  return;
1282
2074
  }
2075
+ const productSource = target.closest("[data-product-source]");
2076
+ if (productSource?.dataset.productSource) {
2077
+ this.setProductSourceMode(productSource.dataset.productSource);
2078
+ return;
2079
+ }
1283
2080
  switch (target.closest("[data-action]")?.dataset.action) {
1284
2081
  case "open-text-dialog":
1285
2082
  this.openTextDialog();
1286
2083
  break;
2084
+ case "edit-text": {
2085
+ const selected = this.selectedTextObjects();
2086
+ if (selected?.length === 1) this.customizer.editText(selected[0].id);
2087
+ break;
2088
+ }
2089
+ case "toggle-text-background":
2090
+ this.toggleSelectedTextBackground();
2091
+ break;
1287
2092
  case "open-image-dialog":
1288
2093
  this.openImageDialog();
1289
2094
  break;
2095
+ case "set-image-as-background":
2096
+ this.setSelectedImageAsBackground();
2097
+ break;
1290
2098
  case "undo":
1291
2099
  this.runHistory("undo");
1292
2100
  break;
@@ -1302,12 +2110,12 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1302
2110
  case "delete-selection":
1303
2111
  this.customizer.deleteSelected();
1304
2112
  break;
2113
+ case "toggle-layers":
2114
+ this.toggleLayers();
2115
+ break;
1305
2116
  case "reset-view":
1306
2117
  this.customizer.resetView();
1307
2118
  break;
1308
- case "export-texture":
1309
- this.exportTexture();
1310
- break;
1311
2119
  case "load-remote":
1312
2120
  this.showDialog(this.productDialog);
1313
2121
  break;
@@ -1357,6 +2165,109 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1357
2165
  next.focus();
1358
2166
  }
1359
2167
  }
2168
+ extensionSlot(placement) {
2169
+ return requiredElement(this.element, `[data-extension-slot="${placement}"]`);
2170
+ }
2171
+ extensionsAt(placement) {
2172
+ return [...this.extensions.values()].filter((extension) => extension.placement === placement).sort((left, right) => left.order - right.order);
2173
+ }
2174
+ renderExtensionSlots() {
2175
+ EXTENSION_SLOTS.forEach((placement) => {
2176
+ this.extensionSlot(placement).replaceChildren(...this.extensionsAt(placement).map((extension) => this.createExtensionButton(extension)));
2177
+ });
2178
+ }
2179
+ renderRegisteredExtensions() {
2180
+ this.renderExtensionSlots();
2181
+ this.renderLayers(true);
2182
+ }
2183
+ createExtensionButton(extension, layerId) {
2184
+ const button = document.createElement("button");
2185
+ button.type = "button";
2186
+ button.classList.add("customforge-workbench__extension-button", `customforge-workbench__extension-button--${extension.placement}`, `customforge-workbench__extension-button--${extension.variant}`, ...extension.classNames);
2187
+ if (extension.placement === "globalActions") button.classList.add("customforge-workbench__button");
2188
+ else if (extension.placement === "layerActions") button.classList.add("customforge-workbench__layer-action");
2189
+ else button.classList.add("customforge-workbench__tool-button");
2190
+ button.dataset.extensionId = extension.id;
2191
+ button.dataset.extensionPlacement = extension.placement;
2192
+ button.dataset.hasIcon = String(Boolean(extension.iconUrl && this.icons.enabled));
2193
+ if (layerId) button.dataset.extensionLayerId = layerId;
2194
+ button.title = extension.label;
2195
+ button.setAttribute("aria-label", extension.label);
2196
+ const showLabel = extension.showLabel || !this.icons.enabled;
2197
+ button.dataset.labelVisible = String(showLabel);
2198
+ if (extension.iconUrl && this.icons.enabled) {
2199
+ const icon = document.createElement("img");
2200
+ icon.className = "customforge-workbench__extension-icon";
2201
+ icon.src = extension.iconUrl;
2202
+ icon.alt = "";
2203
+ icon.setAttribute("aria-hidden", "true");
2204
+ button.append(icon);
2205
+ }
2206
+ const label = document.createElement("span");
2207
+ label.className = showLabel ? "customforge-workbench__extension-label" : "customforge-workbench__icon-label";
2208
+ label.textContent = extension.label;
2209
+ button.append(label);
2210
+ return button;
2211
+ }
2212
+ resolveExtensionPredicate(predicate, context, fallback) {
2213
+ try {
2214
+ return typeof predicate === "function" ? Boolean(predicate(context)) : predicate;
2215
+ } catch (error) {
2216
+ this.setStatus(this.formatError(error), "error");
2217
+ return fallback;
2218
+ }
2219
+ }
2220
+ extensionExecutionKey(id, layerId) {
2221
+ return layerId ? `${id}:${layerId}` : id;
2222
+ }
2223
+ updateLayerActionWidths() {
2224
+ this.layerList.querySelectorAll("[data-object-id]").forEach((item) => {
2225
+ const actions = item.querySelector(".customforge-workbench__layer-actions");
2226
+ if (!actions) return;
2227
+ const width = [...actions.children].filter((child) => child instanceof HTMLElement && !child.hidden).reduce((total, child) => total + child.getBoundingClientRect().width, 0);
2228
+ const visibleActionCount = [...actions.children].filter((child) => child instanceof HTMLElement && !child.hidden).length;
2229
+ item.style.setProperty("--cfw-layer-action-width", `${Math.ceil(width) + Math.max(visibleActionCount - 1, 0) + 7}px`);
2230
+ });
2231
+ }
2232
+ async runExtension(button, event) {
2233
+ const extensionId = button.dataset.extensionId;
2234
+ const extension = extensionId ? this.extensions.get(extensionId) : void 0;
2235
+ if (!extension || this.destroyed) return;
2236
+ const state = this.customizer.getState();
2237
+ const objectsById = new Map(state.objects.map((object) => [object.id, object]));
2238
+ const selection = state.selectedObjectIds.map((id) => objectsById.get(id)).filter((object) => object !== void 0);
2239
+ const layerId = button.dataset.extensionLayerId;
2240
+ const layer = layerId ? objectsById.get(layerId) : void 0;
2241
+ const context = {
2242
+ workbench: this,
2243
+ customizer: this.customizer,
2244
+ state,
2245
+ selection,
2246
+ layer
2247
+ };
2248
+ if (extension.placement === "selectionToolbar" && selection.length === 0 || extension.placement === "layerActions" && !layer || !this.resolveExtensionPredicate(extension.visible, context, false) || this.resolveExtensionPredicate(extension.disabled, context, true)) {
2249
+ this.refreshExtensions();
2250
+ return;
2251
+ }
2252
+ const executionKey = this.extensionExecutionKey(extension.id, layer?.id);
2253
+ if (this.pendingExtensions.get(executionKey) === extension) return;
2254
+ this.pendingExtensions.set(executionKey, extension);
2255
+ this.refreshExtensions();
2256
+ const actionContext = {
2257
+ ...context,
2258
+ event,
2259
+ anchor: button,
2260
+ signal: this.abortController.signal
2261
+ };
2262
+ try {
2263
+ await extension.onClick(actionContext);
2264
+ } catch (error) {
2265
+ if (!this.destroyed && !this.abortController.signal.aborted) this.setStatus(this.formatError(error), "error");
2266
+ } finally {
2267
+ if (this.pendingExtensions.get(executionKey) === extension) this.pendingExtensions.delete(executionKey);
2268
+ if (!this.destroyed) this.refreshExtensions();
2269
+ }
2270
+ }
1360
2271
  action(name) {
1361
2272
  return requiredElement(this.element, `[data-action="${name}"]`);
1362
2273
  }
@@ -1399,27 +2310,50 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1399
2310
  const insertVisible = this.features.addText || this.features.addImage;
1400
2311
  const documentVisible = this.features.saveDesign || this.features.loadDesign;
1401
2312
  const selectionVisible = this.features.deleteSelection;
2313
+ const layersVisible = this.layout.layers;
1402
2314
  this.toolGroup("history").hidden = !historyVisible;
1403
2315
  this.toolGroup("insert").hidden = !insertVisible;
1404
2316
  this.toolGroup("document").hidden = !documentVisible;
1405
- this.toolGroup("history-separator").hidden = !historyVisible || !insertVisible && !documentVisible && !selectionVisible;
1406
- this.toolGroup("document-separator").hidden = !insertVisible || !documentVisible && !selectionVisible;
2317
+ this.toolGroup("history-separator").hidden = !historyVisible || !insertVisible && !documentVisible && !selectionVisible && !layersVisible;
2318
+ this.toolGroup("document-separator").hidden = !insertVisible || !documentVisible && !selectionVisible && !layersVisible;
1407
2319
  this.toolGroup("selection-separator").hidden = !selectionVisible || !historyVisible && !insertVisible && !documentVisible;
1408
- const toolbar = requiredElement(this.element, "[data-layout=\"toolbar\"]");
1409
- toolbar.hidden = !this.layout.toolbar || !historyVisible && !insertVisible && !documentVisible && !selectionVisible;
2320
+ this.toolGroup("layers-separator").hidden = !layersVisible || !historyVisible && !insertVisible && !documentVisible && !selectionVisible;
2321
+ this.element.dataset.toolbar = this.layout.toolbar ? "visible" : "hidden";
1410
2322
  this.element.dataset.layers = this.layout.layers ? "visible" : "hidden";
2323
+ this.element.dataset.layersExpanded = this.layout.layers && this.layersExpanded ? "true" : "false";
2324
+ this.layersToggle.hidden = !this.layout.layers;
2325
+ this.layersToggle.setAttribute("aria-expanded", String(this.layout.layers && this.layersExpanded));
2326
+ renderWorkbenchIcons(this.element, this.icons);
2327
+ this.updateTextToolbar();
2328
+ this.updateImageToolbar();
2329
+ this.refreshExtensions();
2330
+ }
2331
+ updateRegionVisibility() {
2332
+ const hasBuiltInToolbarControls = this.features.undoRedo || this.features.addText || this.features.addImage || this.features.saveDesign || this.features.loadDesign || this.features.deleteSelection || this.layout.layers;
2333
+ const hasToolbarExtensions = !this.extensionSlot("editorToolbar").hidden || !this.extensionSlot("selectionToolbar").hidden;
2334
+ const toolbar = requiredElement(this.element, "[data-layout=\"toolbar\"]");
2335
+ toolbar.hidden = !this.layout.toolbar || !hasBuiltInToolbarControls && !hasToolbarExtensions;
1411
2336
  const brandHidden = requiredElement(this.element, "[data-role=\"brand\"]").hidden;
1412
- const hasGlobalActions = this.features.loadRemoteProduct || this.features.exportTexture;
2337
+ const hasGlobalActions = this.features.loadRemoteProduct || !this.extensionSlot("globalActions").hidden;
1413
2338
  const topbar = requiredElement(this.element, "[data-layout=\"header\"]");
1414
2339
  topbar.hidden = !this.layout.header || brandHidden && !hasGlobalActions;
1415
- renderWorkbenchIcons(this.element, this.icons);
1416
2340
  }
1417
2341
  toolGroup(name) {
1418
2342
  return requiredElement(this.element, `[data-tool-group="${name}"]`);
1419
2343
  }
1420
2344
  updateObjectCount(objectCount) {
1421
- this.objectCountLabel.textContent = formatCount(this.labels, objectCount);
2345
+ const formattedCount = formatCount(this.labels, objectCount);
1422
2346
  requiredElement(this.element, "[data-role=\"layer-count\"]").textContent = String(objectCount);
2347
+ const layersLabel = `${this.labels.layers}: ${formattedCount}`;
2348
+ this.layersToggle.title = layersLabel;
2349
+ this.layersToggle.setAttribute("aria-label", layersLabel);
2350
+ }
2351
+ toggleLayers() {
2352
+ if (!this.layout.layers) return;
2353
+ this.layersExpanded = !this.layersExpanded;
2354
+ this.element.dataset.layersExpanded = String(this.layersExpanded);
2355
+ this.layersToggle.setAttribute("aria-expanded", String(this.layersExpanded));
2356
+ this.refreshExtensions();
1423
2357
  }
1424
2358
  updateHistoryButtons() {
1425
2359
  this.undoButton.disabled = !this.customizer.canUndo();
@@ -1427,45 +2361,195 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1427
2361
  }
1428
2362
  localizeStatus(message) {
1429
2363
  switch (message) {
1430
- case "Loading design": return this.labels.loadDesign;
2364
+ case "Loading design": return this.labels.loadingDesign;
1431
2365
  case "Design loaded": return this.labels.designLoaded;
1432
- case "Loading product": return this.labels.loadProduct;
2366
+ case "Loading product": return this.labels.loadingProduct;
1433
2367
  case "Remote product ready":
1434
2368
  case "Demo product ready": return this.labels.productReady;
1435
2369
  case "Undo complete": return this.labels.undoComplete;
1436
2370
  case "Redo complete": return this.labels.redoComplete;
1437
- default: return message;
2371
+ default: return;
1438
2372
  }
1439
2373
  }
1440
2374
  async runHistory(direction) {
1441
2375
  if (direction === "undo" && !this.customizer.canUndo() || direction === "redo" && !this.customizer.canRedo()) return;
1442
2376
  this.undoButton.disabled = true;
1443
2377
  this.redoButton.disabled = true;
1444
- this.setStatus(direction === "undo" ? this.labels.undo : this.labels.redo, "busy");
2378
+ this.setStatus(direction === "undo" ? this.labels.undoing : this.labels.redoing, "busy");
1445
2379
  try {
1446
2380
  if (direction === "undo" ? await this.customizer.undo() : await this.customizer.redo()) this.setStatus(direction === "undo" ? this.labels.undoComplete : this.labels.redoComplete);
1447
2381
  } catch (error) {
1448
- this.setStatus(errorMessage(error), "error");
2382
+ this.setStatus(this.formatError(error), "error");
1449
2383
  } finally {
1450
2384
  this.updateHistoryButtons();
1451
2385
  }
1452
2386
  }
1453
2387
  saveDesign() {
1454
2388
  try {
1455
- downloadJson("customforge-design.json", this.customizer.saveDesign());
2389
+ downloadJson(this.labels.designFilename, this.customizer.saveDesign());
1456
2390
  this.setStatus(this.labels.designSaved);
1457
2391
  } catch (error) {
1458
- this.setStatus(errorMessage(error), "error");
2392
+ this.setStatus(this.formatError(error), "error");
1459
2393
  }
1460
2394
  }
1461
- exportTexture() {
2395
+ renderTextFontOptions() {
2396
+ const fonts = new Map(this.fontFamilies.map((font) => [font.value, font.label]));
2397
+ this.textPresets.forEach((preset) => {
2398
+ if (!fonts.has(preset.fontFamily)) fonts.set(preset.fontFamily, preset.fontFamily);
2399
+ });
2400
+ const mixed = document.createElement("option");
2401
+ mixed.value = "";
2402
+ mixed.textContent = this.labels.mixedValue;
2403
+ mixed.disabled = true;
2404
+ this.textFontFamily.replaceChildren(mixed, ...Array.from(fonts).map(([fontFamily, label]) => {
2405
+ const option = document.createElement("option");
2406
+ option.value = fontFamily;
2407
+ option.textContent = label;
2408
+ option.style.fontFamily = fontFamily;
2409
+ return option;
2410
+ }));
2411
+ }
2412
+ ensureTextFontOption(fontFamily) {
2413
+ if (Array.from(this.textFontFamily.options).some((option) => option.value === fontFamily)) return;
2414
+ const option = document.createElement("option");
2415
+ option.value = fontFamily;
2416
+ option.textContent = fontFamily;
2417
+ option.style.fontFamily = fontFamily;
2418
+ this.textFontFamily.append(option);
2419
+ }
2420
+ selectedTextObjects() {
2421
+ if (this.selectedObjectIds.size === 0) return;
2422
+ const selected = this.customizer.getObjects().filter((object) => this.selectedObjectIds.has(object.id));
2423
+ if (selected.length !== this.selectedObjectIds.size || selected.some((object) => object.type !== "text")) return;
2424
+ return selected;
2425
+ }
2426
+ selectedImageObject() {
2427
+ if (this.selectedObjectIds.size !== 1) return;
2428
+ const [selectedId] = this.selectedObjectIds;
2429
+ const object = this.customizer.getObjects().find((entry) => entry.id === selectedId);
2430
+ return object?.type === "image" && object.role !== "background" ? object : void 0;
2431
+ }
2432
+ updateImageToolbar() {
2433
+ const object = this.selectedImageObject();
2434
+ const visible = this.layout.toolbar && this.features.addImage && object !== void 0;
2435
+ this.imageToolbar.hidden = !visible;
2436
+ this.action("set-image-as-background").disabled = !visible || Boolean(object?.locked);
2437
+ }
2438
+ setSelectedImageAsBackground() {
2439
+ const object = this.selectedImageObject();
2440
+ if (!object || object.locked) return;
2441
+ this.customizer.setImageAsBackground(object.id);
2442
+ }
2443
+ commonTextValue(objects, read) {
2444
+ const first = read(objects[0]);
2445
+ return objects.every((object) => Object.is(read(object), first)) ? first : void 0;
2446
+ }
2447
+ updateTextToolbar() {
2448
+ const objects = this.selectedTextObjects();
2449
+ const visible = this.layout.toolbar && this.features.textFormatting && objects !== void 0 && objects.length > 0;
2450
+ this.textToolbar.hidden = !visible;
2451
+ if (!visible || !objects) {
2452
+ this.textOptions.open = false;
2453
+ return;
2454
+ }
2455
+ const locked = objects.some((object) => object.locked ?? false);
2456
+ this.textToolbar.querySelectorAll("input, select, button").forEach((control) => {
2457
+ control.disabled = locked;
2458
+ });
2459
+ this.action("edit-text").disabled = locked || objects.length !== 1;
2460
+ const fontFamily = this.commonTextValue(objects, (object) => object.fontFamily);
2461
+ if (fontFamily) this.ensureTextFontOption(fontFamily);
2462
+ this.textFontFamily.value = fontFamily ?? "";
2463
+ this.setNumberControl(this.textFontSize, this.commonTextValue(objects, (object) => object.fontSize));
2464
+ this.setNumberControl(this.textLineHeight, this.commonTextValue(objects, (object) => object.lineHeight ?? 1.16));
2465
+ this.setNumberControl(this.textLetterSpacing, this.commonTextValue(objects, (object) => object.charSpacing ?? 0));
2466
+ const color = this.commonTextValue(objects, (object) => object.color);
2467
+ if (color) this.setColorControl(this.textColor, color);
2468
+ this.updateTextColorPresetState(color);
2469
+ const backgroundColor = this.commonTextValue(objects, (object) => object.backgroundColor ?? "");
2470
+ if (backgroundColor) this.setColorControl(this.textBackgroundColor, backgroundColor);
2471
+ this.setPressedState("[data-text-style=\"bold\"]", objects.map((object) => {
2472
+ const weight = object.fontWeight ?? 700;
2473
+ return weight === "bold" || typeof weight === "number" && weight >= 600;
2474
+ }));
2475
+ this.setPressedState("[data-text-style=\"italic\"]", objects.map((object) => (object.fontStyle ?? "normal") === "italic"));
2476
+ this.setPressedState("[data-text-style=\"underline\"]", objects.map((object) => object.underline ?? false));
2477
+ this.setPressedState("[data-action=\"toggle-text-background\"]", objects.map((object) => Boolean(object.backgroundColor)));
2478
+ for (const alignment of [
2479
+ "left",
2480
+ "center",
2481
+ "right"
2482
+ ]) this.setPressedState(`[data-text-align="${alignment}"]`, objects.map((object) => (object.textAlign ?? "center") === alignment));
2483
+ }
2484
+ setNumberControl(input, value) {
2485
+ input.value = value === void 0 ? "" : String(value);
2486
+ }
2487
+ setColorControl(input, value) {
2488
+ const shortHex = /^#([\da-f])([\da-f])([\da-f])$/i.exec(value);
2489
+ let normalizedValue;
2490
+ if (shortHex) normalizedValue = `#${shortHex[1]}${shortHex[1]}${shortHex[2]}${shortHex[2]}${shortHex[3]}${shortHex[3]}`;
2491
+ else if (/^#[\da-f]{6}$/i.test(value)) normalizedValue = value;
2492
+ if (normalizedValue !== void 0 && input.value.toLowerCase() !== normalizedValue.toLowerCase()) input.value = normalizedValue;
2493
+ }
2494
+ renderTextColorPresets() {
2495
+ const palette = requiredElement(this.textToolbar, "[data-role=\"text-color-palette\"]");
2496
+ const buttons = TEXT_COLOR_PRESETS.map((preset) => {
2497
+ const button = document.createElement("button");
2498
+ const label = `${this.labels.textColor}: ${this.labels[preset.label]}`;
2499
+ button.className = "customforge-workbench__text-color-swatch";
2500
+ button.type = "button";
2501
+ button.dataset.textColor = preset.value;
2502
+ button.style.setProperty("--cfw-text-swatch", preset.value);
2503
+ button.title = label;
2504
+ button.setAttribute("aria-label", label);
2505
+ button.setAttribute("aria-pressed", "false");
2506
+ return button;
2507
+ });
2508
+ palette.replaceChildren(...buttons);
2509
+ }
2510
+ updateTextColorPresetState(color) {
2511
+ this.textToolbar.querySelectorAll("[data-text-color]").forEach((button) => {
2512
+ button.setAttribute("aria-pressed", String(color !== void 0 && button.dataset.textColor?.toLowerCase() === color.toLowerCase()));
2513
+ });
2514
+ }
2515
+ setPressedState(selector, states) {
2516
+ const button = requiredElement(this.textToolbar, selector);
2517
+ const pressed = states.every(Boolean) ? "true" : states.some(Boolean) ? "mixed" : "false";
2518
+ button.setAttribute("aria-pressed", pressed);
2519
+ }
2520
+ updateSelectedText(options) {
2521
+ const objects = this.selectedTextObjects();
2522
+ if (!objects) return;
1462
2523
  try {
1463
- this.customizer.exportTexture();
1464
- this.setStatus(this.labels.exportTexture);
2524
+ objects.forEach((object) => this.customizer.updateText(object.id, options));
2525
+ this.updateTextToolbar();
1465
2526
  } catch (error) {
1466
- this.setStatus(errorMessage(error), "error");
2527
+ this.setStatus(this.formatError(error), "error");
2528
+ }
2529
+ }
2530
+ toggleSelectedTextStyle(style) {
2531
+ const objects = this.selectedTextObjects();
2532
+ if (!objects) return;
2533
+ if (style === "bold") {
2534
+ const enabled = objects.every((object) => {
2535
+ const weight = object.fontWeight ?? 700;
2536
+ return weight === "bold" || typeof weight === "number" && weight >= 600;
2537
+ });
2538
+ this.updateSelectedText({ fontWeight: enabled ? "normal" : "bold" });
2539
+ } else if (style === "italic") {
2540
+ const enabled = objects.every((object) => (object.fontStyle ?? "normal") === "italic");
2541
+ this.updateSelectedText({ fontStyle: enabled ? "normal" : "italic" });
2542
+ } else {
2543
+ const enabled = objects.every((object) => object.underline ?? false);
2544
+ this.updateSelectedText({ underline: !enabled });
1467
2545
  }
1468
2546
  }
2547
+ toggleSelectedTextBackground() {
2548
+ const objects = this.selectedTextObjects();
2549
+ if (!objects) return;
2550
+ const enabled = objects.every((object) => Boolean(object.backgroundColor));
2551
+ this.updateSelectedText({ backgroundColor: enabled ? null : this.textBackgroundColor.value });
2552
+ }
1469
2553
  renderLayers(force = false) {
1470
2554
  const objects = this.customizer.getObjects();
1471
2555
  const signature = JSON.stringify({
@@ -1527,6 +2611,7 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1527
2611
  const actions = document.createElement("span");
1528
2612
  actions.className = "customforge-workbench__layer-actions";
1529
2613
  actions.append(this.createLayerButton("visibility", object.visible === false ? "eye-off" : "eye", object.visible === false ? "showLayer" : "hideLayer", object.visible === false ? this.labels.showLayer : this.labels.hideLayer, "toggleObjectVisibility"), this.createLayerButton("lock", object.locked ? "lock" : "lock-open", object.locked ? "unlock" : "lock", object.locked ? this.labels.unlockLayer : this.labels.lockLayer, "lockObjects"), this.createLayerButton("forward", "arrow-up", "moveForward", this.labels.moveLayerForward, "reorderObjects", isBackground || index === objectCount - 1), this.createLayerButton("backward", "arrow-down", "moveBackward", this.labels.moveLayerBackward, "reorderObjects", isBackground || index <= backgroundCount), this.createLayerButton("remove", "trash-2", "deleteSelection", this.labels.deleteLayer, "deleteSelection"));
2614
+ actions.append(...this.extensionsAt("layerActions").map((extension) => this.createExtensionButton(extension, object.id)));
1530
2615
  item.append(selectButton, actions);
1531
2616
  return item;
1532
2617
  }
@@ -1701,14 +2786,9 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1701
2786
  addTextFromDialog(event) {
1702
2787
  event.preventDefault();
1703
2788
  const input = requiredElement(this.element, "[data-role=\"text-value\"]");
1704
- input.setCustomValidity("");
1705
- if (!this.textForm.reportValidity()) return;
1706
2789
  const text = input.value.trim();
1707
- if (!text) {
1708
- input.setCustomValidity(this.labels.textInputLabel);
1709
- input.reportValidity();
1710
- return;
1711
- }
2790
+ input.setCustomValidity(text ? "" : this.labels.textRequired);
2791
+ if (!this.textForm.reportValidity()) return;
1712
2792
  const color = requiredElement(this.element, "[data-role=\"text-color\"]").value;
1713
2793
  const preset = this.textPresets.find((entry) => entry.id === this.selectedTextPresetId);
1714
2794
  this.customizer.addText({
@@ -1722,7 +2802,7 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1722
2802
  } : {}
1723
2803
  });
1724
2804
  this.closeDialog(this.textDialog);
1725
- this.setStatus(this.labels.addTextConfirm);
2805
+ this.setStatus(this.labels.textAdded);
1726
2806
  }
1727
2807
  renderAssetPresets() {
1728
2808
  this.renderAssetGroup("background-presets", this.backgrounds, "background");
@@ -1810,7 +2890,7 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1810
2890
  const selection = this.selectedImage;
1811
2891
  if (!selection) return;
1812
2892
  this.imageSubmitButton.disabled = true;
1813
- this.setStatus(this.labels.addImage, "busy");
2893
+ this.setStatus(this.labels.addingImage, "busy");
1814
2894
  try {
1815
2895
  await this.customizer.addImage({
1816
2896
  src: selection.src,
@@ -1818,9 +2898,9 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1818
2898
  role: selection.role
1819
2899
  });
1820
2900
  this.closeDialog(this.imageDialog);
1821
- this.setStatus(this.labels.addImageConfirm);
2901
+ this.setStatus(this.labels.imageAdded);
1822
2902
  } catch (error) {
1823
- this.setStatus(errorMessage(error), "error");
2903
+ this.setStatus(this.formatError(error), "error");
1824
2904
  this.imageSubmitButton.disabled = false;
1825
2905
  }
1826
2906
  }
@@ -1845,48 +2925,100 @@ var CustomForgeWorkbench = class CustomForgeWorkbench {
1845
2925
  const file = this.designInput.files?.[0];
1846
2926
  if (!file) return;
1847
2927
  this.loadDesignButton.disabled = true;
1848
- this.setStatus(this.labels.loadDesign, "busy");
2928
+ this.setStatus(this.labels.loadingDesign, "busy");
1849
2929
  try {
1850
2930
  await this.customizer.loadDesign(JSON.parse(await file.text()));
1851
2931
  this.setStatus(this.labels.designLoaded);
1852
2932
  } catch (error) {
1853
- this.setStatus(errorMessage(error), "error");
2933
+ this.setStatus(this.formatError(error), "error");
1854
2934
  } finally {
1855
2935
  this.loadDesignButton.disabled = false;
1856
2936
  this.designInput.value = "";
1857
2937
  }
1858
2938
  }
2939
+ setProductSourceMode(mode) {
2940
+ this.productSourceMode = mode;
2941
+ this.element.querySelectorAll("[data-product-source]").forEach((button) => {
2942
+ const selected = button.dataset.productSource === mode;
2943
+ button.dataset.selected = String(selected);
2944
+ button.setAttribute("aria-pressed", String(selected));
2945
+ });
2946
+ this.element.querySelectorAll("[data-product-source-panel]").forEach((panel) => {
2947
+ panel.hidden = panel.dataset.productSourcePanel !== mode;
2948
+ });
2949
+ this.modelFileInput.disabled = mode !== "file";
2950
+ this.modelFileInput.required = mode === "file";
2951
+ this.modelUrlInput.disabled = mode !== "url";
2952
+ this.modelUrlInput.required = mode === "url";
2953
+ if (mode === "file") this.updateSelectedProductFile();
2954
+ }
2955
+ updateSelectedProductFile() {
2956
+ const file = this.modelFileInput.files?.[0];
2957
+ const valid = !file || file.name.toLowerCase().endsWith(".glb");
2958
+ const validationMessage = this.productSourceMode !== "file" ? "" : !file ? this.labels.modelFileRequired : valid ? "" : this.labels.invalidModelFile;
2959
+ this.modelFileInput.setCustomValidity(validationMessage);
2960
+ requiredElement(this.element, "[data-role=\"model-file-name\"]").textContent = file?.name ?? this.labels.noModelFileSelected;
2961
+ }
2962
+ validateProductForm() {
2963
+ this.updateSelectedProductFile();
2964
+ this.modelUrlInput.setCustomValidity("");
2965
+ if (this.productSourceMode === "url") this.modelUrlInput.setCustomValidity(!this.modelUrlInput.value.trim() ? this.labels.modelUrlRequired : this.modelUrlInput.validity.typeMismatch ? this.labels.invalidModelUrl : "");
2966
+ const textureUrl = requiredElement(this.element, "[data-role=\"texture-url\"]");
2967
+ textureUrl.setCustomValidity("");
2968
+ if (textureUrl.value.trim() && textureUrl.validity.typeMismatch) textureUrl.setCustomValidity(this.labels.invalidTextureUrl);
2969
+ const surfaceMesh = requiredElement(this.element, "[data-role=\"mesh-name\"]");
2970
+ surfaceMesh.setCustomValidity(surfaceMesh.value.trim() ? "" : this.labels.surfaceMeshRequired);
2971
+ }
2972
+ setProductLoading(loading) {
2973
+ const label = requiredElement(this.submitProductButton, "[data-label=\"loadProduct\"]");
2974
+ this.submitProductButton.disabled = loading;
2975
+ this.submitProductButton.dataset.loading = String(loading);
2976
+ this.submitProductButton.setAttribute("aria-busy", String(loading));
2977
+ label.textContent = loading ? this.labels.loadingProduct : this.labels.loadProduct;
2978
+ }
2979
+ replaceActiveProductObjectUrl(nextUrl) {
2980
+ if (this.activeProductObjectUrl && this.activeProductObjectUrl !== nextUrl) URL.revokeObjectURL(this.activeProductObjectUrl);
2981
+ this.activeProductObjectUrl = nextUrl;
2982
+ }
1859
2983
  async loadDemoProduct() {
1860
- this.submitProductButton.disabled = true;
1861
- this.setStatus(this.labels.loadProduct, "busy");
2984
+ this.setProductLoading(true);
2985
+ this.setStatus(this.labels.loadingProduct, "busy");
1862
2986
  try {
1863
2987
  await this.customizer.loadProduct({});
2988
+ this.replaceActiveProductObjectUrl();
1864
2989
  this.closeDialog(this.productDialog);
1865
2990
  this.setStatus(this.labels.productReady);
1866
2991
  } catch (error) {
1867
- this.setStatus(errorMessage(error), "error");
2992
+ this.setStatus(this.formatError(error), "error");
1868
2993
  } finally {
1869
- this.submitProductButton.disabled = false;
2994
+ this.setProductLoading(false);
1870
2995
  }
1871
2996
  }
1872
- async loadRemoteProduct(event) {
2997
+ async loadProductFromDialog(event) {
1873
2998
  event.preventDefault();
2999
+ this.validateProductForm();
1874
3000
  if (!this.remoteForm.reportValidity()) return;
1875
- this.submitProductButton.disabled = true;
1876
- this.setStatus(this.labels.loadProduct, "busy");
3001
+ const file = this.productSourceMode === "file" ? this.modelFileInput.files?.[0] : void 0;
3002
+ let pendingObjectUrl;
3003
+ this.setProductLoading(true);
3004
+ this.setStatus(this.labels.loadingProduct, "busy");
1877
3005
  try {
3006
+ pendingObjectUrl = file ? URL.createObjectURL(file) : void 0;
1878
3007
  await this.customizer.loadProduct({
1879
- modelUrl: requiredElement(this.element, "[data-role=\"model-url\"]").value,
3008
+ modelUrl: pendingObjectUrl ?? this.modelUrlInput.value,
1880
3009
  textureUrl: requiredElement(this.element, "[data-role=\"texture-url\"]").value,
1881
3010
  surfaceMesh: requiredElement(this.element, "[data-role=\"mesh-name\"]").value,
1882
3011
  textureFlipY: requiredElement(this.element, "[data-role=\"flip-texture\"]").checked
1883
3012
  });
3013
+ this.replaceActiveProductObjectUrl(pendingObjectUrl);
3014
+ pendingObjectUrl = void 0;
1884
3015
  this.closeDialog(this.productDialog);
1885
3016
  this.setStatus(this.labels.productReady);
1886
3017
  } catch (error) {
1887
- this.setStatus(errorMessage(error), "error");
3018
+ this.setStatus(this.formatError(error), "error");
1888
3019
  } finally {
1889
- this.submitProductButton.disabled = false;
3020
+ if (pendingObjectUrl) URL.revokeObjectURL(pendingObjectUrl);
3021
+ this.setProductLoading(false);
1890
3022
  }
1891
3023
  }
1892
3024
  };