@pooder/kit 3.4.0 → 4.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/src/geometry.ts CHANGED
@@ -1,98 +1,23 @@
1
1
  import paper from "paper";
2
2
 
3
- export type PositionAnchor =
4
- | "top-left"
5
- | "top-center"
6
- | "top-right"
7
- | "center-left"
8
- | "center"
9
- | "center-right"
10
- | "bottom-left"
11
- | "bottom-center"
12
- | "bottom-right";
13
-
14
- export interface HoleData {
15
- x?: number;
16
- y?: number;
17
- shape?: "circle" | "square";
18
- anchor?: PositionAnchor;
19
- offsetX?: number;
20
- offsetY?: number;
21
- innerRadius: number;
22
- outerRadius: number;
23
- }
24
-
25
- export function resolveHolePosition(
26
- hole: HoleData,
27
- geometry: { x: number; y: number; width: number; height: number },
28
- canvasSize: { width: number; height: number },
29
- ): { x: number; y: number } {
30
- if (hole.anchor) {
31
- const { x, y, width, height } = geometry;
32
- let bx = x; // center x
33
- let by = y; // center y
34
-
35
- // Calculate anchor base position based on shape bounds
36
- // Note: geometry.x/y is the CENTER of the shape
37
- const left = x - width / 2;
38
- const right = x + width / 2;
39
- const top = y - height / 2;
40
- const bottom = y + height / 2;
41
-
42
- switch (hole.anchor) {
43
- case "top-left":
44
- bx = left;
45
- by = top;
46
- break;
47
- case "top-center":
48
- bx = x;
49
- by = top;
50
- break;
51
- case "top-right":
52
- bx = right;
53
- by = top;
54
- break;
55
- case "center-left":
56
- bx = left;
57
- by = y;
58
- break;
59
- case "center":
60
- bx = x;
61
- by = y;
62
- break;
63
- case "center-right":
64
- bx = right;
65
- by = y;
66
- break;
67
- case "bottom-left":
68
- bx = left;
69
- by = bottom;
70
- break;
71
- case "bottom-center":
72
- bx = x;
73
- by = bottom;
74
- break;
75
- case "bottom-right":
76
- bx = right;
77
- by = bottom;
78
- break;
79
- }
80
-
81
- return {
82
- x: bx + (hole.offsetX || 0),
83
- y: by + (hole.offsetY || 0),
84
- };
85
- } else if (hole.x !== undefined && hole.y !== undefined) {
86
- // Legacy / Direct coordinates (Normalized relative to Dieline Geometry)
87
- // Formula: absolute = normalized * width + (center - width/2)
88
- // This handles padding correctly.
89
- const { x, width, y, height } = geometry;
90
- return {
91
- x: hole.x * width + (x - width / 2) + (hole.offsetX || 0),
92
- y: hole.y * height + (y - height / 2) + (hole.offsetY || 0),
93
- };
94
- }
95
- return { x: 0, y: 0 };
3
+ export type FeatureOperation = "add" | "subtract";
4
+ export type FeatureShape = "rect" | "circle";
5
+
6
+ export interface DielineFeature {
7
+ id: string;
8
+ groupId?: string;
9
+ operation: FeatureOperation;
10
+ shape: FeatureShape;
11
+ x: number;
12
+ y: number;
13
+ width?: number;
14
+ height?: number;
15
+ radius?: number;
16
+ rotation?: number;
17
+ placement?: "edge" | "internal";
18
+ color?: string;
19
+ strokeDash?: number[];
20
+ skipCut?: boolean;
96
21
  }
97
22
 
98
23
  export interface GeometryOptions {
@@ -102,7 +27,7 @@ export interface GeometryOptions {
102
27
  radius: number;
103
28
  x: number;
104
29
  y: number;
105
- holes: Array<HoleData>;
30
+ features: Array<DielineFeature>;
106
31
  pathData?: string;
107
32
  canvasWidth?: number;
108
33
  canvasHeight?: number;
@@ -113,6 +38,24 @@ export interface MaskGeometryOptions extends GeometryOptions {
113
38
  canvasHeight: number;
114
39
  }
115
40
 
41
+ /**
42
+ * Resolves the absolute position of a feature based on normalized coordinates.
43
+ */
44
+ export function resolveFeaturePosition(
45
+ feature: DielineFeature,
46
+ geometry: { x: number; y: number; width: number; height: number },
47
+ ): { x: number; y: number } {
48
+ const { x, y, width, height } = geometry;
49
+ // geometry.x/y is the Center.
50
+ const left = x - width / 2;
51
+ const top = y - height / 2;
52
+
53
+ return {
54
+ x: left + feature.x * width,
55
+ y: top + feature.y * height,
56
+ };
57
+ }
58
+
116
59
  /**
117
60
  * Initializes paper.js project if not already initialized.
118
61
  */
@@ -153,9 +96,6 @@ function createBaseShape(options: GeometryOptions): paper.PathItem {
153
96
  path.pathData = pathData;
154
97
  // Align center
155
98
  path.position = center;
156
- // Scale to match width/height if needed?
157
- // For now, assume pathData is correct size, but we might want to support resizing.
158
- // If width/height are provided and different from bounds, we could scale.
159
99
  if (
160
100
  width > 0 &&
161
101
  height > 0 &&
@@ -166,7 +106,6 @@ function createBaseShape(options: GeometryOptions): paper.PathItem {
166
106
  }
167
107
  return path;
168
108
  } else {
169
- // Fallback
170
109
  return new paper.Path.Rectangle({
171
110
  point: [x - width / 2, y - height / 2],
172
111
  size: [Math.max(0, width), Math.max(0, height)],
@@ -175,225 +114,148 @@ function createBaseShape(options: GeometryOptions): paper.PathItem {
175
114
  }
176
115
 
177
116
  /**
178
- * Creates an offset version of the base shape.
179
- * For Rect/Circle, we can just adjust params.
180
- * For Custom shapes, we need a true offset algorithm (Paper.js doesn't have a robust one built-in for all cases,
181
- * but we can simulate it or use a simple scaling if offset is small, OR rely on a library like Clipper.js.
182
- * However, since we want to avoid heavy deps, let's try a simple approach:
183
- * If it's a simple shape, we re-create it.
184
- * If it's custom, we unfortunately have to scale it for now as a poor-man's offset,
185
- * UNLESS we implement a stroke expansion.
186
- *
187
- * Stroke Expansion Trick:
188
- * 1. Create path
189
- * 2. Set strokeWidth = offset * 2
190
- * 3. Convert stroke to path (paper.js has path.expand())
191
- * 4. Union original + expanded (for positive offset) or Subtract (for negative).
117
+ * Creates a Paper.js Item for a single feature.
192
118
  */
193
- function createOffsetShape(
194
- options: GeometryOptions,
195
- offset: number,
119
+ function createFeatureItem(
120
+ feature: DielineFeature,
121
+ center: paper.Point,
196
122
  ): paper.PathItem {
197
- const { shape, width, height, radius, x, y, pathData } = options;
198
- const center = new paper.Point(x, y);
199
-
200
- if (shape === "rect" || shape === "circle" || shape === "ellipse") {
201
- // For standard shapes, we can just adjust the dimensions
202
- const offsetOptions = {
203
- ...options,
204
- width: Math.max(0, width + offset * 2),
205
- height: Math.max(0, height + offset * 2),
206
- radius: radius === 0 ? 0 : Math.max(0, radius + offset),
207
- };
208
- return createBaseShape(offsetOptions);
209
- } else if (shape === "custom" && pathData) {
210
- const original = createBaseShape(options);
211
- if (offset === 0) return original;
212
-
213
- // Use Stroke Expansion for Offset
214
- // Create a copy for stroking
215
- const stroker = original.clone() as paper.Path;
216
- stroker.strokeColor = new paper.Color("black");
217
- stroker.strokeWidth = Math.abs(offset) * 2;
218
- // Round join usually looks better for offsets
219
- stroker.strokeJoin = "round";
220
- stroker.strokeCap = "round";
221
-
222
- // Expand stroke to path
223
- // @ts-ignore - paper.js types might be missing expand depending on version, but it exists in recent versions
224
- // If expand is not available, we might fallback to scaling.
225
- // Assuming modern paper.js
226
- let expanded: paper.Item;
227
- try {
228
- // @ts-ignore
229
- expanded = stroker.expand({ stroke: true, fill: false, insert: false });
230
- } catch (e) {
231
- // Fallback if expand fails or not present
232
- stroker.remove();
233
- // Fallback to scaling (imperfect)
234
- const scaleX =
235
- (original.bounds.width + offset * 2) / original.bounds.width;
236
- const scaleY =
237
- (original.bounds.height + offset * 2) / original.bounds.height;
238
- original.scale(scaleX, scaleY);
239
- return original;
240
- }
241
-
242
- stroker.remove();
243
-
244
- // The expanded stroke is a "ring".
245
- // For positive offset: Union(Original, Ring)
246
- // For negative offset: Subtract(Original, Ring) ? No, that makes a hole.
247
- // For negative offset: We want the "inner" boundary of the ring.
248
-
249
- // Actually, expand() returns a Group or Path.
250
- // If it's a closed path, the ring has an outer and inner boundary.
251
-
252
- let result: paper.PathItem;
253
-
254
- if (offset > 0) {
255
- // @ts-ignore
256
- result = original.unite(expanded);
257
- } else {
258
- // For negative offset (shrink), we want the original MINUS the stroke?
259
- // No, the stroke is centered on the line.
260
- // So the inner edge of the stroke is at -offset.
261
- // We want the area INSIDE the inner edge.
262
- // That is Original SUBTRACT the Ring?
263
- // Yes, if we subtract the ring, we lose the border area.
264
- // @ts-ignore
265
- result = original.subtract(expanded);
266
- }
267
-
268
- // Cleanup
269
- original.remove();
270
- expanded.remove();
123
+ let item: paper.PathItem;
124
+
125
+ if (feature.shape === "rect") {
126
+ const w = feature.width || 10;
127
+ const h = feature.height || 10;
128
+ const r = feature.radius || 0;
129
+ item = new paper.Path.Rectangle({
130
+ point: [center.x - w / 2, center.y - h / 2],
131
+ size: [w, h],
132
+ radius: r,
133
+ });
134
+ } else {
135
+ // Circle
136
+ const r = feature.radius || 5;
137
+ item = new paper.Path.Circle({
138
+ center: center,
139
+ radius: r,
140
+ });
141
+ }
271
142
 
272
- return result;
143
+ if (feature.rotation) {
144
+ item.rotate(feature.rotation, center);
273
145
  }
274
146
 
275
- return createBaseShape(options);
147
+ return item;
276
148
  }
277
149
 
278
150
  /**
279
- * Internal helper to generate the Dieline Shape (Paper Item).
280
- * Caller is responsible for cleanup.
151
+ * Internal helper to generate the Perimeter Shape (Base + Edge Features).
281
152
  */
282
- function getDielineShape(options: GeometryOptions): paper.PathItem {
153
+ function getPerimeterShape(options: GeometryOptions): paper.PathItem {
283
154
  // 1. Create Base Shape
284
155
  let mainShape = createBaseShape(options);
285
156
 
286
- const { holes } = options;
287
-
288
- if (holes && holes.length > 0) {
289
- let lugsPath: paper.PathItem | null = null;
290
- let cutsPath: paper.PathItem | null = null;
291
-
292
- holes.forEach((hole) => {
293
- const center = new paper.Point(hole.x!, hole.y!);
294
-
295
- // Create Lug (Outer Radius)
296
- const lug =
297
- hole.shape === "square"
298
- ? new paper.Path.Rectangle({
299
- point: [
300
- center.x - hole.outerRadius,
301
- center.y - hole.outerRadius,
302
- ],
303
- size: [hole.outerRadius * 2, hole.outerRadius * 2],
304
- })
305
- : new paper.Path.Circle({
306
- center: center,
307
- radius: hole.outerRadius,
308
- });
309
-
310
- // REMOVED: Intersects check. We want to process all holes defined in config.
311
- // If a hole is completely outside, it might form an island, but that's better than missing it.
312
- // Users can remove the hole if they don't want it.
313
-
314
- // Create Cut (Inner Radius)
315
- const cut =
316
- hole.shape === "square"
317
- ? new paper.Path.Rectangle({
318
- point: [
319
- center.x - hole.innerRadius,
320
- center.y - hole.innerRadius,
321
- ],
322
- size: [hole.innerRadius * 2, hole.innerRadius * 2],
323
- })
324
- : new paper.Path.Circle({
325
- center: center,
326
- radius: hole.innerRadius,
327
- });
328
-
329
- // Union Lugs
330
- if (!lugsPath) {
331
- lugsPath = lug;
157
+ const { features } = options;
158
+
159
+ if (features && features.length > 0) {
160
+ // Filter for Edge Features (Default or explicit 'edge')
161
+ const edgeFeatures = features.filter(
162
+ (f) => !f.placement || f.placement === "edge",
163
+ );
164
+
165
+ const adds: paper.PathItem[] = [];
166
+ const subtracts: paper.PathItem[] = [];
167
+
168
+ edgeFeatures.forEach((f) => {
169
+ const pos = resolveFeaturePosition(f, options);
170
+ const center = new paper.Point(pos.x, pos.y);
171
+ const item = createFeatureItem(f, center);
172
+
173
+ if (f.operation === "add") {
174
+ adds.push(item);
332
175
  } else {
176
+ subtracts.push(item);
177
+ }
178
+ });
179
+
180
+ // 2. Process Additions (Union)
181
+ if (adds.length > 0) {
182
+ for (const item of adds) {
333
183
  try {
334
- const temp = lugsPath.unite(lug);
335
- lugsPath.remove();
336
- lug.remove();
337
- lugsPath = temp;
184
+ const temp = mainShape.unite(item);
185
+ mainShape.remove();
186
+ item.remove();
187
+ mainShape = temp;
338
188
  } catch (e) {
339
- console.error("Geometry: Failed to unite lug", e);
340
- // Keep previous lugsPath, ignore this one to prevent crash
341
- lug.remove();
189
+ console.error("Geometry: Failed to unite feature", e);
190
+ item.remove();
342
191
  }
343
192
  }
193
+ }
344
194
 
345
- // Union Cuts
346
- if (!cutsPath) {
347
- cutsPath = cut;
348
- } else {
195
+ // 3. Process Subtractions (Difference)
196
+ if (subtracts.length > 0) {
197
+ for (const item of subtracts) {
349
198
  try {
350
- const temp = cutsPath.unite(cut);
351
- cutsPath.remove();
352
- cut.remove();
353
- cutsPath = temp;
199
+ const temp = mainShape.subtract(item);
200
+ mainShape.remove();
201
+ item.remove();
202
+ mainShape = temp;
354
203
  } catch (e) {
355
- console.error("Geometry: Failed to unite cut", e);
356
- cut.remove();
204
+ console.error("Geometry: Failed to subtract feature", e);
205
+ item.remove();
357
206
  }
358
207
  }
359
- });
360
-
361
- // 2. Add Lugs to Main Shape (Union) - Additive Fusion
362
- if (lugsPath) {
363
- try {
364
- const temp = mainShape.unite(lugsPath);
365
- mainShape.remove();
366
- // @ts-ignore
367
- lugsPath.remove();
368
- mainShape = temp;
369
- } catch (e) {
370
- console.error("Geometry: Failed to unite lugsPath to mainShape", e);
371
- }
372
208
  }
209
+ }
210
+
211
+ return mainShape;
212
+ }
213
+
214
+ /**
215
+ * Applies Internal/Surface features to a shape.
216
+ */
217
+ function applySurfaceFeatures(
218
+ shape: paper.PathItem,
219
+ features: DielineFeature[],
220
+ options: GeometryOptions,
221
+ ): paper.PathItem {
222
+ const internalFeatures = features.filter((f) => f.placement === "internal");
223
+
224
+ if (internalFeatures.length === 0) return shape;
225
+
226
+ let result = shape;
227
+
228
+ // Internal features are usually subtractive (holes)
229
+ // But we support 'add' too (islands? maybe just unite)
230
+
231
+ for (const f of internalFeatures) {
232
+ const pos = resolveFeaturePosition(f, options);
233
+ const center = new paper.Point(pos.x, pos.y);
234
+ const item = createFeatureItem(f, center);
373
235
 
374
- // 3. Subtract Cuts from Main Shape (Difference)
375
- if (cutsPath) {
376
- try {
377
- const temp = mainShape.subtract(cutsPath);
378
- mainShape.remove();
379
- // @ts-ignore
380
- cutsPath.remove();
381
- mainShape = temp;
382
- } catch (e) {
383
- console.error(
384
- "Geometry: Failed to subtract cutsPath from mainShape",
385
- e,
386
- );
236
+ try {
237
+ if (f.operation === "add") {
238
+ const temp = result.unite(item);
239
+ result.remove();
240
+ item.remove();
241
+ result = temp;
242
+ } else {
243
+ const temp = result.subtract(item);
244
+ result.remove();
245
+ item.remove();
246
+ result = temp;
387
247
  }
248
+ } catch (e) {
249
+ console.error("Geometry: Failed to apply surface feature", e);
250
+ item.remove();
388
251
  }
389
252
  }
390
-
391
- return mainShape;
253
+
254
+ return result;
392
255
  }
393
256
 
394
257
  /**
395
258
  * Generates the path data for the Dieline (Product Shape).
396
- * Logic: (BaseShape UNION IntersectingLugs) SUBTRACT Cuts
397
259
  */
398
260
  export function generateDielinePath(options: GeometryOptions): string {
399
261
  const paperWidth = options.canvasWidth || options.width * 2 || 2000;
@@ -401,10 +263,11 @@ export function generateDielinePath(options: GeometryOptions): string {
401
263
  ensurePaper(paperWidth, paperHeight);
402
264
  paper.project.activeLayer.removeChildren();
403
265
 
404
- const mainShape = getDielineShape(options);
266
+ const perimeter = getPerimeterShape(options);
267
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
405
268
 
406
- const pathData = mainShape.pathData;
407
- mainShape.remove();
269
+ const pathData = finalShape.pathData;
270
+ finalShape.remove();
408
271
 
409
272
  return pathData;
410
273
  }
@@ -419,16 +282,14 @@ export function generateMaskPath(options: MaskGeometryOptions): string {
419
282
 
420
283
  const { canvasWidth, canvasHeight } = options;
421
284
 
422
- // 1. Canvas Background
423
285
  const maskRect = new paper.Path.Rectangle({
424
286
  point: [0, 0],
425
287
  size: [canvasWidth, canvasHeight],
426
288
  });
427
289
 
428
- // 2. Re-create Product Shape
429
- const mainShape = getDielineShape(options);
290
+ const perimeter = getPerimeterShape(options);
291
+ const mainShape = applySurfaceFeatures(perimeter, options.features, options);
430
292
 
431
- // 3. Subtract Product from Mask
432
293
  const finalMask = maskRect.subtract(mainShape);
433
294
 
434
295
  maskRect.remove();
@@ -441,92 +302,27 @@ export function generateMaskPath(options: MaskGeometryOptions): string {
441
302
  }
442
303
 
443
304
  /**
444
- * Generates the path data for the Bleed Zone (Area between Original and Offset).
305
+ * Generates the path data for the Bleed Zone.
445
306
  */
446
307
  export function generateBleedZonePath(
447
- options: GeometryOptions,
308
+ originalOptions: GeometryOptions,
309
+ offsetOptions: GeometryOptions,
448
310
  offset: number,
449
311
  ): string {
450
- // Ensure canvas is large enough
451
- const paperWidth = options.canvasWidth || options.width * 2 || 2000;
452
- const paperHeight = options.canvasHeight || options.height * 2 || 2000;
312
+ const paperWidth =
313
+ originalOptions.canvasWidth || originalOptions.width * 2 || 2000;
314
+ const paperHeight =
315
+ originalOptions.canvasHeight || originalOptions.height * 2 || 2000;
453
316
  ensurePaper(paperWidth, paperHeight);
454
317
  paper.project.activeLayer.removeChildren();
455
318
 
456
- // 1. Original Shape
457
- const shapeOriginal = getDielineShape(options);
458
-
459
- // 2. Offset Shape
460
- // We use createOffsetShape for more accurate offset (especially for custom shapes)
461
- // But we still need to respect holes if they exist.
462
- // getDielineShape handles holes.
463
- // The issue is: do holes shrink/expand with bleed?
464
- // Usually, bleed is only for the outer cut. Holes are internal cuts.
465
- // Internal cuts usually also have bleed if they are die-cut, but maybe different direction?
466
- // For simplicity, let's assume we offset the FINAL shape (including holes).
467
-
468
- // Actually, getDielineShape calls createBaseShape.
469
- // Let's modify generateBleedZonePath to use createOffsetShape logic if possible,
470
- // OR just perform offset on the final shape result.
471
-
472
- // The previous logic was: create base shape with adjusted width/height/radius.
473
- // This works for Rect/Circle.
474
- // For Custom, we need createOffsetShape.
475
-
476
- let shapeOffset: paper.PathItem;
477
-
478
- if (options.shape === "custom") {
479
- // For custom shape, we offset the base shape first, then apply holes?
480
- // Or offset the final result?
481
- // Bleed is usually "outside" the cut line.
482
- // If we have a donut, bleed is outside the outer circle AND inside the inner circle?
483
- // Or just outside the outer?
484
- // Let's assume bleed expands the solid area.
485
-
486
- // So we take the final shape (Original) and expand it.
487
- // We can use the same Stroke Expansion trick on the final shape.
488
-
489
- // Since shapeOriginal is already the final shape (Base - Holes),
490
- // we can try to offset it directly.
491
-
492
- const stroker = shapeOriginal.clone() as paper.Path;
493
- stroker.strokeColor = new paper.Color("black");
494
- stroker.strokeWidth = Math.abs(offset) * 2;
495
- stroker.strokeJoin = "round";
496
- stroker.strokeCap = "round";
497
-
498
- let expanded: paper.Item;
499
- try {
500
- // @ts-ignore
501
- expanded = stroker.expand({ stroke: true, fill: false, insert: false });
502
- } catch (e) {
503
- // Fallback
504
- stroker.remove();
505
- shapeOffset = shapeOriginal.clone();
506
- // scaling fallback...
507
- return shapeOffset.pathData; // Fail gracefully
508
- }
509
- stroker.remove();
510
-
511
- if (offset > 0) {
512
- // @ts-ignore
513
- shapeOffset = shapeOriginal.unite(expanded);
514
- } else {
515
- // @ts-ignore
516
- shapeOffset = shapeOriginal.subtract(expanded);
517
- }
518
- expanded.remove();
519
- } else {
520
- // Legacy logic for standard shapes (still valid and fast)
521
- // Adjust dimensions for offset
522
- const offsetOptions: GeometryOptions = {
523
- ...options,
524
- width: Math.max(0, options.width + offset * 2),
525
- height: Math.max(0, options.height + offset * 2),
526
- radius: options.radius === 0 ? 0 : Math.max(0, options.radius + offset),
527
- };
528
- shapeOffset = getDielineShape(offsetOptions);
529
- }
319
+ // 1. Generate Original Shape
320
+ const pOriginal = getPerimeterShape(originalOptions);
321
+ const shapeOriginal = applySurfaceFeatures(pOriginal, originalOptions.features, originalOptions);
322
+
323
+ // 2. Generate Offset Shape
324
+ const pOffset = getPerimeterShape(offsetOptions);
325
+ const shapeOffset = applySurfaceFeatures(pOffset, offsetOptions.features, offsetOptions);
530
326
 
531
327
  // 3. Calculate Difference
532
328
  let bleedZone: paper.PathItem;
@@ -538,7 +334,6 @@ export function generateBleedZonePath(
538
334
 
539
335
  const pathData = bleedZone.pathData;
540
336
 
541
- // Cleanup
542
337
  shapeOriginal.remove();
543
338
  shapeOffset.remove();
544
339
  bleedZone.remove();
@@ -547,8 +342,8 @@ export function generateBleedZonePath(
547
342
  }
548
343
 
549
344
  /**
550
- * Finds the nearest point on the Dieline geometry for a given target point.
551
- * Used for constraining hole movement.
345
+ * Finds the nearest point on the Dieline geometry (Base Shape ONLY) for a given target point.
346
+ * Used for constraining feature movement.
552
347
  */
553
348
  export function getNearestPointOnDieline(
554
349
  point: { x: number; y: number },
@@ -557,6 +352,8 @@ export function getNearestPointOnDieline(
557
352
  ensurePaper(options.width * 2, options.height * 2);
558
353
  paper.project.activeLayer.removeChildren();
559
354
 
355
+ // We constrain to the BASE shape, not including other features,
356
+ // because usually you want to snap to the main edge.
560
357
  const shape = createBaseShape(options);
561
358
 
562
359
  const p = new paper.Point(point.x, point.y);