@pooder/kit 4.3.1 → 5.0.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 (60) hide show
  1. package/.test-dist/src/CanvasService.js +249 -0
  2. package/.test-dist/src/ViewportSystem.js +75 -0
  3. package/.test-dist/src/background.js +203 -0
  4. package/.test-dist/src/bridgeSelection.js +20 -0
  5. package/.test-dist/src/constraints.js +237 -0
  6. package/.test-dist/src/coordinate.js +74 -0
  7. package/.test-dist/src/dieline.js +723 -0
  8. package/.test-dist/src/edgeScale.js +12 -0
  9. package/.test-dist/src/feature.js +752 -0
  10. package/.test-dist/src/featureComplete.js +32 -0
  11. package/.test-dist/src/film.js +167 -0
  12. package/.test-dist/src/geometry.js +506 -0
  13. package/.test-dist/src/image.js +1234 -0
  14. package/.test-dist/src/index.js +35 -0
  15. package/.test-dist/src/maskOps.js +270 -0
  16. package/.test-dist/src/mirror.js +104 -0
  17. package/.test-dist/src/renderSpec.js +2 -0
  18. package/.test-dist/src/ruler.js +343 -0
  19. package/.test-dist/src/sceneLayout.js +99 -0
  20. package/.test-dist/src/sceneLayoutModel.js +196 -0
  21. package/.test-dist/src/sceneView.js +40 -0
  22. package/.test-dist/src/sceneVisibility.js +42 -0
  23. package/.test-dist/src/size.js +332 -0
  24. package/.test-dist/src/tracer.js +544 -0
  25. package/.test-dist/src/units.js +30 -0
  26. package/.test-dist/src/white-ink.js +829 -0
  27. package/.test-dist/src/wrappedOffsets.js +33 -0
  28. package/.test-dist/tests/run.js +94 -0
  29. package/CHANGELOG.md +11 -0
  30. package/dist/index.d.mts +339 -36
  31. package/dist/index.d.ts +339 -36
  32. package/dist/index.js +3572 -850
  33. package/dist/index.mjs +3565 -852
  34. package/package.json +2 -2
  35. package/src/CanvasService.ts +300 -96
  36. package/src/ViewportSystem.ts +92 -92
  37. package/src/background.ts +230 -230
  38. package/src/bridgeSelection.ts +17 -0
  39. package/src/coordinate.ts +106 -106
  40. package/src/dieline.ts +897 -973
  41. package/src/edgeScale.ts +19 -0
  42. package/src/feature.ts +83 -30
  43. package/src/film.ts +194 -194
  44. package/src/geometry.ts +242 -84
  45. package/src/image.ts +1582 -512
  46. package/src/index.ts +14 -10
  47. package/src/maskOps.ts +326 -0
  48. package/src/mirror.ts +128 -128
  49. package/src/renderSpec.ts +18 -0
  50. package/src/ruler.ts +449 -508
  51. package/src/sceneLayout.ts +121 -0
  52. package/src/sceneLayoutModel.ts +335 -0
  53. package/src/sceneVisibility.ts +49 -0
  54. package/src/size.ts +379 -0
  55. package/src/tracer.ts +719 -570
  56. package/src/units.ts +27 -27
  57. package/src/white-ink.ts +1018 -373
  58. package/src/wrappedOffsets.ts +33 -0
  59. package/tests/run.ts +118 -0
  60. package/tsconfig.test.json +15 -15
package/src/index.ts CHANGED
@@ -1,10 +1,14 @@
1
- export * from "./background";
2
- export * from "./dieline";
3
- export * from "./film";
4
- export * from "./feature";
5
- export * from "./image";
6
- export * from "./white-ink";
7
- export * from "./ruler";
8
- export * from "./mirror";
9
- export * from "./units";
10
- export { default as CanvasService } from "./CanvasService";
1
+ export * from "./background";
2
+ export * from "./dieline";
3
+ export * from "./film";
4
+ export * from "./feature";
5
+ export * from "./image";
6
+ export * from "./white-ink";
7
+ export * from "./ruler";
8
+ export * from "./mirror";
9
+ export * from "./size";
10
+ export * from "./sceneLayout";
11
+ export * from "./sceneLayoutModel";
12
+ export * from "./sceneVisibility";
13
+ export * from "./units";
14
+ export { default as CanvasService } from "./CanvasService";
package/src/maskOps.ts ADDED
@@ -0,0 +1,326 @@
1
+ export type MaskMode = "auto" | "alpha" | "whitebg";
2
+
3
+ export interface CreateMaskOptions {
4
+ threshold: number;
5
+ padding: number;
6
+ paddedWidth: number;
7
+ paddedHeight: number;
8
+ maskMode?: MaskMode;
9
+ whiteThreshold?: number;
10
+ alphaOpaqueCutoff?: number;
11
+ }
12
+
13
+ export function createMask(
14
+ imageData: ImageData,
15
+ options: CreateMaskOptions,
16
+ ): Uint8Array {
17
+ const { width, height, data } = imageData;
18
+ const {
19
+ threshold,
20
+ padding,
21
+ paddedWidth,
22
+ paddedHeight,
23
+ maskMode = "auto",
24
+ whiteThreshold = 240,
25
+ alphaOpaqueCutoff = 250,
26
+ } = options;
27
+
28
+ const resolvedMode =
29
+ maskMode === "auto" ? inferMaskMode(imageData, alphaOpaqueCutoff) : maskMode;
30
+
31
+ const mask = new Uint8Array(paddedWidth * paddedHeight);
32
+
33
+ for (let y = 0; y < height; y++) {
34
+ for (let x = 0; x < width; x++) {
35
+ const srcIdx = (y * width + x) * 4;
36
+ const r = data[srcIdx];
37
+ const g = data[srcIdx + 1];
38
+ const b = data[srcIdx + 2];
39
+ const a = data[srcIdx + 3];
40
+ const destIdx = (y + padding) * paddedWidth + (x + padding);
41
+
42
+ if (resolvedMode === "alpha") {
43
+ if (a > threshold) mask[destIdx] = 1;
44
+ } else {
45
+ if (
46
+ a > threshold &&
47
+ !(r > whiteThreshold && g > whiteThreshold && b > whiteThreshold)
48
+ ) {
49
+ mask[destIdx] = 1;
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ return mask;
56
+ }
57
+
58
+ function inferMaskMode(imageData: ImageData, alphaOpaqueCutoff: number): MaskMode {
59
+ const { data } = imageData;
60
+ const total = data.length / 4;
61
+
62
+ let belowOpaque = 0;
63
+ let veryTransparent = 0;
64
+ let minAlpha = 255;
65
+
66
+ for (let i = 3; i < data.length; i += 4) {
67
+ const a = data[i];
68
+ if (a < minAlpha) minAlpha = a;
69
+ if (a < alphaOpaqueCutoff) belowOpaque++;
70
+ if (a < 32) veryTransparent++;
71
+ }
72
+
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";
82
+ }
83
+
84
+ export function circularMorphology(
85
+ mask: Uint8Array,
86
+ width: number,
87
+ height: number,
88
+ radius: number,
89
+ op: "dilate" | "erode" | "closing" | "opening",
90
+ ): Uint8Array {
91
+ const dilate = (m: Uint8Array, r: number) => {
92
+ const horizontalDist = new Int32Array(width * height);
93
+ for (let y = 0; y < height; y++) {
94
+ let lastSolid = -r * 2;
95
+ for (let x = 0; x < width; x++) {
96
+ if (m[y * width + x]) lastSolid = x;
97
+ horizontalDist[y * width + x] = x - lastSolid;
98
+ }
99
+ lastSolid = width + r * 2;
100
+ for (let x = width - 1; x >= 0; x--) {
101
+ if (m[y * width + x]) lastSolid = x;
102
+ horizontalDist[y * width + x] = Math.min(
103
+ horizontalDist[y * width + x],
104
+ lastSolid - x,
105
+ );
106
+ }
107
+ }
108
+
109
+ const result = new Uint8Array(width * height);
110
+ const r2 = r * r;
111
+ for (let x = 0; x < width; x++) {
112
+ for (let y = 0; y < height; y++) {
113
+ let found = false;
114
+ const minY = Math.max(0, y - r);
115
+ const maxY = Math.min(height - 1, y + r);
116
+ for (let dy = minY; dy <= maxY; dy++) {
117
+ const dY = dy - y;
118
+ const hDist = horizontalDist[dy * width + x];
119
+ if (hDist * hDist + dY * dY <= r2) {
120
+ found = true;
121
+ break;
122
+ }
123
+ }
124
+ if (found) result[y * width + x] = 1;
125
+ }
126
+ }
127
+ return result;
128
+ };
129
+
130
+ const erode = (m: Uint8Array, r: number) => {
131
+ const inverted = new Uint8Array(m.length);
132
+ for (let i = 0; i < m.length; i++) inverted[i] = m[i] ? 0 : 1;
133
+ const dilatedInverted = dilate(inverted, r);
134
+ const result = new Uint8Array(m.length);
135
+ for (let i = 0; i < m.length; i++) result[i] = dilatedInverted[i] ? 0 : 1;
136
+ return result;
137
+ };
138
+
139
+ switch (op) {
140
+ case "dilate":
141
+ return dilate(mask, radius);
142
+ case "erode":
143
+ return erode(mask, radius);
144
+ case "closing":
145
+ return erode(dilate(mask, radius), radius);
146
+ case "opening":
147
+ return dilate(erode(mask, radius), radius);
148
+ default:
149
+ return mask;
150
+ }
151
+ }
152
+
153
+ export function fillHoles(
154
+ mask: Uint8Array,
155
+ width: number,
156
+ height: number,
157
+ ): Uint8Array {
158
+ const background = new Uint8Array(width * height);
159
+ const queue: number[] = [];
160
+
161
+ for (let x = 0; x < width; x++) {
162
+ if (mask[x] === 0) {
163
+ background[x] = 1;
164
+ queue.push(x);
165
+ }
166
+ const lastRowIdx = (height - 1) * width + x;
167
+ if (mask[lastRowIdx] === 0) {
168
+ background[lastRowIdx] = 1;
169
+ queue.push(lastRowIdx);
170
+ }
171
+ }
172
+ for (let y = 1; y < height - 1; y++) {
173
+ const leftIdx = y * width;
174
+ const rightIdx = y * width + (width - 1);
175
+ if (mask[leftIdx] === 0) {
176
+ background[leftIdx] = 1;
177
+ queue.push(leftIdx);
178
+ }
179
+ if (mask[rightIdx] === 0) {
180
+ background[rightIdx] = 1;
181
+ queue.push(rightIdx);
182
+ }
183
+ }
184
+
185
+ let head = 0;
186
+ while (head < queue.length) {
187
+ const idx = queue[head++];
188
+ const x = idx % width;
189
+ const y = (idx - x) / width;
190
+
191
+ const up = y > 0 ? idx - width : -1;
192
+ const down = y < height - 1 ? idx + width : -1;
193
+ const left = x > 0 ? idx - 1 : -1;
194
+ const right = x < width - 1 ? idx + 1 : -1;
195
+
196
+ if (up >= 0 && mask[up] === 0 && background[up] === 0) {
197
+ background[up] = 1;
198
+ queue.push(up);
199
+ }
200
+ if (down >= 0 && mask[down] === 0 && background[down] === 0) {
201
+ background[down] = 1;
202
+ queue.push(down);
203
+ }
204
+ if (left >= 0 && mask[left] === 0 && background[left] === 0) {
205
+ background[left] = 1;
206
+ queue.push(left);
207
+ }
208
+ if (right >= 0 && mask[right] === 0 && background[right] === 0) {
209
+ background[right] = 1;
210
+ queue.push(right);
211
+ }
212
+ }
213
+
214
+ const filledMask = new Uint8Array(width * height);
215
+ for (let i = 0; i < width * height; i++) {
216
+ filledMask[i] = background[i] === 0 ? 1 : 0;
217
+ }
218
+
219
+ return filledMask;
220
+ }
221
+
222
+ export function countForeground(mask: Uint8Array): number {
223
+ let c = 0;
224
+ for (let i = 0; i < mask.length; i++) c += mask[i] ? 1 : 0;
225
+ return c;
226
+ }
227
+
228
+ export function isMaskConnected8(
229
+ mask: Uint8Array,
230
+ width: number,
231
+ height: number,
232
+ ): boolean {
233
+ const total = countForeground(mask);
234
+ if (total === 0) return true;
235
+
236
+ let start = -1;
237
+ for (let i = 0; i < mask.length; i++) {
238
+ if (mask[i]) {
239
+ start = i;
240
+ break;
241
+ }
242
+ }
243
+ if (start === -1) return true;
244
+
245
+ const visited = new Uint8Array(mask.length);
246
+ const queue: number[] = [start];
247
+ visited[start] = 1;
248
+ let seen = 1;
249
+
250
+ let head = 0;
251
+ while (head < queue.length) {
252
+ const idx = queue[head++];
253
+ const x = idx % width;
254
+ const y = (idx - x) / width;
255
+
256
+ for (let dy = -1; dy <= 1; dy++) {
257
+ const ny = y + dy;
258
+ if (ny < 0 || ny >= height) continue;
259
+ for (let dx = -1; dx <= 1; dx++) {
260
+ if (dx === 0 && dy === 0) continue;
261
+ const nx = x + dx;
262
+ if (nx < 0 || nx >= width) continue;
263
+ const nidx = ny * width + nx;
264
+ if (mask[nidx] && !visited[nidx]) {
265
+ visited[nidx] = 1;
266
+ queue.push(nidx);
267
+ seen++;
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ return seen === total;
274
+ }
275
+
276
+ export function findMinimalConnectRadius(
277
+ mask: Uint8Array,
278
+ width: number,
279
+ height: number,
280
+ maxRadius: number,
281
+ ): number {
282
+ if (maxRadius <= 0) return 0;
283
+ if (isMaskConnected8(mask, width, height)) return 0;
284
+
285
+ let low = 0;
286
+ let high = 1;
287
+ while (high <= maxRadius) {
288
+ const closed = circularMorphology(mask, width, height, high, "closing");
289
+ if (isMaskConnected8(closed, width, height)) break;
290
+ high *= 2;
291
+ }
292
+ if (high > maxRadius) high = maxRadius;
293
+
294
+ if (
295
+ !isMaskConnected8(
296
+ circularMorphology(mask, width, height, high, "closing"),
297
+ width,
298
+ height,
299
+ )
300
+ ) {
301
+ return high;
302
+ }
303
+
304
+ while (low + 1 < high) {
305
+ const mid = Math.floor((low + high) / 2);
306
+ const closed = circularMorphology(mask, width, height, mid, "closing");
307
+ if (isMaskConnected8(closed, width, height)) {
308
+ high = mid;
309
+ } else {
310
+ low = mid;
311
+ }
312
+ }
313
+
314
+ return high;
315
+ }
316
+
317
+ export function polygonSignedArea(points: Array<{ x: number; y: number }>): number {
318
+ if (points.length < 3) return 0;
319
+ let sum = 0;
320
+ for (let i = 0; i < points.length; i++) {
321
+ const a = points[i];
322
+ const b = points[(i + 1) % points.length];
323
+ sum += a.x * b.y - b.x * a.y;
324
+ }
325
+ return sum / 2;
326
+ }
package/src/mirror.ts CHANGED
@@ -1,128 +1,128 @@
1
- import {
2
- Extension,
3
- ExtensionContext,
4
- ContributionPointIds,
5
- CommandContribution,
6
- ConfigurationContribution,
7
- } from "@pooder/core";
8
- import CanvasService from "./CanvasService";
9
-
10
- export class MirrorTool implements Extension {
11
- id = "pooder.kit.mirror";
12
-
13
- public metadata = {
14
- name: "MirrorTool",
15
- };
16
- private enabled = false;
17
-
18
- private canvasService?: CanvasService;
19
-
20
- constructor(
21
- options?: Partial<{
22
- enabled: boolean;
23
- }>,
24
- ) {
25
- if (options) {
26
- Object.assign(this, options);
27
- }
28
- }
29
-
30
- toJSON() {
31
- return {
32
- enabled: this.enabled,
33
- };
34
- }
35
-
36
- loadFromJSON(json: any) {
37
- this.enabled = json.enabled;
38
- }
39
-
40
- activate(context: ExtensionContext) {
41
- this.canvasService = context.services.get<CanvasService>("CanvasService");
42
- if (!this.canvasService) {
43
- console.warn("CanvasService not found for MirrorTool");
44
- return;
45
- }
46
-
47
- const configService = context.services.get<any>("ConfigurationService");
48
- if (configService) {
49
- // Load initial config
50
- this.enabled = configService.get("mirror.enabled", this.enabled);
51
-
52
- // Listen for changes
53
- configService.onAnyChange((e: { key: string; value: any }) => {
54
- if (e.key === "mirror.enabled") {
55
- this.applyMirror(e.value);
56
- }
57
- });
58
- }
59
-
60
- // Initialize with current state (if enabled was persisted)
61
- if (this.enabled) {
62
- this.applyMirror(true);
63
- }
64
- }
65
-
66
- deactivate(context: ExtensionContext) {
67
- this.applyMirror(false);
68
- this.canvasService = undefined;
69
- }
70
-
71
- contribute() {
72
- return {
73
- [ContributionPointIds.CONFIGURATIONS]: [
74
- {
75
- id: "mirror.enabled",
76
- type: "boolean",
77
- label: "Enable Mirror",
78
- default: false,
79
- },
80
- ] as ConfigurationContribution[],
81
- [ContributionPointIds.COMMANDS]: [
82
- {
83
- command: "setMirror",
84
- title: "Set Mirror",
85
- handler: (enabled: boolean) => {
86
- this.applyMirror(enabled);
87
- return true;
88
- },
89
- },
90
- ] as CommandContribution[],
91
- };
92
- }
93
-
94
- private applyMirror(enabled: boolean) {
95
- if (!this.canvasService) return;
96
- const canvas = this.canvasService.canvas;
97
- if (!canvas) return;
98
-
99
- const width = canvas.width || 800;
100
-
101
- // Fabric.js v6+ uses viewportTransform property
102
- let vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
103
- // Create a copy to avoid mutating the reference directly before setting
104
- vpt = [...vpt];
105
-
106
- // If we are enabling and currently not flipped (scaleX > 0)
107
- // Or disabling and currently flipped (scaleX < 0)
108
- const isFlipped = vpt[0] < 0;
109
-
110
- if (enabled && !isFlipped) {
111
- // Flip scale X
112
- vpt[0] = -vpt[0]; // Flip scale
113
- vpt[4] = width - vpt[4]; // Adjust pan X
114
-
115
- canvas.setViewportTransform(vpt as any);
116
- canvas.requestRenderAll();
117
- this.enabled = true;
118
- } else if (!enabled && isFlipped) {
119
- // Restore
120
- vpt[0] = -vpt[0]; // Unflip scale
121
- vpt[4] = width - vpt[4]; // Restore pan X
122
-
123
- canvas.setViewportTransform(vpt as any);
124
- canvas.requestRenderAll();
125
- this.enabled = false;
126
- }
127
- }
128
- }
1
+ import {
2
+ Extension,
3
+ ExtensionContext,
4
+ ContributionPointIds,
5
+ CommandContribution,
6
+ ConfigurationContribution,
7
+ } from "@pooder/core";
8
+ import CanvasService from "./CanvasService";
9
+
10
+ export class MirrorTool implements Extension {
11
+ id = "pooder.kit.mirror";
12
+
13
+ public metadata = {
14
+ name: "MirrorTool",
15
+ };
16
+ private enabled = false;
17
+
18
+ private canvasService?: CanvasService;
19
+
20
+ constructor(
21
+ options?: Partial<{
22
+ enabled: boolean;
23
+ }>,
24
+ ) {
25
+ if (options) {
26
+ Object.assign(this, options);
27
+ }
28
+ }
29
+
30
+ toJSON() {
31
+ return {
32
+ enabled: this.enabled,
33
+ };
34
+ }
35
+
36
+ loadFromJSON(json: any) {
37
+ this.enabled = json.enabled;
38
+ }
39
+
40
+ activate(context: ExtensionContext) {
41
+ this.canvasService = context.services.get<CanvasService>("CanvasService");
42
+ if (!this.canvasService) {
43
+ console.warn("CanvasService not found for MirrorTool");
44
+ return;
45
+ }
46
+
47
+ const configService = context.services.get<any>("ConfigurationService");
48
+ if (configService) {
49
+ // Load initial config
50
+ this.enabled = configService.get("mirror.enabled", this.enabled);
51
+
52
+ // Listen for changes
53
+ configService.onAnyChange((e: { key: string; value: any }) => {
54
+ if (e.key === "mirror.enabled") {
55
+ this.applyMirror(e.value);
56
+ }
57
+ });
58
+ }
59
+
60
+ // Initialize with current state (if enabled was persisted)
61
+ if (this.enabled) {
62
+ this.applyMirror(true);
63
+ }
64
+ }
65
+
66
+ deactivate(context: ExtensionContext) {
67
+ this.applyMirror(false);
68
+ this.canvasService = undefined;
69
+ }
70
+
71
+ contribute() {
72
+ return {
73
+ [ContributionPointIds.CONFIGURATIONS]: [
74
+ {
75
+ id: "mirror.enabled",
76
+ type: "boolean",
77
+ label: "Enable Mirror",
78
+ default: false,
79
+ },
80
+ ] as ConfigurationContribution[],
81
+ [ContributionPointIds.COMMANDS]: [
82
+ {
83
+ command: "setMirror",
84
+ title: "Set Mirror",
85
+ handler: (enabled: boolean) => {
86
+ this.applyMirror(enabled);
87
+ return true;
88
+ },
89
+ },
90
+ ] as CommandContribution[],
91
+ };
92
+ }
93
+
94
+ private applyMirror(enabled: boolean) {
95
+ if (!this.canvasService) return;
96
+ const canvas = this.canvasService.canvas;
97
+ if (!canvas) return;
98
+
99
+ const width = canvas.width || 800;
100
+
101
+ // Fabric.js v6+ uses viewportTransform property
102
+ let vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
103
+ // Create a copy to avoid mutating the reference directly before setting
104
+ vpt = [...vpt];
105
+
106
+ // If we are enabling and currently not flipped (scaleX > 0)
107
+ // Or disabling and currently flipped (scaleX < 0)
108
+ const isFlipped = vpt[0] < 0;
109
+
110
+ if (enabled && !isFlipped) {
111
+ // Flip scale X
112
+ vpt[0] = -vpt[0]; // Flip scale
113
+ vpt[4] = width - vpt[4]; // Adjust pan X
114
+
115
+ canvas.setViewportTransform(vpt as any);
116
+ canvas.requestRenderAll();
117
+ this.enabled = true;
118
+ } else if (!enabled && isFlipped) {
119
+ // Restore
120
+ vpt[0] = -vpt[0]; // Unflip scale
121
+ vpt[4] = width - vpt[4]; // Restore pan X
122
+
123
+ canvas.setViewportTransform(vpt as any);
124
+ canvas.requestRenderAll();
125
+ this.enabled = false;
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,18 @@
1
+ export type RenderObjectType = "rect" | "image" | "path";
2
+
3
+ export type RenderProps = Record<string, any>;
4
+
5
+ export interface RenderObjectSpec {
6
+ id: string;
7
+ type: RenderObjectType;
8
+ props: RenderProps;
9
+ data?: Record<string, any>;
10
+ src?: string;
11
+ }
12
+
13
+ export interface RenderLayerSpec {
14
+ id: string;
15
+ objects: RenderObjectSpec[];
16
+ props?: RenderProps;
17
+ }
18
+