@pooder/kit 4.3.0 → 4.3.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @pooder/kit
2
2
 
3
+ ## 4.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - bugfix
8
+
3
9
  ## 4.3.0
4
10
 
5
11
  ### Minor Changes
package/dist/index.js CHANGED
@@ -837,47 +837,43 @@ function getPerimeterShape(options) {
837
837
  const bridgeTop = mainBounds.top;
838
838
  const bridgeBottom = itemBounds.top;
839
839
  if (bridgeBottom > bridgeTop) {
840
- const startY = bridgeBottom + 1;
841
- const bridgeRect = new import_paper2.default.Path.Rectangle({
842
- from: [itemBounds.left, bridgeTop],
843
- to: [itemBounds.right, startY],
840
+ const centerX = itemBounds.center.x;
841
+ const ray = new import_paper2.default.Path.Line({
842
+ from: [centerX, bridgeBottom],
843
+ to: [centerX, bridgeTop - 10],
844
+ // Extend slightly past top to ensure intersection
844
845
  insert: false
845
846
  });
846
- const gaps = bridgeRect.subtract(mainShape);
847
- bridgeRect.remove();
848
- let bridgePart = null;
849
- const isBottomPart = (part) => {
850
- return Math.abs(part.bounds.bottom - startY) < 2;
851
- };
852
- if (gaps instanceof import_paper2.default.CompoundPath) {
853
- const children = gaps.children;
854
- let maxBottom = -Infinity;
855
- let bestChild = null;
856
- for (const child of children) {
857
- if (child.bounds.bottom > maxBottom) {
858
- maxBottom = child.bounds.bottom;
859
- bestChild = child;
860
- }
861
- }
862
- if (bestChild && isBottomPart(bestChild)) {
863
- bridgePart = bestChild.clone();
864
- }
865
- } else if (gaps instanceof import_paper2.default.Path) {
866
- if (isBottomPart(gaps)) {
867
- bridgePart = gaps.clone();
847
+ const intersections = mainShape.getIntersections(ray);
848
+ let targetY = bridgeTop;
849
+ let found = false;
850
+ if (intersections && intersections.length > 0) {
851
+ const validHits = intersections.filter((i) => i.point.y < bridgeBottom - 0.1);
852
+ if (validHits.length > 0) {
853
+ validHits.sort((a, b) => b.point.y - a.point.y);
854
+ targetY = validHits[0].point.y;
855
+ found = true;
868
856
  }
869
857
  }
870
- gaps.remove();
871
- if (bridgePart) {
872
- const bounds = bridgePart.bounds;
873
- if (bounds.height > 0) {
874
- const overlap = 1;
875
- const scaleY = (bounds.height + overlap) / bounds.height;
876
- bridgePart.scale(1, scaleY, new import_paper2.default.Point(bounds.center.x, bounds.bottom));
858
+ ray.remove();
859
+ const overlap = 2;
860
+ const rectBottom = bridgeBottom;
861
+ let rectTop = found ? targetY + overlap : bridgeTop;
862
+ if (!found) {
863
+ if (mainBounds.bottom < bridgeBottom) {
864
+ targetY = mainBounds.bottom;
865
+ rectTop = targetY - overlap;
877
866
  }
878
- const unitedItem = item.unite(bridgePart);
867
+ }
868
+ if (rectTop < rectBottom) {
869
+ const bridgeRect = new import_paper2.default.Path.Rectangle({
870
+ from: [itemBounds.left, rectTop],
871
+ to: [itemBounds.right, rectBottom],
872
+ insert: false
873
+ });
874
+ const unitedItem = item.unite(bridgeRect);
879
875
  item.remove();
880
- bridgePart.remove();
876
+ bridgeRect.remove();
881
877
  if (f.operation === "add") {
882
878
  adds.push(unitedItem);
883
879
  } else {
@@ -1435,7 +1431,19 @@ var DielineTool = class {
1435
1431
  title: "Detect Edge from Image",
1436
1432
  handler: async (imageUrl, options) => {
1437
1433
  try {
1438
- const pathData = await ImageTracer.trace(imageUrl, options);
1434
+ const loadImage = (url) => {
1435
+ return new Promise((resolve, reject) => {
1436
+ const img2 = new Image();
1437
+ img2.crossOrigin = "Anonymous";
1438
+ img2.onload = () => resolve(img2);
1439
+ img2.onerror = (e) => reject(e);
1440
+ img2.src = url;
1441
+ });
1442
+ };
1443
+ const [img, pathData] = await Promise.all([
1444
+ loadImage(imageUrl),
1445
+ ImageTracer.trace(imageUrl, options)
1446
+ ]);
1439
1447
  const bounds = getPathBounds(pathData);
1440
1448
  const currentMax = Math.max(s.width, s.height);
1441
1449
  const scale = currentMax / Math.max(bounds.width, bounds.height);
@@ -1444,7 +1452,10 @@ var DielineTool = class {
1444
1452
  return {
1445
1453
  pathData,
1446
1454
  width: newWidth,
1447
- height: newHeight
1455
+ height: newHeight,
1456
+ rawBounds: bounds,
1457
+ imageWidth: img.width,
1458
+ imageHeight: img.height
1448
1459
  };
1449
1460
  } catch (e) {
1450
1461
  console.error("Edge detection failed", e);
package/dist/index.mjs CHANGED
@@ -795,47 +795,43 @@ function getPerimeterShape(options) {
795
795
  const bridgeTop = mainBounds.top;
796
796
  const bridgeBottom = itemBounds.top;
797
797
  if (bridgeBottom > bridgeTop) {
798
- const startY = bridgeBottom + 1;
799
- const bridgeRect = new paper2.Path.Rectangle({
800
- from: [itemBounds.left, bridgeTop],
801
- to: [itemBounds.right, startY],
798
+ const centerX = itemBounds.center.x;
799
+ const ray = new paper2.Path.Line({
800
+ from: [centerX, bridgeBottom],
801
+ to: [centerX, bridgeTop - 10],
802
+ // Extend slightly past top to ensure intersection
802
803
  insert: false
803
804
  });
804
- const gaps = bridgeRect.subtract(mainShape);
805
- bridgeRect.remove();
806
- let bridgePart = null;
807
- const isBottomPart = (part) => {
808
- return Math.abs(part.bounds.bottom - startY) < 2;
809
- };
810
- if (gaps instanceof paper2.CompoundPath) {
811
- const children = gaps.children;
812
- let maxBottom = -Infinity;
813
- let bestChild = null;
814
- for (const child of children) {
815
- if (child.bounds.bottom > maxBottom) {
816
- maxBottom = child.bounds.bottom;
817
- bestChild = child;
818
- }
819
- }
820
- if (bestChild && isBottomPart(bestChild)) {
821
- bridgePart = bestChild.clone();
822
- }
823
- } else if (gaps instanceof paper2.Path) {
824
- if (isBottomPart(gaps)) {
825
- bridgePart = gaps.clone();
805
+ const intersections = mainShape.getIntersections(ray);
806
+ let targetY = bridgeTop;
807
+ let found = false;
808
+ if (intersections && intersections.length > 0) {
809
+ const validHits = intersections.filter((i) => i.point.y < bridgeBottom - 0.1);
810
+ if (validHits.length > 0) {
811
+ validHits.sort((a, b) => b.point.y - a.point.y);
812
+ targetY = validHits[0].point.y;
813
+ found = true;
826
814
  }
827
815
  }
828
- gaps.remove();
829
- if (bridgePart) {
830
- const bounds = bridgePart.bounds;
831
- if (bounds.height > 0) {
832
- const overlap = 1;
833
- const scaleY = (bounds.height + overlap) / bounds.height;
834
- bridgePart.scale(1, scaleY, new paper2.Point(bounds.center.x, bounds.bottom));
816
+ ray.remove();
817
+ const overlap = 2;
818
+ const rectBottom = bridgeBottom;
819
+ let rectTop = found ? targetY + overlap : bridgeTop;
820
+ if (!found) {
821
+ if (mainBounds.bottom < bridgeBottom) {
822
+ targetY = mainBounds.bottom;
823
+ rectTop = targetY - overlap;
835
824
  }
836
- const unitedItem = item.unite(bridgePart);
825
+ }
826
+ if (rectTop < rectBottom) {
827
+ const bridgeRect = new paper2.Path.Rectangle({
828
+ from: [itemBounds.left, rectTop],
829
+ to: [itemBounds.right, rectBottom],
830
+ insert: false
831
+ });
832
+ const unitedItem = item.unite(bridgeRect);
837
833
  item.remove();
838
- bridgePart.remove();
834
+ bridgeRect.remove();
839
835
  if (f.operation === "add") {
840
836
  adds.push(unitedItem);
841
837
  } else {
@@ -1393,7 +1389,19 @@ var DielineTool = class {
1393
1389
  title: "Detect Edge from Image",
1394
1390
  handler: async (imageUrl, options) => {
1395
1391
  try {
1396
- const pathData = await ImageTracer.trace(imageUrl, options);
1392
+ const loadImage = (url) => {
1393
+ return new Promise((resolve, reject) => {
1394
+ const img2 = new Image();
1395
+ img2.crossOrigin = "Anonymous";
1396
+ img2.onload = () => resolve(img2);
1397
+ img2.onerror = (e) => reject(e);
1398
+ img2.src = url;
1399
+ });
1400
+ };
1401
+ const [img, pathData] = await Promise.all([
1402
+ loadImage(imageUrl),
1403
+ ImageTracer.trace(imageUrl, options)
1404
+ ]);
1397
1405
  const bounds = getPathBounds(pathData);
1398
1406
  const currentMax = Math.max(s.width, s.height);
1399
1407
  const scale = currentMax / Math.max(bounds.width, bounds.height);
@@ -1402,7 +1410,10 @@ var DielineTool = class {
1402
1410
  return {
1403
1411
  pathData,
1404
1412
  width: newWidth,
1405
- height: newHeight
1413
+ height: newHeight,
1414
+ rawBounds: bounds,
1415
+ imageWidth: img.width,
1416
+ imageHeight: img.height
1406
1417
  };
1407
1418
  } catch (e) {
1408
1419
  console.error("Edge detection failed", e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pooder/kit",
3
- "version": "4.3.0",
3
+ "version": "4.3.1",
4
4
  "description": "Standard plugins for Pooder editor",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/src/dieline.ts CHANGED
@@ -457,7 +457,22 @@ export class DielineTool implements Extension {
457
457
  title: "Detect Edge from Image",
458
458
  handler: async (imageUrl: string, options?: any) => {
459
459
  try {
460
- const pathData = await ImageTracer.trace(imageUrl, options);
460
+ // Helper to get image dimensions
461
+ const loadImage = (url: string): Promise<HTMLImageElement> => {
462
+ return new Promise((resolve, reject) => {
463
+ const img = new Image();
464
+ img.crossOrigin = "Anonymous";
465
+ img.onload = () => resolve(img);
466
+ img.onerror = (e) => reject(e);
467
+ img.src = url;
468
+ });
469
+ };
470
+
471
+ const [img, pathData] = await Promise.all([
472
+ loadImage(imageUrl),
473
+ ImageTracer.trace(imageUrl, options),
474
+ ]);
475
+
461
476
  const bounds = getPathBounds(pathData);
462
477
 
463
478
  const currentMax = Math.max(s.width, s.height);
@@ -470,6 +485,9 @@ export class DielineTool implements Extension {
470
485
  pathData,
471
486
  width: newWidth,
472
487
  height: newHeight,
488
+ rawBounds: bounds,
489
+ imageWidth: img.width,
490
+ imageHeight: img.height,
473
491
  };
474
492
  } catch (e) {
475
493
  console.error("Edge detection failed", e);
package/src/geometry.ts CHANGED
@@ -182,86 +182,82 @@ function getPerimeterShape(options: GeometryOptions): paper.PathItem {
182
182
  const bridgeBottom = itemBounds.top;
183
183
 
184
184
  if (bridgeBottom > bridgeTop) {
185
- // 1. Create a full column up to the top of the main shape
186
- // Start slightly inside the feature to ensure overlap at the bottom
187
- const startY = bridgeBottom + 1;
188
- const bridgeRect = new paper.Path.Rectangle({
189
- from: [itemBounds.left, bridgeTop],
190
- to: [itemBounds.right, startY],
191
- insert: false,
192
- });
193
-
194
- // 2. Subtract the main shape from this column
195
- // This leaves us with the parts of the column that are NOT inside the main shape (gaps)
196
- const gaps = bridgeRect.subtract(mainShape);
197
- bridgeRect.remove();
198
-
199
- // 3. Find the gap piece that connects to our feature
200
- // It should be the piece with the lowest bottom (highest Y) matching our feature top
201
- let bridgePart: paper.PathItem | null = null;
202
-
203
- // Helper to check if a part is the bottom one
204
- const isBottomPart = (part: paper.PathItem) => {
205
- // Check if bottom aligns with feature top (allow small tolerance)
206
- return Math.abs(part.bounds.bottom - startY) < 2;
207
- };
208
-
209
- if (gaps instanceof paper.CompoundPath) {
210
- // Find the child that is at the bottom
211
- const children = gaps.children;
212
- let maxBottom = -Infinity;
213
- let bestChild = null;
214
-
215
- for (const child of children) {
216
- if (child.bounds.bottom > maxBottom) {
217
- maxBottom = child.bounds.bottom;
218
- bestChild = child;
219
- }
220
- }
221
-
222
- if (bestChild && isBottomPart(bestChild as paper.PathItem)) {
223
- bridgePart = (bestChild as paper.PathItem).clone();
224
- }
225
- } else if (gaps instanceof paper.Path) {
226
- if (isBottomPart(gaps)) {
227
- bridgePart = gaps.clone();
228
- }
229
- }
230
-
231
- gaps.remove();
232
-
233
- if (bridgePart) {
234
- // Overlap fix:
235
- // Scale the bridge up slightly from the bottom to ensure it overlaps with the main shape at the top.
236
- // This prevents hairline gaps due to perfect alignment from subtract().
237
- const bounds = bridgePart.bounds;
238
- if (bounds.height > 0) {
239
- const overlap = 1;
240
- const scaleY = (bounds.height + overlap) / bounds.height;
241
- // Scale around the bottom-center to keep the connection to the feature intact
242
- bridgePart.scale(1, scaleY, new paper.Point(bounds.center.x, bounds.bottom));
243
- }
244
-
245
- // Unite the bridge with the feature
246
- const unitedItem = item.unite(bridgePart);
247
- item.remove();
248
- bridgePart.remove();
249
-
250
- if (f.operation === "add") {
251
- adds.push(unitedItem);
252
- } else {
253
- subtracts.push(unitedItem);
254
- }
255
- } else {
256
- // No bridge needed (feature touches or intersects main shape directly)
257
- // or calculation failed. Fallback to original item.
258
- if (f.operation === "add") {
259
- adds.push(item);
260
- } else {
261
- subtracts.push(item);
262
- }
263
- }
264
- } else {
185
+ // Ray Casting Approach:
186
+ // 1. Create a vertical ray from the center of the feature upwards
187
+ const centerX = itemBounds.center.x;
188
+ const ray = new paper.Path.Line({
189
+ from: [centerX, bridgeBottom],
190
+ to: [centerX, bridgeTop - 10], // Extend slightly past top to ensure intersection
191
+ insert: false
192
+ });
193
+
194
+ // 2. Find intersections with the main shape
195
+ const intersections = mainShape.getIntersections(ray);
196
+
197
+ // 3. Find the lowest intersection point (highest Y)
198
+ // Intersections are usually sorted by offset, but we want to be safe.
199
+ // We want the point with the largest Y that is still <= bridgeBottom.
200
+ let targetY = bridgeTop; // Default to top if no intersection (shouldn't happen if overlapping)
201
+ let found = false;
202
+
203
+ if (intersections && intersections.length > 0) {
204
+ // Filter intersections that are strictly above the feature start
205
+ // (allow small tolerance for touching)
206
+ const validHits = intersections.filter(i => i.point.y < bridgeBottom - 0.1);
207
+
208
+ if (validHits.length > 0) {
209
+ // We want the HIT that is CLOSEST to the feature (Largest Y)
210
+ validHits.sort((a, b) => b.point.y - a.point.y);
211
+ targetY = validHits[0].point.y;
212
+ found = true;
213
+ }
214
+ }
215
+
216
+ ray.remove();
217
+
218
+ // 4. Create the bridge rect
219
+ // If we found a hit, targetY is the surface of the main shape.
220
+ // We want to overlap slightly to ensure union.
221
+ const overlap = 2; // Overlap by 2 units
222
+ const rectBottom = bridgeBottom; // Start at feature top
223
+ let rectTop = found ? targetY + overlap : bridgeTop; // If not found, go all the way? Or maybe fail safe.
224
+
225
+ // If we didn't find an intersection, it might mean the feature is completely below the shape (no X overlap).
226
+ // In that case, maybe we shouldn't bridge? Or bridge to the bounding box bottom?
227
+ // For now, if found, use it. If not, use mainBounds.bottom?
228
+ // Let's assume if !found, we try to project to mainBounds.bottom if it's above us.
229
+ if (!found) {
230
+ if (mainBounds.bottom < bridgeBottom) {
231
+ targetY = mainBounds.bottom;
232
+ rectTop = targetY - overlap; // Penetrate up
233
+ }
234
+ }
235
+
236
+ if (rectTop < rectBottom) {
237
+ const bridgeRect = new paper.Path.Rectangle({
238
+ from: [itemBounds.left, rectTop],
239
+ to: [itemBounds.right, rectBottom],
240
+ insert: false
241
+ });
242
+
243
+ const unitedItem = item.unite(bridgeRect);
244
+ item.remove();
245
+ bridgeRect.remove();
246
+
247
+ if (f.operation === "add") {
248
+ adds.push(unitedItem);
249
+ } else {
250
+ subtracts.push(unitedItem);
251
+ }
252
+ } else {
253
+ // Bridge height is negative or zero, just use item
254
+ if (f.operation === "add") {
255
+ adds.push(item);
256
+ } else {
257
+ subtracts.push(item);
258
+ }
259
+ }
260
+ } else {
265
261
  if (f.operation === "add") {
266
262
  adds.push(item);
267
263
  } else {