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.
- package/CHANGELOG.md +91 -56
- package/LICENSES/NunitoSans-OFL.txt +93 -93
- package/LICENSES/plain-mug-CC-BY-4.0.txt +11 -0
- package/README.md +592 -461
- package/README.zh-CN.md +587 -459
- package/dist/{ProductCustomizer-rMRyWe7L.js → ProductCustomizer-BnjY3v7d.js} +898 -156
- package/dist/ProductCustomizer-BnjY3v7d.js.map +1 -0
- package/dist/bridge/TextureBridge.d.ts +1 -1
- package/dist/core/api.d.ts +94 -0
- package/dist/core/api.d.ts.map +1 -0
- package/dist/core/config.d.ts +3 -12
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/design.d.ts.map +1 -1
- package/dist/core/types.d.ts +183 -5
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/uv.d.ts +8 -0
- package/dist/core/uv.d.ts.map +1 -0
- package/dist/cup_decal_small_margins.glb +0 -0
- package/dist/customizer/ProductCustomizer.d.ts +98 -9
- package/dist/customizer/ProductCustomizer.d.ts.map +1 -1
- package/dist/editor/DesignEditor.d.ts +118 -10
- package/dist/editor/DesignEditor.d.ts.map +1 -1
- package/dist/editor/uvBounds.d.ts +15 -0
- package/dist/editor/uvBounds.d.ts.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/style.css +2120 -1327
- package/dist/viewer/ProductViewer.d.ts +37 -6
- package/dist/viewer/ProductViewer.d.ts.map +1 -1
- package/dist/viewer/uvLayout.d.ts +11 -0
- package/dist/viewer/uvLayout.d.ts.map +1 -0
- package/dist/workbench/CustomForgeWorkbench.d.ts +85 -8
- package/dist/workbench/CustomForgeWorkbench.d.ts.map +1 -1
- package/dist/workbench/config.d.ts +42 -1
- package/dist/workbench/config.d.ts.map +1 -1
- package/dist/workbench/icons.d.ts.map +1 -1
- package/dist/workbench/index.d.ts +3 -3
- package/dist/workbench/index.d.ts.map +1 -1
- package/dist/workbench/template.d.ts.map +1 -1
- package/dist/workbench/types.d.ts +261 -13
- package/dist/workbench/types.d.ts.map +1 -1
- package/dist/workbench.js +1248 -116
- package/dist/workbench.js.map +1 -1
- package/package.json +27 -20
- 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-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
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: "#
|
|
107
|
-
muted: "#
|
|
108
|
-
border: "#
|
|
167
|
+
ink: "#18181b",
|
|
168
|
+
muted: "#71717a",
|
|
169
|
+
border: "#e4e4e7",
|
|
109
170
|
surface: "#ffffff",
|
|
110
|
-
surfaceMuted: "#
|
|
111
|
-
stage: "#
|
|
112
|
-
accent: "#
|
|
113
|
-
accentHover: "#
|
|
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: "
|
|
178
|
+
controlRadius: "4px"
|
|
118
179
|
};
|
|
119
180
|
var defaultTextPresets = [
|
|
120
181
|
{
|
|
121
182
|
id: "modern-bold",
|
|
122
183
|
name: "Modern bold",
|
|
123
|
-
previewText: "
|
|
184
|
+
previewText: "CustomForge",
|
|
124
185
|
fontFamily: "Arial",
|
|
125
|
-
fontSize:
|
|
126
|
-
width:
|
|
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/
|
|
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
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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="
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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-
|
|
940
|
-
<
|
|
941
|
-
<
|
|
942
|
-
<
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
979
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1409
|
-
toolbar
|
|
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.
|
|
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
|
-
|
|
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.
|
|
2364
|
+
case "Loading design": return this.labels.loadingDesign;
|
|
1431
2365
|
case "Design loaded": return this.labels.designLoaded;
|
|
1432
|
-
case "Loading product": return this.labels.
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
2389
|
+
downloadJson(this.labels.designFilename, this.customizer.saveDesign());
|
|
1456
2390
|
this.setStatus(this.labels.designSaved);
|
|
1457
2391
|
} catch (error) {
|
|
1458
|
-
this.setStatus(
|
|
2392
|
+
this.setStatus(this.formatError(error), "error");
|
|
1459
2393
|
}
|
|
1460
2394
|
}
|
|
1461
|
-
|
|
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.
|
|
1464
|
-
this.
|
|
2524
|
+
objects.forEach((object) => this.customizer.updateText(object.id, options));
|
|
2525
|
+
this.updateTextToolbar();
|
|
1465
2526
|
} catch (error) {
|
|
1466
|
-
this.setStatus(
|
|
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
|
-
|
|
1708
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
2901
|
+
this.setStatus(this.labels.imageAdded);
|
|
1822
2902
|
} catch (error) {
|
|
1823
|
-
this.setStatus(
|
|
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.
|
|
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(
|
|
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.
|
|
1861
|
-
this.setStatus(this.labels.
|
|
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(
|
|
2992
|
+
this.setStatus(this.formatError(error), "error");
|
|
1868
2993
|
} finally {
|
|
1869
|
-
this.
|
|
2994
|
+
this.setProductLoading(false);
|
|
1870
2995
|
}
|
|
1871
2996
|
}
|
|
1872
|
-
async
|
|
2997
|
+
async loadProductFromDialog(event) {
|
|
1873
2998
|
event.preventDefault();
|
|
2999
|
+
this.validateProductForm();
|
|
1874
3000
|
if (!this.remoteForm.reportValidity()) return;
|
|
1875
|
-
this.
|
|
1876
|
-
|
|
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:
|
|
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(
|
|
3018
|
+
this.setStatus(this.formatError(error), "error");
|
|
1888
3019
|
} finally {
|
|
1889
|
-
|
|
3020
|
+
if (pendingObjectUrl) URL.revokeObjectURL(pendingObjectUrl);
|
|
3021
|
+
this.setProductLoading(false);
|
|
1890
3022
|
}
|
|
1891
3023
|
}
|
|
1892
3024
|
};
|