@pooder/kit 3.1.0 → 3.3.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 +40 -24
- package/dist/index.d.ts +40 -24
- package/dist/index.js +849 -557
- package/dist/index.mjs +854 -562
- package/package.json +2 -2
- package/src/coordinate.ts +57 -0
- package/src/dieline.ts +196 -129
- package/src/geometry.ts +56 -21
- package/src/hole.ts +163 -56
- package/src/image.ts +355 -363
- 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,267 @@ 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
|
+
let bleedOffset = 0;
|
|
245
|
+
|
|
246
|
+
if (this.context) {
|
|
247
|
+
const configService = this.context.services.get<ConfigurationService>("ConfigurationService");
|
|
248
|
+
if (configService) {
|
|
249
|
+
dielinePhysicalWidth = configService.get("dieline.width") || 500;
|
|
250
|
+
dielinePhysicalHeight = configService.get("dieline.height") || 500;
|
|
251
|
+
bleedOffset = configService.get("dieline.offset") || 0;
|
|
252
|
+
|
|
253
|
+
const paddingValue = configService.get("dieline.padding") || 40;
|
|
254
|
+
let padding = 0;
|
|
255
|
+
if (typeof paddingValue === "number") {
|
|
256
|
+
padding = paddingValue;
|
|
257
|
+
} else if (typeof paddingValue === "string") {
|
|
258
|
+
if (paddingValue.endsWith("%")) {
|
|
259
|
+
const percent = parseFloat(paddingValue) / 100;
|
|
260
|
+
padding = Math.min(canvasW, canvasH) * percent;
|
|
261
|
+
} else {
|
|
262
|
+
padding = parseFloat(paddingValue) || 0;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const layout = Coordinate.calculateLayout(
|
|
267
|
+
{ width: canvasW, height: canvasH },
|
|
268
|
+
{ width: dielinePhysicalWidth, height: dielinePhysicalHeight },
|
|
269
|
+
padding
|
|
270
|
+
);
|
|
271
|
+
layoutScale = layout.scale;
|
|
272
|
+
layoutOffsetX = layout.offsetX;
|
|
273
|
+
layoutOffsetY = layout.offsetY;
|
|
274
|
+
visualWidth = layout.width;
|
|
275
|
+
visualHeight = layout.height;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
243
278
|
|
|
279
|
+
return {
|
|
280
|
+
layoutScale,
|
|
281
|
+
layoutOffsetX,
|
|
282
|
+
layoutOffsetY,
|
|
283
|
+
visualWidth,
|
|
284
|
+
visualHeight,
|
|
285
|
+
dielinePhysicalWidth,
|
|
286
|
+
dielinePhysicalHeight,
|
|
287
|
+
bleedOffset
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private updateImages() {
|
|
292
|
+
if (!this.canvasService) return;
|
|
244
293
|
const layer = this.canvasService.getLayer("user");
|
|
245
294
|
if (!layer) {
|
|
246
295
|
console.warn("[ImageTool] User layer not found");
|
|
247
296
|
return;
|
|
248
297
|
}
|
|
249
298
|
|
|
250
|
-
|
|
299
|
+
// 1. Remove objects that are no longer in items
|
|
300
|
+
const currentIds = new Set(this.items.map(i => i.id));
|
|
301
|
+
for (const [id, obj] of this.objectMap) {
|
|
302
|
+
if (!currentIds.has(id)) {
|
|
303
|
+
layer.remove(obj);
|
|
304
|
+
this.objectMap.delete(id);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
251
307
|
|
|
252
|
-
|
|
308
|
+
// 2. Add or Update objects
|
|
309
|
+
const layout = this.getLayoutInfo();
|
|
253
310
|
|
|
254
|
-
|
|
255
|
-
|
|
311
|
+
this.items.forEach((item, index) => {
|
|
312
|
+
let obj = this.objectMap.get(item.id);
|
|
256
313
|
|
|
257
|
-
if (
|
|
258
|
-
|
|
314
|
+
if (!obj) {
|
|
315
|
+
// New object, load it
|
|
316
|
+
this.loadImage(item, layer, layout);
|
|
259
317
|
} else {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
318
|
+
// Existing object, update properties
|
|
319
|
+
this.updateObjectProperties(obj, item, layout);
|
|
320
|
+
|
|
321
|
+
// Ensure Z-Index order
|
|
322
|
+
// Note: layer.add() appends to end, so if we process in order, they should be roughly correct.
|
|
323
|
+
// However, if we need strict ordering, we might need to verify index.
|
|
324
|
+
// For simplicity, we rely on the fact that if it exists, it's already on canvas.
|
|
325
|
+
// To enforce strict Z-order matching array order:
|
|
326
|
+
// We can check if the object at layer._objects[index] is this object.
|
|
327
|
+
// But Fabric's Group/Layer handling might be complex.
|
|
328
|
+
// A simple way is: remove and re-add if order is wrong, or use moveObjectTo.
|
|
329
|
+
|
|
330
|
+
// Since we are iterating items in order, we can check if the object is at the expected visual index relative to other user images.
|
|
331
|
+
// But for now, let's assume update logic is sufficient.
|
|
332
|
+
// If we want to support reordering, we should probably just `moveTo`
|
|
333
|
+
layer.remove(obj);
|
|
334
|
+
layer.add(obj); // Move to top of layer stack, effectively reordering if we iterate in order
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
layer.dirty = true;
|
|
339
|
+
this.canvasService.requestRenderAll();
|
|
340
|
+
}
|
|
278
341
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (Math.abs(userImage.left - localLeft) > 1)
|
|
283
|
-
updates.left = localLeft;
|
|
284
|
-
}
|
|
342
|
+
private updateObjectProperties(obj: FabricObject, item: ImageItem, layout: any) {
|
|
343
|
+
const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
|
|
344
|
+
const updates: any = {};
|
|
285
345
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
346
|
+
// Opacity
|
|
347
|
+
if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
|
|
348
|
+
|
|
349
|
+
// Angle
|
|
350
|
+
if (item.angle !== undefined && obj.angle !== item.angle) updates.angle = item.angle;
|
|
291
351
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
352
|
+
// Position (Normalized -> Absolute)
|
|
353
|
+
if (item.left !== undefined) {
|
|
354
|
+
const globalLeft = layoutOffsetX + item.left * visualWidth;
|
|
355
|
+
if (Math.abs(obj.left - globalLeft) > 1) updates.left = globalLeft;
|
|
356
|
+
}
|
|
357
|
+
if (item.top !== undefined) {
|
|
358
|
+
const globalTop = layoutOffsetY + item.top * visualHeight;
|
|
359
|
+
if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
|
|
360
|
+
}
|
|
296
361
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
362
|
+
// Scale (Physical Dimensions -> Scale Factor)
|
|
363
|
+
if (item.width !== undefined && obj.width) {
|
|
364
|
+
const targetScaleX = (item.width * layoutScale) / obj.width;
|
|
365
|
+
if (Math.abs(obj.scaleX - targetScaleX) > 0.001) updates.scaleX = targetScaleX;
|
|
366
|
+
}
|
|
367
|
+
if (item.height !== undefined && obj.height) {
|
|
368
|
+
const targetScaleY = (item.height * layoutScale) / obj.height;
|
|
369
|
+
if (Math.abs(obj.scaleY - targetScaleY) > 0.001) updates.scaleY = targetScaleY;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Center origin if not set
|
|
373
|
+
if (obj.originX !== "center") {
|
|
374
|
+
updates.originX = "center";
|
|
375
|
+
updates.originY = "center";
|
|
376
|
+
// Adjust position because origin changed (Fabric logic)
|
|
377
|
+
// For simplicity, we just set it, next cycle will fix pos if needed,
|
|
378
|
+
// or we can calculate the shift. Ideally we set origin on creation.
|
|
305
379
|
}
|
|
306
|
-
}
|
|
307
380
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this._loadingUrl = url;
|
|
381
|
+
if (Object.keys(updates).length > 0) {
|
|
382
|
+
obj.set(updates);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
313
385
|
|
|
314
|
-
|
|
386
|
+
private loadImage(item: ImageItem, layer: any, layout: any) {
|
|
387
|
+
Image.fromURL(item.url, { crossOrigin: "anonymous" })
|
|
315
388
|
.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
|
-
}
|
|
389
|
+
// Double check if item still exists
|
|
390
|
+
if (!this.items.find(i => i.id === item.id)) return;
|
|
353
391
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
this.left = 0.5;
|
|
362
|
-
this.top = 0.5;
|
|
363
|
-
}
|
|
364
|
-
left = this.left;
|
|
365
|
-
top = this.top;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
392
|
+
image.set({
|
|
393
|
+
originX: "center",
|
|
394
|
+
originY: "center",
|
|
395
|
+
data: { id: item.id },
|
|
396
|
+
uniformScaling: true,
|
|
397
|
+
lockScalingFlip: true,
|
|
398
|
+
});
|
|
368
399
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
}
|
|
400
|
+
image.setControlsVisibility({
|
|
401
|
+
mt: false,
|
|
402
|
+
mb: false,
|
|
403
|
+
ml: false,
|
|
404
|
+
mr: false,
|
|
405
|
+
});
|
|
420
406
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
}
|
|
407
|
+
// Initial Layout
|
|
408
|
+
let { width, height, left, top } = item;
|
|
409
|
+
const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight, dielinePhysicalWidth, dielinePhysicalHeight, bleedOffset } = layout;
|
|
410
|
+
|
|
411
|
+
// Auto-scale if needed
|
|
412
|
+
if (width === undefined && height === undefined) {
|
|
413
|
+
// Calculate target dimensions including bleed
|
|
414
|
+
const targetWidth = dielinePhysicalWidth + 2 * bleedOffset;
|
|
415
|
+
const targetHeight = dielinePhysicalHeight + 2 * bleedOffset;
|
|
416
|
+
|
|
417
|
+
// "适应最长边" (Fit to longest side) logic
|
|
418
|
+
const targetMax = Math.max(targetWidth, targetHeight);
|
|
419
|
+
const imageMax = Math.max(image.width || 1, image.height || 1);
|
|
420
|
+
const scale = targetMax / imageMax;
|
|
421
|
+
|
|
422
|
+
width = (image.width || 1) * scale;
|
|
423
|
+
height = (image.height || 1) * scale;
|
|
424
|
+
|
|
425
|
+
// Update item with defaults
|
|
426
|
+
item.width = width;
|
|
427
|
+
item.height = height;
|
|
428
|
+
}
|
|
461
429
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
430
|
+
if (left === undefined && top === undefined) {
|
|
431
|
+
left = 0.5;
|
|
432
|
+
top = 0.5;
|
|
433
|
+
item.left = left;
|
|
434
|
+
item.top = top;
|
|
467
435
|
}
|
|
468
436
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
id: "user-image",
|
|
473
|
-
},
|
|
474
|
-
});
|
|
437
|
+
// Apply Props
|
|
438
|
+
this.updateObjectProperties(image, item, layout);
|
|
439
|
+
|
|
475
440
|
layer.add(image);
|
|
441
|
+
this.objectMap.set(item.id, image);
|
|
476
442
|
|
|
477
|
-
// Bind
|
|
443
|
+
// Bind Events
|
|
478
444
|
image.on("modified", (e: any) => {
|
|
479
|
-
|
|
480
|
-
const globalPoint = util.transformPoint(new Point(0, 0), matrix);
|
|
481
|
-
const canvasW = this.canvasService?.canvas.width || 800;
|
|
482
|
-
const canvasH = this.canvasService?.canvas.height || 600;
|
|
483
|
-
|
|
484
|
-
this.left = Coordinate.toNormalized(globalPoint.x, canvasW);
|
|
485
|
-
this.top = Coordinate.toNormalized(globalPoint.y, canvasH);
|
|
486
|
-
this.angle = e.target.angle;
|
|
487
|
-
|
|
488
|
-
if (image.width) this.width = e.target.width * e.target.scaleX;
|
|
489
|
-
if (image.height) this.height = e.target.height * e.target.scaleY;
|
|
490
|
-
|
|
491
|
-
if (this.context) {
|
|
492
|
-
this.context.eventBus.emit("update");
|
|
493
|
-
}
|
|
445
|
+
this.handleObjectModified(item.id, image);
|
|
494
446
|
});
|
|
495
447
|
|
|
496
448
|
layer.dirty = true;
|
|
497
|
-
this.canvasService
|
|
449
|
+
this.canvasService?.requestRenderAll();
|
|
450
|
+
|
|
451
|
+
// Save defaults if we calculated them
|
|
452
|
+
if (item.width !== width || item.height !== height || item.left !== left || item.top !== top) {
|
|
453
|
+
this.updateImageInConfig(item.id, { width, height, left, top });
|
|
454
|
+
}
|
|
498
455
|
})
|
|
499
456
|
.catch((err) => {
|
|
500
|
-
|
|
501
|
-
console.error("Failed to load image", url, err);
|
|
457
|
+
console.error("Failed to load image", item.url, err);
|
|
502
458
|
});
|
|
503
459
|
}
|
|
460
|
+
|
|
461
|
+
private handleObjectModified(id: string, image: FabricObject) {
|
|
462
|
+
const layout = this.getLayoutInfo();
|
|
463
|
+
const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
|
|
464
|
+
|
|
465
|
+
const matrix = image.calcTransformMatrix();
|
|
466
|
+
const globalPoint = util.transformPoint(new Point(0, 0), matrix);
|
|
467
|
+
|
|
468
|
+
const updates: Partial<ImageItem> = {};
|
|
469
|
+
|
|
470
|
+
// Normalize Position
|
|
471
|
+
updates.left = (globalPoint.x - layoutOffsetX) / visualWidth;
|
|
472
|
+
updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
|
|
473
|
+
updates.angle = image.angle;
|
|
474
|
+
|
|
475
|
+
// Physical Dimensions
|
|
476
|
+
if (image.width) {
|
|
477
|
+
const pixelWidth = image.width * image.scaleX;
|
|
478
|
+
updates.width = pixelWidth / layoutScale;
|
|
479
|
+
}
|
|
480
|
+
if (image.height) {
|
|
481
|
+
const pixelHeight = image.height * image.scaleY;
|
|
482
|
+
updates.height = pixelHeight / layoutScale;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.updateImageInConfig(id, updates);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private updateImageInConfig(id: string, updates: Partial<ImageItem>) {
|
|
489
|
+
const index = this.items.findIndex(i => i.id === id);
|
|
490
|
+
if (index !== -1) {
|
|
491
|
+
const newItems = [...this.items];
|
|
492
|
+
newItems[index] = { ...newItems[index], ...updates };
|
|
493
|
+
this.updateConfig(newItems, true);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
504
496
|
}
|