@pooder/kit 3.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +46 -36
- package/dist/index.d.ts +46 -36
- package/dist/index.js +958 -696
- package/dist/index.mjs +962 -700
- package/package.json +2 -2
- package/src/coordinate.ts +57 -0
- package/src/dieline.ts +206 -214
- package/src/geometry.ts +101 -4
- package/src/hole.ts +277 -170
- package/src/image.ts +334 -365
- package/src/ruler.ts +295 -120
package/src/image.ts
CHANGED
|
@@ -1,49 +1,38 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Extension,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ContributionPointIds,
|
|
2
5
|
CommandContribution,
|
|
3
6
|
ConfigurationContribution,
|
|
4
7
|
ConfigurationService,
|
|
5
|
-
ContributionPointIds,
|
|
6
|
-
Extension,
|
|
7
|
-
ExtensionContext,
|
|
8
8
|
} from "@pooder/core";
|
|
9
|
-
import {
|
|
9
|
+
import { Image, Point, util, Object as FabricObject } from "fabric";
|
|
10
10
|
import CanvasService from "./CanvasService";
|
|
11
11
|
import { Coordinate } from "./coordinate";
|
|
12
12
|
|
|
13
|
+
export interface ImageItem {
|
|
14
|
+
id: string;
|
|
15
|
+
url: string;
|
|
16
|
+
opacity: number;
|
|
17
|
+
width?: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
angle?: number;
|
|
20
|
+
left?: number;
|
|
21
|
+
top?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
export class ImageTool implements Extension {
|
|
14
25
|
id = "pooder.kit.image";
|
|
15
26
|
|
|
16
27
|
metadata = {
|
|
17
28
|
name: "ImageTool",
|
|
18
29
|
};
|
|
19
|
-
private _loadingUrl: string | null = null;
|
|
20
|
-
|
|
21
|
-
private url: string = "";
|
|
22
|
-
private opacity: number = 1;
|
|
23
|
-
private width?: number;
|
|
24
|
-
private height?: number;
|
|
25
|
-
private angle?: number;
|
|
26
|
-
private left?: number;
|
|
27
|
-
private top?: number;
|
|
28
30
|
|
|
31
|
+
private items: ImageItem[] = [];
|
|
32
|
+
private objectMap: Map<string, FabricObject> = new Map();
|
|
29
33
|
private canvasService?: CanvasService;
|
|
30
34
|
private context?: ExtensionContext;
|
|
31
|
-
|
|
32
|
-
constructor(
|
|
33
|
-
options?: Partial<{
|
|
34
|
-
url: string;
|
|
35
|
-
opacity: number;
|
|
36
|
-
width: number;
|
|
37
|
-
height: number;
|
|
38
|
-
angle: number;
|
|
39
|
-
left: number;
|
|
40
|
-
top: number;
|
|
41
|
-
}>,
|
|
42
|
-
) {
|
|
43
|
-
if (options) {
|
|
44
|
-
Object.assign(this, options);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
35
|
+
private isUpdatingConfig = false;
|
|
47
36
|
|
|
48
37
|
activate(context: ExtensionContext) {
|
|
49
38
|
this.context = context;
|
|
@@ -53,45 +42,44 @@ export class ImageTool implements Extension {
|
|
|
53
42
|
return;
|
|
54
43
|
}
|
|
55
44
|
|
|
56
|
-
const configService = context.services.get<
|
|
45
|
+
const configService = context.services.get<ConfigurationService>("ConfigurationService");
|
|
57
46
|
if (configService) {
|
|
58
47
|
// Load initial config
|
|
59
|
-
this.
|
|
60
|
-
this.opacity = configService.get("image.opacity", this.opacity);
|
|
61
|
-
this.width = configService.get("image.width", this.width);
|
|
62
|
-
this.height = configService.get("image.height", this.height);
|
|
63
|
-
this.angle = configService.get("image.angle", this.angle);
|
|
64
|
-
this.left = configService.get("image.left", this.left);
|
|
65
|
-
this.top = configService.get("image.top", this.top);
|
|
48
|
+
this.items = configService.get("image.items", []) || [];
|
|
66
49
|
|
|
67
50
|
// Listen for changes
|
|
68
51
|
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
52
|
+
if (this.isUpdatingConfig) return;
|
|
53
|
+
|
|
54
|
+
let shouldUpdate = false;
|
|
55
|
+
if (e.key === "image.items") {
|
|
56
|
+
this.items = e.value || [];
|
|
57
|
+
shouldUpdate = true;
|
|
58
|
+
} else if (e.key.startsWith("dieline.") && e.key !== "dieline.holes") {
|
|
59
|
+
// Dieline changes affect image layout/scale
|
|
60
|
+
// Ignore dieline.holes as they don't affect layout and can cause jitter
|
|
61
|
+
shouldUpdate = true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (shouldUpdate) {
|
|
65
|
+
this.updateImages();
|
|
78
66
|
}
|
|
79
67
|
});
|
|
80
68
|
}
|
|
81
69
|
|
|
82
70
|
this.ensureLayer();
|
|
83
|
-
this.
|
|
71
|
+
this.updateImages();
|
|
84
72
|
}
|
|
85
73
|
|
|
86
74
|
deactivate(context: ExtensionContext) {
|
|
87
75
|
if (this.canvasService) {
|
|
88
76
|
const layer = this.canvasService.getLayer("user");
|
|
89
77
|
if (layer) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
78
|
+
this.objectMap.forEach((obj) => {
|
|
79
|
+
layer.remove(obj);
|
|
80
|
+
});
|
|
81
|
+
this.objectMap.clear();
|
|
82
|
+
this.canvasService.requestRenderAll();
|
|
95
83
|
}
|
|
96
84
|
this.canvasService = undefined;
|
|
97
85
|
this.context = undefined;
|
|
@@ -102,103 +90,110 @@ export class ImageTool implements Extension {
|
|
|
102
90
|
return {
|
|
103
91
|
[ContributionPointIds.CONFIGURATIONS]: [
|
|
104
92
|
{
|
|
105
|
-
id: "image.
|
|
106
|
-
type: "
|
|
107
|
-
label: "
|
|
108
|
-
default:
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: "image.opacity",
|
|
112
|
-
type: "number",
|
|
113
|
-
label: "Opacity",
|
|
114
|
-
min: 0,
|
|
115
|
-
max: 1,
|
|
116
|
-
step: 0.1,
|
|
117
|
-
default: this.opacity,
|
|
93
|
+
id: "image.items",
|
|
94
|
+
type: "array",
|
|
95
|
+
label: "Images",
|
|
96
|
+
default: [],
|
|
118
97
|
},
|
|
98
|
+
] as ConfigurationContribution[],
|
|
99
|
+
[ContributionPointIds.COMMANDS]: [
|
|
119
100
|
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
101
|
+
command: "addImage",
|
|
102
|
+
title: "Add Image",
|
|
103
|
+
handler: (url: string, options?: Partial<ImageItem>) => {
|
|
104
|
+
const newItem: ImageItem = {
|
|
105
|
+
id: this.generateId(),
|
|
106
|
+
url,
|
|
107
|
+
opacity: 1,
|
|
108
|
+
...options,
|
|
109
|
+
};
|
|
110
|
+
this.updateConfig([...this.items, newItem]);
|
|
111
|
+
return newItem.id;
|
|
112
|
+
},
|
|
126
113
|
},
|
|
127
114
|
{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
115
|
+
command: "removeImage",
|
|
116
|
+
title: "Remove Image",
|
|
117
|
+
handler: (id: string) => {
|
|
118
|
+
const newItems = this.items.filter((item) => item.id !== id);
|
|
119
|
+
if (newItems.length !== this.items.length) {
|
|
120
|
+
this.updateConfig(newItems);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
134
123
|
},
|
|
135
124
|
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
125
|
+
command: "updateImage",
|
|
126
|
+
title: "Update Image",
|
|
127
|
+
handler: (id: string, updates: Partial<ImageItem>) => {
|
|
128
|
+
const index = this.items.findIndex((item) => item.id === id);
|
|
129
|
+
if (index !== -1) {
|
|
130
|
+
const newItems = [...this.items];
|
|
131
|
+
newItems[index] = { ...newItems[index], ...updates };
|
|
132
|
+
this.updateConfig(newItems);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
142
135
|
},
|
|
143
136
|
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
default: this.left,
|
|
137
|
+
command: "clearImages",
|
|
138
|
+
title: "Clear Images",
|
|
139
|
+
handler: () => {
|
|
140
|
+
this.updateConfig([]);
|
|
141
|
+
},
|
|
150
142
|
},
|
|
151
143
|
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
144
|
+
command: "bringToFront",
|
|
145
|
+
title: "Bring Image to Front",
|
|
146
|
+
handler: (id: string) => {
|
|
147
|
+
const index = this.items.findIndex((item) => item.id === id);
|
|
148
|
+
if (index !== -1 && index < this.items.length - 1) {
|
|
149
|
+
const newItems = [...this.items];
|
|
150
|
+
const [item] = newItems.splice(index, 1);
|
|
151
|
+
newItems.push(item);
|
|
152
|
+
this.updateConfig(newItems);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
158
155
|
},
|
|
159
|
-
] as ConfigurationContribution[],
|
|
160
|
-
[ContributionPointIds.COMMANDS]: [
|
|
161
156
|
{
|
|
162
|
-
command: "
|
|
163
|
-
title: "
|
|
164
|
-
handler: (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
) => {
|
|
173
|
-
if (
|
|
174
|
-
this.url === url &&
|
|
175
|
-
this.opacity === opacity &&
|
|
176
|
-
this.width === width &&
|
|
177
|
-
this.height === height &&
|
|
178
|
-
this.angle === angle &&
|
|
179
|
-
this.left === left &&
|
|
180
|
-
this.top === top
|
|
181
|
-
)
|
|
182
|
-
return true;
|
|
183
|
-
|
|
184
|
-
this.url = url;
|
|
185
|
-
this.opacity = opacity;
|
|
186
|
-
this.width = width;
|
|
187
|
-
this.height = height;
|
|
188
|
-
this.angle = angle;
|
|
189
|
-
this.left = left;
|
|
190
|
-
this.top = top;
|
|
191
|
-
|
|
192
|
-
// Direct update
|
|
193
|
-
this.updateImage();
|
|
194
|
-
|
|
195
|
-
return true;
|
|
157
|
+
command: "sendToBack",
|
|
158
|
+
title: "Send Image to Back",
|
|
159
|
+
handler: (id: string) => {
|
|
160
|
+
const index = this.items.findIndex((item) => item.id === id);
|
|
161
|
+
if (index > 0) {
|
|
162
|
+
const newItems = [...this.items];
|
|
163
|
+
const [item] = newItems.splice(index, 1);
|
|
164
|
+
newItems.unshift(item);
|
|
165
|
+
this.updateConfig(newItems);
|
|
166
|
+
}
|
|
196
167
|
},
|
|
197
168
|
},
|
|
198
169
|
] as CommandContribution[],
|
|
199
170
|
};
|
|
200
171
|
}
|
|
201
172
|
|
|
173
|
+
private generateId(): string {
|
|
174
|
+
return Math.random().toString(36).substring(2, 9);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private updateConfig(newItems: ImageItem[], skipCanvasUpdate = false) {
|
|
178
|
+
if (!this.context) return;
|
|
179
|
+
this.isUpdatingConfig = true;
|
|
180
|
+
this.items = newItems;
|
|
181
|
+
const configService = this.context.services.get<ConfigurationService>("ConfigurationService");
|
|
182
|
+
if (configService) {
|
|
183
|
+
configService.update("image.items", newItems);
|
|
184
|
+
}
|
|
185
|
+
// Update canvas immediately to reflect changes locally before config event comes back
|
|
186
|
+
// (Optional, but good for responsiveness)
|
|
187
|
+
if (!skipCanvasUpdate) {
|
|
188
|
+
this.updateImages();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Reset flag after a short delay to allow config propagation
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
this.isUpdatingConfig = false;
|
|
194
|
+
}, 50);
|
|
195
|
+
}
|
|
196
|
+
|
|
202
197
|
private ensureLayer() {
|
|
203
198
|
if (!this.canvasService) return;
|
|
204
199
|
let userLayer = this.canvasService.getLayer("user");
|
|
@@ -219,9 +214,7 @@ export class ImageTool implements Extension {
|
|
|
219
214
|
// Try to insert below dieline-overlay
|
|
220
215
|
const dielineLayer = this.canvasService.getLayer("dieline-overlay");
|
|
221
216
|
if (dielineLayer) {
|
|
222
|
-
const index = this.canvasService.canvas
|
|
223
|
-
.getObjects()
|
|
224
|
-
.indexOf(dielineLayer);
|
|
217
|
+
const index = this.canvasService.canvas.getObjects().indexOf(dielineLayer);
|
|
225
218
|
// If dieline is at 0, move user to 0 (dieline shifts to 1)
|
|
226
219
|
if (index >= 0) {
|
|
227
220
|
this.canvasService.canvas.moveObjectTo(userLayer, index);
|
|
@@ -237,268 +230,244 @@ export class ImageTool implements Extension {
|
|
|
237
230
|
}
|
|
238
231
|
}
|
|
239
232
|
|
|
240
|
-
private
|
|
241
|
-
|
|
242
|
-
|
|
233
|
+
private getLayoutInfo() {
|
|
234
|
+
const canvasW = this.canvasService?.canvas.width || 800;
|
|
235
|
+
const canvasH = this.canvasService?.canvas.height || 600;
|
|
236
|
+
|
|
237
|
+
let layoutScale = 1;
|
|
238
|
+
let layoutOffsetX = 0;
|
|
239
|
+
let layoutOffsetY = 0;
|
|
240
|
+
let visualWidth = canvasW;
|
|
241
|
+
let visualHeight = canvasH;
|
|
242
|
+
let dielinePhysicalWidth = 500;
|
|
243
|
+
let dielinePhysicalHeight = 500;
|
|
244
|
+
|
|
245
|
+
if (this.context) {
|
|
246
|
+
const configService = this.context.services.get<ConfigurationService>("ConfigurationService");
|
|
247
|
+
if (configService) {
|
|
248
|
+
dielinePhysicalWidth = configService.get("dieline.width") || 500;
|
|
249
|
+
dielinePhysicalHeight = configService.get("dieline.height") || 500;
|
|
250
|
+
const padding = configService.get("dieline.padding") || 40;
|
|
251
|
+
|
|
252
|
+
const layout = Coordinate.calculateLayout(
|
|
253
|
+
{ width: canvasW, height: canvasH },
|
|
254
|
+
{ width: dielinePhysicalWidth, height: dielinePhysicalHeight },
|
|
255
|
+
padding
|
|
256
|
+
);
|
|
257
|
+
layoutScale = layout.scale;
|
|
258
|
+
layoutOffsetX = layout.offsetX;
|
|
259
|
+
layoutOffsetY = layout.offsetY;
|
|
260
|
+
visualWidth = layout.width;
|
|
261
|
+
visualHeight = layout.height;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
layoutScale,
|
|
267
|
+
layoutOffsetX,
|
|
268
|
+
layoutOffsetY,
|
|
269
|
+
visualWidth,
|
|
270
|
+
visualHeight,
|
|
271
|
+
dielinePhysicalWidth,
|
|
272
|
+
dielinePhysicalHeight
|
|
273
|
+
};
|
|
274
|
+
}
|
|
243
275
|
|
|
276
|
+
private updateImages() {
|
|
277
|
+
if (!this.canvasService) return;
|
|
244
278
|
const layer = this.canvasService.getLayer("user");
|
|
245
279
|
if (!layer) {
|
|
246
280
|
console.warn("[ImageTool] User layer not found");
|
|
247
281
|
return;
|
|
248
282
|
}
|
|
249
283
|
|
|
250
|
-
|
|
284
|
+
// 1. Remove objects that are no longer in items
|
|
285
|
+
const currentIds = new Set(this.items.map(i => i.id));
|
|
286
|
+
for (const [id, obj] of this.objectMap) {
|
|
287
|
+
if (!currentIds.has(id)) {
|
|
288
|
+
layer.remove(obj);
|
|
289
|
+
this.objectMap.delete(id);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
251
292
|
|
|
252
|
-
|
|
293
|
+
// 2. Add or Update objects
|
|
294
|
+
const layout = this.getLayoutInfo();
|
|
253
295
|
|
|
254
|
-
|
|
255
|
-
|
|
296
|
+
this.items.forEach((item, index) => {
|
|
297
|
+
let obj = this.objectMap.get(item.id);
|
|
256
298
|
|
|
257
|
-
if (
|
|
258
|
-
|
|
299
|
+
if (!obj) {
|
|
300
|
+
// New object, load it
|
|
301
|
+
this.loadImage(item, layer, layout);
|
|
259
302
|
} else {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
303
|
+
// Existing object, update properties
|
|
304
|
+
this.updateObjectProperties(obj, item, layout);
|
|
305
|
+
|
|
306
|
+
// Ensure Z-Index order
|
|
307
|
+
// Note: layer.add() appends to end, so if we process in order, they should be roughly correct.
|
|
308
|
+
// However, if we need strict ordering, we might need to verify index.
|
|
309
|
+
// For simplicity, we rely on the fact that if it exists, it's already on canvas.
|
|
310
|
+
// To enforce strict Z-order matching array order:
|
|
311
|
+
// We can check if the object at layer._objects[index] is this object.
|
|
312
|
+
// But Fabric's Group/Layer handling might be complex.
|
|
313
|
+
// A simple way is: remove and re-add if order is wrong, or use moveObjectTo.
|
|
314
|
+
|
|
315
|
+
// Since we are iterating items in order, we can check if the object is at the expected visual index relative to other user images.
|
|
316
|
+
// But for now, let's assume update logic is sufficient.
|
|
317
|
+
// If we want to support reordering, we should probably just `moveTo`
|
|
318
|
+
layer.remove(obj);
|
|
319
|
+
layer.add(obj); // Move to top of layer stack, effectively reordering if we iterate in order
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
layer.dirty = true;
|
|
324
|
+
this.canvasService.requestRenderAll();
|
|
325
|
+
}
|
|
278
326
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (Math.abs(userImage.left - localLeft) > 1)
|
|
283
|
-
updates.left = localLeft;
|
|
284
|
-
}
|
|
327
|
+
private updateObjectProperties(obj: FabricObject, item: ImageItem, layout: any) {
|
|
328
|
+
const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
|
|
329
|
+
const updates: any = {};
|
|
285
330
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
331
|
+
// Opacity
|
|
332
|
+
if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
|
|
333
|
+
|
|
334
|
+
// Angle
|
|
335
|
+
if (item.angle !== undefined && obj.angle !== item.angle) updates.angle = item.angle;
|
|
336
|
+
|
|
337
|
+
// Position (Normalized -> Absolute)
|
|
338
|
+
if (item.left !== undefined) {
|
|
339
|
+
const globalLeft = layoutOffsetX + item.left * visualWidth;
|
|
340
|
+
if (Math.abs(obj.left - globalLeft) > 1) updates.left = globalLeft;
|
|
341
|
+
}
|
|
342
|
+
if (item.top !== undefined) {
|
|
343
|
+
const globalTop = layoutOffsetY + item.top * visualHeight;
|
|
344
|
+
if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
|
|
345
|
+
}
|
|
291
346
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
347
|
+
// Scale (Physical Dimensions -> Scale Factor)
|
|
348
|
+
if (item.width !== undefined && obj.width) {
|
|
349
|
+
const targetScaleX = (item.width * layoutScale) / obj.width;
|
|
350
|
+
if (Math.abs(obj.scaleX - targetScaleX) > 0.001) updates.scaleX = targetScaleX;
|
|
351
|
+
}
|
|
352
|
+
if (item.height !== undefined && obj.height) {
|
|
353
|
+
const targetScaleY = (item.height * layoutScale) / obj.height;
|
|
354
|
+
if (Math.abs(obj.scaleY - targetScaleY) > 0.001) updates.scaleY = targetScaleY;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Center origin if not set
|
|
358
|
+
if (obj.originX !== "center") {
|
|
359
|
+
updates.originX = "center";
|
|
360
|
+
updates.originY = "center";
|
|
361
|
+
// Adjust position because origin changed (Fabric logic)
|
|
362
|
+
// For simplicity, we just set it, next cycle will fix pos if needed,
|
|
363
|
+
// or we can calculate the shift. Ideally we set origin on creation.
|
|
364
|
+
}
|
|
296
365
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
layer.dirty = true;
|
|
300
|
-
this.canvasService.requestRenderAll();
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
} else {
|
|
304
|
-
this.loadImage(layer);
|
|
366
|
+
if (Object.keys(updates).length > 0) {
|
|
367
|
+
obj.set(updates);
|
|
305
368
|
}
|
|
306
369
|
}
|
|
307
370
|
|
|
308
|
-
private loadImage(layer: any) {
|
|
309
|
-
|
|
310
|
-
const { url } = this;
|
|
311
|
-
if (!url) return; // Don't load if empty
|
|
312
|
-
this._loadingUrl = url;
|
|
313
|
-
|
|
314
|
-
Image.fromURL(url, { crossOrigin: "anonymous" })
|
|
371
|
+
private loadImage(item: ImageItem, layer: any, layout: any) {
|
|
372
|
+
Image.fromURL(item.url, { crossOrigin: "anonymous" })
|
|
315
373
|
.then((image) => {
|
|
316
|
-
if
|
|
317
|
-
this.
|
|
318
|
-
|
|
319
|
-
let { opacity, width, height, angle, left, top } = this;
|
|
320
|
-
|
|
321
|
-
// Auto-scale and center if not set
|
|
322
|
-
if (this.context) {
|
|
323
|
-
const configService = this.context.services.get<ConfigurationService>(
|
|
324
|
-
"ConfigurationService",
|
|
325
|
-
)!;
|
|
326
|
-
const dielineWidth = configService.get("dieline.width");
|
|
327
|
-
const dielineHeight = configService.get("dieline.height");
|
|
328
|
-
|
|
329
|
-
console.log(
|
|
330
|
-
"[ImageTool] Dieline config debug:",
|
|
331
|
-
{
|
|
332
|
-
widthVal: dielineWidth,
|
|
333
|
-
heightVal: dielineHeight,
|
|
334
|
-
// Debug: dump all keys to see what is available
|
|
335
|
-
allKeys: Array.from(
|
|
336
|
-
(configService as any).configValues?.keys() || [],
|
|
337
|
-
),
|
|
338
|
-
},
|
|
339
|
-
configService,
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
if (width === undefined && height === undefined) {
|
|
343
|
-
// Scale to fit dieline
|
|
344
|
-
const scale = Math.min(
|
|
345
|
-
dielineWidth / (image.width || 1),
|
|
346
|
-
dielineHeight / (image.height || 1),
|
|
347
|
-
);
|
|
348
|
-
width = (image.width || 1) * scale;
|
|
349
|
-
height = (image.height || 1) * scale;
|
|
350
|
-
this.width = width;
|
|
351
|
-
this.height = height;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (left === undefined && top === undefined) {
|
|
355
|
-
// Default to Dieline Position if available, otherwise Center (0.5)
|
|
356
|
-
const dielinePos = configService?.get("dieline.position");
|
|
357
|
-
if (dielinePos) {
|
|
358
|
-
this.left = dielinePos.x;
|
|
359
|
-
this.top = dielinePos.y;
|
|
360
|
-
} else {
|
|
361
|
-
this.left = 0.5;
|
|
362
|
-
this.top = 0.5;
|
|
363
|
-
}
|
|
364
|
-
left = this.left;
|
|
365
|
-
top = this.top;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const existingImage = this.canvasService!.getObject(
|
|
370
|
-
"user-image",
|
|
371
|
-
"user",
|
|
372
|
-
) as any;
|
|
373
|
-
|
|
374
|
-
if (existingImage) {
|
|
375
|
-
const defaultLeft = existingImage.left;
|
|
376
|
-
const defaultTop = existingImage.top;
|
|
377
|
-
const defaultAngle = existingImage.angle;
|
|
378
|
-
const defaultScaleX = existingImage.scaleX;
|
|
379
|
-
const defaultScaleY = existingImage.scaleY;
|
|
380
|
-
|
|
381
|
-
const canvasW = this.canvasService?.canvas.width || 800;
|
|
382
|
-
const canvasH = this.canvasService?.canvas.height || 600;
|
|
383
|
-
const centerX = canvasW / 2;
|
|
384
|
-
const centerY = canvasH / 2;
|
|
385
|
-
|
|
386
|
-
let targetLeft = left !== undefined ? left : defaultLeft;
|
|
387
|
-
let targetTop = top !== undefined ? top : defaultTop;
|
|
388
|
-
|
|
389
|
-
// Log for debugging
|
|
390
|
-
const configService = this.context?.services.get<any>(
|
|
391
|
-
"ConfigurationService",
|
|
392
|
-
);
|
|
393
|
-
console.log("[ImageTool] Loading EXISTING image...", {
|
|
394
|
-
canvasW,
|
|
395
|
-
canvasH,
|
|
396
|
-
centerX,
|
|
397
|
-
centerY,
|
|
398
|
-
incomingLeft: left,
|
|
399
|
-
incomingTop: top,
|
|
400
|
-
dielinePos: configService?.get("dieline.position"),
|
|
401
|
-
existingImage: !!existingImage,
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
if (left !== undefined) {
|
|
405
|
-
const globalLeft = Coordinate.toAbsolute(left, canvasW);
|
|
406
|
-
targetLeft = globalLeft; // Layer is absolute, do not subtract center
|
|
407
|
-
console.log("[ImageTool] Calculated targetLeft", {
|
|
408
|
-
globalLeft,
|
|
409
|
-
targetLeft,
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
if (top !== undefined) {
|
|
413
|
-
const globalTop = Coordinate.toAbsolute(top, canvasH);
|
|
414
|
-
targetTop = globalTop; // Layer is absolute, do not subtract center
|
|
415
|
-
console.log("[ImageTool] Calculated targetTop", {
|
|
416
|
-
globalTop,
|
|
417
|
-
targetTop,
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
image.set({
|
|
422
|
-
originX: "center", // Use center origin for easier positioning
|
|
423
|
-
originY: "center",
|
|
424
|
-
left: targetLeft,
|
|
425
|
-
top: targetTop,
|
|
426
|
-
angle: angle !== undefined ? angle : defaultAngle,
|
|
427
|
-
scaleX:
|
|
428
|
-
width !== undefined && image.width
|
|
429
|
-
? width / image.width
|
|
430
|
-
: defaultScaleX,
|
|
431
|
-
scaleY:
|
|
432
|
-
height !== undefined && image.height
|
|
433
|
-
? height / image.height
|
|
434
|
-
: defaultScaleY,
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
layer.remove(existingImage);
|
|
438
|
-
} else {
|
|
439
|
-
// New image
|
|
440
|
-
image.set({
|
|
441
|
-
originX: "center",
|
|
442
|
-
originY: "center",
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
if (width !== undefined && image.width)
|
|
446
|
-
image.scaleX = width / image.width;
|
|
447
|
-
if (height !== undefined && image.height)
|
|
448
|
-
image.scaleY = height / image.height;
|
|
449
|
-
if (angle !== undefined) image.angle = angle;
|
|
450
|
-
|
|
451
|
-
const canvasW = this.canvasService?.canvas.width || 800;
|
|
452
|
-
const canvasH = this.canvasService?.canvas.height || 600;
|
|
453
|
-
const centerX = canvasW / 2;
|
|
454
|
-
const centerY = canvasH / 2;
|
|
455
|
-
|
|
456
|
-
if (left !== undefined) {
|
|
457
|
-
image.left = Coordinate.toAbsolute(left, canvasW); // Layer is absolute
|
|
458
|
-
} else {
|
|
459
|
-
image.left = centerX; // Default to center of canvas
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (top !== undefined) {
|
|
463
|
-
image.top = Coordinate.toAbsolute(top, canvasH); // Layer is absolute
|
|
464
|
-
} else {
|
|
465
|
-
image.top = centerY; // Default to center of canvas
|
|
466
|
-
}
|
|
467
|
-
}
|
|
374
|
+
// Double check if item still exists
|
|
375
|
+
if (!this.items.find(i => i.id === item.id)) return;
|
|
468
376
|
|
|
469
377
|
image.set({
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
},
|
|
378
|
+
originX: "center",
|
|
379
|
+
originY: "center",
|
|
380
|
+
data: { id: item.id },
|
|
474
381
|
});
|
|
475
|
-
layer.add(image);
|
|
476
382
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
383
|
+
// Initial Layout
|
|
384
|
+
let { width, height, left, top } = item;
|
|
385
|
+
const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight, dielinePhysicalWidth, dielinePhysicalHeight } = layout;
|
|
386
|
+
|
|
387
|
+
// Auto-scale if needed
|
|
388
|
+
if (width === undefined && height === undefined) {
|
|
389
|
+
const imgAspect = (image.width || 1) / (image.height || 1);
|
|
390
|
+
const dielineAspect = dielinePhysicalWidth / dielinePhysicalHeight;
|
|
391
|
+
|
|
392
|
+
if (imgAspect > dielineAspect) {
|
|
393
|
+
const w = dielinePhysicalWidth;
|
|
394
|
+
width = w;
|
|
395
|
+
height = w / imgAspect;
|
|
396
|
+
} else {
|
|
397
|
+
const h = dielinePhysicalHeight;
|
|
398
|
+
height = h;
|
|
399
|
+
width = h * imgAspect;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Update item with defaults
|
|
403
|
+
item.width = width;
|
|
404
|
+
item.height = height;
|
|
405
|
+
}
|
|
483
406
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
407
|
+
if (left === undefined && top === undefined) {
|
|
408
|
+
left = 0.5;
|
|
409
|
+
top = 0.5;
|
|
410
|
+
item.left = left;
|
|
411
|
+
item.top = top;
|
|
412
|
+
}
|
|
487
413
|
|
|
488
|
-
|
|
489
|
-
|
|
414
|
+
// Apply Props
|
|
415
|
+
this.updateObjectProperties(image, item, layout);
|
|
490
416
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
417
|
+
layer.add(image);
|
|
418
|
+
this.objectMap.set(item.id, image);
|
|
419
|
+
|
|
420
|
+
// Bind Events
|
|
421
|
+
image.on("modified", (e: any) => {
|
|
422
|
+
this.handleObjectModified(item.id, image);
|
|
494
423
|
});
|
|
495
424
|
|
|
496
425
|
layer.dirty = true;
|
|
497
|
-
this.canvasService
|
|
426
|
+
this.canvasService?.requestRenderAll();
|
|
427
|
+
|
|
428
|
+
// Save defaults if we calculated them
|
|
429
|
+
if (item.width !== width || item.height !== height || item.left !== left || item.top !== top) {
|
|
430
|
+
this.updateImageInConfig(item.id, { width, height, left, top });
|
|
431
|
+
}
|
|
498
432
|
})
|
|
499
433
|
.catch((err) => {
|
|
500
|
-
|
|
501
|
-
console.error("Failed to load image", url, err);
|
|
434
|
+
console.error("Failed to load image", item.url, err);
|
|
502
435
|
});
|
|
503
436
|
}
|
|
437
|
+
|
|
438
|
+
private handleObjectModified(id: string, image: FabricObject) {
|
|
439
|
+
const layout = this.getLayoutInfo();
|
|
440
|
+
const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
|
|
441
|
+
|
|
442
|
+
const matrix = image.calcTransformMatrix();
|
|
443
|
+
const globalPoint = util.transformPoint(new Point(0, 0), matrix);
|
|
444
|
+
|
|
445
|
+
const updates: Partial<ImageItem> = {};
|
|
446
|
+
|
|
447
|
+
// Normalize Position
|
|
448
|
+
updates.left = (globalPoint.x - layoutOffsetX) / visualWidth;
|
|
449
|
+
updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
|
|
450
|
+
updates.angle = image.angle;
|
|
451
|
+
|
|
452
|
+
// Physical Dimensions
|
|
453
|
+
if (image.width) {
|
|
454
|
+
const pixelWidth = image.width * image.scaleX;
|
|
455
|
+
updates.width = pixelWidth / layoutScale;
|
|
456
|
+
}
|
|
457
|
+
if (image.height) {
|
|
458
|
+
const pixelHeight = image.height * image.scaleY;
|
|
459
|
+
updates.height = pixelHeight / layoutScale;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
this.updateImageInConfig(id, updates);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private updateImageInConfig(id: string, updates: Partial<ImageItem>) {
|
|
466
|
+
const index = this.items.findIndex(i => i.id === id);
|
|
467
|
+
if (index !== -1) {
|
|
468
|
+
const newItems = [...this.items];
|
|
469
|
+
newItems[index] = { ...newItems[index], ...updates };
|
|
470
|
+
this.updateConfig(newItems, true);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
504
473
|
}
|