@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/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
- EdgeFeature,
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: EdgeFeature[] = [];
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: EdgeFeature[];
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: EdgeFeature = {
165
+ const newFeature: DielineFeature = {
135
166
  id: Date.now().toString(),
136
167
  operation: type,
137
- target: "original",
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 EdgeFeature[];
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: EdgeFeature = {
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: EdgeFeature = {
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 EdgeFeature[];
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?: EdgeFeature,
238
+ feature?: DielineFeature,
206
239
  ): DielineGeometry {
207
- if (feature?.target === "offset" && geometry.offset !== 0) {
208
- return {
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: EdgeFeature | undefined;
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: EdgeFeature; index: number }[] } =
568
+ const groups: { [key: string]: { feature: DielineFeature; index: number }[] } =
506
569
  {};
507
- const singles: { feature: EdgeFeature; index: number }[] = [];
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: EdgeFeature,
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
- selectable: true,
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
- selectable: true,
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: EdgeFeature | undefined;
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
+ }