@pooder/kit 5.4.0 → 6.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.
- package/.test-dist/src/coordinate.js +74 -0
- package/.test-dist/src/extensions/background.js +547 -0
- package/.test-dist/src/extensions/bridgeSelection.js +20 -0
- package/.test-dist/src/extensions/constraints.js +237 -0
- package/.test-dist/src/extensions/dieline.js +931 -0
- package/.test-dist/src/extensions/dielineShape.js +66 -0
- package/.test-dist/src/extensions/edgeScale.js +12 -0
- package/.test-dist/src/extensions/feature.js +910 -0
- package/.test-dist/src/extensions/featureComplete.js +32 -0
- package/.test-dist/src/extensions/film.js +226 -0
- package/.test-dist/src/extensions/geometry.js +609 -0
- package/.test-dist/src/extensions/image.js +1613 -0
- package/.test-dist/src/extensions/index.js +28 -0
- package/.test-dist/src/extensions/maskOps.js +334 -0
- package/.test-dist/src/extensions/mirror.js +104 -0
- package/.test-dist/src/extensions/ruler.js +442 -0
- package/.test-dist/src/extensions/sceneLayout.js +96 -0
- package/.test-dist/src/extensions/sceneLayoutModel.js +202 -0
- package/.test-dist/src/extensions/sceneVisibility.js +55 -0
- package/.test-dist/src/extensions/size.js +331 -0
- package/.test-dist/src/extensions/tracer.js +709 -0
- package/.test-dist/src/extensions/white-ink.js +1200 -0
- package/.test-dist/src/extensions/wrappedOffsets.js +33 -0
- package/.test-dist/src/index.js +18 -0
- package/.test-dist/src/services/CanvasService.js +1011 -0
- package/.test-dist/src/services/ViewportSystem.js +76 -0
- package/.test-dist/src/services/index.js +25 -0
- package/.test-dist/src/services/renderSpec.js +2 -0
- package/.test-dist/src/services/visibility.js +54 -0
- package/.test-dist/src/units.js +30 -0
- package/.test-dist/tests/run.js +148 -0
- package/CHANGELOG.md +6 -0
- package/dist/index.d.mts +150 -62
- package/dist/index.d.ts +150 -62
- package/dist/index.js +2219 -1714
- package/dist/index.mjs +2226 -1718
- package/package.json +1 -1
- package/src/coordinate.ts +106 -106
- package/src/extensions/background.ts +716 -323
- package/src/extensions/bridgeSelection.ts +17 -17
- package/src/extensions/constraints.ts +322 -322
- package/src/extensions/dieline.ts +1169 -1149
- package/src/extensions/dielineShape.ts +109 -109
- package/src/extensions/edgeScale.ts +19 -19
- package/src/extensions/feature.ts +1140 -1137
- package/src/extensions/featureComplete.ts +46 -46
- package/src/extensions/film.ts +270 -266
- package/src/extensions/geometry.ts +851 -885
- package/src/extensions/image.ts +2007 -2054
- package/src/extensions/index.ts +10 -11
- package/src/extensions/maskOps.ts +283 -283
- package/src/extensions/mirror.ts +128 -128
- package/src/extensions/ruler.ts +664 -654
- package/src/extensions/sceneLayout.ts +140 -140
- package/src/extensions/sceneLayoutModel.ts +364 -364
- package/src/extensions/size.ts +389 -389
- package/src/extensions/tracer.ts +1019 -1019
- package/src/extensions/white-ink.ts +1508 -1575
- package/src/extensions/wrappedOffsets.ts +33 -33
- package/src/index.ts +2 -2
- package/src/services/CanvasService.ts +1286 -832
- package/src/services/ViewportSystem.ts +95 -95
- package/src/services/index.ts +4 -3
- package/src/services/renderSpec.ts +83 -53
- package/src/services/visibility.ts +78 -0
- package/src/units.ts +27 -27
- package/tests/run.ts +253 -118
- package/tsconfig.test.json +15 -15
- package/src/extensions/sceneVisibility.ts +0 -64
|
@@ -1,885 +1,851 @@
|
|
|
1
|
-
import paper from "paper";
|
|
2
|
-
import { pickExitIndex, scoreOutsideAbove } from "./bridgeSelection";
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_DIELINE_SHAPE,
|
|
5
|
-
getHeartShapeParams,
|
|
6
|
-
getShapeFitMode,
|
|
7
|
-
} from "./dielineShape";
|
|
8
|
-
import type {
|
|
9
|
-
BuiltinDielineShape,
|
|
10
|
-
DielineShape,
|
|
11
|
-
DielineShapeStyle,
|
|
12
|
-
ShapeFitMode,
|
|
13
|
-
} from "./dielineShape";
|
|
14
|
-
import { sampleWrappedOffsets, wrappedDistance } from "./wrappedOffsets";
|
|
15
|
-
|
|
16
|
-
export type FeatureOperation = "add" | "subtract";
|
|
17
|
-
export type FeatureShape = "rect" | "circle";
|
|
18
|
-
|
|
19
|
-
export interface DielineFeature {
|
|
20
|
-
id: string;
|
|
21
|
-
groupId?: string;
|
|
22
|
-
operation: FeatureOperation;
|
|
23
|
-
shape: FeatureShape;
|
|
24
|
-
x: number;
|
|
25
|
-
y: number;
|
|
26
|
-
width?: number;
|
|
27
|
-
height?: number;
|
|
28
|
-
radius?: number;
|
|
29
|
-
rotation?: number;
|
|
30
|
-
// Rendering behavior: 'edge' (modifies perimeter) or 'surface' (hole/island)
|
|
31
|
-
renderBehavior?: "edge" | "surface";
|
|
32
|
-
color?: string;
|
|
33
|
-
strokeDash?: number[];
|
|
34
|
-
skipCut?: boolean;
|
|
35
|
-
bridge?: {
|
|
36
|
-
type: "vertical";
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface GeometryOptions {
|
|
41
|
-
shape: DielineShape;
|
|
42
|
-
width: number;
|
|
43
|
-
height: number;
|
|
44
|
-
radius: number;
|
|
45
|
-
x: number;
|
|
46
|
-
y: number;
|
|
47
|
-
features: Array<DielineFeature>;
|
|
48
|
-
pathData?: string;
|
|
49
|
-
shapeStyle?: DielineShapeStyle;
|
|
50
|
-
customSourceWidthPx?: number;
|
|
51
|
-
customSourceHeightPx?: number;
|
|
52
|
-
canvasWidth?: number;
|
|
53
|
-
canvasHeight?: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
eps
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
pointsB
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta))),
|
|
171
|
-
})),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
item
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
heartPath.cubicCurveTo(
|
|
273
|
-
new paper.Point(
|
|
274
|
-
new paper.Point(
|
|
275
|
-
new paper.Point(
|
|
276
|
-
);
|
|
277
|
-
heartPath.cubicCurveTo(
|
|
278
|
-
new paper.Point(
|
|
279
|
-
new paper.Point(
|
|
280
|
-
new paper.Point(0,
|
|
281
|
-
);
|
|
282
|
-
heartPath.cubicCurveTo(
|
|
283
|
-
new paper.Point(
|
|
284
|
-
new paper.Point(
|
|
285
|
-
new paper.Point(
|
|
286
|
-
);
|
|
287
|
-
heartPath.cubicCurveTo(
|
|
288
|
-
new paper.Point(1
|
|
289
|
-
new paper.Point(
|
|
290
|
-
new paper.Point(
|
|
291
|
-
);
|
|
292
|
-
heartPath.cubicCurveTo(
|
|
293
|
-
new paper.Point(
|
|
294
|
-
new paper.Point(
|
|
295
|
-
new paper.Point(
|
|
296
|
-
);
|
|
297
|
-
heartPath.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
width
|
|
377
|
-
height
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
path
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
feature
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
bridgePoly.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const temp = result.
|
|
707
|
-
result.remove();
|
|
708
|
-
item.remove();
|
|
709
|
-
result = normalizePathItem(temp);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
)
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
);
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
// We constrain to the BASE shape, not including other features,
|
|
853
|
-
// because usually you want to snap to the main edge.
|
|
854
|
-
const shape = createBaseShape(options);
|
|
855
|
-
|
|
856
|
-
const p = new paper.Point(point.x, point.y);
|
|
857
|
-
const location = shape.getNearestLocation(p);
|
|
858
|
-
|
|
859
|
-
const result = {
|
|
860
|
-
x: location.point.x,
|
|
861
|
-
y: location.point.y,
|
|
862
|
-
normal: location.normal ? { x: location.normal.x, y: location.normal.y } : undefined
|
|
863
|
-
};
|
|
864
|
-
shape.remove();
|
|
865
|
-
|
|
866
|
-
return result;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
export function getPathBounds(pathData: string): {
|
|
870
|
-
x: number;
|
|
871
|
-
y: number;
|
|
872
|
-
width: number;
|
|
873
|
-
height: number;
|
|
874
|
-
} {
|
|
875
|
-
const path = new paper.Path();
|
|
876
|
-
path.pathData = pathData;
|
|
877
|
-
const bounds = path.bounds;
|
|
878
|
-
path.remove();
|
|
879
|
-
return {
|
|
880
|
-
x: bounds.x,
|
|
881
|
-
y: bounds.y,
|
|
882
|
-
width: bounds.width,
|
|
883
|
-
height: bounds.height,
|
|
884
|
-
};
|
|
885
|
-
}
|
|
1
|
+
import paper from "paper";
|
|
2
|
+
import { pickExitIndex, scoreOutsideAbove } from "./bridgeSelection";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_DIELINE_SHAPE,
|
|
5
|
+
getHeartShapeParams,
|
|
6
|
+
getShapeFitMode,
|
|
7
|
+
} from "./dielineShape";
|
|
8
|
+
import type {
|
|
9
|
+
BuiltinDielineShape,
|
|
10
|
+
DielineShape,
|
|
11
|
+
DielineShapeStyle,
|
|
12
|
+
ShapeFitMode,
|
|
13
|
+
} from "./dielineShape";
|
|
14
|
+
import { sampleWrappedOffsets, wrappedDistance } from "./wrappedOffsets";
|
|
15
|
+
|
|
16
|
+
export type FeatureOperation = "add" | "subtract";
|
|
17
|
+
export type FeatureShape = "rect" | "circle";
|
|
18
|
+
|
|
19
|
+
export interface DielineFeature {
|
|
20
|
+
id: string;
|
|
21
|
+
groupId?: string;
|
|
22
|
+
operation: FeatureOperation;
|
|
23
|
+
shape: FeatureShape;
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
radius?: number;
|
|
29
|
+
rotation?: number;
|
|
30
|
+
// Rendering behavior: 'edge' (modifies perimeter) or 'surface' (hole/island)
|
|
31
|
+
renderBehavior?: "edge" | "surface";
|
|
32
|
+
color?: string;
|
|
33
|
+
strokeDash?: number[];
|
|
34
|
+
skipCut?: boolean;
|
|
35
|
+
bridge?: {
|
|
36
|
+
type: "vertical";
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GeometryOptions {
|
|
41
|
+
shape: DielineShape;
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
radius: number;
|
|
45
|
+
x: number;
|
|
46
|
+
y: number;
|
|
47
|
+
features: Array<DielineFeature>;
|
|
48
|
+
pathData?: string;
|
|
49
|
+
shapeStyle?: DielineShapeStyle;
|
|
50
|
+
customSourceWidthPx?: number;
|
|
51
|
+
customSourceHeightPx?: number;
|
|
52
|
+
canvasWidth?: number;
|
|
53
|
+
canvasHeight?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolves the absolute position of a feature based on normalized coordinates.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveFeaturePosition(
|
|
60
|
+
feature: DielineFeature,
|
|
61
|
+
geometry: { x: number; y: number; width: number; height: number },
|
|
62
|
+
): { x: number; y: number } {
|
|
63
|
+
const { x, y, width, height } = geometry;
|
|
64
|
+
// geometry.x/y is the Center.
|
|
65
|
+
const left = x - width / 2;
|
|
66
|
+
const top = y - height / 2;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
x: left + feature.x * width,
|
|
70
|
+
y: top + feature.y * height,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Initializes paper.js project if not already initialized.
|
|
76
|
+
*/
|
|
77
|
+
function ensurePaper(width: number, height: number) {
|
|
78
|
+
if (!paper.project) {
|
|
79
|
+
paper.setup(new paper.Size(width, height));
|
|
80
|
+
} else {
|
|
81
|
+
paper.view.viewSize = new paper.Size(width, height);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isBridgeDebugEnabled = () =>
|
|
86
|
+
Boolean((globalThis as any).__POODER_BRIDGE_DEBUG__);
|
|
87
|
+
|
|
88
|
+
function normalizePathItem(shape: paper.PathItem): paper.PathItem {
|
|
89
|
+
let result: any = shape;
|
|
90
|
+
if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
|
|
91
|
+
if (typeof result.reduce === "function") result = result.reduce({});
|
|
92
|
+
if (typeof result.reorient === "function") result = result.reorient(true, true);
|
|
93
|
+
if (typeof result.reduce === "function") result = result.reduce({});
|
|
94
|
+
return result as paper.PathItem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getBridgeDelta(itemBounds: paper.Rectangle, overlap: number) {
|
|
98
|
+
return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getExitHit(args: {
|
|
102
|
+
mainShape: paper.Path;
|
|
103
|
+
x: number;
|
|
104
|
+
bridgeBottom: number;
|
|
105
|
+
toY: number;
|
|
106
|
+
eps: number;
|
|
107
|
+
delta: number;
|
|
108
|
+
overlap: number;
|
|
109
|
+
op: FeatureOperation;
|
|
110
|
+
}) {
|
|
111
|
+
const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
|
|
112
|
+
|
|
113
|
+
const ray = new paper.Path.Line({
|
|
114
|
+
from: [x, bridgeBottom],
|
|
115
|
+
to: [x, toY],
|
|
116
|
+
insert: false,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const intersections = mainShape.getIntersections(ray) || [];
|
|
120
|
+
ray.remove();
|
|
121
|
+
|
|
122
|
+
const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
|
|
123
|
+
if (validHits.length === 0) return null;
|
|
124
|
+
|
|
125
|
+
validHits.sort((a, b) => b.point.y - a.point.y);
|
|
126
|
+
const flags = validHits.map((h) => {
|
|
127
|
+
const above = h.point.add(new paper.Point(0, -delta));
|
|
128
|
+
const below = h.point.add(new paper.Point(0, delta));
|
|
129
|
+
return {
|
|
130
|
+
insideAbove: mainShape.contains(above),
|
|
131
|
+
insideBelow: mainShape.contains(below),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const idx = pickExitIndex(flags);
|
|
136
|
+
if (idx < 0) return null;
|
|
137
|
+
|
|
138
|
+
if (isBridgeDebugEnabled()) {
|
|
139
|
+
console.debug("Geometry: Bridge ray", {
|
|
140
|
+
x,
|
|
141
|
+
validHits: validHits.length,
|
|
142
|
+
idx,
|
|
143
|
+
delta,
|
|
144
|
+
overlap,
|
|
145
|
+
op,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hit = validHits[idx];
|
|
150
|
+
return { point: hit.point, location: hit };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function selectOuterChain(args: {
|
|
154
|
+
mainShape: paper.Path;
|
|
155
|
+
pointsA: paper.Point[];
|
|
156
|
+
pointsB: paper.Point[];
|
|
157
|
+
delta: number;
|
|
158
|
+
overlap: number;
|
|
159
|
+
op: FeatureOperation;
|
|
160
|
+
}) {
|
|
161
|
+
const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
|
|
162
|
+
|
|
163
|
+
const scoreA = scoreOutsideAbove(
|
|
164
|
+
pointsA.map((p) => ({
|
|
165
|
+
outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta))),
|
|
166
|
+
})),
|
|
167
|
+
);
|
|
168
|
+
const scoreB = scoreOutsideAbove(
|
|
169
|
+
pointsB.map((p) => ({
|
|
170
|
+
outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta))),
|
|
171
|
+
})),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const ratioA = scoreA / pointsA.length;
|
|
175
|
+
const ratioB = scoreB / pointsB.length;
|
|
176
|
+
|
|
177
|
+
if (isBridgeDebugEnabled()) {
|
|
178
|
+
console.debug("Geometry: Bridge chain", {
|
|
179
|
+
scoreA,
|
|
180
|
+
scoreB,
|
|
181
|
+
lenA: pointsA.length,
|
|
182
|
+
lenB: pointsB.length,
|
|
183
|
+
ratioA,
|
|
184
|
+
ratioB,
|
|
185
|
+
delta,
|
|
186
|
+
overlap,
|
|
187
|
+
op,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const ratioEps = 1e-6;
|
|
192
|
+
if (Math.abs(ratioA - ratioB) > ratioEps) {
|
|
193
|
+
return ratioA > ratioB ? pointsA : pointsB;
|
|
194
|
+
}
|
|
195
|
+
if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
|
|
196
|
+
return pointsA.length <= pointsB.length ? pointsA : pointsB;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Creates the base dieline shape (Rect/Circle/Ellipse/Heart/Custom).
|
|
201
|
+
*/
|
|
202
|
+
type BuiltinShapeBuilder = (options: GeometryOptions) => paper.PathItem;
|
|
203
|
+
|
|
204
|
+
function fitPathItemToRect(
|
|
205
|
+
item: paper.PathItem,
|
|
206
|
+
rect: { left: number; top: number; width: number; height: number },
|
|
207
|
+
fitMode: ShapeFitMode,
|
|
208
|
+
): paper.PathItem {
|
|
209
|
+
const { left, top, width, height } = rect;
|
|
210
|
+
const bounds = item.bounds;
|
|
211
|
+
if (
|
|
212
|
+
width <= 0 ||
|
|
213
|
+
height <= 0 ||
|
|
214
|
+
!Number.isFinite(bounds.width) ||
|
|
215
|
+
!Number.isFinite(bounds.height) ||
|
|
216
|
+
bounds.width <= 0 ||
|
|
217
|
+
bounds.height <= 0
|
|
218
|
+
) {
|
|
219
|
+
item.position = new paper.Point(left + width / 2, top + height / 2);
|
|
220
|
+
return item;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
item.translate(new paper.Point(-bounds.left, -bounds.top));
|
|
224
|
+
if (fitMode === "stretch") {
|
|
225
|
+
item.scale(width / bounds.width, height / bounds.height, new paper.Point(0, 0));
|
|
226
|
+
item.translate(new paper.Point(left, top));
|
|
227
|
+
return item;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const uniformScale = Math.min(width / bounds.width, height / bounds.height);
|
|
231
|
+
item.scale(uniformScale, uniformScale, new paper.Point(0, 0));
|
|
232
|
+
const scaledWidth = bounds.width * uniformScale;
|
|
233
|
+
const scaledHeight = bounds.height * uniformScale;
|
|
234
|
+
item.translate(
|
|
235
|
+
new paper.Point(
|
|
236
|
+
left + (width - scaledWidth) / 2,
|
|
237
|
+
top + (height - scaledHeight) / 2,
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
return item;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createNormalizedHeartPath(params: {
|
|
244
|
+
lobeSpread: number;
|
|
245
|
+
notchDepth: number;
|
|
246
|
+
tipSharpness: number;
|
|
247
|
+
}): paper.Path {
|
|
248
|
+
const { lobeSpread, notchDepth, tipSharpness } = params;
|
|
249
|
+
|
|
250
|
+
const halfSpread = 0.22 + lobeSpread * 0.18;
|
|
251
|
+
const notchY = 0.06 + notchDepth * 0.2;
|
|
252
|
+
const shoulderY = 0.24 + notchDepth * 0.2;
|
|
253
|
+
const topLift = 0.12 + (1 - notchDepth) * 0.06;
|
|
254
|
+
const topY = notchY - topLift;
|
|
255
|
+
const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
|
|
256
|
+
const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
|
|
257
|
+
const tipCtrlX = 0.34 - tipSharpness * 0.2;
|
|
258
|
+
const notchCtrlX = 0.06 + lobeSpread * 0.06;
|
|
259
|
+
const lobeCtrlX = 0.1 + lobeSpread * 0.08;
|
|
260
|
+
const notchCtrlY = notchY - topLift * 0.45;
|
|
261
|
+
|
|
262
|
+
const xPeakL = 0.5 - halfSpread;
|
|
263
|
+
const xPeakR = 0.5 + halfSpread;
|
|
264
|
+
|
|
265
|
+
const heartPath = new paper.Path({ insert: false });
|
|
266
|
+
heartPath.moveTo(new paper.Point(0.5, notchY));
|
|
267
|
+
heartPath.cubicCurveTo(
|
|
268
|
+
new paper.Point(0.5 - notchCtrlX, notchCtrlY),
|
|
269
|
+
new paper.Point(xPeakL + lobeCtrlX, topY),
|
|
270
|
+
new paper.Point(xPeakL, topY),
|
|
271
|
+
);
|
|
272
|
+
heartPath.cubicCurveTo(
|
|
273
|
+
new paper.Point(xPeakL - lobeCtrlX, topY),
|
|
274
|
+
new paper.Point(0, sideCtrlY),
|
|
275
|
+
new paper.Point(0, shoulderY),
|
|
276
|
+
);
|
|
277
|
+
heartPath.cubicCurveTo(
|
|
278
|
+
new paper.Point(0, lowerCtrlY),
|
|
279
|
+
new paper.Point(tipCtrlX, 1),
|
|
280
|
+
new paper.Point(0.5, 1),
|
|
281
|
+
);
|
|
282
|
+
heartPath.cubicCurveTo(
|
|
283
|
+
new paper.Point(1 - tipCtrlX, 1),
|
|
284
|
+
new paper.Point(1, lowerCtrlY),
|
|
285
|
+
new paper.Point(1, shoulderY),
|
|
286
|
+
);
|
|
287
|
+
heartPath.cubicCurveTo(
|
|
288
|
+
new paper.Point(1, sideCtrlY),
|
|
289
|
+
new paper.Point(xPeakR + lobeCtrlX, topY),
|
|
290
|
+
new paper.Point(xPeakR, topY),
|
|
291
|
+
);
|
|
292
|
+
heartPath.cubicCurveTo(
|
|
293
|
+
new paper.Point(xPeakR - lobeCtrlX, topY),
|
|
294
|
+
new paper.Point(0.5 + notchCtrlX, notchCtrlY),
|
|
295
|
+
new paper.Point(0.5, notchY),
|
|
296
|
+
);
|
|
297
|
+
heartPath.closed = true;
|
|
298
|
+
return heartPath;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createHeartBaseShape(options: GeometryOptions): paper.PathItem {
|
|
302
|
+
const { x, y, width, height } = options;
|
|
303
|
+
const w = Math.max(0, width);
|
|
304
|
+
const h = Math.max(0, height);
|
|
305
|
+
const left = x - w / 2;
|
|
306
|
+
const top = y - h / 2;
|
|
307
|
+
const fitMode = getShapeFitMode(options.shapeStyle);
|
|
308
|
+
const heartParams = getHeartShapeParams(options.shapeStyle);
|
|
309
|
+
const rawHeart = createNormalizedHeartPath(heartParams);
|
|
310
|
+
return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const BUILTIN_SHAPE_BUILDERS: Record<BuiltinDielineShape, BuiltinShapeBuilder> =
|
|
314
|
+
{
|
|
315
|
+
rect: (options) => {
|
|
316
|
+
const { x, y, width, height, radius } = options;
|
|
317
|
+
return new paper.Path.Rectangle({
|
|
318
|
+
point: [x - width / 2, y - height / 2],
|
|
319
|
+
size: [Math.max(0, width), Math.max(0, height)],
|
|
320
|
+
radius: Math.max(0, radius),
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
circle: (options) => {
|
|
324
|
+
const { x, y, width, height } = options;
|
|
325
|
+
const r = Math.min(width, height) / 2;
|
|
326
|
+
return new paper.Path.Circle({
|
|
327
|
+
center: new paper.Point(x, y),
|
|
328
|
+
radius: Math.max(0, r),
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
ellipse: (options) => {
|
|
332
|
+
const { x, y, width, height } = options;
|
|
333
|
+
return new paper.Path.Ellipse({
|
|
334
|
+
center: new paper.Point(x, y),
|
|
335
|
+
radius: [Math.max(0, width / 2), Math.max(0, height / 2)],
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
heart: createHeartBaseShape,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
function createCustomBaseShape(options: GeometryOptions): paper.PathItem | null {
|
|
342
|
+
const {
|
|
343
|
+
pathData,
|
|
344
|
+
customSourceWidthPx,
|
|
345
|
+
customSourceHeightPx,
|
|
346
|
+
x,
|
|
347
|
+
y,
|
|
348
|
+
width,
|
|
349
|
+
height,
|
|
350
|
+
} = options;
|
|
351
|
+
if (typeof pathData !== "string" || pathData.trim().length === 0) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const center = new paper.Point(x, y);
|
|
356
|
+
const hasMultipleSubPaths = ((pathData.match(/[Mm]/g) || []).length ?? 0) > 1;
|
|
357
|
+
const path: paper.PathItem = hasMultipleSubPaths
|
|
358
|
+
? new paper.CompoundPath(pathData)
|
|
359
|
+
: (() => {
|
|
360
|
+
const single = new paper.Path();
|
|
361
|
+
single.pathData = pathData;
|
|
362
|
+
return single;
|
|
363
|
+
})();
|
|
364
|
+
const sourceWidth = Number(customSourceWidthPx ?? 0);
|
|
365
|
+
const sourceHeight = Number(customSourceHeightPx ?? 0);
|
|
366
|
+
if (
|
|
367
|
+
Number.isFinite(sourceWidth) &&
|
|
368
|
+
Number.isFinite(sourceHeight) &&
|
|
369
|
+
sourceWidth > 0 &&
|
|
370
|
+
sourceHeight > 0 &&
|
|
371
|
+
width > 0 &&
|
|
372
|
+
height > 0
|
|
373
|
+
) {
|
|
374
|
+
// Preserve original detect-space offset/expand by mapping source image
|
|
375
|
+
// coordinates directly into the target dieline frame.
|
|
376
|
+
const targetLeft = x - width / 2;
|
|
377
|
+
const targetTop = y - height / 2;
|
|
378
|
+
path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
|
|
379
|
+
path.translate(new paper.Point(targetLeft, targetTop));
|
|
380
|
+
return path;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
|
|
384
|
+
// Fallback for malformed custom-path metadata.
|
|
385
|
+
path.position = center;
|
|
386
|
+
path.scale(width / path.bounds.width, height / path.bounds.height);
|
|
387
|
+
return path;
|
|
388
|
+
}
|
|
389
|
+
path.position = center;
|
|
390
|
+
return path;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function createBaseShape(options: GeometryOptions): paper.PathItem {
|
|
394
|
+
const { shape } = options;
|
|
395
|
+
if (shape === "custom") {
|
|
396
|
+
const customShape = createCustomBaseShape(options);
|
|
397
|
+
if (customShape) return customShape;
|
|
398
|
+
return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
|
|
399
|
+
}
|
|
400
|
+
return BUILTIN_SHAPE_BUILDERS[shape](options);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function resolveBridgeBasePath(
|
|
404
|
+
shape: paper.PathItem,
|
|
405
|
+
anchor: paper.Point,
|
|
406
|
+
): paper.Path | null {
|
|
407
|
+
if (shape instanceof paper.Path) {
|
|
408
|
+
return shape;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (shape instanceof paper.CompoundPath) {
|
|
412
|
+
const children = (shape.children || []).filter(
|
|
413
|
+
(child): child is paper.Path => child instanceof paper.Path,
|
|
414
|
+
);
|
|
415
|
+
if (!children.length) return null;
|
|
416
|
+
let best = children[0];
|
|
417
|
+
let bestDistance = Infinity;
|
|
418
|
+
for (const child of children) {
|
|
419
|
+
const location = child.getNearestLocation(anchor);
|
|
420
|
+
const point = location?.point;
|
|
421
|
+
if (!point) continue;
|
|
422
|
+
const distance = point.getDistance(anchor);
|
|
423
|
+
if (distance < bestDistance) {
|
|
424
|
+
bestDistance = distance;
|
|
425
|
+
best = child;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return best;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Creates a Paper.js Item for a single feature.
|
|
436
|
+
*/
|
|
437
|
+
function createFeatureItem(
|
|
438
|
+
feature: DielineFeature,
|
|
439
|
+
center: paper.Point,
|
|
440
|
+
): paper.PathItem {
|
|
441
|
+
let item: paper.PathItem;
|
|
442
|
+
|
|
443
|
+
if (feature.shape === "rect") {
|
|
444
|
+
const w = feature.width || 10;
|
|
445
|
+
const h = feature.height || 10;
|
|
446
|
+
const r = feature.radius || 0;
|
|
447
|
+
item = new paper.Path.Rectangle({
|
|
448
|
+
point: [center.x - w / 2, center.y - h / 2],
|
|
449
|
+
size: [w, h],
|
|
450
|
+
radius: r,
|
|
451
|
+
});
|
|
452
|
+
} else {
|
|
453
|
+
// Circle
|
|
454
|
+
const r = feature.radius || 5;
|
|
455
|
+
item = new paper.Path.Circle({
|
|
456
|
+
center: center,
|
|
457
|
+
radius: r,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (feature.rotation) {
|
|
462
|
+
item.rotate(feature.rotation, center);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return item;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Internal helper to generate the Perimeter Shape (Base + Edge Features).
|
|
470
|
+
*/
|
|
471
|
+
function getPerimeterShape(options: GeometryOptions): paper.PathItem {
|
|
472
|
+
// 1. Create Base Shape
|
|
473
|
+
let mainShape = createBaseShape(options);
|
|
474
|
+
|
|
475
|
+
const { features } = options;
|
|
476
|
+
|
|
477
|
+
if (features && features.length > 0) {
|
|
478
|
+
// Filter for Edge Features (Default is Edge, unless explicit 'surface')
|
|
479
|
+
const edgeFeatures = features.filter(
|
|
480
|
+
(f) => !f.renderBehavior || f.renderBehavior === "edge",
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const adds: paper.PathItem[] = [];
|
|
484
|
+
const subtracts: paper.PathItem[] = [];
|
|
485
|
+
|
|
486
|
+
edgeFeatures.forEach((f) => {
|
|
487
|
+
const pos = resolveFeaturePosition(f, options);
|
|
488
|
+
const center = new paper.Point(pos.x, pos.y);
|
|
489
|
+
const item = createFeatureItem(f, center);
|
|
490
|
+
|
|
491
|
+
// Handle Bridge logic: Create a connection shape to the main body
|
|
492
|
+
if (f.bridge && f.bridge.type === "vertical") {
|
|
493
|
+
const itemBounds = item.bounds;
|
|
494
|
+
const mainBounds = mainShape.bounds;
|
|
495
|
+
const bridgeTop = mainBounds.top;
|
|
496
|
+
const bridgeBottom = itemBounds.top;
|
|
497
|
+
|
|
498
|
+
if (bridgeBottom > bridgeTop) {
|
|
499
|
+
const overlap = 2;
|
|
500
|
+
const rayPadding = 10;
|
|
501
|
+
const eps = 0.1;
|
|
502
|
+
const delta = getBridgeDelta(itemBounds, overlap);
|
|
503
|
+
|
|
504
|
+
const toY = bridgeTop - rayPadding;
|
|
505
|
+
const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
|
|
506
|
+
const xLeft = itemBounds.left + inset;
|
|
507
|
+
const xRight = itemBounds.right - inset;
|
|
508
|
+
const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
|
|
509
|
+
const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
|
|
510
|
+
|
|
511
|
+
if (canBridge && bridgeBasePath) {
|
|
512
|
+
const leftHit = getExitHit({
|
|
513
|
+
mainShape: bridgeBasePath,
|
|
514
|
+
x: xLeft,
|
|
515
|
+
bridgeBottom,
|
|
516
|
+
toY,
|
|
517
|
+
eps,
|
|
518
|
+
delta,
|
|
519
|
+
overlap,
|
|
520
|
+
op: f.operation,
|
|
521
|
+
});
|
|
522
|
+
const rightHit = getExitHit({
|
|
523
|
+
mainShape: bridgeBasePath,
|
|
524
|
+
x: xRight,
|
|
525
|
+
bridgeBottom,
|
|
526
|
+
toY,
|
|
527
|
+
eps,
|
|
528
|
+
delta,
|
|
529
|
+
overlap,
|
|
530
|
+
op: f.operation,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
if (leftHit && rightHit) {
|
|
534
|
+
const pathLength = bridgeBasePath.length;
|
|
535
|
+
const leftOffset = leftHit.location.offset;
|
|
536
|
+
const rightOffset = rightHit.location.offset;
|
|
537
|
+
|
|
538
|
+
const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
|
|
539
|
+
const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
|
|
540
|
+
const countFor = (d: number) =>
|
|
541
|
+
Math.max(8, Math.min(80, Math.ceil(d / 6)));
|
|
542
|
+
|
|
543
|
+
const offsetsA = sampleWrappedOffsets(
|
|
544
|
+
pathLength,
|
|
545
|
+
leftOffset,
|
|
546
|
+
rightOffset,
|
|
547
|
+
countFor(distanceA),
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
const offsetsB = sampleWrappedOffsets(
|
|
551
|
+
pathLength,
|
|
552
|
+
rightOffset,
|
|
553
|
+
leftOffset,
|
|
554
|
+
countFor(distanceB),
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
const pointsA = offsetsA
|
|
558
|
+
.map((o) => bridgeBasePath.getPointAt(o))
|
|
559
|
+
.filter((p): p is paper.Point => Boolean(p));
|
|
560
|
+
const pointsB = offsetsB
|
|
561
|
+
.map((o) => bridgeBasePath.getPointAt(o))
|
|
562
|
+
.filter((p): p is paper.Point => Boolean(p));
|
|
563
|
+
|
|
564
|
+
if (pointsA.length >= 2 && pointsB.length >= 2) {
|
|
565
|
+
let topBase = selectOuterChain({
|
|
566
|
+
mainShape: bridgeBasePath,
|
|
567
|
+
pointsA,
|
|
568
|
+
pointsB,
|
|
569
|
+
delta,
|
|
570
|
+
overlap,
|
|
571
|
+
op: f.operation,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const dist2 = (a: paper.Point, b: paper.Point) => {
|
|
575
|
+
const dx = a.x - b.x;
|
|
576
|
+
const dy = a.y - b.y;
|
|
577
|
+
return dx * dx + dy * dy;
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
if (
|
|
581
|
+
dist2(topBase[0], leftHit.point) >
|
|
582
|
+
dist2(topBase[0], rightHit.point)
|
|
583
|
+
) {
|
|
584
|
+
topBase = topBase.slice().reverse();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
topBase = topBase.slice();
|
|
588
|
+
topBase[0] = leftHit.point;
|
|
589
|
+
topBase[topBase.length - 1] = rightHit.point;
|
|
590
|
+
|
|
591
|
+
const capShiftY =
|
|
592
|
+
f.operation === "subtract"
|
|
593
|
+
? -Math.max(overlap * 2, delta)
|
|
594
|
+
: overlap;
|
|
595
|
+
const topPoints = topBase.map((p) =>
|
|
596
|
+
p.add(new paper.Point(0, capShiftY)),
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
const bridgeBottomY = bridgeBottom + overlap * 2;
|
|
600
|
+
const bridgePoly = new paper.Path({ insert: false });
|
|
601
|
+
for (const p of topPoints) bridgePoly.add(p);
|
|
602
|
+
bridgePoly.add(new paper.Point(xRight, bridgeBottomY));
|
|
603
|
+
bridgePoly.add(new paper.Point(xLeft, bridgeBottomY));
|
|
604
|
+
bridgePoly.closed = true;
|
|
605
|
+
|
|
606
|
+
const unitedItem = item.unite(bridgePoly);
|
|
607
|
+
item.remove();
|
|
608
|
+
bridgePoly.remove();
|
|
609
|
+
|
|
610
|
+
if (f.operation === "add") {
|
|
611
|
+
adds.push(unitedItem);
|
|
612
|
+
} else {
|
|
613
|
+
subtracts.push(unitedItem);
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (f.operation === "add") {
|
|
621
|
+
adds.push(item);
|
|
622
|
+
} else {
|
|
623
|
+
subtracts.push(item);
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
if (f.operation === "add") {
|
|
627
|
+
adds.push(item);
|
|
628
|
+
} else {
|
|
629
|
+
subtracts.push(item);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
if (f.operation === "add") {
|
|
634
|
+
adds.push(item);
|
|
635
|
+
} else {
|
|
636
|
+
subtracts.push(item);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// 2. Process Additions (Union)
|
|
642
|
+
if (adds.length > 0) {
|
|
643
|
+
for (const item of adds) {
|
|
644
|
+
try {
|
|
645
|
+
const temp = mainShape.unite(item);
|
|
646
|
+
mainShape.remove();
|
|
647
|
+
item.remove();
|
|
648
|
+
mainShape = normalizePathItem(temp);
|
|
649
|
+
} catch (e) {
|
|
650
|
+
console.error("Geometry: Failed to unite feature", e);
|
|
651
|
+
item.remove();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// 3. Process Subtractions (Difference)
|
|
657
|
+
if (subtracts.length > 0) {
|
|
658
|
+
for (const item of subtracts) {
|
|
659
|
+
try {
|
|
660
|
+
const temp = mainShape.subtract(item);
|
|
661
|
+
mainShape.remove();
|
|
662
|
+
item.remove();
|
|
663
|
+
mainShape = normalizePathItem(temp);
|
|
664
|
+
} catch (e) {
|
|
665
|
+
console.error("Geometry: Failed to subtract feature", e);
|
|
666
|
+
item.remove();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return mainShape;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Applies Internal/Surface features to a shape.
|
|
677
|
+
*/
|
|
678
|
+
function applySurfaceFeatures(
|
|
679
|
+
shape: paper.PathItem,
|
|
680
|
+
features: DielineFeature[],
|
|
681
|
+
options: GeometryOptions,
|
|
682
|
+
): paper.PathItem {
|
|
683
|
+
const surfaceFeatures = features.filter(
|
|
684
|
+
(f) => f.renderBehavior === "surface",
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
if (surfaceFeatures.length === 0) return shape;
|
|
688
|
+
|
|
689
|
+
let result = shape;
|
|
690
|
+
|
|
691
|
+
// Internal features are usually subtractive (holes)
|
|
692
|
+
// But we support 'add' too (islands? maybe just unite)
|
|
693
|
+
|
|
694
|
+
for (const f of surfaceFeatures) {
|
|
695
|
+
const pos = resolveFeaturePosition(f, options);
|
|
696
|
+
const center = new paper.Point(pos.x, pos.y);
|
|
697
|
+
const item = createFeatureItem(f, center);
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
if (f.operation === "add") {
|
|
701
|
+
const temp = result.unite(item);
|
|
702
|
+
result.remove();
|
|
703
|
+
item.remove();
|
|
704
|
+
result = normalizePathItem(temp);
|
|
705
|
+
} else {
|
|
706
|
+
const temp = result.subtract(item);
|
|
707
|
+
result.remove();
|
|
708
|
+
item.remove();
|
|
709
|
+
result = normalizePathItem(temp);
|
|
710
|
+
}
|
|
711
|
+
} catch (e) {
|
|
712
|
+
console.error("Geometry: Failed to apply surface feature", e);
|
|
713
|
+
item.remove();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Generates the path data for the Dieline (Product Shape).
|
|
722
|
+
*/
|
|
723
|
+
export function generateDielinePath(options: GeometryOptions): string {
|
|
724
|
+
const paperWidth = options.canvasWidth || options.width * 2 || 2000;
|
|
725
|
+
const paperHeight = options.canvasHeight || options.height * 2 || 2000;
|
|
726
|
+
ensurePaper(paperWidth, paperHeight);
|
|
727
|
+
paper.project.activeLayer.removeChildren();
|
|
728
|
+
|
|
729
|
+
const perimeter = getPerimeterShape(options);
|
|
730
|
+
const finalShape = applySurfaceFeatures(perimeter, options.features, options);
|
|
731
|
+
|
|
732
|
+
const pathData = finalShape.pathData;
|
|
733
|
+
finalShape.remove();
|
|
734
|
+
|
|
735
|
+
return pathData;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Generates the path data for the Bleed Zone.
|
|
740
|
+
*/
|
|
741
|
+
export function generateBleedZonePath(
|
|
742
|
+
originalOptions: GeometryOptions,
|
|
743
|
+
offsetOptions: GeometryOptions,
|
|
744
|
+
offset: number,
|
|
745
|
+
): string {
|
|
746
|
+
const paperWidth =
|
|
747
|
+
originalOptions.canvasWidth || originalOptions.width * 2 || 2000;
|
|
748
|
+
const paperHeight =
|
|
749
|
+
originalOptions.canvasHeight || originalOptions.height * 2 || 2000;
|
|
750
|
+
ensurePaper(paperWidth, paperHeight);
|
|
751
|
+
paper.project.activeLayer.removeChildren();
|
|
752
|
+
|
|
753
|
+
// 1. Generate Original Shape
|
|
754
|
+
const pOriginal = getPerimeterShape(originalOptions);
|
|
755
|
+
const shapeOriginal = applySurfaceFeatures(
|
|
756
|
+
pOriginal,
|
|
757
|
+
originalOptions.features,
|
|
758
|
+
originalOptions,
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
// 2. Generate Offset Shape
|
|
762
|
+
const pOffset = getPerimeterShape(offsetOptions);
|
|
763
|
+
const shapeOffset = applySurfaceFeatures(
|
|
764
|
+
pOffset,
|
|
765
|
+
offsetOptions.features,
|
|
766
|
+
offsetOptions,
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
// 3. Calculate Difference
|
|
770
|
+
let bleedZone: paper.PathItem;
|
|
771
|
+
if (offset > 0) {
|
|
772
|
+
bleedZone = shapeOffset.subtract(shapeOriginal);
|
|
773
|
+
} else {
|
|
774
|
+
bleedZone = shapeOriginal.subtract(shapeOffset);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const pathData = bleedZone.pathData;
|
|
778
|
+
|
|
779
|
+
shapeOriginal.remove();
|
|
780
|
+
shapeOffset.remove();
|
|
781
|
+
bleedZone.remove();
|
|
782
|
+
|
|
783
|
+
return pathData;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Finds the lowest point (Max Y) on the Dieline geometry (Base Shape ONLY).
|
|
788
|
+
*/
|
|
789
|
+
export function getLowestPointOnDieline(
|
|
790
|
+
options: GeometryOptions,
|
|
791
|
+
): { x: number; y: number } {
|
|
792
|
+
ensurePaper(options.width * 2, options.height * 2);
|
|
793
|
+
paper.project.activeLayer.removeChildren();
|
|
794
|
+
|
|
795
|
+
const shape = createBaseShape(options);
|
|
796
|
+
const bounds = shape.bounds;
|
|
797
|
+
|
|
798
|
+
const result = {
|
|
799
|
+
x: bounds.center.x,
|
|
800
|
+
y: bounds.bottom,
|
|
801
|
+
};
|
|
802
|
+
shape.remove();
|
|
803
|
+
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Finds the nearest point on the Dieline geometry (Base Shape ONLY) for a given target point.
|
|
809
|
+
* Used for constraining feature movement.
|
|
810
|
+
*/
|
|
811
|
+
export function getNearestPointOnDieline(
|
|
812
|
+
point: { x: number; y: number },
|
|
813
|
+
options: GeometryOptions,
|
|
814
|
+
): { x: number; y: number; normal?: { x: number; y: number } } {
|
|
815
|
+
ensurePaper(options.width * 2, options.height * 2);
|
|
816
|
+
paper.project.activeLayer.removeChildren();
|
|
817
|
+
|
|
818
|
+
// We constrain to the BASE shape, not including other features,
|
|
819
|
+
// because usually you want to snap to the main edge.
|
|
820
|
+
const shape = createBaseShape(options);
|
|
821
|
+
|
|
822
|
+
const p = new paper.Point(point.x, point.y);
|
|
823
|
+
const location = shape.getNearestLocation(p);
|
|
824
|
+
|
|
825
|
+
const result = {
|
|
826
|
+
x: location.point.x,
|
|
827
|
+
y: location.point.y,
|
|
828
|
+
normal: location.normal ? { x: location.normal.x, y: location.normal.y } : undefined
|
|
829
|
+
};
|
|
830
|
+
shape.remove();
|
|
831
|
+
|
|
832
|
+
return result;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export function getPathBounds(pathData: string): {
|
|
836
|
+
x: number;
|
|
837
|
+
y: number;
|
|
838
|
+
width: number;
|
|
839
|
+
height: number;
|
|
840
|
+
} {
|
|
841
|
+
const path = new paper.Path();
|
|
842
|
+
path.pathData = pathData;
|
|
843
|
+
const bounds = path.bounds;
|
|
844
|
+
path.remove();
|
|
845
|
+
return {
|
|
846
|
+
x: bounds.x,
|
|
847
|
+
y: bounds.y,
|
|
848
|
+
width: bounds.width,
|
|
849
|
+
height: bounds.height,
|
|
850
|
+
};
|
|
851
|
+
}
|