@pooder/kit 4.1.0 → 4.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.
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WhiteInkTool = void 0;
4
+ const core_1 = require("@pooder/core");
5
+ const fabric_1 = require("fabric");
6
+ class WhiteInkTool {
7
+ constructor(options) {
8
+ this.id = "pooder.kit.white-ink";
9
+ this.metadata = {
10
+ name: "WhiteInkTool",
11
+ };
12
+ this.customMask = "";
13
+ this.opacity = 1;
14
+ this.enableClip = false;
15
+ this._loadingUrl = null;
16
+ if (options) {
17
+ Object.assign(this, options);
18
+ }
19
+ }
20
+ activate(context) {
21
+ this.canvasService = context.services.get("CanvasService");
22
+ if (!this.canvasService) {
23
+ console.warn("CanvasService not found for WhiteInkTool");
24
+ return;
25
+ }
26
+ const configService = context.services.get("ConfigurationService");
27
+ if (configService) {
28
+ // Load initial config
29
+ this.customMask = configService.get("whiteInk.customMask", this.customMask);
30
+ this.opacity = configService.get("whiteInk.opacity", this.opacity);
31
+ this.enableClip = configService.get("whiteInk.enableClip", this.enableClip);
32
+ // Listen for changes
33
+ configService.onAnyChange((e) => {
34
+ if (e.key.startsWith("whiteInk.")) {
35
+ const prop = e.key.split(".")[1];
36
+ console.log(`[WhiteInkTool] Config change detected: ${e.key} -> ${e.value}`);
37
+ if (prop && prop in this) {
38
+ this[prop] = e.value;
39
+ this.updateWhiteInk();
40
+ }
41
+ }
42
+ });
43
+ }
44
+ this.setup();
45
+ this.updateWhiteInk();
46
+ }
47
+ deactivate(context) {
48
+ this.teardown();
49
+ this.canvasService = undefined;
50
+ }
51
+ contribute() {
52
+ return {
53
+ [core_1.ContributionPointIds.CONFIGURATIONS]: [
54
+ {
55
+ id: "whiteInk.customMask",
56
+ type: "string",
57
+ label: "Custom Mask URL",
58
+ default: "",
59
+ },
60
+ {
61
+ id: "whiteInk.opacity",
62
+ type: "number",
63
+ label: "Opacity",
64
+ min: 0,
65
+ max: 1,
66
+ step: 0.01,
67
+ default: 1,
68
+ },
69
+ {
70
+ id: "whiteInk.enableClip",
71
+ type: "boolean",
72
+ label: "Enable Clip",
73
+ default: false,
74
+ },
75
+ ],
76
+ [core_1.ContributionPointIds.COMMANDS]: [
77
+ {
78
+ command: "setWhiteInkImage",
79
+ title: "Set White Ink Image",
80
+ handler: (customMask, opacity, enableClip = true) => {
81
+ if (this.customMask === customMask &&
82
+ this.opacity === opacity &&
83
+ this.enableClip === enableClip)
84
+ return true;
85
+ this.customMask = customMask;
86
+ this.opacity = opacity;
87
+ this.enableClip = enableClip;
88
+ this.updateWhiteInk();
89
+ return true;
90
+ },
91
+ },
92
+ ],
93
+ };
94
+ }
95
+ setup() {
96
+ if (!this.canvasService)
97
+ return;
98
+ const canvas = this.canvasService.canvas;
99
+ let userLayer = this.canvasService.getLayer("user");
100
+ if (!userLayer) {
101
+ userLayer = this.canvasService.createLayer("user", {
102
+ width: canvas.width,
103
+ height: canvas.height,
104
+ left: 0,
105
+ top: 0,
106
+ originX: "left",
107
+ originY: "top",
108
+ selectable: false,
109
+ evented: true,
110
+ subTargetCheck: true,
111
+ interactive: true,
112
+ });
113
+ canvas.add(userLayer);
114
+ }
115
+ if (!this.syncHandler) {
116
+ this.syncHandler = (e) => {
117
+ const target = e.target;
118
+ if (target && target.data?.id === "user-image") {
119
+ this.syncWithUserImage();
120
+ }
121
+ };
122
+ canvas.on("object:moving", this.syncHandler);
123
+ canvas.on("object:scaling", this.syncHandler);
124
+ canvas.on("object:rotating", this.syncHandler);
125
+ canvas.on("object:modified", this.syncHandler);
126
+ }
127
+ }
128
+ teardown() {
129
+ if (!this.canvasService)
130
+ return;
131
+ const canvas = this.canvasService.canvas;
132
+ if (this.syncHandler) {
133
+ canvas.off("object:moving", this.syncHandler);
134
+ canvas.off("object:scaling", this.syncHandler);
135
+ canvas.off("object:rotating", this.syncHandler);
136
+ canvas.off("object:modified", this.syncHandler);
137
+ this.syncHandler = undefined;
138
+ }
139
+ const layer = this.canvasService.getLayer("user");
140
+ if (layer) {
141
+ const whiteInk = this.canvasService.getObject("white-ink", "user");
142
+ if (whiteInk) {
143
+ layer.remove(whiteInk);
144
+ }
145
+ }
146
+ const userImage = this.canvasService.getObject("user-image", "user");
147
+ if (userImage && userImage.clipPath) {
148
+ userImage.set({ clipPath: undefined });
149
+ }
150
+ this.canvasService.requestRenderAll();
151
+ }
152
+ updateWhiteInk() {
153
+ if (!this.canvasService)
154
+ return;
155
+ const { customMask, opacity, enableClip } = this;
156
+ const layer = this.canvasService.getLayer("user");
157
+ if (!layer) {
158
+ console.warn("[WhiteInkTool] User layer not found");
159
+ return;
160
+ }
161
+ const whiteInk = this.canvasService.getObject("white-ink", "user");
162
+ const userImage = this.canvasService.getObject("user-image", "user");
163
+ if (!customMask) {
164
+ if (whiteInk) {
165
+ layer.remove(whiteInk);
166
+ }
167
+ if (userImage && userImage.clipPath) {
168
+ userImage.set({ clipPath: undefined });
169
+ }
170
+ layer.dirty = true;
171
+ this.canvasService.requestRenderAll();
172
+ return;
173
+ }
174
+ // Check if we need to load/reload white ink backing
175
+ if (whiteInk) {
176
+ const currentSrc = whiteInk.getSrc?.() || whiteInk._element?.src;
177
+ if (currentSrc !== customMask) {
178
+ this.loadWhiteInk(layer, customMask, opacity, enableClip, whiteInk);
179
+ }
180
+ else {
181
+ if (whiteInk.opacity !== opacity) {
182
+ whiteInk.set({ opacity });
183
+ layer.dirty = true;
184
+ this.canvasService.requestRenderAll();
185
+ }
186
+ }
187
+ }
188
+ else {
189
+ this.loadWhiteInk(layer, customMask, opacity, enableClip);
190
+ }
191
+ // Handle Clip Path Toggle
192
+ if (userImage) {
193
+ if (enableClip) {
194
+ if (!userImage.clipPath) {
195
+ this.applyClipPath(customMask);
196
+ }
197
+ }
198
+ else {
199
+ if (userImage.clipPath) {
200
+ userImage.set({ clipPath: undefined });
201
+ layer.dirty = true;
202
+ this.canvasService.requestRenderAll();
203
+ }
204
+ }
205
+ }
206
+ }
207
+ loadWhiteInk(layer, url, opacity, enableClip, oldImage) {
208
+ if (!this.canvasService)
209
+ return;
210
+ if (this._loadingUrl === url)
211
+ return;
212
+ this._loadingUrl = url;
213
+ fabric_1.FabricImage.fromURL(url, { crossOrigin: "anonymous" })
214
+ .then((image) => {
215
+ if (this._loadingUrl !== url)
216
+ return;
217
+ this._loadingUrl = null;
218
+ if (oldImage) {
219
+ layer.remove(oldImage);
220
+ }
221
+ image.filters?.push(new fabric_1.filters.BlendColor({
222
+ color: "#FFFFFF",
223
+ mode: "add",
224
+ }));
225
+ image.applyFilters();
226
+ image.set({
227
+ opacity,
228
+ selectable: false,
229
+ evented: false,
230
+ data: {
231
+ id: "white-ink",
232
+ },
233
+ });
234
+ // Add to layer
235
+ layer.add(image);
236
+ // Ensure white-ink is behind user-image
237
+ const userImage = this.canvasService.getObject("user-image", "user");
238
+ if (userImage) {
239
+ // Re-adding moves it to the top of the stack
240
+ layer.remove(userImage);
241
+ layer.add(userImage);
242
+ }
243
+ // Apply clip path to user-image if enabled
244
+ if (enableClip) {
245
+ this.applyClipPath(url);
246
+ }
247
+ else if (userImage) {
248
+ userImage.set({ clipPath: undefined });
249
+ }
250
+ // Sync position immediately
251
+ this.syncWithUserImage();
252
+ layer.dirty = true;
253
+ this.canvasService.requestRenderAll();
254
+ })
255
+ .catch((err) => {
256
+ console.error("Failed to load white ink mask", url, err);
257
+ this._loadingUrl = null;
258
+ });
259
+ }
260
+ applyClipPath(url) {
261
+ if (!this.canvasService)
262
+ return;
263
+ const userImage = this.canvasService.getObject("user-image", "user");
264
+ if (!userImage)
265
+ return;
266
+ fabric_1.FabricImage.fromURL(url, { crossOrigin: "anonymous" })
267
+ .then((maskImage) => {
268
+ // Configure clipPath
269
+ maskImage.set({
270
+ originX: "center",
271
+ originY: "center",
272
+ left: 0,
273
+ top: 0,
274
+ // Scale to fit userImage if dimensions differ
275
+ scaleX: userImage.width / maskImage.width,
276
+ scaleY: userImage.height / maskImage.height,
277
+ });
278
+ userImage.set({ clipPath: maskImage });
279
+ const layer = this.canvasService.getLayer("user");
280
+ if (layer)
281
+ layer.dirty = true;
282
+ this.canvasService.requestRenderAll();
283
+ })
284
+ .catch((err) => {
285
+ console.error("Failed to load clip path", url, err);
286
+ });
287
+ }
288
+ syncWithUserImage() {
289
+ if (!this.canvasService)
290
+ return;
291
+ const userImage = this.canvasService.getObject("user-image", "user");
292
+ const whiteInk = this.canvasService.getObject("white-ink", "user");
293
+ if (userImage && whiteInk) {
294
+ whiteInk.set({
295
+ left: userImage.left,
296
+ top: userImage.top,
297
+ scaleX: userImage.scaleX,
298
+ scaleY: userImage.scaleY,
299
+ angle: userImage.angle,
300
+ skewX: userImage.skewX,
301
+ skewY: userImage.skewY,
302
+ flipX: userImage.flipX,
303
+ flipY: userImage.flipY,
304
+ originX: userImage.originX,
305
+ originY: userImage.originY,
306
+ });
307
+ }
308
+ }
309
+ }
310
+ exports.WhiteInkTool = WhiteInkTool;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const constraints_1 = require("../src/constraints");
4
+ const featureComplete_1 = require("../src/featureComplete");
5
+ function assert(condition, message) {
6
+ if (!condition)
7
+ throw new Error(message);
8
+ }
9
+ function closeTo(a, b, eps = 1e-6) {
10
+ return Math.abs(a - b) <= eps;
11
+ }
12
+ function testTangentBottom() {
13
+ const feature = {
14
+ id: "stand",
15
+ operation: "add",
16
+ shape: "rect",
17
+ x: 0.5,
18
+ y: 0.2,
19
+ width: 40,
20
+ height: 20,
21
+ constraints: { type: "tangent-bottom", params: { gap: 0 } },
22
+ };
23
+ const out = constraints_1.ConstraintRegistry.apply(feature.x, feature.y, feature, {
24
+ dielineWidth: 100,
25
+ dielineHeight: 100,
26
+ });
27
+ assert(closeTo(out.y, 1.1), `tangent-bottom y expected 1.1, got ${out.y}`);
28
+ assert(closeTo(out.x, 0.5), `tangent-bottom x expected 0.5, got ${out.x}`);
29
+ }
30
+ function testCompleteFeaturesStrict() {
31
+ const updates = [];
32
+ const illegal = {
33
+ id: "stand",
34
+ operation: "add",
35
+ shape: "rect",
36
+ x: 0.5,
37
+ y: 0.5,
38
+ width: 40,
39
+ height: 20,
40
+ constraints: { type: "tangent-bottom", params: { gap: 0 } },
41
+ };
42
+ const failed = (0, featureComplete_1.completeFeaturesStrict)([illegal], { dielineWidth: 100, dielineHeight: 100 }, (next) => updates.push({ key: "dieline.features", value: next }));
43
+ assert(failed.ok === false, "completeFeatures should fail for illegal features");
44
+ assert(updates.length === 0, "illegal draft should not update configuration");
45
+ const legal = {
46
+ ...illegal,
47
+ y: 1.1,
48
+ };
49
+ const ok = (0, featureComplete_1.completeFeaturesStrict)([legal], { dielineWidth: 100, dielineHeight: 100 }, (next) => updates.push({ key: "dieline.features", value: next }));
50
+ assert(ok.ok === true, "completeFeatures should succeed for legal features");
51
+ assert(updates.length === 1, "legal draft should update configuration once");
52
+ assert(updates[0].key === "dieline.features", "should update dieline.features");
53
+ assert(closeTo(updates[0].value[0].y, 1.1), "saved feature y should remain 1.1");
54
+ }
55
+ function main() {
56
+ testTangentBottom();
57
+ testCompleteFeaturesStrict();
58
+ console.log("ok");
59
+ }
60
+ main();
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @pooder/kit
2
2
 
3
+ ## 4.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - viewport system, constraints and features
8
+
3
9
  ## 4.1.0
4
10
 
5
11
  ### Minor Changes
package/dist/index.d.mts CHANGED
@@ -23,7 +23,22 @@ declare class BackgroundTool implements Extension {
23
23
  private updateBackground;
24
24
  }
25
25
 
26
+ interface Point {
27
+ x: number;
28
+ y: number;
29
+ }
30
+ interface Size {
31
+ width: number;
32
+ height: number;
33
+ }
26
34
  type Unit = "px" | "mm" | "cm" | "in";
35
+ interface Layout {
36
+ scale: number;
37
+ offsetX: number;
38
+ offsetY: number;
39
+ width: number;
40
+ height: number;
41
+ }
27
42
 
28
43
  type FeatureOperation = "add" | "subtract";
29
44
  type FeatureShape = "rect" | "circle";
@@ -50,7 +65,8 @@ interface DielineFeature {
50
65
 
51
66
  interface DielineGeometry {
52
67
  shape: "rect" | "circle" | "ellipse" | "custom";
53
- unit: Unit;
68
+ unit: "mm";
69
+ displayUnit: Unit;
54
70
  x: number;
55
71
  y: number;
56
72
  width: number;
@@ -69,7 +85,7 @@ interface LineStyle {
69
85
  style: "solid" | "dashed" | "hidden";
70
86
  }
71
87
  interface DielineState {
72
- unit: Unit;
88
+ displayUnit: Unit;
73
89
  shape: "rect" | "circle" | "ellipse" | "custom";
74
90
  width: number;
75
91
  height: number;
@@ -136,7 +152,7 @@ declare class FeatureTool implements Extension {
136
152
  metadata: {
137
153
  name: string;
138
154
  };
139
- private features;
155
+ private workingFeatures;
140
156
  private canvasService?;
141
157
  private context?;
142
158
  private isUpdatingConfig;
@@ -155,6 +171,12 @@ declare class FeatureTool implements Extension {
155
171
  contribute(): {
156
172
  [ContributionPointIds.COMMANDS]: CommandContribution[];
157
173
  };
174
+ private cloneFeatures;
175
+ private emitWorkingChange;
176
+ private refreshGeometry;
177
+ private setWorkingFeatures;
178
+ private updateWorkingGroupPosition;
179
+ private completeFeatures;
158
180
  private addFeature;
159
181
  private addDoubleLayerHole;
160
182
  private getGeometryForFeature;
@@ -249,7 +271,7 @@ declare class RulerTool implements Extension {
249
271
  private fontSize;
250
272
  private dielineWidth;
251
273
  private dielineHeight;
252
- private dielineUnit;
274
+ private dielineDisplayUnit;
253
275
  private dielinePadding;
254
276
  private dielineOffset;
255
277
  private canvasService?;
@@ -297,8 +319,31 @@ declare class MirrorTool implements Extension {
297
319
  private applyMirror;
298
320
  }
299
321
 
322
+ declare function parseLengthToMm(input: number | string, defaultUnit: Unit): number;
323
+ declare function formatMm(valueMm: number, displayUnit: Unit, fractionDigits?: number): string;
324
+
325
+ declare class ViewportSystem {
326
+ private _containerSize;
327
+ private _physicalSize;
328
+ private _padding;
329
+ private _layout;
330
+ constructor(containerSize?: Size, physicalSize?: Size, padding?: number);
331
+ get layout(): Layout;
332
+ get scale(): number;
333
+ get offset(): Point;
334
+ updateContainer(width: number, height: number): void;
335
+ updatePhysical(width: number, height: number): void;
336
+ setPadding(padding: number): void;
337
+ private updateLayout;
338
+ toPixel(value: number): number;
339
+ toPhysical(value: number): number;
340
+ toPixelPoint(point: Point): Point;
341
+ toPhysicalPoint(point: Point): Point;
342
+ }
343
+
300
344
  declare class CanvasService implements Service {
301
345
  canvas: Canvas;
346
+ viewport: ViewportSystem;
302
347
  private eventBus?;
303
348
  constructor(el: HTMLCanvasElement | string | Canvas, options?: any);
304
349
  setEventBus(eventBus: EventBus): void;
@@ -320,4 +365,4 @@ declare class CanvasService implements Service {
320
365
  requestRenderAll(): void;
321
366
  }
322
367
 
323
- export { BackgroundTool, CanvasService, type DielineGeometry, type DielineState, DielineTool, FeatureTool, FilmTool, type ImageItem, ImageTool, type LineStyle, MirrorTool, RulerTool, WhiteInkTool };
368
+ export { BackgroundTool, CanvasService, type DielineGeometry, type DielineState, DielineTool, FeatureTool, FilmTool, type ImageItem, ImageTool, type LineStyle, MirrorTool, RulerTool, WhiteInkTool, formatMm, parseLengthToMm };
package/dist/index.d.ts CHANGED
@@ -23,7 +23,22 @@ declare class BackgroundTool implements Extension {
23
23
  private updateBackground;
24
24
  }
25
25
 
26
+ interface Point {
27
+ x: number;
28
+ y: number;
29
+ }
30
+ interface Size {
31
+ width: number;
32
+ height: number;
33
+ }
26
34
  type Unit = "px" | "mm" | "cm" | "in";
35
+ interface Layout {
36
+ scale: number;
37
+ offsetX: number;
38
+ offsetY: number;
39
+ width: number;
40
+ height: number;
41
+ }
27
42
 
28
43
  type FeatureOperation = "add" | "subtract";
29
44
  type FeatureShape = "rect" | "circle";
@@ -50,7 +65,8 @@ interface DielineFeature {
50
65
 
51
66
  interface DielineGeometry {
52
67
  shape: "rect" | "circle" | "ellipse" | "custom";
53
- unit: Unit;
68
+ unit: "mm";
69
+ displayUnit: Unit;
54
70
  x: number;
55
71
  y: number;
56
72
  width: number;
@@ -69,7 +85,7 @@ interface LineStyle {
69
85
  style: "solid" | "dashed" | "hidden";
70
86
  }
71
87
  interface DielineState {
72
- unit: Unit;
88
+ displayUnit: Unit;
73
89
  shape: "rect" | "circle" | "ellipse" | "custom";
74
90
  width: number;
75
91
  height: number;
@@ -136,7 +152,7 @@ declare class FeatureTool implements Extension {
136
152
  metadata: {
137
153
  name: string;
138
154
  };
139
- private features;
155
+ private workingFeatures;
140
156
  private canvasService?;
141
157
  private context?;
142
158
  private isUpdatingConfig;
@@ -155,6 +171,12 @@ declare class FeatureTool implements Extension {
155
171
  contribute(): {
156
172
  [ContributionPointIds.COMMANDS]: CommandContribution[];
157
173
  };
174
+ private cloneFeatures;
175
+ private emitWorkingChange;
176
+ private refreshGeometry;
177
+ private setWorkingFeatures;
178
+ private updateWorkingGroupPosition;
179
+ private completeFeatures;
158
180
  private addFeature;
159
181
  private addDoubleLayerHole;
160
182
  private getGeometryForFeature;
@@ -249,7 +271,7 @@ declare class RulerTool implements Extension {
249
271
  private fontSize;
250
272
  private dielineWidth;
251
273
  private dielineHeight;
252
- private dielineUnit;
274
+ private dielineDisplayUnit;
253
275
  private dielinePadding;
254
276
  private dielineOffset;
255
277
  private canvasService?;
@@ -297,8 +319,31 @@ declare class MirrorTool implements Extension {
297
319
  private applyMirror;
298
320
  }
299
321
 
322
+ declare function parseLengthToMm(input: number | string, defaultUnit: Unit): number;
323
+ declare function formatMm(valueMm: number, displayUnit: Unit, fractionDigits?: number): string;
324
+
325
+ declare class ViewportSystem {
326
+ private _containerSize;
327
+ private _physicalSize;
328
+ private _padding;
329
+ private _layout;
330
+ constructor(containerSize?: Size, physicalSize?: Size, padding?: number);
331
+ get layout(): Layout;
332
+ get scale(): number;
333
+ get offset(): Point;
334
+ updateContainer(width: number, height: number): void;
335
+ updatePhysical(width: number, height: number): void;
336
+ setPadding(padding: number): void;
337
+ private updateLayout;
338
+ toPixel(value: number): number;
339
+ toPhysical(value: number): number;
340
+ toPixelPoint(point: Point): Point;
341
+ toPhysicalPoint(point: Point): Point;
342
+ }
343
+
300
344
  declare class CanvasService implements Service {
301
345
  canvas: Canvas;
346
+ viewport: ViewportSystem;
302
347
  private eventBus?;
303
348
  constructor(el: HTMLCanvasElement | string | Canvas, options?: any);
304
349
  setEventBus(eventBus: EventBus): void;
@@ -320,4 +365,4 @@ declare class CanvasService implements Service {
320
365
  requestRenderAll(): void;
321
366
  }
322
367
 
323
- export { BackgroundTool, CanvasService, type DielineGeometry, type DielineState, DielineTool, FeatureTool, FilmTool, type ImageItem, ImageTool, type LineStyle, MirrorTool, RulerTool, WhiteInkTool };
368
+ export { BackgroundTool, CanvasService, type DielineGeometry, type DielineState, DielineTool, FeatureTool, FilmTool, type ImageItem, ImageTool, type LineStyle, MirrorTool, RulerTool, WhiteInkTool, formatMm, parseLengthToMm };