@pooder/kit 5.1.0 → 5.3.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 (86) hide show
  1. package/.test-dist/src/CanvasService.js +249 -249
  2. package/.test-dist/src/ViewportSystem.js +75 -75
  3. package/.test-dist/src/background.js +203 -203
  4. package/.test-dist/src/bridgeSelection.js +20 -20
  5. package/.test-dist/src/constraints.js +237 -237
  6. package/.test-dist/src/dieline.js +818 -818
  7. package/.test-dist/src/edgeScale.js +12 -12
  8. package/.test-dist/src/extensions/background.js +203 -0
  9. package/.test-dist/src/extensions/bridgeSelection.js +20 -0
  10. package/.test-dist/src/extensions/constraints.js +237 -0
  11. package/.test-dist/src/extensions/dieline.js +828 -0
  12. package/.test-dist/src/extensions/edgeScale.js +12 -0
  13. package/.test-dist/src/extensions/feature.js +825 -0
  14. package/.test-dist/src/extensions/featureComplete.js +32 -0
  15. package/.test-dist/src/extensions/film.js +167 -0
  16. package/.test-dist/src/extensions/geometry.js +545 -0
  17. package/.test-dist/src/extensions/image.js +1529 -0
  18. package/.test-dist/src/extensions/index.js +30 -0
  19. package/.test-dist/src/extensions/maskOps.js +279 -0
  20. package/.test-dist/src/extensions/mirror.js +104 -0
  21. package/.test-dist/src/extensions/ruler.js +345 -0
  22. package/.test-dist/src/extensions/sceneLayout.js +96 -0
  23. package/.test-dist/src/extensions/sceneLayoutModel.js +196 -0
  24. package/.test-dist/src/extensions/sceneVisibility.js +62 -0
  25. package/.test-dist/src/extensions/size.js +331 -0
  26. package/.test-dist/src/extensions/tracer.js +538 -0
  27. package/.test-dist/src/extensions/white-ink.js +1190 -0
  28. package/.test-dist/src/extensions/wrappedOffsets.js +33 -0
  29. package/.test-dist/src/feature.js +826 -826
  30. package/.test-dist/src/featureComplete.js +32 -32
  31. package/.test-dist/src/film.js +167 -167
  32. package/.test-dist/src/geometry.js +506 -506
  33. package/.test-dist/src/image.js +1250 -1250
  34. package/.test-dist/src/index.js +2 -19
  35. package/.test-dist/src/maskOps.js +270 -270
  36. package/.test-dist/src/mirror.js +104 -104
  37. package/.test-dist/src/renderSpec.js +2 -2
  38. package/.test-dist/src/ruler.js +343 -343
  39. package/.test-dist/src/sceneLayout.js +99 -99
  40. package/.test-dist/src/sceneLayoutModel.js +196 -196
  41. package/.test-dist/src/sceneView.js +40 -40
  42. package/.test-dist/src/sceneVisibility.js +42 -42
  43. package/.test-dist/src/services/CanvasService.js +249 -0
  44. package/.test-dist/src/services/ViewportSystem.js +76 -0
  45. package/.test-dist/src/services/index.js +24 -0
  46. package/.test-dist/src/services/renderSpec.js +2 -0
  47. package/.test-dist/src/size.js +332 -332
  48. package/.test-dist/src/tracer.js +544 -544
  49. package/.test-dist/src/white-ink.js +829 -829
  50. package/.test-dist/src/wrappedOffsets.js +33 -33
  51. package/CHANGELOG.md +12 -0
  52. package/dist/index.d.mts +14 -0
  53. package/dist/index.d.ts +14 -0
  54. package/dist/index.js +3521 -3220
  55. package/dist/index.mjs +3532 -3226
  56. package/package.json +1 -1
  57. package/src/coordinate.ts +106 -106
  58. package/src/extensions/background.ts +230 -230
  59. package/src/extensions/bridgeSelection.ts +17 -17
  60. package/src/extensions/constraints.ts +322 -322
  61. package/src/extensions/dieline.ts +20 -17
  62. package/src/extensions/edgeScale.ts +19 -19
  63. package/src/extensions/feature.ts +1021 -1021
  64. package/src/extensions/featureComplete.ts +46 -46
  65. package/src/extensions/film.ts +194 -194
  66. package/src/extensions/geometry.ts +719 -719
  67. package/src/extensions/image.ts +1924 -1594
  68. package/src/extensions/index.ts +11 -11
  69. package/src/extensions/maskOps.ts +365 -299
  70. package/src/extensions/mirror.ts +128 -128
  71. package/src/extensions/ruler.ts +451 -451
  72. package/src/extensions/sceneLayout.ts +140 -140
  73. package/src/extensions/sceneLayoutModel.ts +342 -342
  74. package/src/extensions/sceneVisibility.ts +71 -71
  75. package/src/extensions/size.ts +389 -389
  76. package/src/extensions/tracer.ts +302 -370
  77. package/src/extensions/white-ink.ts +1489 -1366
  78. package/src/extensions/wrappedOffsets.ts +33 -33
  79. package/src/index.ts +2 -2
  80. package/src/services/CanvasService.ts +300 -300
  81. package/src/services/ViewportSystem.ts +95 -95
  82. package/src/services/index.ts +3 -3
  83. package/src/services/renderSpec.ts +18 -18
  84. package/src/units.ts +27 -27
  85. package/tests/run.ts +118 -118
  86. package/tsconfig.test.json +15 -15
@@ -1,506 +1,506 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.resolveFeaturePosition = resolveFeaturePosition;
7
- exports.generateDielinePath = generateDielinePath;
8
- exports.generateMaskPath = generateMaskPath;
9
- exports.generateBleedZonePath = generateBleedZonePath;
10
- exports.getLowestPointOnDieline = getLowestPointOnDieline;
11
- exports.getNearestPointOnDieline = getNearestPointOnDieline;
12
- exports.getPathBounds = getPathBounds;
13
- const paper_1 = __importDefault(require("paper"));
14
- const bridgeSelection_1 = require("./bridgeSelection");
15
- const wrappedOffsets_1 = require("./wrappedOffsets");
16
- /**
17
- * Resolves the absolute position of a feature based on normalized coordinates.
18
- */
19
- function resolveFeaturePosition(feature, geometry) {
20
- const { x, y, width, height } = geometry;
21
- // geometry.x/y is the Center.
22
- const left = x - width / 2;
23
- const top = y - height / 2;
24
- return {
25
- x: left + feature.x * width,
26
- y: top + feature.y * height,
27
- };
28
- }
29
- /**
30
- * Initializes paper.js project if not already initialized.
31
- */
32
- function ensurePaper(width, height) {
33
- if (!paper_1.default.project) {
34
- paper_1.default.setup(new paper_1.default.Size(width, height));
35
- }
36
- else {
37
- paper_1.default.view.viewSize = new paper_1.default.Size(width, height);
38
- }
39
- }
40
- const isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
41
- function normalizePathItem(shape) {
42
- let result = shape;
43
- if (typeof result.resolveCrossings === "function")
44
- result = result.resolveCrossings();
45
- if (typeof result.reduce === "function")
46
- result = result.reduce({});
47
- if (typeof result.reorient === "function")
48
- result = result.reorient(true, true);
49
- if (typeof result.reduce === "function")
50
- result = result.reduce({});
51
- return result;
52
- }
53
- function getBridgeDelta(itemBounds, overlap) {
54
- return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
55
- }
56
- function getExitHit(args) {
57
- const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
58
- const ray = new paper_1.default.Path.Line({
59
- from: [x, bridgeBottom],
60
- to: [x, toY],
61
- insert: false,
62
- });
63
- const intersections = mainShape.getIntersections(ray) || [];
64
- ray.remove();
65
- const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
66
- if (validHits.length === 0)
67
- return null;
68
- validHits.sort((a, b) => b.point.y - a.point.y);
69
- const flags = validHits.map((h) => {
70
- const above = h.point.add(new paper_1.default.Point(0, -delta));
71
- const below = h.point.add(new paper_1.default.Point(0, delta));
72
- return {
73
- insideAbove: mainShape.contains(above),
74
- insideBelow: mainShape.contains(below),
75
- };
76
- });
77
- const idx = (0, bridgeSelection_1.pickExitIndex)(flags);
78
- if (idx < 0)
79
- return null;
80
- if (isBridgeDebugEnabled()) {
81
- console.debug("Geometry: Bridge ray", {
82
- x,
83
- validHits: validHits.length,
84
- idx,
85
- delta,
86
- overlap,
87
- op,
88
- });
89
- }
90
- const hit = validHits[idx];
91
- return { point: hit.point, location: hit };
92
- }
93
- function selectOuterChain(args) {
94
- const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
95
- const scoreA = (0, bridgeSelection_1.scoreOutsideAbove)(pointsA.map((p) => ({
96
- outsideAbove: !mainShape.contains(p.add(new paper_1.default.Point(0, -delta))),
97
- })));
98
- const scoreB = (0, bridgeSelection_1.scoreOutsideAbove)(pointsB.map((p) => ({
99
- outsideAbove: !mainShape.contains(p.add(new paper_1.default.Point(0, -delta))),
100
- })));
101
- const ratioA = scoreA / pointsA.length;
102
- const ratioB = scoreB / pointsB.length;
103
- if (isBridgeDebugEnabled()) {
104
- console.debug("Geometry: Bridge chain", {
105
- scoreA,
106
- scoreB,
107
- lenA: pointsA.length,
108
- lenB: pointsB.length,
109
- ratioA,
110
- ratioB,
111
- delta,
112
- overlap,
113
- op,
114
- });
115
- }
116
- const ratioEps = 1e-6;
117
- if (Math.abs(ratioA - ratioB) > ratioEps) {
118
- return ratioA > ratioB ? pointsA : pointsB;
119
- }
120
- if (scoreA !== scoreB)
121
- return scoreA > scoreB ? pointsA : pointsB;
122
- return pointsA.length <= pointsB.length ? pointsA : pointsB;
123
- }
124
- /**
125
- * Creates the base dieline shape (Rect/Circle/Ellipse/Custom)
126
- */
127
- function createBaseShape(options) {
128
- const { shape, width, height, radius, x, y, pathData } = options;
129
- const center = new paper_1.default.Point(x, y);
130
- if (shape === "rect") {
131
- return new paper_1.default.Path.Rectangle({
132
- point: [x - width / 2, y - height / 2],
133
- size: [Math.max(0, width), Math.max(0, height)],
134
- radius: Math.max(0, radius),
135
- });
136
- }
137
- else if (shape === "circle") {
138
- const r = Math.min(width, height) / 2;
139
- return new paper_1.default.Path.Circle({
140
- center: center,
141
- radius: Math.max(0, r),
142
- });
143
- }
144
- else if (shape === "ellipse") {
145
- return new paper_1.default.Path.Ellipse({
146
- center: center,
147
- radius: [Math.max(0, width / 2), Math.max(0, height / 2)],
148
- });
149
- }
150
- else if (shape === "custom" && pathData) {
151
- const path = new paper_1.default.Path();
152
- path.pathData = pathData;
153
- // Align center
154
- path.position = center;
155
- if (width > 0 &&
156
- height > 0 &&
157
- path.bounds.width > 0 &&
158
- path.bounds.height > 0) {
159
- path.scale(width / path.bounds.width, height / path.bounds.height);
160
- }
161
- return path;
162
- }
163
- else {
164
- return new paper_1.default.Path.Rectangle({
165
- point: [x - width / 2, y - height / 2],
166
- size: [Math.max(0, width), Math.max(0, height)],
167
- });
168
- }
169
- }
170
- /**
171
- * Creates a Paper.js Item for a single feature.
172
- */
173
- function createFeatureItem(feature, center) {
174
- let item;
175
- if (feature.shape === "rect") {
176
- const w = feature.width || 10;
177
- const h = feature.height || 10;
178
- const r = feature.radius || 0;
179
- item = new paper_1.default.Path.Rectangle({
180
- point: [center.x - w / 2, center.y - h / 2],
181
- size: [w, h],
182
- radius: r,
183
- });
184
- }
185
- else {
186
- // Circle
187
- const r = feature.radius || 5;
188
- item = new paper_1.default.Path.Circle({
189
- center: center,
190
- radius: r,
191
- });
192
- }
193
- if (feature.rotation) {
194
- item.rotate(feature.rotation, center);
195
- }
196
- return item;
197
- }
198
- /**
199
- * Internal helper to generate the Perimeter Shape (Base + Edge Features).
200
- */
201
- function getPerimeterShape(options) {
202
- // 1. Create Base Shape
203
- let mainShape = createBaseShape(options);
204
- const { features } = options;
205
- if (features && features.length > 0) {
206
- // Filter for Edge Features (Default is Edge, unless explicit 'surface')
207
- const edgeFeatures = features.filter((f) => !f.renderBehavior || f.renderBehavior === "edge");
208
- const adds = [];
209
- const subtracts = [];
210
- edgeFeatures.forEach((f) => {
211
- const pos = resolveFeaturePosition(f, options);
212
- const center = new paper_1.default.Point(pos.x, pos.y);
213
- const item = createFeatureItem(f, center);
214
- // Handle Bridge logic: Create a connection shape to the main body
215
- if (f.bridge && f.bridge.type === "vertical") {
216
- const itemBounds = item.bounds;
217
- const mainBounds = mainShape.bounds;
218
- const bridgeTop = mainBounds.top;
219
- const bridgeBottom = itemBounds.top;
220
- if (bridgeBottom > bridgeTop) {
221
- const overlap = 2;
222
- const rayPadding = 10;
223
- const eps = 0.1;
224
- const delta = getBridgeDelta(itemBounds, overlap);
225
- const toY = bridgeTop - rayPadding;
226
- const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
227
- const xLeft = itemBounds.left + inset;
228
- const xRight = itemBounds.right - inset;
229
- if (!(mainShape instanceof paper_1.default.Path)) {
230
- throw new Error("Geometry: Bridge requires base shape to be a Path");
231
- }
232
- const leftHit = getExitHit({
233
- mainShape,
234
- x: xLeft,
235
- bridgeBottom,
236
- toY,
237
- eps,
238
- delta,
239
- overlap,
240
- op: f.operation,
241
- });
242
- const rightHit = getExitHit({
243
- mainShape,
244
- x: xRight,
245
- bridgeBottom,
246
- toY,
247
- eps,
248
- delta,
249
- overlap,
250
- op: f.operation,
251
- });
252
- if (!leftHit || !rightHit || xRight - xLeft <= eps) {
253
- throw new Error("Geometry: Bridge ray intersection not found");
254
- }
255
- const path = mainShape;
256
- const pathLength = path.length;
257
- const leftOffset = leftHit.location.offset;
258
- const rightOffset = rightHit.location.offset;
259
- const distanceA = (0, wrappedOffsets_1.wrappedDistance)(pathLength, leftOffset, rightOffset);
260
- const distanceB = (0, wrappedOffsets_1.wrappedDistance)(pathLength, rightOffset, leftOffset);
261
- const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
262
- const offsetsA = (0, wrappedOffsets_1.sampleWrappedOffsets)(pathLength, leftOffset, rightOffset, countFor(distanceA));
263
- const offsetsB = (0, wrappedOffsets_1.sampleWrappedOffsets)(pathLength, rightOffset, leftOffset, countFor(distanceB));
264
- const pointsA = offsetsA
265
- .map((o) => path.getPointAt(o))
266
- .filter((p) => Boolean(p));
267
- const pointsB = offsetsB
268
- .map((o) => path.getPointAt(o))
269
- .filter((p) => Boolean(p));
270
- if (pointsA.length < 2 || pointsB.length < 2) {
271
- throw new Error("Geometry: Bridge contour sampling failed");
272
- }
273
- let topBase = selectOuterChain({
274
- mainShape,
275
- pointsA,
276
- pointsB,
277
- delta,
278
- overlap,
279
- op: f.operation,
280
- });
281
- const dist2 = (a, b) => {
282
- const dx = a.x - b.x;
283
- const dy = a.y - b.y;
284
- return dx * dx + dy * dy;
285
- };
286
- if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
287
- topBase = topBase.slice().reverse();
288
- }
289
- topBase = topBase.slice();
290
- topBase[0] = leftHit.point;
291
- topBase[topBase.length - 1] = rightHit.point;
292
- const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
293
- const topPoints = topBase.map((p) => p.add(new paper_1.default.Point(0, capShiftY)));
294
- const bridgeBottomY = bridgeBottom + overlap * 2;
295
- const bridgePoly = new paper_1.default.Path({ insert: false });
296
- for (const p of topPoints)
297
- bridgePoly.add(p);
298
- bridgePoly.add(new paper_1.default.Point(xRight, bridgeBottomY));
299
- bridgePoly.add(new paper_1.default.Point(xLeft, bridgeBottomY));
300
- bridgePoly.closed = true;
301
- const unitedItem = item.unite(bridgePoly);
302
- item.remove();
303
- bridgePoly.remove();
304
- if (f.operation === "add") {
305
- adds.push(unitedItem);
306
- }
307
- else {
308
- subtracts.push(unitedItem);
309
- }
310
- }
311
- else {
312
- if (f.operation === "add") {
313
- adds.push(item);
314
- }
315
- else {
316
- subtracts.push(item);
317
- }
318
- }
319
- }
320
- else {
321
- if (f.operation === "add") {
322
- adds.push(item);
323
- }
324
- else {
325
- subtracts.push(item);
326
- }
327
- }
328
- });
329
- // 2. Process Additions (Union)
330
- if (adds.length > 0) {
331
- for (const item of adds) {
332
- try {
333
- const temp = mainShape.unite(item);
334
- mainShape.remove();
335
- item.remove();
336
- mainShape = normalizePathItem(temp);
337
- }
338
- catch (e) {
339
- console.error("Geometry: Failed to unite feature", e);
340
- item.remove();
341
- }
342
- }
343
- }
344
- // 3. Process Subtractions (Difference)
345
- if (subtracts.length > 0) {
346
- for (const item of subtracts) {
347
- try {
348
- const temp = mainShape.subtract(item);
349
- mainShape.remove();
350
- item.remove();
351
- mainShape = normalizePathItem(temp);
352
- }
353
- catch (e) {
354
- console.error("Geometry: Failed to subtract feature", e);
355
- item.remove();
356
- }
357
- }
358
- }
359
- }
360
- return mainShape;
361
- }
362
- /**
363
- * Applies Internal/Surface features to a shape.
364
- */
365
- function applySurfaceFeatures(shape, features, options) {
366
- const surfaceFeatures = features.filter((f) => f.renderBehavior === "surface");
367
- if (surfaceFeatures.length === 0)
368
- return shape;
369
- let result = shape;
370
- // Internal features are usually subtractive (holes)
371
- // But we support 'add' too (islands? maybe just unite)
372
- for (const f of surfaceFeatures) {
373
- const pos = resolveFeaturePosition(f, options);
374
- const center = new paper_1.default.Point(pos.x, pos.y);
375
- const item = createFeatureItem(f, center);
376
- try {
377
- if (f.operation === "add") {
378
- const temp = result.unite(item);
379
- result.remove();
380
- item.remove();
381
- result = normalizePathItem(temp);
382
- }
383
- else {
384
- const temp = result.subtract(item);
385
- result.remove();
386
- item.remove();
387
- result = normalizePathItem(temp);
388
- }
389
- }
390
- catch (e) {
391
- console.error("Geometry: Failed to apply surface feature", e);
392
- item.remove();
393
- }
394
- }
395
- return result;
396
- }
397
- /**
398
- * Generates the path data for the Dieline (Product Shape).
399
- */
400
- function generateDielinePath(options) {
401
- const paperWidth = options.canvasWidth || options.width * 2 || 2000;
402
- const paperHeight = options.canvasHeight || options.height * 2 || 2000;
403
- ensurePaper(paperWidth, paperHeight);
404
- paper_1.default.project.activeLayer.removeChildren();
405
- const perimeter = getPerimeterShape(options);
406
- const finalShape = applySurfaceFeatures(perimeter, options.features, options);
407
- const pathData = finalShape.pathData;
408
- finalShape.remove();
409
- return pathData;
410
- }
411
- /**
412
- * Generates the path data for the Mask (Background Overlay).
413
- * Logic: Canvas SUBTRACT ProductShape
414
- */
415
- function generateMaskPath(options) {
416
- ensurePaper(options.canvasWidth, options.canvasHeight);
417
- paper_1.default.project.activeLayer.removeChildren();
418
- const { canvasWidth, canvasHeight } = options;
419
- const maskRect = new paper_1.default.Path.Rectangle({
420
- point: [0, 0],
421
- size: [canvasWidth, canvasHeight],
422
- });
423
- const perimeter = getPerimeterShape(options);
424
- const mainShape = applySurfaceFeatures(perimeter, options.features, options);
425
- const finalMask = maskRect.subtract(mainShape);
426
- maskRect.remove();
427
- mainShape.remove();
428
- const pathData = finalMask.pathData;
429
- finalMask.remove();
430
- return pathData;
431
- }
432
- /**
433
- * Generates the path data for the Bleed Zone.
434
- */
435
- function generateBleedZonePath(originalOptions, offsetOptions, offset) {
436
- const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2000;
437
- const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2000;
438
- ensurePaper(paperWidth, paperHeight);
439
- paper_1.default.project.activeLayer.removeChildren();
440
- // 1. Generate Original Shape
441
- const pOriginal = getPerimeterShape(originalOptions);
442
- const shapeOriginal = applySurfaceFeatures(pOriginal, originalOptions.features, originalOptions);
443
- // 2. Generate Offset Shape
444
- const pOffset = getPerimeterShape(offsetOptions);
445
- const shapeOffset = applySurfaceFeatures(pOffset, offsetOptions.features, offsetOptions);
446
- // 3. Calculate Difference
447
- let bleedZone;
448
- if (offset > 0) {
449
- bleedZone = shapeOffset.subtract(shapeOriginal);
450
- }
451
- else {
452
- bleedZone = shapeOriginal.subtract(shapeOffset);
453
- }
454
- const pathData = bleedZone.pathData;
455
- shapeOriginal.remove();
456
- shapeOffset.remove();
457
- bleedZone.remove();
458
- return pathData;
459
- }
460
- /**
461
- * Finds the lowest point (Max Y) on the Dieline geometry (Base Shape ONLY).
462
- */
463
- function getLowestPointOnDieline(options) {
464
- ensurePaper(options.width * 2, options.height * 2);
465
- paper_1.default.project.activeLayer.removeChildren();
466
- const shape = createBaseShape(options);
467
- const bounds = shape.bounds;
468
- const result = {
469
- x: bounds.center.x,
470
- y: bounds.bottom,
471
- };
472
- shape.remove();
473
- return result;
474
- }
475
- /**
476
- * Finds the nearest point on the Dieline geometry (Base Shape ONLY) for a given target point.
477
- * Used for constraining feature movement.
478
- */
479
- function getNearestPointOnDieline(point, options) {
480
- ensurePaper(options.width * 2, options.height * 2);
481
- paper_1.default.project.activeLayer.removeChildren();
482
- // We constrain to the BASE shape, not including other features,
483
- // because usually you want to snap to the main edge.
484
- const shape = createBaseShape(options);
485
- const p = new paper_1.default.Point(point.x, point.y);
486
- const location = shape.getNearestLocation(p);
487
- const result = {
488
- x: location.point.x,
489
- y: location.point.y,
490
- normal: location.normal ? { x: location.normal.x, y: location.normal.y } : undefined
491
- };
492
- shape.remove();
493
- return result;
494
- }
495
- function getPathBounds(pathData) {
496
- const path = new paper_1.default.Path();
497
- path.pathData = pathData;
498
- const bounds = path.bounds;
499
- path.remove();
500
- return {
501
- x: bounds.x,
502
- y: bounds.y,
503
- width: bounds.width,
504
- height: bounds.height,
505
- };
506
- }
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveFeaturePosition = resolveFeaturePosition;
7
+ exports.generateDielinePath = generateDielinePath;
8
+ exports.generateMaskPath = generateMaskPath;
9
+ exports.generateBleedZonePath = generateBleedZonePath;
10
+ exports.getLowestPointOnDieline = getLowestPointOnDieline;
11
+ exports.getNearestPointOnDieline = getNearestPointOnDieline;
12
+ exports.getPathBounds = getPathBounds;
13
+ const paper_1 = __importDefault(require("paper"));
14
+ const bridgeSelection_1 = require("./bridgeSelection");
15
+ const wrappedOffsets_1 = require("./wrappedOffsets");
16
+ /**
17
+ * Resolves the absolute position of a feature based on normalized coordinates.
18
+ */
19
+ function resolveFeaturePosition(feature, geometry) {
20
+ const { x, y, width, height } = geometry;
21
+ // geometry.x/y is the Center.
22
+ const left = x - width / 2;
23
+ const top = y - height / 2;
24
+ return {
25
+ x: left + feature.x * width,
26
+ y: top + feature.y * height,
27
+ };
28
+ }
29
+ /**
30
+ * Initializes paper.js project if not already initialized.
31
+ */
32
+ function ensurePaper(width, height) {
33
+ if (!paper_1.default.project) {
34
+ paper_1.default.setup(new paper_1.default.Size(width, height));
35
+ }
36
+ else {
37
+ paper_1.default.view.viewSize = new paper_1.default.Size(width, height);
38
+ }
39
+ }
40
+ const isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
41
+ function normalizePathItem(shape) {
42
+ let result = shape;
43
+ if (typeof result.resolveCrossings === "function")
44
+ result = result.resolveCrossings();
45
+ if (typeof result.reduce === "function")
46
+ result = result.reduce({});
47
+ if (typeof result.reorient === "function")
48
+ result = result.reorient(true, true);
49
+ if (typeof result.reduce === "function")
50
+ result = result.reduce({});
51
+ return result;
52
+ }
53
+ function getBridgeDelta(itemBounds, overlap) {
54
+ return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
55
+ }
56
+ function getExitHit(args) {
57
+ const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
58
+ const ray = new paper_1.default.Path.Line({
59
+ from: [x, bridgeBottom],
60
+ to: [x, toY],
61
+ insert: false,
62
+ });
63
+ const intersections = mainShape.getIntersections(ray) || [];
64
+ ray.remove();
65
+ const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
66
+ if (validHits.length === 0)
67
+ return null;
68
+ validHits.sort((a, b) => b.point.y - a.point.y);
69
+ const flags = validHits.map((h) => {
70
+ const above = h.point.add(new paper_1.default.Point(0, -delta));
71
+ const below = h.point.add(new paper_1.default.Point(0, delta));
72
+ return {
73
+ insideAbove: mainShape.contains(above),
74
+ insideBelow: mainShape.contains(below),
75
+ };
76
+ });
77
+ const idx = (0, bridgeSelection_1.pickExitIndex)(flags);
78
+ if (idx < 0)
79
+ return null;
80
+ if (isBridgeDebugEnabled()) {
81
+ console.debug("Geometry: Bridge ray", {
82
+ x,
83
+ validHits: validHits.length,
84
+ idx,
85
+ delta,
86
+ overlap,
87
+ op,
88
+ });
89
+ }
90
+ const hit = validHits[idx];
91
+ return { point: hit.point, location: hit };
92
+ }
93
+ function selectOuterChain(args) {
94
+ const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
95
+ const scoreA = (0, bridgeSelection_1.scoreOutsideAbove)(pointsA.map((p) => ({
96
+ outsideAbove: !mainShape.contains(p.add(new paper_1.default.Point(0, -delta))),
97
+ })));
98
+ const scoreB = (0, bridgeSelection_1.scoreOutsideAbove)(pointsB.map((p) => ({
99
+ outsideAbove: !mainShape.contains(p.add(new paper_1.default.Point(0, -delta))),
100
+ })));
101
+ const ratioA = scoreA / pointsA.length;
102
+ const ratioB = scoreB / pointsB.length;
103
+ if (isBridgeDebugEnabled()) {
104
+ console.debug("Geometry: Bridge chain", {
105
+ scoreA,
106
+ scoreB,
107
+ lenA: pointsA.length,
108
+ lenB: pointsB.length,
109
+ ratioA,
110
+ ratioB,
111
+ delta,
112
+ overlap,
113
+ op,
114
+ });
115
+ }
116
+ const ratioEps = 1e-6;
117
+ if (Math.abs(ratioA - ratioB) > ratioEps) {
118
+ return ratioA > ratioB ? pointsA : pointsB;
119
+ }
120
+ if (scoreA !== scoreB)
121
+ return scoreA > scoreB ? pointsA : pointsB;
122
+ return pointsA.length <= pointsB.length ? pointsA : pointsB;
123
+ }
124
+ /**
125
+ * Creates the base dieline shape (Rect/Circle/Ellipse/Custom)
126
+ */
127
+ function createBaseShape(options) {
128
+ const { shape, width, height, radius, x, y, pathData } = options;
129
+ const center = new paper_1.default.Point(x, y);
130
+ if (shape === "rect") {
131
+ return new paper_1.default.Path.Rectangle({
132
+ point: [x - width / 2, y - height / 2],
133
+ size: [Math.max(0, width), Math.max(0, height)],
134
+ radius: Math.max(0, radius),
135
+ });
136
+ }
137
+ else if (shape === "circle") {
138
+ const r = Math.min(width, height) / 2;
139
+ return new paper_1.default.Path.Circle({
140
+ center: center,
141
+ radius: Math.max(0, r),
142
+ });
143
+ }
144
+ else if (shape === "ellipse") {
145
+ return new paper_1.default.Path.Ellipse({
146
+ center: center,
147
+ radius: [Math.max(0, width / 2), Math.max(0, height / 2)],
148
+ });
149
+ }
150
+ else if (shape === "custom" && pathData) {
151
+ const path = new paper_1.default.Path();
152
+ path.pathData = pathData;
153
+ // Align center
154
+ path.position = center;
155
+ if (width > 0 &&
156
+ height > 0 &&
157
+ path.bounds.width > 0 &&
158
+ path.bounds.height > 0) {
159
+ path.scale(width / path.bounds.width, height / path.bounds.height);
160
+ }
161
+ return path;
162
+ }
163
+ else {
164
+ return new paper_1.default.Path.Rectangle({
165
+ point: [x - width / 2, y - height / 2],
166
+ size: [Math.max(0, width), Math.max(0, height)],
167
+ });
168
+ }
169
+ }
170
+ /**
171
+ * Creates a Paper.js Item for a single feature.
172
+ */
173
+ function createFeatureItem(feature, center) {
174
+ let item;
175
+ if (feature.shape === "rect") {
176
+ const w = feature.width || 10;
177
+ const h = feature.height || 10;
178
+ const r = feature.radius || 0;
179
+ item = new paper_1.default.Path.Rectangle({
180
+ point: [center.x - w / 2, center.y - h / 2],
181
+ size: [w, h],
182
+ radius: r,
183
+ });
184
+ }
185
+ else {
186
+ // Circle
187
+ const r = feature.radius || 5;
188
+ item = new paper_1.default.Path.Circle({
189
+ center: center,
190
+ radius: r,
191
+ });
192
+ }
193
+ if (feature.rotation) {
194
+ item.rotate(feature.rotation, center);
195
+ }
196
+ return item;
197
+ }
198
+ /**
199
+ * Internal helper to generate the Perimeter Shape (Base + Edge Features).
200
+ */
201
+ function getPerimeterShape(options) {
202
+ // 1. Create Base Shape
203
+ let mainShape = createBaseShape(options);
204
+ const { features } = options;
205
+ if (features && features.length > 0) {
206
+ // Filter for Edge Features (Default is Edge, unless explicit 'surface')
207
+ const edgeFeatures = features.filter((f) => !f.renderBehavior || f.renderBehavior === "edge");
208
+ const adds = [];
209
+ const subtracts = [];
210
+ edgeFeatures.forEach((f) => {
211
+ const pos = resolveFeaturePosition(f, options);
212
+ const center = new paper_1.default.Point(pos.x, pos.y);
213
+ const item = createFeatureItem(f, center);
214
+ // Handle Bridge logic: Create a connection shape to the main body
215
+ if (f.bridge && f.bridge.type === "vertical") {
216
+ const itemBounds = item.bounds;
217
+ const mainBounds = mainShape.bounds;
218
+ const bridgeTop = mainBounds.top;
219
+ const bridgeBottom = itemBounds.top;
220
+ if (bridgeBottom > bridgeTop) {
221
+ const overlap = 2;
222
+ const rayPadding = 10;
223
+ const eps = 0.1;
224
+ const delta = getBridgeDelta(itemBounds, overlap);
225
+ const toY = bridgeTop - rayPadding;
226
+ const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
227
+ const xLeft = itemBounds.left + inset;
228
+ const xRight = itemBounds.right - inset;
229
+ if (!(mainShape instanceof paper_1.default.Path)) {
230
+ throw new Error("Geometry: Bridge requires base shape to be a Path");
231
+ }
232
+ const leftHit = getExitHit({
233
+ mainShape,
234
+ x: xLeft,
235
+ bridgeBottom,
236
+ toY,
237
+ eps,
238
+ delta,
239
+ overlap,
240
+ op: f.operation,
241
+ });
242
+ const rightHit = getExitHit({
243
+ mainShape,
244
+ x: xRight,
245
+ bridgeBottom,
246
+ toY,
247
+ eps,
248
+ delta,
249
+ overlap,
250
+ op: f.operation,
251
+ });
252
+ if (!leftHit || !rightHit || xRight - xLeft <= eps) {
253
+ throw new Error("Geometry: Bridge ray intersection not found");
254
+ }
255
+ const path = mainShape;
256
+ const pathLength = path.length;
257
+ const leftOffset = leftHit.location.offset;
258
+ const rightOffset = rightHit.location.offset;
259
+ const distanceA = (0, wrappedOffsets_1.wrappedDistance)(pathLength, leftOffset, rightOffset);
260
+ const distanceB = (0, wrappedOffsets_1.wrappedDistance)(pathLength, rightOffset, leftOffset);
261
+ const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
262
+ const offsetsA = (0, wrappedOffsets_1.sampleWrappedOffsets)(pathLength, leftOffset, rightOffset, countFor(distanceA));
263
+ const offsetsB = (0, wrappedOffsets_1.sampleWrappedOffsets)(pathLength, rightOffset, leftOffset, countFor(distanceB));
264
+ const pointsA = offsetsA
265
+ .map((o) => path.getPointAt(o))
266
+ .filter((p) => Boolean(p));
267
+ const pointsB = offsetsB
268
+ .map((o) => path.getPointAt(o))
269
+ .filter((p) => Boolean(p));
270
+ if (pointsA.length < 2 || pointsB.length < 2) {
271
+ throw new Error("Geometry: Bridge contour sampling failed");
272
+ }
273
+ let topBase = selectOuterChain({
274
+ mainShape,
275
+ pointsA,
276
+ pointsB,
277
+ delta,
278
+ overlap,
279
+ op: f.operation,
280
+ });
281
+ const dist2 = (a, b) => {
282
+ const dx = a.x - b.x;
283
+ const dy = a.y - b.y;
284
+ return dx * dx + dy * dy;
285
+ };
286
+ if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
287
+ topBase = topBase.slice().reverse();
288
+ }
289
+ topBase = topBase.slice();
290
+ topBase[0] = leftHit.point;
291
+ topBase[topBase.length - 1] = rightHit.point;
292
+ const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
293
+ const topPoints = topBase.map((p) => p.add(new paper_1.default.Point(0, capShiftY)));
294
+ const bridgeBottomY = bridgeBottom + overlap * 2;
295
+ const bridgePoly = new paper_1.default.Path({ insert: false });
296
+ for (const p of topPoints)
297
+ bridgePoly.add(p);
298
+ bridgePoly.add(new paper_1.default.Point(xRight, bridgeBottomY));
299
+ bridgePoly.add(new paper_1.default.Point(xLeft, bridgeBottomY));
300
+ bridgePoly.closed = true;
301
+ const unitedItem = item.unite(bridgePoly);
302
+ item.remove();
303
+ bridgePoly.remove();
304
+ if (f.operation === "add") {
305
+ adds.push(unitedItem);
306
+ }
307
+ else {
308
+ subtracts.push(unitedItem);
309
+ }
310
+ }
311
+ else {
312
+ if (f.operation === "add") {
313
+ adds.push(item);
314
+ }
315
+ else {
316
+ subtracts.push(item);
317
+ }
318
+ }
319
+ }
320
+ else {
321
+ if (f.operation === "add") {
322
+ adds.push(item);
323
+ }
324
+ else {
325
+ subtracts.push(item);
326
+ }
327
+ }
328
+ });
329
+ // 2. Process Additions (Union)
330
+ if (adds.length > 0) {
331
+ for (const item of adds) {
332
+ try {
333
+ const temp = mainShape.unite(item);
334
+ mainShape.remove();
335
+ item.remove();
336
+ mainShape = normalizePathItem(temp);
337
+ }
338
+ catch (e) {
339
+ console.error("Geometry: Failed to unite feature", e);
340
+ item.remove();
341
+ }
342
+ }
343
+ }
344
+ // 3. Process Subtractions (Difference)
345
+ if (subtracts.length > 0) {
346
+ for (const item of subtracts) {
347
+ try {
348
+ const temp = mainShape.subtract(item);
349
+ mainShape.remove();
350
+ item.remove();
351
+ mainShape = normalizePathItem(temp);
352
+ }
353
+ catch (e) {
354
+ console.error("Geometry: Failed to subtract feature", e);
355
+ item.remove();
356
+ }
357
+ }
358
+ }
359
+ }
360
+ return mainShape;
361
+ }
362
+ /**
363
+ * Applies Internal/Surface features to a shape.
364
+ */
365
+ function applySurfaceFeatures(shape, features, options) {
366
+ const surfaceFeatures = features.filter((f) => f.renderBehavior === "surface");
367
+ if (surfaceFeatures.length === 0)
368
+ return shape;
369
+ let result = shape;
370
+ // Internal features are usually subtractive (holes)
371
+ // But we support 'add' too (islands? maybe just unite)
372
+ for (const f of surfaceFeatures) {
373
+ const pos = resolveFeaturePosition(f, options);
374
+ const center = new paper_1.default.Point(pos.x, pos.y);
375
+ const item = createFeatureItem(f, center);
376
+ try {
377
+ if (f.operation === "add") {
378
+ const temp = result.unite(item);
379
+ result.remove();
380
+ item.remove();
381
+ result = normalizePathItem(temp);
382
+ }
383
+ else {
384
+ const temp = result.subtract(item);
385
+ result.remove();
386
+ item.remove();
387
+ result = normalizePathItem(temp);
388
+ }
389
+ }
390
+ catch (e) {
391
+ console.error("Geometry: Failed to apply surface feature", e);
392
+ item.remove();
393
+ }
394
+ }
395
+ return result;
396
+ }
397
+ /**
398
+ * Generates the path data for the Dieline (Product Shape).
399
+ */
400
+ function generateDielinePath(options) {
401
+ const paperWidth = options.canvasWidth || options.width * 2 || 2000;
402
+ const paperHeight = options.canvasHeight || options.height * 2 || 2000;
403
+ ensurePaper(paperWidth, paperHeight);
404
+ paper_1.default.project.activeLayer.removeChildren();
405
+ const perimeter = getPerimeterShape(options);
406
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
407
+ const pathData = finalShape.pathData;
408
+ finalShape.remove();
409
+ return pathData;
410
+ }
411
+ /**
412
+ * Generates the path data for the Mask (Background Overlay).
413
+ * Logic: Canvas SUBTRACT ProductShape
414
+ */
415
+ function generateMaskPath(options) {
416
+ ensurePaper(options.canvasWidth, options.canvasHeight);
417
+ paper_1.default.project.activeLayer.removeChildren();
418
+ const { canvasWidth, canvasHeight } = options;
419
+ const maskRect = new paper_1.default.Path.Rectangle({
420
+ point: [0, 0],
421
+ size: [canvasWidth, canvasHeight],
422
+ });
423
+ const perimeter = getPerimeterShape(options);
424
+ const mainShape = applySurfaceFeatures(perimeter, options.features, options);
425
+ const finalMask = maskRect.subtract(mainShape);
426
+ maskRect.remove();
427
+ mainShape.remove();
428
+ const pathData = finalMask.pathData;
429
+ finalMask.remove();
430
+ return pathData;
431
+ }
432
+ /**
433
+ * Generates the path data for the Bleed Zone.
434
+ */
435
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
436
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2000;
437
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2000;
438
+ ensurePaper(paperWidth, paperHeight);
439
+ paper_1.default.project.activeLayer.removeChildren();
440
+ // 1. Generate Original Shape
441
+ const pOriginal = getPerimeterShape(originalOptions);
442
+ const shapeOriginal = applySurfaceFeatures(pOriginal, originalOptions.features, originalOptions);
443
+ // 2. Generate Offset Shape
444
+ const pOffset = getPerimeterShape(offsetOptions);
445
+ const shapeOffset = applySurfaceFeatures(pOffset, offsetOptions.features, offsetOptions);
446
+ // 3. Calculate Difference
447
+ let bleedZone;
448
+ if (offset > 0) {
449
+ bleedZone = shapeOffset.subtract(shapeOriginal);
450
+ }
451
+ else {
452
+ bleedZone = shapeOriginal.subtract(shapeOffset);
453
+ }
454
+ const pathData = bleedZone.pathData;
455
+ shapeOriginal.remove();
456
+ shapeOffset.remove();
457
+ bleedZone.remove();
458
+ return pathData;
459
+ }
460
+ /**
461
+ * Finds the lowest point (Max Y) on the Dieline geometry (Base Shape ONLY).
462
+ */
463
+ function getLowestPointOnDieline(options) {
464
+ ensurePaper(options.width * 2, options.height * 2);
465
+ paper_1.default.project.activeLayer.removeChildren();
466
+ const shape = createBaseShape(options);
467
+ const bounds = shape.bounds;
468
+ const result = {
469
+ x: bounds.center.x,
470
+ y: bounds.bottom,
471
+ };
472
+ shape.remove();
473
+ return result;
474
+ }
475
+ /**
476
+ * Finds the nearest point on the Dieline geometry (Base Shape ONLY) for a given target point.
477
+ * Used for constraining feature movement.
478
+ */
479
+ function getNearestPointOnDieline(point, options) {
480
+ ensurePaper(options.width * 2, options.height * 2);
481
+ paper_1.default.project.activeLayer.removeChildren();
482
+ // We constrain to the BASE shape, not including other features,
483
+ // because usually you want to snap to the main edge.
484
+ const shape = createBaseShape(options);
485
+ const p = new paper_1.default.Point(point.x, point.y);
486
+ const location = shape.getNearestLocation(p);
487
+ const result = {
488
+ x: location.point.x,
489
+ y: location.point.y,
490
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : undefined
491
+ };
492
+ shape.remove();
493
+ return result;
494
+ }
495
+ function getPathBounds(pathData) {
496
+ const path = new paper_1.default.Path();
497
+ path.pathData = pathData;
498
+ const bounds = path.bounds;
499
+ path.remove();
500
+ return {
501
+ x: bounds.x,
502
+ y: bounds.y,
503
+ width: bounds.width,
504
+ height: bounds.height,
505
+ };
506
+ }