@pooder/kit 3.5.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +295 -40
- package/dist/index.mjs +295 -40
- package/package.json +2 -2
- package/src/CanvasService.ts +89 -65
- package/src/background.ts +230 -230
- package/src/constraints.ts +158 -0
- package/src/coordinate.ts +106 -106
- package/src/dieline.ts +47 -22
- package/src/feature.ts +99 -31
- package/src/film.ts +194 -194
- package/src/geometry.ts +80 -27
- package/src/image.ts +512 -471
- package/src/mirror.ts +128 -128
- package/src/ruler.ts +500 -500
- package/src/tracer.ts +570 -570
- package/src/white-ink.ts +373 -373
package/src/feature.ts
CHANGED
|
@@ -11,10 +11,11 @@ import CanvasService from "./CanvasService";
|
|
|
11
11
|
import { DielineGeometry } from "./dieline";
|
|
12
12
|
import {
|
|
13
13
|
getNearestPointOnDieline,
|
|
14
|
-
|
|
14
|
+
DielineFeature,
|
|
15
15
|
resolveFeaturePosition,
|
|
16
16
|
} from "./geometry";
|
|
17
17
|
import { Coordinate } from "./coordinate";
|
|
18
|
+
import { ConstraintRegistry } from "./constraints";
|
|
18
19
|
|
|
19
20
|
export class FeatureTool implements Extension {
|
|
20
21
|
id = "pooder.kit.feature";
|
|
@@ -23,10 +24,11 @@ export class FeatureTool implements Extension {
|
|
|
23
24
|
name: "FeatureTool",
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
private features:
|
|
27
|
+
private features: DielineFeature[] = [];
|
|
27
28
|
private canvasService?: CanvasService;
|
|
28
29
|
private context?: ExtensionContext;
|
|
29
30
|
private isUpdatingConfig = false;
|
|
31
|
+
private isToolActive = false;
|
|
30
32
|
|
|
31
33
|
private handleMoving: ((e: any) => void) | null = null;
|
|
32
34
|
private handleModified: ((e: any) => void) | null = null;
|
|
@@ -37,7 +39,7 @@ export class FeatureTool implements Extension {
|
|
|
37
39
|
|
|
38
40
|
constructor(
|
|
39
41
|
options?: Partial<{
|
|
40
|
-
features:
|
|
42
|
+
features: DielineFeature[];
|
|
41
43
|
}>,
|
|
42
44
|
) {
|
|
43
45
|
if (options) {
|
|
@@ -70,15 +72,44 @@ export class FeatureTool implements Extension {
|
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
// Listen to tool activation
|
|
76
|
+
context.eventBus.on("tool:activated", this.onToolActivated);
|
|
77
|
+
|
|
73
78
|
this.setup();
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
deactivate(context: ExtensionContext) {
|
|
82
|
+
context.eventBus.off("tool:activated", this.onToolActivated);
|
|
77
83
|
this.teardown();
|
|
78
84
|
this.canvasService = undefined;
|
|
79
85
|
this.context = undefined;
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
private onToolActivated = (event: { id: string }) => {
|
|
89
|
+
this.isToolActive = event.id === this.id;
|
|
90
|
+
this.updateVisibility();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
private updateVisibility() {
|
|
94
|
+
if (!this.canvasService) return;
|
|
95
|
+
const canvas = this.canvasService.canvas;
|
|
96
|
+
const markers = canvas
|
|
97
|
+
.getObjects()
|
|
98
|
+
.filter((obj: any) => obj.data?.type === "feature-marker");
|
|
99
|
+
|
|
100
|
+
markers.forEach((marker: any) => {
|
|
101
|
+
// If tool active, allow selection. If not, disable selection.
|
|
102
|
+
// Also might want to hide them entirely or just disable interaction.
|
|
103
|
+
// Assuming we only want to see/edit holes when tool is active.
|
|
104
|
+
marker.set({
|
|
105
|
+
visible: this.isToolActive, // Or just selectable: false if we want them visible but locked
|
|
106
|
+
selectable: this.isToolActive,
|
|
107
|
+
evented: this.isToolActive
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
canvas.requestRenderAll();
|
|
111
|
+
}
|
|
112
|
+
|
|
82
113
|
contribute() {
|
|
83
114
|
return {
|
|
84
115
|
[ContributionPointIds.COMMANDS]: [
|
|
@@ -131,10 +162,10 @@ export class FeatureTool implements Extension {
|
|
|
131
162
|
const defaultSize = Coordinate.convertUnit(10, "mm", unit);
|
|
132
163
|
|
|
133
164
|
// Default to top edge center
|
|
134
|
-
const newFeature:
|
|
165
|
+
const newFeature: DielineFeature = {
|
|
135
166
|
id: Date.now().toString(),
|
|
136
167
|
operation: type,
|
|
137
|
-
|
|
168
|
+
placement: "edge",
|
|
138
169
|
shape: "rect",
|
|
139
170
|
x: 0.5,
|
|
140
171
|
y: 0, // Top edge
|
|
@@ -147,7 +178,7 @@ export class FeatureTool implements Extension {
|
|
|
147
178
|
const current = configService.get(
|
|
148
179
|
"dieline.features",
|
|
149
180
|
[],
|
|
150
|
-
) as
|
|
181
|
+
) as DielineFeature[];
|
|
151
182
|
configService.update("dieline.features", [...current, newFeature]);
|
|
152
183
|
}
|
|
153
184
|
return true;
|
|
@@ -167,11 +198,12 @@ export class FeatureTool implements Extension {
|
|
|
167
198
|
const timestamp = Date.now();
|
|
168
199
|
|
|
169
200
|
// 1. Lug (Outer) - Add
|
|
170
|
-
const lug:
|
|
201
|
+
const lug: DielineFeature = {
|
|
171
202
|
id: `${timestamp}-lug`,
|
|
172
203
|
groupId,
|
|
173
204
|
operation: "add",
|
|
174
205
|
shape: "circle",
|
|
206
|
+
placement: "edge",
|
|
175
207
|
x: 0.5,
|
|
176
208
|
y: 0,
|
|
177
209
|
radius: lugRadius, // 20mm
|
|
@@ -179,11 +211,12 @@ export class FeatureTool implements Extension {
|
|
|
179
211
|
};
|
|
180
212
|
|
|
181
213
|
// 2. Hole (Inner) - Subtract
|
|
182
|
-
const hole:
|
|
214
|
+
const hole: DielineFeature = {
|
|
183
215
|
id: `${timestamp}-hole`,
|
|
184
216
|
groupId,
|
|
185
217
|
operation: "subtract",
|
|
186
218
|
shape: "circle",
|
|
219
|
+
placement: "edge",
|
|
187
220
|
x: 0.5,
|
|
188
221
|
y: 0,
|
|
189
222
|
radius: holeRadius, // 15mm
|
|
@@ -194,7 +227,7 @@ export class FeatureTool implements Extension {
|
|
|
194
227
|
const current = configService.get(
|
|
195
228
|
"dieline.features",
|
|
196
229
|
[],
|
|
197
|
-
) as
|
|
230
|
+
) as DielineFeature[];
|
|
198
231
|
configService.update("dieline.features", [...current, lug, hole]);
|
|
199
232
|
}
|
|
200
233
|
return true;
|
|
@@ -202,19 +235,10 @@ export class FeatureTool implements Extension {
|
|
|
202
235
|
|
|
203
236
|
private getGeometryForFeature(
|
|
204
237
|
geometry: DielineGeometry,
|
|
205
|
-
feature?:
|
|
238
|
+
feature?: DielineFeature,
|
|
206
239
|
): DielineGeometry {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
...geometry,
|
|
210
|
-
width: geometry.width + geometry.offset * 2,
|
|
211
|
-
height: geometry.height + geometry.offset * 2,
|
|
212
|
-
radius:
|
|
213
|
-
geometry.radius === 0
|
|
214
|
-
? 0
|
|
215
|
-
: Math.max(0, geometry.radius + geometry.offset),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
240
|
+
// Legacy support or specialized scaling can go here if needed
|
|
241
|
+
// Currently all features operate on the base geometry (or scaled version of it)
|
|
218
242
|
return geometry;
|
|
219
243
|
}
|
|
220
244
|
|
|
@@ -258,7 +282,7 @@ export class FeatureTool implements Extension {
|
|
|
258
282
|
if (!this.currentGeometry) return;
|
|
259
283
|
|
|
260
284
|
// Determine feature to use for snapping context
|
|
261
|
-
let feature:
|
|
285
|
+
let feature: DielineFeature | undefined;
|
|
262
286
|
if (target.data?.isGroup) {
|
|
263
287
|
const indices = target.data?.indices as number[];
|
|
264
288
|
if (indices && indices.length > 0) {
|
|
@@ -288,7 +312,7 @@ export class FeatureTool implements Extension {
|
|
|
288
312
|
const minDim = Math.min(target.getScaledWidth(), target.getScaledHeight());
|
|
289
313
|
const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
|
|
290
314
|
|
|
291
|
-
const snapped = this.constrainPosition(p, geometry, limit);
|
|
315
|
+
const snapped = this.constrainPosition(p, geometry, limit, feature);
|
|
292
316
|
|
|
293
317
|
target.set({
|
|
294
318
|
left: snapped.x,
|
|
@@ -409,8 +433,47 @@ export class FeatureTool implements Extension {
|
|
|
409
433
|
private constrainPosition(
|
|
410
434
|
p: Point,
|
|
411
435
|
geometry: DielineGeometry,
|
|
412
|
-
limit: number
|
|
436
|
+
limit: number,
|
|
437
|
+
feature?: DielineFeature
|
|
413
438
|
): { x: number; y: number } {
|
|
439
|
+
if (feature && feature.constraints) {
|
|
440
|
+
// Use Constraint Registry
|
|
441
|
+
// Convert to normalized coordinates
|
|
442
|
+
const minX = geometry.x - geometry.width / 2;
|
|
443
|
+
const minY = geometry.y - geometry.height / 2;
|
|
444
|
+
|
|
445
|
+
const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
|
|
446
|
+
const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
|
|
447
|
+
|
|
448
|
+
const scale = geometry.scale || 1;
|
|
449
|
+
const dielineWidth = geometry.width / scale;
|
|
450
|
+
const dielineHeight = geometry.height / scale;
|
|
451
|
+
|
|
452
|
+
const constrained = ConstraintRegistry.apply(nx, ny, feature, {
|
|
453
|
+
dielineWidth,
|
|
454
|
+
dielineHeight,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
x: minX + constrained.x * geometry.width,
|
|
459
|
+
y: minY + constrained.y * geometry.height,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (feature && feature.placement === "internal") {
|
|
464
|
+
// Constrain to bounds
|
|
465
|
+
// geometry.x/y is center
|
|
466
|
+
const minX = geometry.x - geometry.width / 2;
|
|
467
|
+
const maxX = geometry.x + geometry.width / 2;
|
|
468
|
+
const minY = geometry.y - geometry.height / 2;
|
|
469
|
+
const maxY = geometry.y + geometry.height / 2;
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
x: Math.max(minX, Math.min(maxX, p.x)),
|
|
473
|
+
y: Math.max(minY, Math.min(maxY, p.y))
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
414
477
|
// Use geometry helper to find nearest point on Base Shape
|
|
415
478
|
// geometry object matches GeometryOptions structure required by getNearestPointOnDieline
|
|
416
479
|
// except for 'features' which we don't need for base shape snapping
|
|
@@ -502,9 +565,9 @@ export class FeatureTool implements Extension {
|
|
|
502
565
|
const finalScale = scale;
|
|
503
566
|
|
|
504
567
|
// Group features by groupId
|
|
505
|
-
const groups: { [key: string]: { feature:
|
|
568
|
+
const groups: { [key: string]: { feature: DielineFeature; index: number }[] } =
|
|
506
569
|
{};
|
|
507
|
-
const singles: { feature:
|
|
570
|
+
const singles: { feature: DielineFeature; index: number }[] = [];
|
|
508
571
|
|
|
509
572
|
this.features.forEach((f, i) => {
|
|
510
573
|
if (f.groupId) {
|
|
@@ -517,7 +580,7 @@ export class FeatureTool implements Extension {
|
|
|
517
580
|
|
|
518
581
|
// Helper to create marker shape
|
|
519
582
|
const createMarkerShape = (
|
|
520
|
-
feature:
|
|
583
|
+
feature: DielineFeature,
|
|
521
584
|
pos: { x: number; y: number },
|
|
522
585
|
) => {
|
|
523
586
|
// Features are in the same unit as geometry.unit
|
|
@@ -578,7 +641,9 @@ export class FeatureTool implements Extension {
|
|
|
578
641
|
const marker = createMarkerShape(feature, pos);
|
|
579
642
|
|
|
580
643
|
marker.set({
|
|
581
|
-
|
|
644
|
+
visible: this.isToolActive,
|
|
645
|
+
selectable: this.isToolActive,
|
|
646
|
+
evented: this.isToolActive,
|
|
582
647
|
hasControls: false,
|
|
583
648
|
hasBorders: false,
|
|
584
649
|
hoverCursor: "move",
|
|
@@ -633,7 +698,9 @@ export class FeatureTool implements Extension {
|
|
|
633
698
|
});
|
|
634
699
|
|
|
635
700
|
const groupObj = new Group(shapes, {
|
|
636
|
-
|
|
701
|
+
visible: this.isToolActive,
|
|
702
|
+
selectable: this.isToolActive,
|
|
703
|
+
evented: this.isToolActive,
|
|
637
704
|
hasControls: false,
|
|
638
705
|
hasBorders: false,
|
|
639
706
|
hoverCursor: "move",
|
|
@@ -689,7 +756,7 @@ export class FeatureTool implements Extension {
|
|
|
689
756
|
|
|
690
757
|
markers.forEach((marker: any) => {
|
|
691
758
|
// Find associated feature
|
|
692
|
-
let feature:
|
|
759
|
+
let feature: DielineFeature | undefined;
|
|
693
760
|
if (marker.data?.isGroup) {
|
|
694
761
|
const indices = marker.data?.indices as number[];
|
|
695
762
|
if (indices && indices.length > 0) {
|
|
@@ -714,7 +781,8 @@ export class FeatureTool implements Extension {
|
|
|
714
781
|
const snapped = this.constrainPosition(
|
|
715
782
|
new Point(marker.left, marker.top),
|
|
716
783
|
geometry,
|
|
717
|
-
limit
|
|
784
|
+
limit,
|
|
785
|
+
feature
|
|
718
786
|
);
|
|
719
787
|
marker.set({ left: snapped.x, top: snapped.y });
|
|
720
788
|
marker.setCoords();
|
package/src/film.ts
CHANGED
|
@@ -1,194 +1,194 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Extension,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
ContributionPointIds,
|
|
5
|
-
CommandContribution,
|
|
6
|
-
ConfigurationContribution,
|
|
7
|
-
} from "@pooder/core";
|
|
8
|
-
import { FabricImage as Image } from "fabric";
|
|
9
|
-
import CanvasService from "./CanvasService";
|
|
10
|
-
|
|
11
|
-
export class FilmTool implements Extension {
|
|
12
|
-
id = "pooder.kit.film";
|
|
13
|
-
|
|
14
|
-
public metadata = {
|
|
15
|
-
name: "FilmTool",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
private url: string = "";
|
|
19
|
-
private opacity: number = 0.5;
|
|
20
|
-
|
|
21
|
-
private canvasService?: CanvasService;
|
|
22
|
-
|
|
23
|
-
constructor(
|
|
24
|
-
options?: Partial<{
|
|
25
|
-
url: string;
|
|
26
|
-
opacity: number;
|
|
27
|
-
}>,
|
|
28
|
-
) {
|
|
29
|
-
if (options) {
|
|
30
|
-
Object.assign(this, options);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
activate(context: ExtensionContext) {
|
|
35
|
-
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
36
|
-
if (!this.canvasService) {
|
|
37
|
-
console.warn("CanvasService not found for FilmTool");
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const configService = context.services.get<any>("ConfigurationService");
|
|
42
|
-
if (configService) {
|
|
43
|
-
// Load initial config
|
|
44
|
-
this.url = configService.get("film.url", this.url);
|
|
45
|
-
this.opacity = configService.get("film.opacity", this.opacity);
|
|
46
|
-
|
|
47
|
-
// Listen for changes
|
|
48
|
-
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
49
|
-
if (e.key.startsWith("film.")) {
|
|
50
|
-
const prop = e.key.split(".")[1];
|
|
51
|
-
console.log(
|
|
52
|
-
`[FilmTool] Config change detected: ${e.key} -> ${e.value}`,
|
|
53
|
-
);
|
|
54
|
-
if (prop && prop in this) {
|
|
55
|
-
(this as any)[prop] = e.value;
|
|
56
|
-
this.updateFilm();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
this.initLayer();
|
|
63
|
-
this.updateFilm();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
deactivate(context: ExtensionContext) {
|
|
67
|
-
if (this.canvasService) {
|
|
68
|
-
const layer = this.canvasService.getLayer("overlay");
|
|
69
|
-
if (layer) {
|
|
70
|
-
const img = this.canvasService.getObject("film-image", "overlay");
|
|
71
|
-
if (img) {
|
|
72
|
-
layer.remove(img);
|
|
73
|
-
this.canvasService.requestRenderAll();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
this.canvasService = undefined;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
contribute() {
|
|
81
|
-
return {
|
|
82
|
-
[ContributionPointIds.CONFIGURATIONS]: [
|
|
83
|
-
{
|
|
84
|
-
id: "film.url",
|
|
85
|
-
type: "string",
|
|
86
|
-
label: "Film Image URL",
|
|
87
|
-
default: "",
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: "film.opacity",
|
|
91
|
-
type: "number",
|
|
92
|
-
label: "Opacity",
|
|
93
|
-
min: 0,
|
|
94
|
-
max: 1,
|
|
95
|
-
step: 0.1,
|
|
96
|
-
default: 0.5,
|
|
97
|
-
},
|
|
98
|
-
] as ConfigurationContribution[],
|
|
99
|
-
[ContributionPointIds.COMMANDS]: [
|
|
100
|
-
{
|
|
101
|
-
command: "setFilmImage",
|
|
102
|
-
title: "Set Film Image",
|
|
103
|
-
handler: (url: string, opacity: number) => {
|
|
104
|
-
if (this.url === url && this.opacity === opacity) return true;
|
|
105
|
-
|
|
106
|
-
this.url = url;
|
|
107
|
-
this.opacity = opacity;
|
|
108
|
-
|
|
109
|
-
this.updateFilm();
|
|
110
|
-
|
|
111
|
-
return true;
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
] as CommandContribution[],
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private initLayer() {
|
|
119
|
-
if (!this.canvasService) return;
|
|
120
|
-
let overlayLayer = this.canvasService.getLayer("overlay");
|
|
121
|
-
if (!overlayLayer) {
|
|
122
|
-
const width = this.canvasService.canvas.width || 800;
|
|
123
|
-
const height = this.canvasService.canvas.height || 600;
|
|
124
|
-
|
|
125
|
-
const layer = this.canvasService.createLayer("overlay", {
|
|
126
|
-
width,
|
|
127
|
-
height,
|
|
128
|
-
left: 0,
|
|
129
|
-
top: 0,
|
|
130
|
-
originX: "left",
|
|
131
|
-
originY: "top",
|
|
132
|
-
selectable: false,
|
|
133
|
-
evented: false,
|
|
134
|
-
subTargetCheck: false,
|
|
135
|
-
interactive: false,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
this.canvasService.canvas.bringObjectToFront(layer);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
private async updateFilm() {
|
|
143
|
-
if (!this.canvasService) return;
|
|
144
|
-
const layer = this.canvasService.getLayer("overlay");
|
|
145
|
-
if (!layer) {
|
|
146
|
-
console.warn("[FilmTool] Overlay layer not found");
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const { url, opacity } = this;
|
|
151
|
-
|
|
152
|
-
if (!url) {
|
|
153
|
-
const img = this.canvasService.getObject("film-image", "overlay");
|
|
154
|
-
if (img) {
|
|
155
|
-
layer.remove(img);
|
|
156
|
-
this.canvasService.requestRenderAll();
|
|
157
|
-
}
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const width = this.canvasService.canvas.width || 800;
|
|
162
|
-
const height = this.canvasService.canvas.height || 600;
|
|
163
|
-
|
|
164
|
-
let img = this.canvasService.getObject("film-image", "overlay") as Image;
|
|
165
|
-
try {
|
|
166
|
-
if (img) {
|
|
167
|
-
if (img.getSrc() !== url) {
|
|
168
|
-
await img.setSrc(url);
|
|
169
|
-
}
|
|
170
|
-
img.set({ opacity });
|
|
171
|
-
} else {
|
|
172
|
-
img = await Image.fromURL(url, { crossOrigin: "anonymous" });
|
|
173
|
-
img.scaleToWidth(width);
|
|
174
|
-
if (img.getScaledHeight() < height) img.scaleToHeight(height);
|
|
175
|
-
img.set({
|
|
176
|
-
originX: "left",
|
|
177
|
-
originY: "top",
|
|
178
|
-
left: 0,
|
|
179
|
-
top: 0,
|
|
180
|
-
opacity,
|
|
181
|
-
selectable: false,
|
|
182
|
-
evented: false,
|
|
183
|
-
data: { id: "film-image" },
|
|
184
|
-
});
|
|
185
|
-
layer.add(img);
|
|
186
|
-
}
|
|
187
|
-
this.canvasService.requestRenderAll();
|
|
188
|
-
} catch (error) {
|
|
189
|
-
console.error("[FilmTool] Failed to load film image", url, error);
|
|
190
|
-
}
|
|
191
|
-
layer.dirty = true;
|
|
192
|
-
this.canvasService.requestRenderAll();
|
|
193
|
-
}
|
|
194
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
Extension,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ContributionPointIds,
|
|
5
|
+
CommandContribution,
|
|
6
|
+
ConfigurationContribution,
|
|
7
|
+
} from "@pooder/core";
|
|
8
|
+
import { FabricImage as Image } from "fabric";
|
|
9
|
+
import CanvasService from "./CanvasService";
|
|
10
|
+
|
|
11
|
+
export class FilmTool implements Extension {
|
|
12
|
+
id = "pooder.kit.film";
|
|
13
|
+
|
|
14
|
+
public metadata = {
|
|
15
|
+
name: "FilmTool",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
private url: string = "";
|
|
19
|
+
private opacity: number = 0.5;
|
|
20
|
+
|
|
21
|
+
private canvasService?: CanvasService;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
options?: Partial<{
|
|
25
|
+
url: string;
|
|
26
|
+
opacity: number;
|
|
27
|
+
}>,
|
|
28
|
+
) {
|
|
29
|
+
if (options) {
|
|
30
|
+
Object.assign(this, options);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
activate(context: ExtensionContext) {
|
|
35
|
+
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
36
|
+
if (!this.canvasService) {
|
|
37
|
+
console.warn("CanvasService not found for FilmTool");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const configService = context.services.get<any>("ConfigurationService");
|
|
42
|
+
if (configService) {
|
|
43
|
+
// Load initial config
|
|
44
|
+
this.url = configService.get("film.url", this.url);
|
|
45
|
+
this.opacity = configService.get("film.opacity", this.opacity);
|
|
46
|
+
|
|
47
|
+
// Listen for changes
|
|
48
|
+
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
49
|
+
if (e.key.startsWith("film.")) {
|
|
50
|
+
const prop = e.key.split(".")[1];
|
|
51
|
+
console.log(
|
|
52
|
+
`[FilmTool] Config change detected: ${e.key} -> ${e.value}`,
|
|
53
|
+
);
|
|
54
|
+
if (prop && prop in this) {
|
|
55
|
+
(this as any)[prop] = e.value;
|
|
56
|
+
this.updateFilm();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.initLayer();
|
|
63
|
+
this.updateFilm();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
deactivate(context: ExtensionContext) {
|
|
67
|
+
if (this.canvasService) {
|
|
68
|
+
const layer = this.canvasService.getLayer("overlay");
|
|
69
|
+
if (layer) {
|
|
70
|
+
const img = this.canvasService.getObject("film-image", "overlay");
|
|
71
|
+
if (img) {
|
|
72
|
+
layer.remove(img);
|
|
73
|
+
this.canvasService.requestRenderAll();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.canvasService = undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
contribute() {
|
|
81
|
+
return {
|
|
82
|
+
[ContributionPointIds.CONFIGURATIONS]: [
|
|
83
|
+
{
|
|
84
|
+
id: "film.url",
|
|
85
|
+
type: "string",
|
|
86
|
+
label: "Film Image URL",
|
|
87
|
+
default: "",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "film.opacity",
|
|
91
|
+
type: "number",
|
|
92
|
+
label: "Opacity",
|
|
93
|
+
min: 0,
|
|
94
|
+
max: 1,
|
|
95
|
+
step: 0.1,
|
|
96
|
+
default: 0.5,
|
|
97
|
+
},
|
|
98
|
+
] as ConfigurationContribution[],
|
|
99
|
+
[ContributionPointIds.COMMANDS]: [
|
|
100
|
+
{
|
|
101
|
+
command: "setFilmImage",
|
|
102
|
+
title: "Set Film Image",
|
|
103
|
+
handler: (url: string, opacity: number) => {
|
|
104
|
+
if (this.url === url && this.opacity === opacity) return true;
|
|
105
|
+
|
|
106
|
+
this.url = url;
|
|
107
|
+
this.opacity = opacity;
|
|
108
|
+
|
|
109
|
+
this.updateFilm();
|
|
110
|
+
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
] as CommandContribution[],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private initLayer() {
|
|
119
|
+
if (!this.canvasService) return;
|
|
120
|
+
let overlayLayer = this.canvasService.getLayer("overlay");
|
|
121
|
+
if (!overlayLayer) {
|
|
122
|
+
const width = this.canvasService.canvas.width || 800;
|
|
123
|
+
const height = this.canvasService.canvas.height || 600;
|
|
124
|
+
|
|
125
|
+
const layer = this.canvasService.createLayer("overlay", {
|
|
126
|
+
width,
|
|
127
|
+
height,
|
|
128
|
+
left: 0,
|
|
129
|
+
top: 0,
|
|
130
|
+
originX: "left",
|
|
131
|
+
originY: "top",
|
|
132
|
+
selectable: false,
|
|
133
|
+
evented: false,
|
|
134
|
+
subTargetCheck: false,
|
|
135
|
+
interactive: false,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.canvasService.canvas.bringObjectToFront(layer);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async updateFilm() {
|
|
143
|
+
if (!this.canvasService) return;
|
|
144
|
+
const layer = this.canvasService.getLayer("overlay");
|
|
145
|
+
if (!layer) {
|
|
146
|
+
console.warn("[FilmTool] Overlay layer not found");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { url, opacity } = this;
|
|
151
|
+
|
|
152
|
+
if (!url) {
|
|
153
|
+
const img = this.canvasService.getObject("film-image", "overlay");
|
|
154
|
+
if (img) {
|
|
155
|
+
layer.remove(img);
|
|
156
|
+
this.canvasService.requestRenderAll();
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const width = this.canvasService.canvas.width || 800;
|
|
162
|
+
const height = this.canvasService.canvas.height || 600;
|
|
163
|
+
|
|
164
|
+
let img = this.canvasService.getObject("film-image", "overlay") as Image;
|
|
165
|
+
try {
|
|
166
|
+
if (img) {
|
|
167
|
+
if (img.getSrc() !== url) {
|
|
168
|
+
await img.setSrc(url);
|
|
169
|
+
}
|
|
170
|
+
img.set({ opacity });
|
|
171
|
+
} else {
|
|
172
|
+
img = await Image.fromURL(url, { crossOrigin: "anonymous" });
|
|
173
|
+
img.scaleToWidth(width);
|
|
174
|
+
if (img.getScaledHeight() < height) img.scaleToHeight(height);
|
|
175
|
+
img.set({
|
|
176
|
+
originX: "left",
|
|
177
|
+
originY: "top",
|
|
178
|
+
left: 0,
|
|
179
|
+
top: 0,
|
|
180
|
+
opacity,
|
|
181
|
+
selectable: false,
|
|
182
|
+
evented: false,
|
|
183
|
+
data: { id: "film-image" },
|
|
184
|
+
});
|
|
185
|
+
layer.add(img);
|
|
186
|
+
}
|
|
187
|
+
this.canvasService.requestRenderAll();
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error("[FilmTool] Failed to load film image", url, error);
|
|
190
|
+
}
|
|
191
|
+
layer.dirty = true;
|
|
192
|
+
this.canvasService.requestRenderAll();
|
|
193
|
+
}
|
|
194
|
+
}
|