@pooder/kit 5.0.4 → 5.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/index.d.mts +248 -267
  3. package/dist/index.d.ts +248 -267
  4. package/dist/index.js +6729 -5797
  5. package/dist/index.mjs +6690 -5741
  6. package/package.json +2 -2
  7. package/src/{background.ts → extensions/background.ts} +1 -1
  8. package/src/{dieline.ts → extensions/dieline.ts} +39 -17
  9. package/src/{feature.ts → extensions/feature.ts} +80 -67
  10. package/src/{film.ts → extensions/film.ts} +1 -1
  11. package/src/{geometry.ts → extensions/geometry.ts} +151 -105
  12. package/src/{image.ts → extensions/image.ts} +436 -93
  13. package/src/extensions/index.ts +11 -0
  14. package/src/{maskOps.ts → extensions/maskOps.ts} +28 -10
  15. package/src/{mirror.ts → extensions/mirror.ts} +1 -1
  16. package/src/{ruler.ts → extensions/ruler.ts} +5 -3
  17. package/src/extensions/sceneLayout.ts +140 -0
  18. package/src/{sceneLayoutModel.ts → extensions/sceneLayoutModel.ts} +17 -10
  19. package/src/extensions/sceneVisibility.ts +71 -0
  20. package/src/{size.ts → extensions/size.ts} +23 -13
  21. package/src/{tracer.ts → extensions/tracer.ts} +374 -45
  22. package/src/{white-ink.ts → extensions/white-ink.ts} +620 -236
  23. package/src/index.ts +2 -14
  24. package/src/{ViewportSystem.ts → services/ViewportSystem.ts} +5 -2
  25. package/src/services/index.ts +3 -0
  26. package/src/sceneLayout.ts +0 -121
  27. package/src/sceneVisibility.ts +0 -49
  28. /package/src/{bridgeSelection.ts → extensions/bridgeSelection.ts} +0 -0
  29. /package/src/{constraints.ts → extensions/constraints.ts} +0 -0
  30. /package/src/{edgeScale.ts → extensions/edgeScale.ts} +0 -0
  31. /package/src/{featureComplete.ts → extensions/featureComplete.ts} +0 -0
  32. /package/src/{wrappedOffsets.ts → extensions/wrappedOffsets.ts} +0 -0
  33. /package/src/{CanvasService.ts → services/CanvasService.ts} +0 -0
  34. /package/src/{renderSpec.ts → services/renderSpec.ts} +0 -0
@@ -10,6 +10,13 @@ export interface CreateMaskOptions {
10
10
  alphaOpaqueCutoff?: number;
11
11
  }
12
12
 
13
+ export interface AlphaAnalysis {
14
+ total: number;
15
+ minAlpha: number;
16
+ belowOpaqueRatio: number;
17
+ veryTransparentRatio: number;
18
+ }
19
+
13
20
  export function createMask(
14
21
  imageData: ImageData,
15
22
  options: CreateMaskOptions,
@@ -55,7 +62,21 @@ export function createMask(
55
62
  return mask;
56
63
  }
57
64
 
58
- function inferMaskMode(imageData: ImageData, alphaOpaqueCutoff: number): MaskMode {
65
+ export function inferMaskMode(
66
+ imageData: ImageData,
67
+ alphaOpaqueCutoff: number,
68
+ ): MaskMode {
69
+ const analysis = analyzeAlpha(imageData, alphaOpaqueCutoff);
70
+ if (analysis.minAlpha === 255) return "whitebg";
71
+ if (analysis.veryTransparentRatio >= 0.0005) return "alpha";
72
+ if (analysis.belowOpaqueRatio >= 0.01) return "alpha";
73
+ return "whitebg";
74
+ }
75
+
76
+ export function analyzeAlpha(
77
+ imageData: ImageData,
78
+ alphaOpaqueCutoff: number,
79
+ ): AlphaAnalysis {
59
80
  const { data } = imageData;
60
81
  const total = data.length / 4;
61
82
 
@@ -70,15 +91,12 @@ function inferMaskMode(imageData: ImageData, alphaOpaqueCutoff: number): MaskMod
70
91
  if (a < 32) veryTransparent++;
71
92
  }
72
93
 
73
- if (minAlpha === 255) return "whitebg";
74
-
75
- const belowOpaqueRatio = belowOpaque / total;
76
- const veryTransparentRatio = veryTransparent / total;
77
-
78
- if (veryTransparentRatio >= 0.0005) return "alpha";
79
- if (belowOpaqueRatio >= 0.01) return "alpha";
80
-
81
- return "whitebg";
94
+ return {
95
+ total,
96
+ minAlpha,
97
+ belowOpaqueRatio: belowOpaque / total,
98
+ veryTransparentRatio: veryTransparent / total,
99
+ };
82
100
  }
83
101
 
84
102
  export function circularMorphology(
@@ -5,7 +5,7 @@ import {
5
5
  CommandContribution,
6
6
  ConfigurationContribution,
7
7
  } from "@pooder/core";
8
- import CanvasService from "./CanvasService";
8
+ import { CanvasService } from "../services";
9
9
 
10
10
  export class MirrorTool implements Extension {
11
11
  id = "pooder.kit.mirror";
@@ -7,8 +7,8 @@ import {
7
7
  ConfigurationService,
8
8
  } from "@pooder/core";
9
9
  import { Rect, Line, Text, Group, Polygon } from "fabric";
10
- import CanvasService from "./CanvasService";
11
- import { formatMm } from "./units";
10
+ import { CanvasService } from "../services";
11
+ import { formatMm } from "../units";
12
12
  import { computeSceneLayout, readSizeState } from "./sceneLayoutModel";
13
13
 
14
14
  export class RulerTool implements Extension {
@@ -304,7 +304,9 @@ export class RulerTool implements Extension {
304
304
  const rulerBottom = rulerRect.top + rulerRect.height;
305
305
 
306
306
  // Display Dimensions (Physical)
307
- const displayWidthMm = useCutAsRuler ? layout.cutWidthMm : layout.trimWidthMm;
307
+ const displayWidthMm = useCutAsRuler
308
+ ? layout.cutWidthMm
309
+ : layout.trimWidthMm;
308
310
  const displayHeightMm = useCutAsRuler
309
311
  ? layout.cutHeightMm
310
312
  : layout.trimHeightMm;
@@ -0,0 +1,140 @@
1
+ import {
2
+ COMMAND_SERVICE,
3
+ CONFIGURATION_SERVICE,
4
+ CommandService,
5
+ ConfigurationService,
6
+ Service,
7
+ ServiceContext,
8
+ } from "@pooder/core";
9
+ import { CanvasService } from "../services";
10
+ import {
11
+ buildSceneGeometry,
12
+ computeSceneLayout,
13
+ readSizeState,
14
+ type SceneGeometrySnapshot,
15
+ type SceneLayoutSnapshot,
16
+ } from "./sceneLayoutModel";
17
+
18
+ interface ConfigChangeEvent {
19
+ key: string;
20
+ value: unknown;
21
+ oldValue: unknown;
22
+ }
23
+
24
+ const CONFIG_WATCH_PREFIXES = ["size.", "dieline."] as const;
25
+ const CANVAS_SERVICE_ID = "CanvasService";
26
+ const GET_SCENE_LAYOUT_COMMAND = "getSceneLayout";
27
+ const GET_SCENE_GEOMETRY_COMMAND = "getSceneGeometry";
28
+
29
+ export class SceneLayoutService implements Service {
30
+ private context?: ServiceContext;
31
+ private canvasService?: CanvasService;
32
+ private configService?: ConfigurationService;
33
+ private lastLayout: SceneLayoutSnapshot | null = null;
34
+ private lastGeometry: SceneGeometrySnapshot | null = null;
35
+ private onConfigChange?: { dispose(): void };
36
+ private commandDisposables: Array<{ dispose(): void }> = [];
37
+
38
+ init(context: ServiceContext) {
39
+ if (this.context) {
40
+ this.dispose(this.context);
41
+ }
42
+
43
+ const canvasService =
44
+ context.get<CanvasService>(CANVAS_SERVICE_ID);
45
+ const configService =
46
+ context.get<ConfigurationService>(CONFIGURATION_SERVICE);
47
+ const commandService = context.get<CommandService>(COMMAND_SERVICE);
48
+
49
+ if (!canvasService || !configService || !commandService) {
50
+ throw new Error(
51
+ "[SceneLayoutService] CanvasService, ConfigurationService and CommandService are required.",
52
+ );
53
+ }
54
+
55
+ this.context = context;
56
+ this.canvasService = canvasService;
57
+ this.configService = configService;
58
+
59
+ this.commandDisposables.push(
60
+ commandService.registerCommand(GET_SCENE_LAYOUT_COMMAND, () =>
61
+ this.getLayout(),
62
+ ),
63
+ commandService.registerCommand(GET_SCENE_GEOMETRY_COMMAND, () =>
64
+ this.getGeometry(),
65
+ ),
66
+ );
67
+
68
+ this.onConfigChange = configService.onAnyChange(this.onConfigChanged);
69
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
70
+ this.refresh();
71
+ }
72
+
73
+ dispose(context: ServiceContext) {
74
+ const activeContext = this.context ?? context;
75
+ activeContext.eventBus.off("canvas:resized", this.onCanvasResized);
76
+ this.onConfigChange?.dispose();
77
+ this.onConfigChange = undefined;
78
+ this.commandDisposables.forEach((item) => item.dispose());
79
+ this.commandDisposables = [];
80
+ this.context = undefined;
81
+ this.canvasService = undefined;
82
+ this.configService = undefined;
83
+ this.lastLayout = null;
84
+ this.lastGeometry = null;
85
+ }
86
+
87
+ private onCanvasResized = () => {
88
+ this.refresh();
89
+ };
90
+
91
+ private onConfigChanged = (e: ConfigChangeEvent) => {
92
+ if (CONFIG_WATCH_PREFIXES.some((prefix) => e.key.startsWith(prefix))) {
93
+ this.refresh();
94
+ }
95
+ };
96
+
97
+ private refresh() {
98
+ const layout = this.getLayout(true);
99
+ if (!layout) {
100
+ this.lastGeometry = null;
101
+ return;
102
+ }
103
+
104
+ this.context?.eventBus.emit("scene:layout:change", layout);
105
+
106
+ const geometry = this.getGeometry(true);
107
+ if (geometry) {
108
+ this.context?.eventBus.emit("scene:geometry:change", geometry);
109
+ }
110
+ }
111
+
112
+ getLayout(forceRefresh = false): SceneLayoutSnapshot | null {
113
+ if (!this.canvasService || !this.configService) return null;
114
+ if (!forceRefresh && this.lastLayout) return this.lastLayout;
115
+
116
+ const state = readSizeState(this.configService);
117
+ const layout = computeSceneLayout(this.canvasService, state);
118
+ if (!layout) {
119
+ this.lastLayout = null;
120
+ return null;
121
+ }
122
+
123
+ this.lastLayout = layout;
124
+ return layout;
125
+ }
126
+
127
+ getGeometry(forceRefresh = false): SceneGeometrySnapshot | null {
128
+ if (!this.configService) return null;
129
+ const layout = this.getLayout(forceRefresh);
130
+ if (!layout) {
131
+ this.lastGeometry = null;
132
+ return null;
133
+ }
134
+ if (!forceRefresh && this.lastGeometry) return this.lastGeometry;
135
+
136
+ const geometry = buildSceneGeometry(this.configService, layout);
137
+ this.lastGeometry = geometry;
138
+ return geometry;
139
+ }
140
+ }
@@ -1,7 +1,7 @@
1
1
  import type { ConfigurationService } from "@pooder/core";
2
- import type CanvasService from "./CanvasService";
3
- import { Coordinate, Unit } from "./coordinate";
4
- import { parseLengthToMm } from "./units";
2
+ import type { CanvasService } from "../services";
3
+ import { Coordinate, Unit } from "../coordinate";
4
+ import { parseLengthToMm } from "../units";
5
5
 
6
6
  export type SizeConstraintMode = "free" | "lockAspect" | "equal";
7
7
  export type CutMode = "trim" | "outset" | "inset";
@@ -158,17 +158,17 @@ export function readSizeState(configService: ConfigurationService): SizeState {
158
158
  );
159
159
  const actualHeightMm = sanitizeMmValue(
160
160
  parseLengthToMm(
161
- configService.get("size.actualHeightMm", DEFAULT_SIZE_STATE.actualHeightMm),
161
+ configService.get(
162
+ "size.actualHeightMm",
163
+ DEFAULT_SIZE_STATE.actualHeightMm,
164
+ ),
162
165
  "mm",
163
166
  ),
164
167
  { minMm, maxMm, stepMm },
165
168
  );
166
169
 
167
170
  const aspectRaw = Number(
168
- configService.get(
169
- "size.aspectRatio",
170
- DEFAULT_SIZE_STATE.aspectRatio,
171
- ),
171
+ configService.get("size.aspectRatio", DEFAULT_SIZE_STATE.aspectRatio),
172
172
  );
173
173
  const aspectRatio =
174
174
  Number.isFinite(aspectRaw) && aspectRaw > 0
@@ -265,7 +265,11 @@ export function computeSceneLayout(
265
265
  return null;
266
266
  }
267
267
 
268
- const paddingPx = resolvePaddingPx(size.viewPadding, canvasWidth, canvasHeight);
268
+ const paddingPx = resolvePaddingPx(
269
+ size.viewPadding,
270
+ canvasWidth,
271
+ canvasHeight,
272
+ );
269
273
  canvasService.viewport.updateContainer(canvasWidth, canvasHeight);
270
274
  canvasService.viewport.setPadding(paddingPx);
271
275
  canvasService.viewport.updatePhysical(viewWidthMm, viewHeightMm);
@@ -316,7 +320,10 @@ export function buildSceneGeometry(
316
320
  configService: ConfigurationService,
317
321
  layout: SceneLayoutSnapshot,
318
322
  ): SceneGeometrySnapshot {
319
- const radiusMm = parseLengthToMm(configService.get("dieline.radius", 0), "mm");
323
+ const radiusMm = parseLengthToMm(
324
+ configService.get("dieline.radius", 0),
325
+ "mm",
326
+ );
320
327
  const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
321
328
 
322
329
  return {
@@ -0,0 +1,71 @@
1
+ import { Service, ServiceContext, WORKBENCH_SERVICE } from "@pooder/core";
2
+ import { CanvasService } from "../services";
3
+
4
+ const CANVAS_SERVICE_ID = "CanvasService";
5
+ const HIDDEN_DIELINE_TOOLS = new Set(["pooder.kit.image", "pooder.kit.white-ink"]);
6
+ const HIDDEN_RULER_TOOLS = new Set(["pooder.kit.white-ink"]);
7
+
8
+ export class SceneVisibilityService implements Service {
9
+ private context?: ServiceContext;
10
+ private activeToolId: string | null = null;
11
+ private canvasService?: CanvasService;
12
+
13
+ init(context: ServiceContext) {
14
+ if (this.context) {
15
+ this.dispose(this.context);
16
+ }
17
+
18
+ const canvasService =
19
+ context.get<CanvasService>(CANVAS_SERVICE_ID);
20
+ if (!canvasService) {
21
+ throw new Error("[SceneVisibilityService] CanvasService is required.");
22
+ }
23
+
24
+ this.context = context;
25
+ this.canvasService = canvasService;
26
+ this.activeToolId = context.get(WORKBENCH_SERVICE)?.activeToolId ?? null;
27
+ context.eventBus.on("tool:activated", this.onToolActivated);
28
+ context.eventBus.on("object:added", this.onObjectAdded);
29
+ this.apply();
30
+ }
31
+
32
+ dispose(context: ServiceContext) {
33
+ const activeContext = this.context ?? context;
34
+ activeContext.eventBus.off("tool:activated", this.onToolActivated);
35
+ activeContext.eventBus.off("object:added", this.onObjectAdded);
36
+ this.context = undefined;
37
+ this.activeToolId = null;
38
+ this.canvasService = undefined;
39
+ }
40
+
41
+ private onToolActivated = (e: { id: string | null }) => {
42
+ this.activeToolId = e.id;
43
+ this.apply();
44
+ };
45
+
46
+ private onObjectAdded = () => {
47
+ this.apply();
48
+ };
49
+
50
+ private apply() {
51
+ if (!this.canvasService) return;
52
+
53
+ const dielineLayer = this.canvasService.getLayer("dieline-overlay");
54
+ if (dielineLayer) {
55
+ const visible = !HIDDEN_DIELINE_TOOLS.has(this.activeToolId || "");
56
+ if (dielineLayer.visible !== visible) {
57
+ dielineLayer.set({ visible });
58
+ }
59
+ }
60
+
61
+ const rulerLayer = this.canvasService.getLayer("ruler-overlay");
62
+ if (rulerLayer) {
63
+ const visible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
64
+ if (rulerLayer.visible !== visible) {
65
+ rulerLayer.set({ visible });
66
+ }
67
+ }
68
+
69
+ this.canvasService.requestRenderAll();
70
+ }
71
+ }
@@ -6,7 +6,7 @@ import {
6
6
  Extension,
7
7
  ExtensionContext,
8
8
  } from "@pooder/core";
9
- import CanvasService from "./CanvasService";
9
+ import { CanvasService } from "../services";
10
10
  import {
11
11
  fromMm,
12
12
  normalizeConstraintMode,
@@ -18,7 +18,7 @@ import {
18
18
  type SizeConstraintMode,
19
19
  computeSceneLayout,
20
20
  } from "./sceneLayoutModel";
21
- import type { Unit } from "./coordinate";
21
+ import type { Unit } from "../coordinate";
22
22
 
23
23
  type ChangedField = "width" | "height" | "both";
24
24
 
@@ -194,7 +194,9 @@ export class SizeTool implements Extension {
194
194
  }
195
195
 
196
196
  private getConfigService(): ConfigurationService | undefined {
197
- return this.context?.services.get<ConfigurationService>("ConfigurationService");
197
+ return this.context?.services.get<ConfigurationService>(
198
+ "ConfigurationService",
199
+ );
198
200
  }
199
201
 
200
202
  private ensureDefaults(configService: ConfigurationService) {
@@ -264,7 +266,7 @@ export class SizeTool implements Extension {
264
266
  ? nextHeightMm
265
267
  : changed === "width"
266
268
  ? nextWidthMm
267
- : providedWidthMm ?? providedHeightMm ?? nextWidthMm;
269
+ : (providedWidthMm ?? providedHeightMm ?? nextWidthMm);
268
270
  nextWidthMm = anchor;
269
271
  nextHeightMm = anchor;
270
272
  } else if (state.constraintMode === "lockAspect") {
@@ -311,11 +313,14 @@ export class SizeTool implements Extension {
311
313
  configService.update("size.aspectRatio", ratio);
312
314
  }
313
315
  if (mode === "equal") {
314
- const value = sanitizeMmValue(Math.max(state.actualWidthMm, state.actualHeightMm), {
315
- minMm: state.minMm,
316
- maxMm: state.maxMm,
317
- stepMm: state.stepMm,
318
- });
316
+ const value = sanitizeMmValue(
317
+ Math.max(state.actualWidthMm, state.actualHeightMm),
318
+ {
319
+ minMm: state.minMm,
320
+ maxMm: state.maxMm,
321
+ stepMm: state.stepMm,
322
+ },
323
+ );
319
324
  configService.update("size.actualWidthMm", value);
320
325
  configService.update("size.actualHeightMm", value);
321
326
  configService.update("size.aspectRatio", 1);
@@ -353,15 +358,20 @@ export class SizeTool implements Extension {
353
358
 
354
359
  const all = this.canvasService.canvas.getObjects() as any[];
355
360
  const active = this.canvasService.canvas.getActiveObject() as any;
356
- const activeId = active?.data?.layerId === "image.user" ? active?.data?.id : null;
361
+ const activeId =
362
+ active?.data?.layerId === "image.user" ? active?.data?.id : null;
357
363
  const targetId = id || activeId;
358
364
  const target =
359
- all.find((obj) => obj?.data?.layerId === "image.user" && obj?.data?.id === targetId) ||
360
- all.find((obj) => obj?.data?.layerId === "image.user");
365
+ all.find(
366
+ (obj) =>
367
+ obj?.data?.layerId === "image.user" && obj?.data?.id === targetId,
368
+ ) || all.find((obj) => obj?.data?.layerId === "image.user");
361
369
  if (!target) return null;
362
370
 
363
371
  const objectWidthPx = Math.abs((target.width || 0) * (target.scaleX || 1));
364
- const objectHeightPx = Math.abs((target.height || 0) * (target.scaleY || 1));
372
+ const objectHeightPx = Math.abs(
373
+ (target.height || 0) * (target.scaleY || 1),
374
+ );
365
375
  if (objectWidthPx <= 0 || objectHeightPx <= 0) return null;
366
376
 
367
377
  const widthMm = objectWidthPx / layout.scale;