@pooder/kit 6.2.1 → 6.2.2

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/dist/index.js CHANGED
@@ -2270,967 +2270,1097 @@ var BackgroundTool = class {
2270
2270
  var import_core2 = require("@pooder/core");
2271
2271
  var import_fabric2 = require("fabric");
2272
2272
 
2273
- // src/extensions/geometry.ts
2274
- var import_paper = __toESM(require("paper"));
2275
-
2276
- // src/extensions/bridgeSelection.ts
2277
- function pickExitIndex(hits) {
2278
- for (let i = 0; i < hits.length; i++) {
2279
- const h = hits[i];
2280
- if (h.insideBelow && !h.insideAbove) return i;
2281
- }
2282
- return -1;
2273
+ // src/shared/scene/frame.ts
2274
+ function emptyFrameRect() {
2275
+ return { left: 0, top: 0, width: 0, height: 0 };
2283
2276
  }
2284
- function scoreOutsideAbove(samples) {
2285
- let score = 0;
2286
- for (const s of samples) {
2287
- if (s.outsideAbove) score++;
2277
+ function resolveCutFrameRect(canvasService, configService) {
2278
+ if (!canvasService || !configService) {
2279
+ return emptyFrameRect();
2288
2280
  }
2289
- return score;
2290
- }
2291
-
2292
- // src/extensions/wrappedOffsets.ts
2293
- function wrappedDistance(total, start, end) {
2294
- if (!Number.isFinite(total) || total <= 0) return 0;
2295
- if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
2296
- const s = (start % total + total) % total;
2297
- const e = (end % total + total) % total;
2298
- return e >= s ? e - s : total - s + e;
2299
- }
2300
- function sampleWrappedOffsets(total, start, end, count) {
2301
- if (!Number.isFinite(total) || total <= 0) return [];
2302
- if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
2303
- const n = Math.max(0, Math.floor(count));
2304
- if (n <= 0) return [];
2305
- const dist = wrappedDistance(total, start, end);
2306
- if (n === 1) return [(start % total + total) % total];
2307
- const step = dist / (n - 1);
2308
- const offsets = [];
2309
- for (let i = 0; i < n; i++) {
2310
- const raw = start + step * i;
2311
- const wrapped = (raw % total + total) % total;
2312
- offsets.push(wrapped);
2281
+ const sizeState = readSizeState(configService);
2282
+ const layout = computeSceneLayout(canvasService, sizeState);
2283
+ if (!layout) {
2284
+ return emptyFrameRect();
2313
2285
  }
2314
- return offsets;
2286
+ return canvasService.toSceneRect({
2287
+ left: layout.cutRect.left,
2288
+ top: layout.cutRect.top,
2289
+ width: layout.cutRect.width,
2290
+ height: layout.cutRect.height
2291
+ });
2315
2292
  }
2316
-
2317
- // src/extensions/geometry.ts
2318
- function resolveFeaturePosition(feature, geometry) {
2319
- const { x, y, width, height } = geometry;
2320
- const left = x - width / 2;
2321
- const top = y - height / 2;
2293
+ function toLayoutSceneRect(rect) {
2322
2294
  return {
2323
- x: left + feature.x * width,
2324
- y: top + feature.y * height
2295
+ left: rect.left,
2296
+ top: rect.top,
2297
+ width: rect.width,
2298
+ height: rect.height,
2299
+ space: "scene"
2325
2300
  };
2326
2301
  }
2327
- function ensurePaper(width, height) {
2328
- if (!import_paper.default.project) {
2329
- import_paper.default.setup(new import_paper.default.Size(width, height));
2330
- } else {
2331
- import_paper.default.view.viewSize = new import_paper.default.Size(width, height);
2332
- }
2333
- }
2334
- var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
2335
- function normalizePathItem(shape) {
2336
- let result = shape;
2337
- if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
2338
- if (typeof result.reduce === "function") result = result.reduce({});
2339
- if (typeof result.reorient === "function") result = result.reorient(true, true);
2340
- if (typeof result.reduce === "function") result = result.reduce({});
2341
- return result;
2342
- }
2343
- function getBridgeDelta(itemBounds, overlap) {
2344
- return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
2345
- }
2346
- function getExitHit(args) {
2347
- const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
2348
- const ray = new import_paper.default.Path.Line({
2349
- from: [x, bridgeBottom],
2350
- to: [x, toY],
2351
- insert: false
2352
- });
2353
- const intersections = mainShape.getIntersections(ray) || [];
2354
- ray.remove();
2355
- const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
2356
- if (validHits.length === 0) return null;
2357
- validHits.sort((a, b) => b.point.y - a.point.y);
2358
- const flags = validHits.map((h) => {
2359
- const above = h.point.add(new import_paper.default.Point(0, -delta));
2360
- const below = h.point.add(new import_paper.default.Point(0, delta));
2361
- return {
2362
- insideAbove: mainShape.contains(above),
2363
- insideBelow: mainShape.contains(below)
2364
- };
2365
- });
2366
- const idx = pickExitIndex(flags);
2367
- if (idx < 0) return null;
2368
- if (isBridgeDebugEnabled()) {
2369
- console.debug("Geometry: Bridge ray", {
2370
- x,
2371
- validHits: validHits.length,
2372
- idx,
2373
- delta,
2374
- overlap,
2375
- op
2376
- });
2377
- }
2378
- const hit = validHits[idx];
2379
- return { point: hit.point, location: hit };
2302
+
2303
+ // src/shared/runtime/sessionState.ts
2304
+ function cloneWithJson(value) {
2305
+ return JSON.parse(JSON.stringify(value));
2380
2306
  }
2381
- function selectOuterChain(args) {
2382
- const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
2383
- const scoreA = scoreOutsideAbove(
2384
- pointsA.map((p) => ({
2385
- outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
2386
- }))
2387
- );
2388
- const scoreB = scoreOutsideAbove(
2389
- pointsB.map((p) => ({
2390
- outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
2391
- }))
2392
- );
2393
- const ratioA = scoreA / pointsA.length;
2394
- const ratioB = scoreB / pointsB.length;
2395
- if (isBridgeDebugEnabled()) {
2396
- console.debug("Geometry: Bridge chain", {
2397
- scoreA,
2398
- scoreB,
2399
- lenA: pointsA.length,
2400
- lenB: pointsB.length,
2401
- ratioA,
2402
- ratioB,
2403
- delta,
2404
- overlap,
2405
- op
2406
- });
2407
- }
2408
- const ratioEps = 1e-6;
2409
- if (Math.abs(ratioA - ratioB) > ratioEps) {
2410
- return ratioA > ratioB ? pointsA : pointsB;
2307
+ function applyCommittedSnapshot(session, nextCommitted, options) {
2308
+ const clone = options.clone;
2309
+ session.committed = clone(nextCommitted);
2310
+ const shouldPreserveDirtyWorking = options.toolActive && options.preserveDirtyWorking !== false && session.hasWorkingChanges;
2311
+ if (!shouldPreserveDirtyWorking) {
2312
+ session.working = clone(session.committed);
2313
+ session.hasWorkingChanges = false;
2411
2314
  }
2412
- if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
2413
- return pointsA.length <= pointsB.length ? pointsA : pointsB;
2414
2315
  }
2415
- function fitPathItemToRect(item, rect, fitMode) {
2416
- const { left, top, width, height } = rect;
2417
- const bounds = item.bounds;
2418
- if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
2419
- item.position = new import_paper.default.Point(left + width / 2, top + height / 2);
2420
- return item;
2421
- }
2422
- item.translate(new import_paper.default.Point(-bounds.left, -bounds.top));
2423
- if (fitMode === "stretch") {
2424
- item.scale(width / bounds.width, height / bounds.height, new import_paper.default.Point(0, 0));
2425
- item.translate(new import_paper.default.Point(left, top));
2426
- return item;
2316
+ function runDeferredConfigUpdate(state, action, cooldownMs = 0) {
2317
+ state.isUpdatingConfig = true;
2318
+ action();
2319
+ if (cooldownMs <= 0) {
2320
+ state.isUpdatingConfig = false;
2321
+ return;
2427
2322
  }
2428
- const uniformScale = Math.min(width / bounds.width, height / bounds.height);
2429
- item.scale(uniformScale, uniformScale, new import_paper.default.Point(0, 0));
2430
- const scaledWidth = bounds.width * uniformScale;
2431
- const scaledHeight = bounds.height * uniformScale;
2432
- item.translate(
2433
- new import_paper.default.Point(
2434
- left + (width - scaledWidth) / 2,
2435
- top + (height - scaledHeight) / 2
2436
- )
2437
- );
2438
- return item;
2439
- }
2440
- function createNormalizedHeartPath(params) {
2441
- const { lobeSpread, notchDepth, tipSharpness } = params;
2442
- const halfSpread = 0.22 + lobeSpread * 0.18;
2443
- const notchY = 0.06 + notchDepth * 0.2;
2444
- const shoulderY = 0.24 + notchDepth * 0.2;
2445
- const topLift = 0.12 + (1 - notchDepth) * 0.06;
2446
- const topY = notchY - topLift;
2447
- const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
2448
- const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
2449
- const tipCtrlX = 0.34 - tipSharpness * 0.2;
2450
- const notchCtrlX = 0.06 + lobeSpread * 0.06;
2451
- const lobeCtrlX = 0.1 + lobeSpread * 0.08;
2452
- const notchCtrlY = notchY - topLift * 0.45;
2453
- const xPeakL = 0.5 - halfSpread;
2454
- const xPeakR = 0.5 + halfSpread;
2455
- const heartPath = new import_paper.default.Path({ insert: false });
2456
- heartPath.moveTo(new import_paper.default.Point(0.5, notchY));
2457
- heartPath.cubicCurveTo(
2458
- new import_paper.default.Point(0.5 - notchCtrlX, notchCtrlY),
2459
- new import_paper.default.Point(xPeakL + lobeCtrlX, topY),
2460
- new import_paper.default.Point(xPeakL, topY)
2461
- );
2462
- heartPath.cubicCurveTo(
2463
- new import_paper.default.Point(xPeakL - lobeCtrlX, topY),
2464
- new import_paper.default.Point(0, sideCtrlY),
2465
- new import_paper.default.Point(0, shoulderY)
2466
- );
2467
- heartPath.cubicCurveTo(
2468
- new import_paper.default.Point(0, lowerCtrlY),
2469
- new import_paper.default.Point(tipCtrlX, 1),
2470
- new import_paper.default.Point(0.5, 1)
2471
- );
2472
- heartPath.cubicCurveTo(
2473
- new import_paper.default.Point(1 - tipCtrlX, 1),
2474
- new import_paper.default.Point(1, lowerCtrlY),
2475
- new import_paper.default.Point(1, shoulderY)
2476
- );
2477
- heartPath.cubicCurveTo(
2478
- new import_paper.default.Point(1, sideCtrlY),
2479
- new import_paper.default.Point(xPeakR + lobeCtrlX, topY),
2480
- new import_paper.default.Point(xPeakR, topY)
2481
- );
2482
- heartPath.cubicCurveTo(
2483
- new import_paper.default.Point(xPeakR - lobeCtrlX, topY),
2484
- new import_paper.default.Point(0.5 + notchCtrlX, notchCtrlY),
2485
- new import_paper.default.Point(0.5, notchY)
2486
- );
2487
- heartPath.closed = true;
2488
- return heartPath;
2323
+ setTimeout(() => {
2324
+ state.isUpdatingConfig = false;
2325
+ }, cooldownMs);
2489
2326
  }
2490
- function createHeartBaseShape(options) {
2491
- const { x, y, width, height } = options;
2492
- const w = Math.max(0, width);
2493
- const h = Math.max(0, height);
2494
- const left = x - w / 2;
2495
- const top = y - h / 2;
2496
- const fitMode = getShapeFitMode(options.shapeStyle);
2497
- const heartParams = getHeartShapeParams(options.shapeStyle);
2498
- const rawHeart = createNormalizedHeartPath(heartParams);
2499
- return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
2500
- }
2501
- var BUILTIN_SHAPE_BUILDERS = {
2502
- rect: (options) => {
2503
- const { x, y, width, height, radius } = options;
2504
- return new import_paper.default.Path.Rectangle({
2505
- point: [x - width / 2, y - height / 2],
2506
- size: [Math.max(0, width), Math.max(0, height)],
2507
- radius: Math.max(0, radius)
2508
- });
2509
- },
2510
- circle: (options) => {
2511
- const { x, y, width, height } = options;
2512
- const r = Math.min(width, height) / 2;
2513
- return new import_paper.default.Path.Circle({
2514
- center: new import_paper.default.Point(x, y),
2515
- radius: Math.max(0, r)
2516
- });
2517
- },
2518
- ellipse: (options) => {
2519
- const { x, y, width, height } = options;
2520
- return new import_paper.default.Path.Ellipse({
2521
- center: new import_paper.default.Point(x, y),
2522
- radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
2523
- });
2524
- },
2525
- heart: createHeartBaseShape
2526
- };
2527
- function createCustomBaseShape(options) {
2528
- var _a;
2529
- const {
2530
- pathData,
2531
- customSourceWidthPx,
2532
- customSourceHeightPx,
2533
- x,
2534
- y,
2535
- width,
2536
- height
2537
- } = options;
2538
- if (typeof pathData !== "string" || pathData.trim().length === 0) {
2539
- return null;
2540
- }
2541
- const center = new import_paper.default.Point(x, y);
2542
- const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
2543
- const path = hasMultipleSubPaths ? new import_paper.default.CompoundPath(pathData) : (() => {
2544
- const single = new import_paper.default.Path();
2545
- single.pathData = pathData;
2546
- return single;
2547
- })();
2548
- const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
2549
- const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
2550
- if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
2551
- const targetLeft = x - width / 2;
2552
- const targetTop = y - height / 2;
2553
- path.scale(width / sourceWidth, height / sourceHeight, new import_paper.default.Point(0, 0));
2554
- path.translate(new import_paper.default.Point(targetLeft, targetTop));
2555
- return path;
2556
- }
2557
- if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
2558
- path.position = center;
2559
- path.scale(width / path.bounds.width, height / path.bounds.height);
2560
- return path;
2561
- }
2562
- path.position = center;
2563
- return path;
2564
- }
2565
- function createBaseShape(options) {
2566
- const { shape } = options;
2567
- if (shape === "custom") {
2568
- const customShape = createCustomBaseShape(options);
2569
- if (customShape) return customShape;
2570
- return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
2571
- }
2572
- return BUILTIN_SHAPE_BUILDERS[shape](options);
2573
- }
2574
- function resolveBridgeBasePath(shape, anchor) {
2575
- if (shape instanceof import_paper.default.Path) {
2576
- return shape;
2577
- }
2578
- if (shape instanceof import_paper.default.CompoundPath) {
2579
- const children = (shape.children || []).filter(
2580
- (child) => child instanceof import_paper.default.Path
2581
- );
2582
- if (!children.length) return null;
2583
- let best = children[0];
2584
- let bestDistance = Infinity;
2585
- for (const child of children) {
2586
- const location = child.getNearestLocation(anchor);
2587
- const point = location == null ? void 0 : location.point;
2588
- if (!point) continue;
2589
- const distance = point.getDistance(anchor);
2590
- if (distance < bestDistance) {
2591
- bestDistance = distance;
2592
- best = child;
2327
+
2328
+ // src/extensions/image/commands.ts
2329
+ function createImageCommands(tool) {
2330
+ return [
2331
+ {
2332
+ command: "addImage",
2333
+ id: "addImage",
2334
+ title: "Add Image",
2335
+ handler: async (url, options) => {
2336
+ const result = await tool.upsertImageEntry(url, {
2337
+ mode: "add",
2338
+ addOptions: options
2339
+ });
2340
+ return result.id;
2593
2341
  }
2594
- }
2595
- return best;
2596
- }
2597
- return null;
2598
- }
2599
- function createFeatureItem(feature, center) {
2600
- let item;
2601
- if (feature.shape === "rect") {
2602
- const w = feature.width || 10;
2603
- const h = feature.height || 10;
2604
- const r = feature.radius || 0;
2605
- item = new import_paper.default.Path.Rectangle({
2606
- point: [center.x - w / 2, center.y - h / 2],
2607
- size: [w, h],
2608
- radius: r
2609
- });
2610
- } else {
2611
- const r = feature.radius || 5;
2612
- item = new import_paper.default.Path.Circle({
2613
- center,
2614
- radius: r
2615
- });
2616
- }
2617
- if (feature.rotation) {
2618
- item.rotate(feature.rotation, center);
2619
- }
2620
- return item;
2621
- }
2622
- function getPerimeterShape(options) {
2623
- let mainShape = createBaseShape(options);
2624
- const { features } = options;
2625
- if (features && features.length > 0) {
2626
- const edgeFeatures = features.filter(
2627
- (f) => !f.renderBehavior || f.renderBehavior === "edge"
2628
- );
2629
- const adds = [];
2630
- const subtracts = [];
2631
- edgeFeatures.forEach((f) => {
2632
- const pos = resolveFeaturePosition(f, options);
2633
- const center = new import_paper.default.Point(pos.x, pos.y);
2634
- const item = createFeatureItem(f, center);
2635
- if (f.bridge && f.bridge.type === "vertical") {
2636
- const itemBounds = item.bounds;
2637
- const mainBounds = mainShape.bounds;
2638
- const bridgeTop = mainBounds.top;
2639
- const bridgeBottom = itemBounds.top;
2640
- if (bridgeBottom > bridgeTop) {
2641
- const overlap = 2;
2642
- const rayPadding = 10;
2643
- const eps = 0.1;
2644
- const delta = getBridgeDelta(itemBounds, overlap);
2645
- const toY = bridgeTop - rayPadding;
2646
- const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
2647
- const xLeft = itemBounds.left + inset;
2648
- const xRight = itemBounds.right - inset;
2649
- const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
2650
- const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
2651
- if (canBridge && bridgeBasePath) {
2652
- const leftHit = getExitHit({
2653
- mainShape: bridgeBasePath,
2654
- x: xLeft,
2655
- bridgeBottom,
2656
- toY,
2657
- eps,
2658
- delta,
2659
- overlap,
2660
- op: f.operation
2661
- });
2662
- const rightHit = getExitHit({
2663
- mainShape: bridgeBasePath,
2664
- x: xRight,
2665
- bridgeBottom,
2666
- toY,
2667
- eps,
2668
- delta,
2669
- overlap,
2670
- op: f.operation
2342
+ },
2343
+ {
2344
+ command: "upsertImage",
2345
+ id: "upsertImage",
2346
+ title: "Upsert Image",
2347
+ handler: async (url, options = {}) => {
2348
+ return await tool.upsertImageEntry(url, options);
2349
+ }
2350
+ },
2351
+ {
2352
+ command: "getWorkingImages",
2353
+ id: "getWorkingImages",
2354
+ title: "Get Working Images",
2355
+ handler: () => {
2356
+ return tool.cloneItems(tool.workingItems);
2357
+ }
2358
+ },
2359
+ {
2360
+ command: "setWorkingImage",
2361
+ id: "setWorkingImage",
2362
+ title: "Set Working Image",
2363
+ handler: (id, updates) => {
2364
+ tool.updateImageInWorking(id, updates);
2365
+ }
2366
+ },
2367
+ {
2368
+ command: "resetWorkingImages",
2369
+ id: "resetWorkingImages",
2370
+ title: "Reset Working Images",
2371
+ handler: () => {
2372
+ tool.workingItems = tool.cloneItems(tool.items);
2373
+ tool.hasWorkingChanges = false;
2374
+ tool.updateImages();
2375
+ tool.emitWorkingChange();
2376
+ }
2377
+ },
2378
+ {
2379
+ command: "completeImages",
2380
+ id: "completeImages",
2381
+ title: "Complete Images",
2382
+ handler: async () => {
2383
+ return await tool.commitWorkingImagesAsCropped();
2384
+ }
2385
+ },
2386
+ {
2387
+ command: "exportUserCroppedImage",
2388
+ id: "exportUserCroppedImage",
2389
+ title: "Export User Cropped Image",
2390
+ handler: async (options = {}) => {
2391
+ return await tool.exportUserCroppedImage(options);
2392
+ }
2393
+ },
2394
+ {
2395
+ command: "fitImageToArea",
2396
+ id: "fitImageToArea",
2397
+ title: "Fit Image to Area",
2398
+ handler: async (id, area) => {
2399
+ await tool.fitImageToArea(id, area);
2400
+ }
2401
+ },
2402
+ {
2403
+ command: "fitImageToDefaultArea",
2404
+ id: "fitImageToDefaultArea",
2405
+ title: "Fit Image to Default Area",
2406
+ handler: async (id) => {
2407
+ await tool.fitImageToDefaultArea(id);
2408
+ }
2409
+ },
2410
+ {
2411
+ command: "focusImage",
2412
+ id: "focusImage",
2413
+ title: "Focus Image",
2414
+ handler: (id, options = {}) => {
2415
+ return tool.setImageFocus(id, options);
2416
+ }
2417
+ },
2418
+ {
2419
+ command: "removeImage",
2420
+ id: "removeImage",
2421
+ title: "Remove Image",
2422
+ handler: (id) => {
2423
+ const removed = tool.items.find((item) => item.id === id);
2424
+ const next = tool.items.filter((item) => item.id !== id);
2425
+ if (next.length !== tool.items.length) {
2426
+ tool.purgeSourceSizeCacheForItem(removed);
2427
+ if (tool.focusedImageId === id) {
2428
+ tool.setImageFocus(null, {
2429
+ syncCanvasSelection: true,
2430
+ skipRender: true
2671
2431
  });
2672
- if (leftHit && rightHit) {
2673
- const pathLength = bridgeBasePath.length;
2674
- const leftOffset = leftHit.location.offset;
2675
- const rightOffset = rightHit.location.offset;
2676
- const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
2677
- const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
2678
- const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
2679
- const offsetsA = sampleWrappedOffsets(
2680
- pathLength,
2681
- leftOffset,
2682
- rightOffset,
2683
- countFor(distanceA)
2684
- );
2685
- const offsetsB = sampleWrappedOffsets(
2686
- pathLength,
2687
- rightOffset,
2688
- leftOffset,
2689
- countFor(distanceB)
2690
- );
2691
- const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
2692
- const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
2693
- if (pointsA.length >= 2 && pointsB.length >= 2) {
2694
- let topBase = selectOuterChain({
2695
- mainShape: bridgeBasePath,
2696
- pointsA,
2697
- pointsB,
2698
- delta,
2699
- overlap,
2700
- op: f.operation
2701
- });
2702
- const dist2 = (a, b) => {
2703
- const dx = a.x - b.x;
2704
- const dy = a.y - b.y;
2705
- return dx * dx + dy * dy;
2706
- };
2707
- if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
2708
- topBase = topBase.slice().reverse();
2709
- }
2710
- topBase = topBase.slice();
2711
- topBase[0] = leftHit.point;
2712
- topBase[topBase.length - 1] = rightHit.point;
2713
- const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
2714
- const topPoints = topBase.map(
2715
- (p) => p.add(new import_paper.default.Point(0, capShiftY))
2716
- );
2717
- const bridgeBottomY = bridgeBottom + overlap * 2;
2718
- const bridgePoly = new import_paper.default.Path({ insert: false });
2719
- for (const p of topPoints) bridgePoly.add(p);
2720
- bridgePoly.add(new import_paper.default.Point(xRight, bridgeBottomY));
2721
- bridgePoly.add(new import_paper.default.Point(xLeft, bridgeBottomY));
2722
- bridgePoly.closed = true;
2723
- const unitedItem = item.unite(bridgePoly);
2724
- item.remove();
2725
- bridgePoly.remove();
2726
- if (f.operation === "add") {
2727
- adds.push(unitedItem);
2728
- } else {
2729
- subtracts.push(unitedItem);
2730
- }
2731
- return;
2732
- }
2733
- }
2734
- }
2735
- if (f.operation === "add") {
2736
- adds.push(item);
2737
- } else {
2738
- subtracts.push(item);
2739
- }
2740
- } else {
2741
- if (f.operation === "add") {
2742
- adds.push(item);
2743
- } else {
2744
- subtracts.push(item);
2745
2432
  }
2746
- }
2747
- } else {
2748
- if (f.operation === "add") {
2749
- adds.push(item);
2750
- } else {
2751
- subtracts.push(item);
2433
+ tool.updateConfig(next);
2752
2434
  }
2753
2435
  }
2754
- });
2755
- if (adds.length > 0) {
2756
- for (const item of adds) {
2757
- try {
2758
- const temp = mainShape.unite(item);
2759
- mainShape.remove();
2760
- item.remove();
2761
- mainShape = normalizePathItem(temp);
2762
- } catch (e) {
2763
- console.error("Geometry: Failed to unite feature", e);
2764
- item.remove();
2436
+ },
2437
+ {
2438
+ command: "updateImage",
2439
+ id: "updateImage",
2440
+ title: "Update Image",
2441
+ handler: async (id, updates, options = {}) => {
2442
+ await tool.updateImage(id, updates, options);
2443
+ }
2444
+ },
2445
+ {
2446
+ command: "clearImages",
2447
+ id: "clearImages",
2448
+ title: "Clear Images",
2449
+ handler: () => {
2450
+ tool.sourceSizeCache.clear();
2451
+ tool.setImageFocus(null, {
2452
+ syncCanvasSelection: true,
2453
+ skipRender: true
2454
+ });
2455
+ tool.updateConfig([]);
2456
+ }
2457
+ },
2458
+ {
2459
+ command: "bringToFront",
2460
+ id: "bringToFront",
2461
+ title: "Bring Image to Front",
2462
+ handler: (id) => {
2463
+ const index = tool.items.findIndex((item) => item.id === id);
2464
+ if (index !== -1 && index < tool.items.length - 1) {
2465
+ const next = [...tool.items];
2466
+ const [item] = next.splice(index, 1);
2467
+ next.push(item);
2468
+ tool.updateConfig(next);
2765
2469
  }
2766
2470
  }
2767
- }
2768
- if (subtracts.length > 0) {
2769
- for (const item of subtracts) {
2770
- try {
2771
- const temp = mainShape.subtract(item);
2772
- mainShape.remove();
2773
- item.remove();
2774
- mainShape = normalizePathItem(temp);
2775
- } catch (e) {
2776
- console.error("Geometry: Failed to subtract feature", e);
2777
- item.remove();
2471
+ },
2472
+ {
2473
+ command: "sendToBack",
2474
+ id: "sendToBack",
2475
+ title: "Send Image to Back",
2476
+ handler: (id) => {
2477
+ const index = tool.items.findIndex((item) => item.id === id);
2478
+ if (index > 0) {
2479
+ const next = [...tool.items];
2480
+ const [item] = next.splice(index, 1);
2481
+ next.unshift(item);
2482
+ tool.updateConfig(next);
2778
2483
  }
2779
2484
  }
2780
2485
  }
2486
+ ];
2487
+ }
2488
+
2489
+ // src/extensions/image/config.ts
2490
+ function createImageConfigurations() {
2491
+ return [
2492
+ {
2493
+ id: "image.items",
2494
+ type: "array",
2495
+ label: "Images",
2496
+ default: []
2497
+ },
2498
+ {
2499
+ id: "image.debug",
2500
+ type: "boolean",
2501
+ label: "Image Debug Log",
2502
+ default: false
2503
+ },
2504
+ {
2505
+ id: "image.control.cornerSize",
2506
+ type: "number",
2507
+ label: "Image Control Corner Size",
2508
+ min: 4,
2509
+ max: 64,
2510
+ step: 1,
2511
+ default: 14
2512
+ },
2513
+ {
2514
+ id: "image.control.touchCornerSize",
2515
+ type: "number",
2516
+ label: "Image Control Touch Corner Size",
2517
+ min: 8,
2518
+ max: 96,
2519
+ step: 1,
2520
+ default: 24
2521
+ },
2522
+ {
2523
+ id: "image.control.cornerStyle",
2524
+ type: "select",
2525
+ label: "Image Control Corner Style",
2526
+ options: ["circle", "rect"],
2527
+ default: "circle"
2528
+ },
2529
+ {
2530
+ id: "image.control.cornerColor",
2531
+ type: "color",
2532
+ label: "Image Control Corner Color",
2533
+ default: "#ffffff"
2534
+ },
2535
+ {
2536
+ id: "image.control.cornerStrokeColor",
2537
+ type: "color",
2538
+ label: "Image Control Corner Stroke Color",
2539
+ default: "#1677ff"
2540
+ },
2541
+ {
2542
+ id: "image.control.transparentCorners",
2543
+ type: "boolean",
2544
+ label: "Image Control Transparent Corners",
2545
+ default: false
2546
+ },
2547
+ {
2548
+ id: "image.control.borderColor",
2549
+ type: "color",
2550
+ label: "Image Control Border Color",
2551
+ default: "#1677ff"
2552
+ },
2553
+ {
2554
+ id: "image.control.borderScaleFactor",
2555
+ type: "number",
2556
+ label: "Image Control Border Width",
2557
+ min: 0.5,
2558
+ max: 8,
2559
+ step: 0.1,
2560
+ default: 1.5
2561
+ },
2562
+ {
2563
+ id: "image.control.padding",
2564
+ type: "number",
2565
+ label: "Image Control Padding",
2566
+ min: 0,
2567
+ max: 64,
2568
+ step: 1,
2569
+ default: 0
2570
+ },
2571
+ {
2572
+ id: "image.frame.strokeColor",
2573
+ type: "color",
2574
+ label: "Image Frame Stroke Color",
2575
+ default: "#808080"
2576
+ },
2577
+ {
2578
+ id: "image.frame.strokeWidth",
2579
+ type: "number",
2580
+ label: "Image Frame Stroke Width",
2581
+ min: 0,
2582
+ max: 20,
2583
+ step: 0.5,
2584
+ default: 2
2585
+ },
2586
+ {
2587
+ id: "image.frame.strokeStyle",
2588
+ type: "select",
2589
+ label: "Image Frame Stroke Style",
2590
+ options: ["solid", "dashed", "hidden"],
2591
+ default: "dashed"
2592
+ },
2593
+ {
2594
+ id: "image.frame.dashLength",
2595
+ type: "number",
2596
+ label: "Image Frame Dash Length",
2597
+ min: 1,
2598
+ max: 40,
2599
+ step: 1,
2600
+ default: 8
2601
+ },
2602
+ {
2603
+ id: "image.frame.innerBackground",
2604
+ type: "color",
2605
+ label: "Image Frame Inner Background",
2606
+ default: "rgba(0,0,0,0)"
2607
+ },
2608
+ {
2609
+ id: "image.frame.outerBackground",
2610
+ type: "color",
2611
+ label: "Image Frame Outer Background",
2612
+ default: "#f5f5f5"
2613
+ }
2614
+ ];
2615
+ }
2616
+
2617
+ // src/extensions/geometry.ts
2618
+ var import_paper = __toESM(require("paper"));
2619
+
2620
+ // src/extensions/bridgeSelection.ts
2621
+ function pickExitIndex(hits) {
2622
+ for (let i = 0; i < hits.length; i++) {
2623
+ const h = hits[i];
2624
+ if (h.insideBelow && !h.insideAbove) return i;
2781
2625
  }
2782
- return mainShape;
2626
+ return -1;
2783
2627
  }
2784
- function applySurfaceFeatures(shape, features, options) {
2785
- const surfaceFeatures = features.filter(
2786
- (f) => f.renderBehavior === "surface"
2787
- );
2788
- if (surfaceFeatures.length === 0) return shape;
2789
- let result = shape;
2790
- for (const f of surfaceFeatures) {
2791
- const pos = resolveFeaturePosition(f, options);
2792
- const center = new import_paper.default.Point(pos.x, pos.y);
2793
- const item = createFeatureItem(f, center);
2794
- try {
2795
- if (f.operation === "add") {
2796
- const temp = result.unite(item);
2797
- result.remove();
2798
- item.remove();
2799
- result = normalizePathItem(temp);
2800
- } else {
2801
- const temp = result.subtract(item);
2802
- result.remove();
2803
- item.remove();
2804
- result = normalizePathItem(temp);
2805
- }
2806
- } catch (e) {
2807
- console.error("Geometry: Failed to apply surface feature", e);
2808
- item.remove();
2809
- }
2628
+ function scoreOutsideAbove(samples) {
2629
+ let score = 0;
2630
+ for (const s of samples) {
2631
+ if (s.outsideAbove) score++;
2810
2632
  }
2811
- return result;
2633
+ return score;
2812
2634
  }
2813
- function generateDielinePath(options) {
2814
- const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
2815
- const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
2816
- ensurePaper(paperWidth, paperHeight);
2817
- import_paper.default.project.activeLayer.removeChildren();
2818
- const perimeter = getPerimeterShape(options);
2819
- const finalShape = applySurfaceFeatures(perimeter, options.features, options);
2820
- const pathData = finalShape.pathData;
2821
- finalShape.remove();
2822
- return pathData;
2635
+
2636
+ // src/extensions/wrappedOffsets.ts
2637
+ function wrappedDistance(total, start, end) {
2638
+ if (!Number.isFinite(total) || total <= 0) return 0;
2639
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
2640
+ const s = (start % total + total) % total;
2641
+ const e = (end % total + total) % total;
2642
+ return e >= s ? e - s : total - s + e;
2823
2643
  }
2824
- function generateBleedZonePath(originalOptions, offsetOptions, offset) {
2825
- const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
2826
- const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
2827
- ensurePaper(paperWidth, paperHeight);
2828
- import_paper.default.project.activeLayer.removeChildren();
2829
- const pOriginal = getPerimeterShape(originalOptions);
2830
- const shapeOriginal = applySurfaceFeatures(
2831
- pOriginal,
2832
- originalOptions.features,
2833
- originalOptions
2834
- );
2835
- const pOffset = getPerimeterShape(offsetOptions);
2836
- const shapeOffset = applySurfaceFeatures(
2837
- pOffset,
2838
- offsetOptions.features,
2839
- offsetOptions
2840
- );
2841
- let bleedZone;
2842
- if (offset > 0) {
2843
- bleedZone = shapeOffset.subtract(shapeOriginal);
2844
- } else {
2845
- bleedZone = shapeOriginal.subtract(shapeOffset);
2644
+ function sampleWrappedOffsets(total, start, end, count) {
2645
+ if (!Number.isFinite(total) || total <= 0) return [];
2646
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
2647
+ const n = Math.max(0, Math.floor(count));
2648
+ if (n <= 0) return [];
2649
+ const dist = wrappedDistance(total, start, end);
2650
+ if (n === 1) return [(start % total + total) % total];
2651
+ const step = dist / (n - 1);
2652
+ const offsets = [];
2653
+ for (let i = 0; i < n; i++) {
2654
+ const raw = start + step * i;
2655
+ const wrapped = (raw % total + total) % total;
2656
+ offsets.push(wrapped);
2846
2657
  }
2847
- const pathData = bleedZone.pathData;
2848
- shapeOriginal.remove();
2849
- shapeOffset.remove();
2850
- bleedZone.remove();
2851
- return pathData;
2658
+ return offsets;
2852
2659
  }
2853
- function getLowestPointOnDieline(options) {
2854
- ensurePaper(options.width * 2, options.height * 2);
2855
- import_paper.default.project.activeLayer.removeChildren();
2856
- const shape = createBaseShape(options);
2857
- const bounds = shape.bounds;
2858
- const result = {
2859
- x: bounds.center.x,
2860
- y: bounds.bottom
2660
+
2661
+ // src/extensions/geometry.ts
2662
+ function resolveFeaturePosition(feature, geometry) {
2663
+ const { x, y, width, height } = geometry;
2664
+ const left = x - width / 2;
2665
+ const top = y - height / 2;
2666
+ return {
2667
+ x: left + feature.x * width,
2668
+ y: top + feature.y * height
2861
2669
  };
2862
- shape.remove();
2863
- return result;
2864
2670
  }
2865
- function getNearestPointOnDieline(point, options) {
2866
- ensurePaper(options.width * 2, options.height * 2);
2867
- import_paper.default.project.activeLayer.removeChildren();
2868
- const shape = createBaseShape(options);
2869
- const p = new import_paper.default.Point(point.x, point.y);
2870
- const location = shape.getNearestLocation(p);
2871
- const result = {
2872
- x: location.point.x,
2873
- y: location.point.y,
2874
- normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
2875
- };
2876
- shape.remove();
2671
+ function ensurePaper(width, height) {
2672
+ if (!import_paper.default.project) {
2673
+ import_paper.default.setup(new import_paper.default.Size(width, height));
2674
+ } else {
2675
+ import_paper.default.view.viewSize = new import_paper.default.Size(width, height);
2676
+ }
2677
+ }
2678
+ var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
2679
+ function normalizePathItem(shape) {
2680
+ let result = shape;
2681
+ if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
2682
+ if (typeof result.reduce === "function") result = result.reduce({});
2683
+ if (typeof result.reorient === "function") result = result.reorient(true, true);
2684
+ if (typeof result.reduce === "function") result = result.reduce({});
2877
2685
  return result;
2878
2686
  }
2879
- function getPathBounds(pathData) {
2880
- const path = new import_paper.default.Path();
2881
- path.pathData = pathData;
2882
- const bounds = path.bounds;
2883
- path.remove();
2884
- return {
2885
- x: bounds.x,
2886
- y: bounds.y,
2887
- width: bounds.width,
2888
- height: bounds.height
2889
- };
2687
+ function getBridgeDelta(itemBounds, overlap) {
2688
+ return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
2890
2689
  }
2891
-
2892
- // src/shared/scene/frame.ts
2893
- function emptyFrameRect() {
2894
- return { left: 0, top: 0, width: 0, height: 0 };
2690
+ function getExitHit(args) {
2691
+ const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
2692
+ const ray = new import_paper.default.Path.Line({
2693
+ from: [x, bridgeBottom],
2694
+ to: [x, toY],
2695
+ insert: false
2696
+ });
2697
+ const intersections = mainShape.getIntersections(ray) || [];
2698
+ ray.remove();
2699
+ const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
2700
+ if (validHits.length === 0) return null;
2701
+ validHits.sort((a, b) => b.point.y - a.point.y);
2702
+ const flags = validHits.map((h) => {
2703
+ const above = h.point.add(new import_paper.default.Point(0, -delta));
2704
+ const below = h.point.add(new import_paper.default.Point(0, delta));
2705
+ return {
2706
+ insideAbove: mainShape.contains(above),
2707
+ insideBelow: mainShape.contains(below)
2708
+ };
2709
+ });
2710
+ const idx = pickExitIndex(flags);
2711
+ if (idx < 0) return null;
2712
+ if (isBridgeDebugEnabled()) {
2713
+ console.debug("Geometry: Bridge ray", {
2714
+ x,
2715
+ validHits: validHits.length,
2716
+ idx,
2717
+ delta,
2718
+ overlap,
2719
+ op
2720
+ });
2721
+ }
2722
+ const hit = validHits[idx];
2723
+ return { point: hit.point, location: hit };
2724
+ }
2725
+ function selectOuterChain(args) {
2726
+ const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
2727
+ const scoreA = scoreOutsideAbove(
2728
+ pointsA.map((p) => ({
2729
+ outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
2730
+ }))
2731
+ );
2732
+ const scoreB = scoreOutsideAbove(
2733
+ pointsB.map((p) => ({
2734
+ outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
2735
+ }))
2736
+ );
2737
+ const ratioA = scoreA / pointsA.length;
2738
+ const ratioB = scoreB / pointsB.length;
2739
+ if (isBridgeDebugEnabled()) {
2740
+ console.debug("Geometry: Bridge chain", {
2741
+ scoreA,
2742
+ scoreB,
2743
+ lenA: pointsA.length,
2744
+ lenB: pointsB.length,
2745
+ ratioA,
2746
+ ratioB,
2747
+ delta,
2748
+ overlap,
2749
+ op
2750
+ });
2751
+ }
2752
+ const ratioEps = 1e-6;
2753
+ if (Math.abs(ratioA - ratioB) > ratioEps) {
2754
+ return ratioA > ratioB ? pointsA : pointsB;
2755
+ }
2756
+ if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
2757
+ return pointsA.length <= pointsB.length ? pointsA : pointsB;
2895
2758
  }
2896
- function resolveCutFrameRect(canvasService, configService) {
2897
- if (!canvasService || !configService) {
2898
- return emptyFrameRect();
2759
+ function fitPathItemToRect(item, rect, fitMode) {
2760
+ const { left, top, width, height } = rect;
2761
+ const bounds = item.bounds;
2762
+ if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
2763
+ item.position = new import_paper.default.Point(left + width / 2, top + height / 2);
2764
+ return item;
2899
2765
  }
2900
- const sizeState = readSizeState(configService);
2901
- const layout = computeSceneLayout(canvasService, sizeState);
2902
- if (!layout) {
2903
- return emptyFrameRect();
2766
+ item.translate(new import_paper.default.Point(-bounds.left, -bounds.top));
2767
+ if (fitMode === "stretch") {
2768
+ item.scale(width / bounds.width, height / bounds.height, new import_paper.default.Point(0, 0));
2769
+ item.translate(new import_paper.default.Point(left, top));
2770
+ return item;
2904
2771
  }
2905
- return canvasService.toSceneRect({
2906
- left: layout.cutRect.left,
2907
- top: layout.cutRect.top,
2908
- width: layout.cutRect.width,
2909
- height: layout.cutRect.height
2910
- });
2911
- }
2912
- function toLayoutSceneRect(rect) {
2913
- return {
2914
- left: rect.left,
2915
- top: rect.top,
2916
- width: rect.width,
2917
- height: rect.height,
2918
- space: "scene"
2919
- };
2772
+ const uniformScale = Math.min(width / bounds.width, height / bounds.height);
2773
+ item.scale(uniformScale, uniformScale, new import_paper.default.Point(0, 0));
2774
+ const scaledWidth = bounds.width * uniformScale;
2775
+ const scaledHeight = bounds.height * uniformScale;
2776
+ item.translate(
2777
+ new import_paper.default.Point(
2778
+ left + (width - scaledWidth) / 2,
2779
+ top + (height - scaledHeight) / 2
2780
+ )
2781
+ );
2782
+ return item;
2920
2783
  }
2921
-
2922
- // src/shared/runtime/sessionState.ts
2923
- function cloneWithJson(value) {
2924
- return JSON.parse(JSON.stringify(value));
2784
+ function createNormalizedHeartPath(params) {
2785
+ const { lobeSpread, notchDepth, tipSharpness } = params;
2786
+ const halfSpread = 0.22 + lobeSpread * 0.18;
2787
+ const notchY = 0.06 + notchDepth * 0.2;
2788
+ const shoulderY = 0.24 + notchDepth * 0.2;
2789
+ const topLift = 0.12 + (1 - notchDepth) * 0.06;
2790
+ const topY = notchY - topLift;
2791
+ const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
2792
+ const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
2793
+ const tipCtrlX = 0.34 - tipSharpness * 0.2;
2794
+ const notchCtrlX = 0.06 + lobeSpread * 0.06;
2795
+ const lobeCtrlX = 0.1 + lobeSpread * 0.08;
2796
+ const notchCtrlY = notchY - topLift * 0.45;
2797
+ const xPeakL = 0.5 - halfSpread;
2798
+ const xPeakR = 0.5 + halfSpread;
2799
+ const heartPath = new import_paper.default.Path({ insert: false });
2800
+ heartPath.moveTo(new import_paper.default.Point(0.5, notchY));
2801
+ heartPath.cubicCurveTo(
2802
+ new import_paper.default.Point(0.5 - notchCtrlX, notchCtrlY),
2803
+ new import_paper.default.Point(xPeakL + lobeCtrlX, topY),
2804
+ new import_paper.default.Point(xPeakL, topY)
2805
+ );
2806
+ heartPath.cubicCurveTo(
2807
+ new import_paper.default.Point(xPeakL - lobeCtrlX, topY),
2808
+ new import_paper.default.Point(0, sideCtrlY),
2809
+ new import_paper.default.Point(0, shoulderY)
2810
+ );
2811
+ heartPath.cubicCurveTo(
2812
+ new import_paper.default.Point(0, lowerCtrlY),
2813
+ new import_paper.default.Point(tipCtrlX, 1),
2814
+ new import_paper.default.Point(0.5, 1)
2815
+ );
2816
+ heartPath.cubicCurveTo(
2817
+ new import_paper.default.Point(1 - tipCtrlX, 1),
2818
+ new import_paper.default.Point(1, lowerCtrlY),
2819
+ new import_paper.default.Point(1, shoulderY)
2820
+ );
2821
+ heartPath.cubicCurveTo(
2822
+ new import_paper.default.Point(1, sideCtrlY),
2823
+ new import_paper.default.Point(xPeakR + lobeCtrlX, topY),
2824
+ new import_paper.default.Point(xPeakR, topY)
2825
+ );
2826
+ heartPath.cubicCurveTo(
2827
+ new import_paper.default.Point(xPeakR - lobeCtrlX, topY),
2828
+ new import_paper.default.Point(0.5 + notchCtrlX, notchCtrlY),
2829
+ new import_paper.default.Point(0.5, notchY)
2830
+ );
2831
+ heartPath.closed = true;
2832
+ return heartPath;
2925
2833
  }
2926
- function applyCommittedSnapshot(session, nextCommitted, options) {
2927
- const clone = options.clone;
2928
- session.committed = clone(nextCommitted);
2929
- const shouldPreserveDirtyWorking = options.toolActive && options.preserveDirtyWorking !== false && session.hasWorkingChanges;
2930
- if (!shouldPreserveDirtyWorking) {
2931
- session.working = clone(session.committed);
2932
- session.hasWorkingChanges = false;
2933
- }
2834
+ function createHeartBaseShape(options) {
2835
+ const { x, y, width, height } = options;
2836
+ const w = Math.max(0, width);
2837
+ const h = Math.max(0, height);
2838
+ const left = x - w / 2;
2839
+ const top = y - h / 2;
2840
+ const fitMode = getShapeFitMode(options.shapeStyle);
2841
+ const heartParams = getHeartShapeParams(options.shapeStyle);
2842
+ const rawHeart = createNormalizedHeartPath(heartParams);
2843
+ return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
2934
2844
  }
2935
- function runDeferredConfigUpdate(state, action, cooldownMs = 0) {
2936
- state.isUpdatingConfig = true;
2937
- action();
2938
- if (cooldownMs <= 0) {
2939
- state.isUpdatingConfig = false;
2940
- return;
2845
+ var BUILTIN_SHAPE_BUILDERS = {
2846
+ rect: (options) => {
2847
+ const { x, y, width, height, radius } = options;
2848
+ return new import_paper.default.Path.Rectangle({
2849
+ point: [x - width / 2, y - height / 2],
2850
+ size: [Math.max(0, width), Math.max(0, height)],
2851
+ radius: Math.max(0, radius)
2852
+ });
2853
+ },
2854
+ circle: (options) => {
2855
+ const { x, y, width, height } = options;
2856
+ const r = Math.min(width, height) / 2;
2857
+ return new import_paper.default.Path.Circle({
2858
+ center: new import_paper.default.Point(x, y),
2859
+ radius: Math.max(0, r)
2860
+ });
2861
+ },
2862
+ ellipse: (options) => {
2863
+ const { x, y, width, height } = options;
2864
+ return new import_paper.default.Path.Ellipse({
2865
+ center: new import_paper.default.Point(x, y),
2866
+ radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
2867
+ });
2868
+ },
2869
+ heart: createHeartBaseShape
2870
+ };
2871
+ function createCustomBaseShape(options) {
2872
+ var _a;
2873
+ const {
2874
+ pathData,
2875
+ customSourceWidthPx,
2876
+ customSourceHeightPx,
2877
+ x,
2878
+ y,
2879
+ width,
2880
+ height
2881
+ } = options;
2882
+ if (typeof pathData !== "string" || pathData.trim().length === 0) {
2883
+ return null;
2941
2884
  }
2942
- setTimeout(() => {
2943
- state.isUpdatingConfig = false;
2944
- }, cooldownMs);
2945
- }
2946
-
2947
- // src/extensions/image/commands.ts
2948
- function createImageCommands(tool) {
2949
- return [
2950
- {
2951
- command: "addImage",
2952
- id: "addImage",
2953
- title: "Add Image",
2954
- handler: async (url, options) => {
2955
- const result = await tool.upsertImageEntry(url, {
2956
- mode: "add",
2957
- addOptions: options
2958
- });
2959
- return result.id;
2960
- }
2961
- },
2962
- {
2963
- command: "upsertImage",
2964
- id: "upsertImage",
2965
- title: "Upsert Image",
2966
- handler: async (url, options = {}) => {
2967
- return await tool.upsertImageEntry(url, options);
2968
- }
2969
- },
2970
- {
2971
- command: "getWorkingImages",
2972
- id: "getWorkingImages",
2973
- title: "Get Working Images",
2974
- handler: () => {
2975
- return tool.cloneItems(tool.workingItems);
2976
- }
2977
- },
2978
- {
2979
- command: "setWorkingImage",
2980
- id: "setWorkingImage",
2981
- title: "Set Working Image",
2982
- handler: (id, updates) => {
2983
- tool.updateImageInWorking(id, updates);
2984
- }
2985
- },
2986
- {
2987
- command: "resetWorkingImages",
2988
- id: "resetWorkingImages",
2989
- title: "Reset Working Images",
2990
- handler: () => {
2991
- tool.workingItems = tool.cloneItems(tool.items);
2992
- tool.hasWorkingChanges = false;
2993
- tool.updateImages();
2994
- tool.emitWorkingChange();
2995
- }
2996
- },
2997
- {
2998
- command: "completeImages",
2999
- id: "completeImages",
3000
- title: "Complete Images",
3001
- handler: async () => {
3002
- return await tool.commitWorkingImagesAsCropped();
3003
- }
3004
- },
3005
- {
3006
- command: "exportUserCroppedImage",
3007
- id: "exportUserCroppedImage",
3008
- title: "Export User Cropped Image",
3009
- handler: async (options = {}) => {
3010
- return await tool.exportUserCroppedImage(options);
3011
- }
3012
- },
3013
- {
3014
- command: "fitImageToArea",
3015
- id: "fitImageToArea",
3016
- title: "Fit Image to Area",
3017
- handler: async (id, area) => {
3018
- await tool.fitImageToArea(id, area);
3019
- }
3020
- },
3021
- {
3022
- command: "fitImageToDefaultArea",
3023
- id: "fitImageToDefaultArea",
3024
- title: "Fit Image to Default Area",
3025
- handler: async (id) => {
3026
- await tool.fitImageToDefaultArea(id);
3027
- }
3028
- },
3029
- {
3030
- command: "focusImage",
3031
- id: "focusImage",
3032
- title: "Focus Image",
3033
- handler: (id, options = {}) => {
3034
- return tool.setImageFocus(id, options);
2885
+ const center = new import_paper.default.Point(x, y);
2886
+ const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
2887
+ const path = hasMultipleSubPaths ? new import_paper.default.CompoundPath(pathData) : (() => {
2888
+ const single = new import_paper.default.Path();
2889
+ single.pathData = pathData;
2890
+ return single;
2891
+ })();
2892
+ const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
2893
+ const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
2894
+ if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
2895
+ const targetLeft = x - width / 2;
2896
+ const targetTop = y - height / 2;
2897
+ path.scale(width / sourceWidth, height / sourceHeight, new import_paper.default.Point(0, 0));
2898
+ path.translate(new import_paper.default.Point(targetLeft, targetTop));
2899
+ return path;
2900
+ }
2901
+ if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
2902
+ path.position = center;
2903
+ path.scale(width / path.bounds.width, height / path.bounds.height);
2904
+ return path;
2905
+ }
2906
+ path.position = center;
2907
+ return path;
2908
+ }
2909
+ function createBaseShape(options) {
2910
+ const { shape } = options;
2911
+ if (shape === "custom") {
2912
+ const customShape = createCustomBaseShape(options);
2913
+ if (customShape) return customShape;
2914
+ return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
2915
+ }
2916
+ return BUILTIN_SHAPE_BUILDERS[shape](options);
2917
+ }
2918
+ function resolveBridgeBasePath(shape, anchor) {
2919
+ if (shape instanceof import_paper.default.Path) {
2920
+ return shape;
2921
+ }
2922
+ if (shape instanceof import_paper.default.CompoundPath) {
2923
+ const children = (shape.children || []).filter(
2924
+ (child) => child instanceof import_paper.default.Path
2925
+ );
2926
+ if (!children.length) return null;
2927
+ let best = children[0];
2928
+ let bestDistance = Infinity;
2929
+ for (const child of children) {
2930
+ const location = child.getNearestLocation(anchor);
2931
+ const point = location == null ? void 0 : location.point;
2932
+ if (!point) continue;
2933
+ const distance = point.getDistance(anchor);
2934
+ if (distance < bestDistance) {
2935
+ bestDistance = distance;
2936
+ best = child;
3035
2937
  }
3036
- },
3037
- {
3038
- command: "removeImage",
3039
- id: "removeImage",
3040
- title: "Remove Image",
3041
- handler: (id) => {
3042
- const removed = tool.items.find((item) => item.id === id);
3043
- const next = tool.items.filter((item) => item.id !== id);
3044
- if (next.length !== tool.items.length) {
3045
- tool.purgeSourceSizeCacheForItem(removed);
3046
- if (tool.focusedImageId === id) {
3047
- tool.setImageFocus(null, {
3048
- syncCanvasSelection: true,
3049
- skipRender: true
2938
+ }
2939
+ return best;
2940
+ }
2941
+ return null;
2942
+ }
2943
+ function createFeatureItem(feature, center) {
2944
+ let item;
2945
+ if (feature.shape === "rect") {
2946
+ const w = feature.width || 10;
2947
+ const h = feature.height || 10;
2948
+ const r = feature.radius || 0;
2949
+ item = new import_paper.default.Path.Rectangle({
2950
+ point: [center.x - w / 2, center.y - h / 2],
2951
+ size: [w, h],
2952
+ radius: r
2953
+ });
2954
+ } else {
2955
+ const r = feature.radius || 5;
2956
+ item = new import_paper.default.Path.Circle({
2957
+ center,
2958
+ radius: r
2959
+ });
2960
+ }
2961
+ if (feature.rotation) {
2962
+ item.rotate(feature.rotation, center);
2963
+ }
2964
+ return item;
2965
+ }
2966
+ function getPerimeterShape(options) {
2967
+ let mainShape = createBaseShape(options);
2968
+ const { features } = options;
2969
+ if (features && features.length > 0) {
2970
+ const edgeFeatures = features.filter(
2971
+ (f) => !f.renderBehavior || f.renderBehavior === "edge"
2972
+ );
2973
+ const adds = [];
2974
+ const subtracts = [];
2975
+ edgeFeatures.forEach((f) => {
2976
+ const pos = resolveFeaturePosition(f, options);
2977
+ const center = new import_paper.default.Point(pos.x, pos.y);
2978
+ const item = createFeatureItem(f, center);
2979
+ if (f.bridge && f.bridge.type === "vertical") {
2980
+ const itemBounds = item.bounds;
2981
+ const mainBounds = mainShape.bounds;
2982
+ const bridgeTop = mainBounds.top;
2983
+ const bridgeBottom = itemBounds.top;
2984
+ if (bridgeBottom > bridgeTop) {
2985
+ const overlap = 2;
2986
+ const rayPadding = 10;
2987
+ const eps = 0.1;
2988
+ const delta = getBridgeDelta(itemBounds, overlap);
2989
+ const toY = bridgeTop - rayPadding;
2990
+ const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
2991
+ const xLeft = itemBounds.left + inset;
2992
+ const xRight = itemBounds.right - inset;
2993
+ const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
2994
+ const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
2995
+ if (canBridge && bridgeBasePath) {
2996
+ const leftHit = getExitHit({
2997
+ mainShape: bridgeBasePath,
2998
+ x: xLeft,
2999
+ bridgeBottom,
3000
+ toY,
3001
+ eps,
3002
+ delta,
3003
+ overlap,
3004
+ op: f.operation
3005
+ });
3006
+ const rightHit = getExitHit({
3007
+ mainShape: bridgeBasePath,
3008
+ x: xRight,
3009
+ bridgeBottom,
3010
+ toY,
3011
+ eps,
3012
+ delta,
3013
+ overlap,
3014
+ op: f.operation
3050
3015
  });
3016
+ if (leftHit && rightHit) {
3017
+ const pathLength = bridgeBasePath.length;
3018
+ const leftOffset = leftHit.location.offset;
3019
+ const rightOffset = rightHit.location.offset;
3020
+ const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
3021
+ const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
3022
+ const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
3023
+ const offsetsA = sampleWrappedOffsets(
3024
+ pathLength,
3025
+ leftOffset,
3026
+ rightOffset,
3027
+ countFor(distanceA)
3028
+ );
3029
+ const offsetsB = sampleWrappedOffsets(
3030
+ pathLength,
3031
+ rightOffset,
3032
+ leftOffset,
3033
+ countFor(distanceB)
3034
+ );
3035
+ const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
3036
+ const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
3037
+ if (pointsA.length >= 2 && pointsB.length >= 2) {
3038
+ let topBase = selectOuterChain({
3039
+ mainShape: bridgeBasePath,
3040
+ pointsA,
3041
+ pointsB,
3042
+ delta,
3043
+ overlap,
3044
+ op: f.operation
3045
+ });
3046
+ const dist2 = (a, b) => {
3047
+ const dx = a.x - b.x;
3048
+ const dy = a.y - b.y;
3049
+ return dx * dx + dy * dy;
3050
+ };
3051
+ if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
3052
+ topBase = topBase.slice().reverse();
3053
+ }
3054
+ topBase = topBase.slice();
3055
+ topBase[0] = leftHit.point;
3056
+ topBase[topBase.length - 1] = rightHit.point;
3057
+ const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
3058
+ const topPoints = topBase.map(
3059
+ (p) => p.add(new import_paper.default.Point(0, capShiftY))
3060
+ );
3061
+ const bridgeBottomY = bridgeBottom + overlap * 2;
3062
+ const bridgePoly = new import_paper.default.Path({ insert: false });
3063
+ for (const p of topPoints) bridgePoly.add(p);
3064
+ bridgePoly.add(new import_paper.default.Point(xRight, bridgeBottomY));
3065
+ bridgePoly.add(new import_paper.default.Point(xLeft, bridgeBottomY));
3066
+ bridgePoly.closed = true;
3067
+ const unitedItem = item.unite(bridgePoly);
3068
+ item.remove();
3069
+ bridgePoly.remove();
3070
+ if (f.operation === "add") {
3071
+ adds.push(unitedItem);
3072
+ } else {
3073
+ subtracts.push(unitedItem);
3074
+ }
3075
+ return;
3076
+ }
3077
+ }
3078
+ }
3079
+ if (f.operation === "add") {
3080
+ adds.push(item);
3081
+ } else {
3082
+ subtracts.push(item);
3083
+ }
3084
+ } else {
3085
+ if (f.operation === "add") {
3086
+ adds.push(item);
3087
+ } else {
3088
+ subtracts.push(item);
3051
3089
  }
3052
- tool.updateConfig(next);
3090
+ }
3091
+ } else {
3092
+ if (f.operation === "add") {
3093
+ adds.push(item);
3094
+ } else {
3095
+ subtracts.push(item);
3053
3096
  }
3054
3097
  }
3055
- },
3056
- {
3057
- command: "updateImage",
3058
- id: "updateImage",
3059
- title: "Update Image",
3060
- handler: async (id, updates, options = {}) => {
3061
- await tool.updateImage(id, updates, options);
3062
- }
3063
- },
3064
- {
3065
- command: "clearImages",
3066
- id: "clearImages",
3067
- title: "Clear Images",
3068
- handler: () => {
3069
- tool.sourceSizeCache.clear();
3070
- tool.setImageFocus(null, {
3071
- syncCanvasSelection: true,
3072
- skipRender: true
3073
- });
3074
- tool.updateConfig([]);
3075
- }
3076
- },
3077
- {
3078
- command: "bringToFront",
3079
- id: "bringToFront",
3080
- title: "Bring Image to Front",
3081
- handler: (id) => {
3082
- const index = tool.items.findIndex((item) => item.id === id);
3083
- if (index !== -1 && index < tool.items.length - 1) {
3084
- const next = [...tool.items];
3085
- const [item] = next.splice(index, 1);
3086
- next.push(item);
3087
- tool.updateConfig(next);
3098
+ });
3099
+ if (adds.length > 0) {
3100
+ for (const item of adds) {
3101
+ try {
3102
+ const temp = mainShape.unite(item);
3103
+ mainShape.remove();
3104
+ item.remove();
3105
+ mainShape = normalizePathItem(temp);
3106
+ } catch (e) {
3107
+ console.error("Geometry: Failed to unite feature", e);
3108
+ item.remove();
3088
3109
  }
3089
3110
  }
3090
- },
3091
- {
3092
- command: "sendToBack",
3093
- id: "sendToBack",
3094
- title: "Send Image to Back",
3095
- handler: (id) => {
3096
- const index = tool.items.findIndex((item) => item.id === id);
3097
- if (index > 0) {
3098
- const next = [...tool.items];
3099
- const [item] = next.splice(index, 1);
3100
- next.unshift(item);
3101
- tool.updateConfig(next);
3111
+ }
3112
+ if (subtracts.length > 0) {
3113
+ for (const item of subtracts) {
3114
+ try {
3115
+ const temp = mainShape.subtract(item);
3116
+ mainShape.remove();
3117
+ item.remove();
3118
+ mainShape = normalizePathItem(temp);
3119
+ } catch (e) {
3120
+ console.error("Geometry: Failed to subtract feature", e);
3121
+ item.remove();
3102
3122
  }
3103
3123
  }
3104
3124
  }
3105
- ];
3125
+ }
3126
+ return mainShape;
3127
+ }
3128
+ function applySurfaceFeatures(shape, features, options) {
3129
+ const surfaceFeatures = features.filter(
3130
+ (f) => f.renderBehavior === "surface"
3131
+ );
3132
+ if (surfaceFeatures.length === 0) return shape;
3133
+ let result = shape;
3134
+ for (const f of surfaceFeatures) {
3135
+ const pos = resolveFeaturePosition(f, options);
3136
+ const center = new import_paper.default.Point(pos.x, pos.y);
3137
+ const item = createFeatureItem(f, center);
3138
+ try {
3139
+ if (f.operation === "add") {
3140
+ const temp = result.unite(item);
3141
+ result.remove();
3142
+ item.remove();
3143
+ result = normalizePathItem(temp);
3144
+ } else {
3145
+ const temp = result.subtract(item);
3146
+ result.remove();
3147
+ item.remove();
3148
+ result = normalizePathItem(temp);
3149
+ }
3150
+ } catch (e) {
3151
+ console.error("Geometry: Failed to apply surface feature", e);
3152
+ item.remove();
3153
+ }
3154
+ }
3155
+ return result;
3156
+ }
3157
+ function generateDielinePath(options) {
3158
+ const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
3159
+ const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
3160
+ ensurePaper(paperWidth, paperHeight);
3161
+ import_paper.default.project.activeLayer.removeChildren();
3162
+ const perimeter = getPerimeterShape(options);
3163
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
3164
+ const pathData = finalShape.pathData;
3165
+ finalShape.remove();
3166
+ return pathData;
3167
+ }
3168
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
3169
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
3170
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
3171
+ ensurePaper(paperWidth, paperHeight);
3172
+ import_paper.default.project.activeLayer.removeChildren();
3173
+ const pOriginal = getPerimeterShape(originalOptions);
3174
+ const shapeOriginal = applySurfaceFeatures(
3175
+ pOriginal,
3176
+ originalOptions.features,
3177
+ originalOptions
3178
+ );
3179
+ const pOffset = getPerimeterShape(offsetOptions);
3180
+ const shapeOffset = applySurfaceFeatures(
3181
+ pOffset,
3182
+ offsetOptions.features,
3183
+ offsetOptions
3184
+ );
3185
+ let bleedZone;
3186
+ if (offset > 0) {
3187
+ bleedZone = shapeOffset.subtract(shapeOriginal);
3188
+ } else {
3189
+ bleedZone = shapeOriginal.subtract(shapeOffset);
3190
+ }
3191
+ const pathData = bleedZone.pathData;
3192
+ shapeOriginal.remove();
3193
+ shapeOffset.remove();
3194
+ bleedZone.remove();
3195
+ return pathData;
3196
+ }
3197
+ function getLowestPointOnDieline(options) {
3198
+ ensurePaper(options.width * 2, options.height * 2);
3199
+ import_paper.default.project.activeLayer.removeChildren();
3200
+ const shape = createBaseShape(options);
3201
+ const bounds = shape.bounds;
3202
+ const result = {
3203
+ x: bounds.center.x,
3204
+ y: bounds.bottom
3205
+ };
3206
+ shape.remove();
3207
+ return result;
3208
+ }
3209
+ function getNearestPointOnDieline(point, options) {
3210
+ ensurePaper(options.width * 2, options.height * 2);
3211
+ import_paper.default.project.activeLayer.removeChildren();
3212
+ const shape = createBaseShape(options);
3213
+ const p = new import_paper.default.Point(point.x, point.y);
3214
+ const location = shape.getNearestLocation(p);
3215
+ const result = {
3216
+ x: location.point.x,
3217
+ y: location.point.y,
3218
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
3219
+ };
3220
+ shape.remove();
3221
+ return result;
3106
3222
  }
3107
3223
 
3108
- // src/extensions/image/config.ts
3109
- function createImageConfigurations() {
3224
+ // src/extensions/image/sessionOverlay.ts
3225
+ var EPSILON = 1e-4;
3226
+ var SHAPE_OUTLINE_COLOR = "rgba(255, 0, 0, 0.9)";
3227
+ var DEFAULT_HATCH_FILL = "rgba(255, 0, 0, 0.22)";
3228
+ function buildRectPath(width, height) {
3229
+ return `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`;
3230
+ }
3231
+ function buildViewportMaskPath(viewport, cutRect) {
3232
+ const cutLeft = cutRect.left - viewport.left;
3233
+ const cutTop = cutRect.top - viewport.top;
3110
3234
  return [
3111
- {
3112
- id: "image.items",
3113
- type: "array",
3114
- label: "Images",
3115
- default: []
3116
- },
3117
- {
3118
- id: "image.debug",
3119
- type: "boolean",
3120
- label: "Image Debug Log",
3121
- default: false
3122
- },
3123
- {
3124
- id: "image.control.cornerSize",
3125
- type: "number",
3126
- label: "Image Control Corner Size",
3127
- min: 4,
3128
- max: 64,
3129
- step: 1,
3130
- default: 14
3131
- },
3132
- {
3133
- id: "image.control.touchCornerSize",
3134
- type: "number",
3135
- label: "Image Control Touch Corner Size",
3136
- min: 8,
3137
- max: 96,
3138
- step: 1,
3139
- default: 24
3140
- },
3141
- {
3142
- id: "image.control.cornerStyle",
3143
- type: "select",
3144
- label: "Image Control Corner Style",
3145
- options: ["circle", "rect"],
3146
- default: "circle"
3147
- },
3148
- {
3149
- id: "image.control.cornerColor",
3150
- type: "color",
3151
- label: "Image Control Corner Color",
3152
- default: "#ffffff"
3153
- },
3154
- {
3155
- id: "image.control.cornerStrokeColor",
3156
- type: "color",
3157
- label: "Image Control Corner Stroke Color",
3158
- default: "#1677ff"
3159
- },
3160
- {
3161
- id: "image.control.transparentCorners",
3162
- type: "boolean",
3163
- label: "Image Control Transparent Corners",
3164
- default: false
3165
- },
3166
- {
3167
- id: "image.control.borderColor",
3168
- type: "color",
3169
- label: "Image Control Border Color",
3170
- default: "#1677ff"
3171
- },
3172
- {
3173
- id: "image.control.borderScaleFactor",
3174
- type: "number",
3175
- label: "Image Control Border Width",
3176
- min: 0.5,
3177
- max: 8,
3178
- step: 0.1,
3179
- default: 1.5
3180
- },
3181
- {
3182
- id: "image.control.padding",
3183
- type: "number",
3184
- label: "Image Control Padding",
3185
- min: 0,
3186
- max: 64,
3187
- step: 1,
3188
- default: 0
3189
- },
3190
- {
3191
- id: "image.frame.strokeColor",
3192
- type: "color",
3193
- label: "Image Frame Stroke Color",
3194
- default: "#808080"
3195
- },
3196
- {
3197
- id: "image.frame.strokeWidth",
3198
- type: "number",
3199
- label: "Image Frame Stroke Width",
3200
- min: 0,
3201
- max: 20,
3202
- step: 0.5,
3203
- default: 2
3204
- },
3205
- {
3206
- id: "image.frame.strokeStyle",
3207
- type: "select",
3208
- label: "Image Frame Stroke Style",
3209
- options: ["solid", "dashed", "hidden"],
3210
- default: "dashed"
3211
- },
3212
- {
3213
- id: "image.frame.dashLength",
3214
- type: "number",
3215
- label: "Image Frame Dash Length",
3216
- min: 1,
3217
- max: 40,
3218
- step: 1,
3219
- default: 8
3220
- },
3221
- {
3222
- id: "image.frame.innerBackground",
3223
- type: "color",
3224
- label: "Image Frame Inner Background",
3225
- default: "rgba(0,0,0,0)"
3226
- },
3227
- {
3228
- id: "image.frame.outerBackground",
3229
- type: "color",
3230
- label: "Image Frame Outer Background",
3231
- default: "#f5f5f5"
3235
+ buildRectPath(viewport.width, viewport.height),
3236
+ `M ${cutLeft} ${cutTop} L ${cutLeft + cutRect.width} ${cutTop} L ${cutLeft + cutRect.width} ${cutTop + cutRect.height} L ${cutLeft} ${cutTop + cutRect.height} Z`
3237
+ ].join(" ");
3238
+ }
3239
+ function resolveCutShapeRadiusPx(geometry, cutRect) {
3240
+ const visualRadius = Number.isFinite(geometry.radius) ? Math.max(0, geometry.radius) : 0;
3241
+ const visualOffset = Number.isFinite(geometry.offset) ? geometry.offset : 0;
3242
+ const rawCutRadius = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
3243
+ const maxRadius = Math.max(0, Math.min(cutRect.width, cutRect.height) / 2);
3244
+ return Math.max(0, Math.min(maxRadius, rawCutRadius));
3245
+ }
3246
+ function buildBuiltinShapeOverlayPaths(cutRect, geometry) {
3247
+ if (!geometry || geometry.shape === "custom") {
3248
+ return null;
3249
+ }
3250
+ const radius = resolveCutShapeRadiusPx(geometry, cutRect);
3251
+ if (geometry.shape === "rect" && radius <= EPSILON) {
3252
+ return null;
3253
+ }
3254
+ const shapePathData = generateDielinePath({
3255
+ shape: geometry.shape,
3256
+ shapeStyle: geometry.shapeStyle,
3257
+ width: Math.max(1, cutRect.width),
3258
+ height: Math.max(1, cutRect.height),
3259
+ radius,
3260
+ x: cutRect.width / 2,
3261
+ y: cutRect.height / 2,
3262
+ features: [],
3263
+ canvasWidth: Math.max(1, cutRect.width),
3264
+ canvasHeight: Math.max(1, cutRect.height)
3265
+ });
3266
+ if (!shapePathData) {
3267
+ return null;
3268
+ }
3269
+ return {
3270
+ shapePathData,
3271
+ hatchPathData: `${buildRectPath(cutRect.width, cutRect.height)} ${shapePathData}`
3272
+ };
3273
+ }
3274
+ function buildImageSessionOverlaySpecs(args) {
3275
+ const { viewport, layout, geometry, visual, hatchPattern } = args;
3276
+ const cutRect = layout.cutRect;
3277
+ const specs = [];
3278
+ specs.push({
3279
+ id: "image.cropMask.rect",
3280
+ type: "path",
3281
+ space: "screen",
3282
+ data: { id: "image.cropMask.rect", zIndex: 1 },
3283
+ props: {
3284
+ pathData: buildViewportMaskPath(viewport, cutRect),
3285
+ left: viewport.left,
3286
+ top: viewport.top,
3287
+ originX: "left",
3288
+ originY: "top",
3289
+ fill: visual.outerBackground,
3290
+ stroke: null,
3291
+ fillRule: "evenodd",
3292
+ selectable: false,
3293
+ evented: false,
3294
+ excludeFromExport: true,
3295
+ objectCaching: false
3232
3296
  }
3233
- ];
3297
+ });
3298
+ const shapeOverlay = buildBuiltinShapeOverlayPaths(cutRect, geometry);
3299
+ if (shapeOverlay) {
3300
+ specs.push({
3301
+ id: "image.cropShapeHatch",
3302
+ type: "path",
3303
+ space: "screen",
3304
+ data: { id: "image.cropShapeHatch", zIndex: 5 },
3305
+ props: {
3306
+ pathData: shapeOverlay.hatchPathData,
3307
+ left: cutRect.left,
3308
+ top: cutRect.top,
3309
+ originX: "left",
3310
+ originY: "top",
3311
+ fill: hatchPattern || DEFAULT_HATCH_FILL,
3312
+ opacity: hatchPattern ? 1 : 0.8,
3313
+ stroke: null,
3314
+ fillRule: "evenodd",
3315
+ selectable: false,
3316
+ evented: false,
3317
+ excludeFromExport: true,
3318
+ objectCaching: false
3319
+ }
3320
+ });
3321
+ specs.push({
3322
+ id: "image.cropShapeOutline",
3323
+ type: "path",
3324
+ space: "screen",
3325
+ data: { id: "image.cropShapeOutline", zIndex: 6 },
3326
+ props: {
3327
+ pathData: shapeOverlay.shapePathData,
3328
+ left: cutRect.left,
3329
+ top: cutRect.top,
3330
+ originX: "left",
3331
+ originY: "top",
3332
+ fill: "transparent",
3333
+ stroke: SHAPE_OUTLINE_COLOR,
3334
+ strokeWidth: 1,
3335
+ selectable: false,
3336
+ evented: false,
3337
+ excludeFromExport: true,
3338
+ objectCaching: false
3339
+ }
3340
+ });
3341
+ }
3342
+ specs.push({
3343
+ id: "image.cropFrame",
3344
+ type: "rect",
3345
+ space: "screen",
3346
+ data: { id: "image.cropFrame", zIndex: 7 },
3347
+ props: {
3348
+ left: cutRect.left,
3349
+ top: cutRect.top,
3350
+ width: cutRect.width,
3351
+ height: cutRect.height,
3352
+ originX: "left",
3353
+ originY: "top",
3354
+ fill: visual.innerBackground,
3355
+ stroke: visual.strokeStyle === "hidden" ? "rgba(0,0,0,0)" : visual.strokeColor,
3356
+ strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
3357
+ strokeDashArray: visual.strokeStyle === "dashed" ? [visual.dashLength, visual.dashLength] : void 0,
3358
+ selectable: false,
3359
+ evented: false,
3360
+ excludeFromExport: true
3361
+ }
3362
+ });
3363
+ return specs;
3234
3364
  }
3235
3365
 
3236
3366
  // src/extensions/image/ImageTool.ts
@@ -3692,11 +3822,21 @@ var ImageTool = class {
3692
3822
  (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
3693
3823
  }
3694
3824
  }
3825
+ clearSnapGuideContext() {
3826
+ var _a;
3827
+ const topContext = (_a = this.canvasService) == null ? void 0 : _a.canvas.contextTop;
3828
+ if (!this.canvasService || !topContext) return;
3829
+ this.canvasService.canvas.clearContext(topContext);
3830
+ }
3695
3831
  clearSnapPreview() {
3696
3832
  var _a;
3833
+ const shouldClearCanvas = this.hasRenderedSnapGuides || !!this.activeSnapX || !!this.activeSnapY;
3697
3834
  this.activeSnapX = null;
3698
3835
  this.activeSnapY = null;
3699
3836
  this.hasRenderedSnapGuides = false;
3837
+ if (shouldClearCanvas) {
3838
+ this.clearSnapGuideContext();
3839
+ }
3700
3840
  (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
3701
3841
  }
3702
3842
  endMoveSnapInteraction() {
@@ -4100,9 +4240,6 @@ var ImageTool = class {
4100
4240
  }
4101
4241
  return this.canvasService.toScreenRect(frame || this.getFrameRect());
4102
4242
  }
4103
- toLayoutSceneRect(rect) {
4104
- return toLayoutSceneRect(rect);
4105
- }
4106
4243
  async resolveDefaultFitArea() {
4107
4244
  if (!this.canvasService) return null;
4108
4245
  const frame = this.getFrameRect();
@@ -4229,74 +4366,37 @@ var ImageTool = class {
4229
4366
  outerBackground: this.getConfig("image.frame.outerBackground", "#f5f5f5") || "#f5f5f5"
4230
4367
  };
4231
4368
  }
4232
- toSceneGeometryLike(raw) {
4233
- const shape = raw == null ? void 0 : raw.shape;
4234
- if (!isDielineShape(shape)) {
4369
+ resolveSessionOverlayState() {
4370
+ if (!this.canvasService || !this.context) {
4235
4371
  return null;
4236
4372
  }
4237
- const radiusRaw = Number(raw == null ? void 0 : raw.radius);
4238
- const offsetRaw = Number(raw == null ? void 0 : raw.offset);
4239
- const unit = typeof (raw == null ? void 0 : raw.unit) === "string" ? raw.unit : "px";
4240
- const radius = unit === "scene" || !this.canvasService ? radiusRaw : this.canvasService.toSceneLength(radiusRaw);
4241
- const offset = unit === "scene" || !this.canvasService ? offsetRaw : this.canvasService.toSceneLength(offsetRaw);
4242
- return {
4243
- shape,
4244
- shapeStyle: normalizeShapeStyle(raw == null ? void 0 : raw.shapeStyle),
4245
- radius: Number.isFinite(radius) ? radius : 0,
4246
- offset: Number.isFinite(offset) ? offset : 0
4247
- };
4248
- }
4249
- async resolveSceneGeometryForOverlay() {
4250
- if (!this.context) return null;
4251
- const commandService = this.context.services.get("CommandService");
4252
- if (commandService) {
4253
- try {
4254
- const raw = await Promise.resolve(
4255
- commandService.executeCommand("getSceneGeometry")
4256
- );
4257
- const geometry2 = this.toSceneGeometryLike(raw);
4258
- if (geometry2) {
4259
- this.debug("overlay:sceneGeometry:command", geometry2);
4260
- return geometry2;
4261
- }
4262
- this.debug("overlay:sceneGeometry:command:invalid", { raw });
4263
- } catch (error) {
4264
- this.debug("overlay:sceneGeometry:command:error", {
4265
- error: error instanceof Error ? error.message : String(error)
4266
- });
4267
- }
4268
- }
4269
- if (!this.canvasService) return null;
4270
4373
  const configService = this.context.services.get(
4271
4374
  "ConfigurationService"
4272
4375
  );
4273
- if (!configService) return null;
4274
- const sizeState = readSizeState(configService);
4275
- const layout = computeSceneLayout(this.canvasService, sizeState);
4276
- if (!layout) {
4277
- this.debug("overlay:sceneGeometry:fallback:missing-layout");
4376
+ if (!configService) {
4278
4377
  return null;
4279
4378
  }
4280
- const geometry = this.toSceneGeometryLike(
4281
- buildSceneGeometry(configService, layout)
4379
+ const layout = computeSceneLayout(
4380
+ this.canvasService,
4381
+ readSizeState(configService)
4282
4382
  );
4283
- if (geometry) {
4284
- this.debug("overlay:sceneGeometry:fallback", geometry);
4383
+ if (!layout) {
4384
+ this.debug("overlay:layout:missing");
4385
+ return null;
4285
4386
  }
4286
- return geometry;
4287
- }
4288
- resolveCutShapeRadius(geometry, frame) {
4289
- const visualRadius = Number.isFinite(geometry.radius) ? Math.max(0, geometry.radius) : 0;
4290
- const visualOffset = Number.isFinite(geometry.offset) ? geometry.offset : 0;
4291
- const rawCutRadius = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
4292
- const maxRadius = Math.max(0, Math.min(frame.width, frame.height) / 2);
4293
- return Math.max(0, Math.min(maxRadius, rawCutRadius));
4387
+ const geometry = buildSceneGeometry(configService, layout);
4388
+ this.debug("overlay:state:resolved", {
4389
+ cutRect: layout.cutRect,
4390
+ shape: geometry.shape,
4391
+ shapeStyle: geometry.shapeStyle,
4392
+ radius: geometry.radius,
4393
+ offset: geometry.offset
4394
+ });
4395
+ return { layout, geometry };
4294
4396
  }
4295
4397
  getCropShapeHatchPattern(color = "rgba(255, 0, 0, 0.6)") {
4296
- var _a;
4297
4398
  if (typeof document === "undefined") return void 0;
4298
- const sceneScale = ((_a = this.canvasService) == null ? void 0 : _a.getSceneScale()) || 1;
4299
- const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
4399
+ const cacheKey = color;
4300
4400
  if (this.cropShapeHatchPattern && this.cropShapeHatchPatternColor === color && this.cropShapeHatchPatternKey === cacheKey) {
4301
4401
  return this.cropShapeHatchPattern;
4302
4402
  }
@@ -4326,138 +4426,11 @@ var ImageTool = class {
4326
4426
  // @ts-ignore: Fabric Pattern accepts canvas source here.
4327
4427
  repetition: "repeat"
4328
4428
  });
4329
- pattern.patternTransform = [
4330
- 1 / sceneScale,
4331
- 0,
4332
- 0,
4333
- 1 / sceneScale,
4334
- 0,
4335
- 0
4336
- ];
4337
4429
  this.cropShapeHatchPattern = pattern;
4338
4430
  this.cropShapeHatchPatternColor = color;
4339
4431
  this.cropShapeHatchPatternKey = cacheKey;
4340
4432
  return pattern;
4341
4433
  }
4342
- buildCropShapeOverlaySpecs(frame, sceneGeometry) {
4343
- var _a, _b;
4344
- if (!sceneGeometry) {
4345
- this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
4346
- return [];
4347
- }
4348
- if (sceneGeometry.shape === "custom") {
4349
- this.debug("overlay:shape:skip", { reason: "shape-custom" });
4350
- return [];
4351
- }
4352
- const shape = sceneGeometry.shape;
4353
- const shapeStyle = sceneGeometry.shapeStyle;
4354
- const inset = 0;
4355
- const shapeWidth = Math.max(1, frame.width);
4356
- const shapeHeight = Math.max(1, frame.height);
4357
- const radius = this.resolveCutShapeRadius(sceneGeometry, frame);
4358
- this.debug("overlay:shape:geometry", {
4359
- shape,
4360
- frameWidth: frame.width,
4361
- frameHeight: frame.height,
4362
- offset: sceneGeometry.offset,
4363
- shapeStyle,
4364
- inset,
4365
- shapeWidth,
4366
- shapeHeight,
4367
- baseRadius: sceneGeometry.radius,
4368
- radius
4369
- });
4370
- const isSameAsFrame = Math.abs(shapeWidth - frame.width) <= 1e-4 && Math.abs(shapeHeight - frame.height) <= 1e-4;
4371
- if (shape === "rect" && radius <= 1e-4 && isSameAsFrame) {
4372
- this.debug("overlay:shape:skip", {
4373
- reason: "shape-rect-no-radius"
4374
- });
4375
- return [];
4376
- }
4377
- const baseOptions = {
4378
- shape,
4379
- width: shapeWidth,
4380
- height: shapeHeight,
4381
- radius,
4382
- x: frame.width / 2,
4383
- y: frame.height / 2,
4384
- features: [],
4385
- shapeStyle,
4386
- canvasWidth: frame.width,
4387
- canvasHeight: frame.height
4388
- };
4389
- try {
4390
- const shapePathData = generateDielinePath(baseOptions);
4391
- const outerRectPathData = `M 0 0 L ${frame.width} 0 L ${frame.width} ${frame.height} L 0 ${frame.height} Z`;
4392
- const hatchPathData = `${outerRectPathData} ${shapePathData}`;
4393
- if (!shapePathData || !hatchPathData) {
4394
- this.debug("overlay:shape:skip", {
4395
- reason: "path-generation-empty",
4396
- shape,
4397
- radius
4398
- });
4399
- return [];
4400
- }
4401
- const patternFill = this.getCropShapeHatchPattern();
4402
- const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
4403
- const shapeBounds = getPathBounds(shapePathData);
4404
- const hatchBounds = getPathBounds(hatchPathData);
4405
- const frameRect = this.toLayoutSceneRect(frame);
4406
- const hatchPathLength = hatchPathData.length;
4407
- const shapePathLength = shapePathData.length;
4408
- const specs = [
4409
- {
4410
- id: "image.cropShapeHatch",
4411
- type: "path",
4412
- data: { id: "image.cropShapeHatch", zIndex: 5 },
4413
- layout: {
4414
- reference: "custom",
4415
- referenceRect: frameRect,
4416
- alignX: "start",
4417
- alignY: "start",
4418
- offsetX: hatchBounds.x,
4419
- offsetY: hatchBounds.y
4420
- },
4421
- props: {
4422
- pathData: hatchPathData,
4423
- originX: "left",
4424
- originY: "top",
4425
- fill: hatchFill,
4426
- opacity: patternFill ? 1 : 0.8,
4427
- stroke: "rgba(255, 0, 0, 0.9)",
4428
- strokeWidth: (_b = (_a = this.canvasService) == null ? void 0 : _a.toSceneLength(1)) != null ? _b : 1,
4429
- fillRule: "evenodd",
4430
- selectable: false,
4431
- evented: false,
4432
- excludeFromExport: true,
4433
- objectCaching: false
4434
- }
4435
- }
4436
- ];
4437
- this.debug("overlay:shape:built", {
4438
- shape,
4439
- radius,
4440
- inset,
4441
- shapeWidth,
4442
- shapeHeight,
4443
- fillRule: "evenodd",
4444
- shapePathLength,
4445
- hatchPathLength,
4446
- shapeBounds,
4447
- hatchBounds,
4448
- hatchFillType: hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
4449
- ids: specs.map((spec) => spec.id)
4450
- });
4451
- return specs;
4452
- } catch (error) {
4453
- this.debug("overlay:shape:error", {
4454
- shape,
4455
- radius,
4456
- error: error instanceof Error ? error.message : String(error)
4457
- });
4458
- return [];
4459
- }
4460
- }
4461
4434
  resolveRenderImageState(item) {
4462
4435
  var _a;
4463
4436
  const active = this.isToolActive;
@@ -4539,172 +4512,35 @@ var ImageTool = class {
4539
4512
  }
4540
4513
  return specs;
4541
4514
  }
4542
- buildOverlaySpecs(frame, sceneGeometry) {
4515
+ buildOverlaySpecs(overlayState) {
4543
4516
  const visible = this.isImageEditingVisible();
4544
- if (!visible || frame.width <= 0 || frame.height <= 0 || !this.canvasService) {
4517
+ if (!visible || !overlayState || !this.canvasService) {
4545
4518
  this.debug("overlay:hidden", {
4546
4519
  visible,
4547
- frame,
4520
+ cutRect: overlayState == null ? void 0 : overlayState.layout.cutRect,
4548
4521
  isToolActive: this.isToolActive,
4549
4522
  isImageSelectionActive: this.isImageSelectionActive,
4550
4523
  focusedImageId: this.focusedImageId
4551
4524
  });
4552
4525
  return [];
4553
4526
  }
4554
- const viewport = this.canvasService.getSceneViewportRect();
4555
- const canvasW = viewport.width || 0;
4556
- const canvasH = viewport.height || 0;
4557
- const canvasLeft = viewport.left || 0;
4558
- const canvasTop = viewport.top || 0;
4527
+ const viewport = this.canvasService.getScreenViewportRect();
4559
4528
  const visual = this.getFrameVisualConfig();
4560
- const strokeWidthScene = this.canvasService.toSceneLength(
4561
- visual.strokeWidth
4562
- );
4563
- const dashLengthScene = this.canvasService.toSceneLength(visual.dashLength);
4564
- const frameLeft = Math.max(
4565
- canvasLeft,
4566
- Math.min(canvasLeft + canvasW, frame.left)
4567
- );
4568
- const frameTop = Math.max(
4569
- canvasTop,
4570
- Math.min(canvasTop + canvasH, frame.top)
4571
- );
4572
- const frameRight = Math.max(
4573
- frameLeft,
4574
- Math.min(canvasLeft + canvasW, frame.left + frame.width)
4575
- );
4576
- const frameBottom = Math.max(
4577
- frameTop,
4578
- Math.min(canvasTop + canvasH, frame.top + frame.height)
4579
- );
4580
- const visibleFrameH = Math.max(0, frameBottom - frameTop);
4581
- const topH = Math.max(0, frameTop - canvasTop);
4582
- const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
4583
- const leftW = Math.max(0, frameLeft - canvasLeft);
4584
- const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
4585
- const viewportRect = this.toLayoutSceneRect({
4586
- left: canvasLeft,
4587
- top: canvasTop,
4588
- width: canvasW,
4589
- height: canvasH
4590
- });
4591
- const visibleFrameBandRect = this.toLayoutSceneRect({
4592
- left: canvasLeft,
4593
- top: frameTop,
4594
- width: canvasW,
4595
- height: visibleFrameH
4596
- });
4597
- const frameRect = this.toLayoutSceneRect(frame);
4598
- const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
4599
- const mask = [
4600
- {
4601
- id: "image.cropMask.top",
4602
- type: "rect",
4603
- data: { id: "image.cropMask.top", zIndex: 1 },
4604
- layout: {
4605
- reference: "custom",
4606
- referenceRect: viewportRect,
4607
- alignX: "start",
4608
- alignY: "start",
4609
- width: "100%",
4610
- height: topH
4611
- },
4612
- props: {
4613
- originX: "left",
4614
- originY: "top",
4615
- fill: visual.outerBackground,
4616
- selectable: false,
4617
- evented: false
4618
- }
4619
- },
4620
- {
4621
- id: "image.cropMask.bottom",
4622
- type: "rect",
4623
- data: { id: "image.cropMask.bottom", zIndex: 2 },
4624
- layout: {
4625
- reference: "custom",
4626
- referenceRect: viewportRect,
4627
- alignX: "start",
4628
- alignY: "end",
4629
- width: "100%",
4630
- height: bottomH
4631
- },
4632
- props: {
4633
- originX: "left",
4634
- originY: "top",
4635
- fill: visual.outerBackground,
4636
- selectable: false,
4637
- evented: false
4638
- }
4639
- },
4640
- {
4641
- id: "image.cropMask.left",
4642
- type: "rect",
4643
- data: { id: "image.cropMask.left", zIndex: 3 },
4644
- layout: {
4645
- reference: "custom",
4646
- referenceRect: visibleFrameBandRect,
4647
- alignX: "start",
4648
- alignY: "start",
4649
- width: leftW,
4650
- height: "100%"
4651
- },
4652
- props: {
4653
- originX: "left",
4654
- originY: "top",
4655
- fill: visual.outerBackground,
4656
- selectable: false,
4657
- evented: false
4658
- }
4659
- },
4660
- {
4661
- id: "image.cropMask.right",
4662
- type: "rect",
4663
- data: { id: "image.cropMask.right", zIndex: 4 },
4664
- layout: {
4665
- reference: "custom",
4666
- referenceRect: visibleFrameBandRect,
4667
- alignX: "end",
4668
- alignY: "start",
4669
- width: rightW,
4670
- height: "100%"
4671
- },
4672
- props: {
4673
- originX: "left",
4674
- originY: "top",
4675
- fill: visual.outerBackground,
4676
- selectable: false,
4677
- evented: false
4678
- }
4679
- }
4680
- ];
4681
- const frameSpec = {
4682
- id: "image.cropFrame",
4683
- type: "rect",
4684
- data: { id: "image.cropFrame", zIndex: 7 },
4685
- layout: {
4686
- reference: "custom",
4687
- referenceRect: frameRect,
4688
- alignX: "start",
4689
- alignY: "start",
4690
- width: "100%",
4691
- height: "100%"
4529
+ const specs = buildImageSessionOverlaySpecs({
4530
+ viewport: {
4531
+ left: viewport.left,
4532
+ top: viewport.top,
4533
+ width: viewport.width,
4534
+ height: viewport.height
4692
4535
  },
4693
- props: {
4694
- originX: "left",
4695
- originY: "top",
4696
- fill: visual.innerBackground,
4697
- stroke: visual.strokeStyle === "hidden" ? "rgba(0,0,0,0)" : visual.strokeColor,
4698
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
4699
- strokeDashArray: visual.strokeStyle === "dashed" ? [dashLengthScene, dashLengthScene] : void 0,
4700
- selectable: false,
4701
- evented: false
4702
- }
4703
- };
4704
- const specs = shapeOverlay.length > 0 ? [...mask, ...shapeOverlay] : [...mask, ...shapeOverlay, frameSpec];
4536
+ layout: overlayState.layout,
4537
+ geometry: overlayState.geometry,
4538
+ visual,
4539
+ hatchPattern: this.getCropShapeHatchPattern()
4540
+ });
4705
4541
  this.debug("overlay:built", {
4706
- frame,
4707
- shape: sceneGeometry == null ? void 0 : sceneGeometry.shape,
4542
+ cutRect: overlayState.layout.cutRect,
4543
+ shape: overlayState.geometry.shape,
4708
4544
  overlayIds: specs.map((spec) => {
4709
4545
  var _a;
4710
4546
  return {
@@ -4733,10 +4569,9 @@ var ImageTool = class {
4733
4569
  }
4734
4570
  const imageSpecs = await this.buildImageSpecs(renderItems, frame);
4735
4571
  if (seq !== this.renderSeq) return;
4736
- const sceneGeometry = await this.resolveSceneGeometryForOverlay();
4737
- if (seq !== this.renderSeq) return;
4572
+ const overlayState = this.resolveSessionOverlayState();
4738
4573
  this.imageSpecs = imageSpecs;
4739
- this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
4574
+ this.overlaySpecs = this.buildOverlaySpecs(overlayState);
4740
4575
  await this.canvasService.flushRenderFromProducers();
4741
4576
  if (seq !== this.renderSeq) return;
4742
4577
  this.refreshImageObjectInteractionState();
@@ -5986,6 +5821,13 @@ function buildDielineRenderBundle(options) {
5986
5821
  canvasWidth,
5987
5822
  canvasHeight
5988
5823
  };
5824
+ const cutFrameRect = {
5825
+ left: cx - cutW / 2,
5826
+ top: cy - cutH / 2,
5827
+ width: cutW,
5828
+ height: cutH,
5829
+ space: "screen"
5830
+ };
5989
5831
  const specs = [];
5990
5832
  if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)" && !hasImages) {
5991
5833
  specs.push({
@@ -6107,9 +5949,13 @@ function buildDielineRenderBundle(options) {
6107
5949
  width: cutW,
6108
5950
  height: cutH,
6109
5951
  radius: cutR,
6110
- x: cx,
6111
- y: cy,
6112
- features: cutFeatures
5952
+ // Build the clip path in the cut frame's local coordinates so Fabric
5953
+ // does not have to infer placement from the standalone path bounds.
5954
+ x: cutW / 2,
5955
+ y: cutH / 2,
5956
+ features: cutFeatures,
5957
+ canvasWidth: cutW,
5958
+ canvasHeight: cutH
6113
5959
  });
6114
5960
  if (!clipPathData) {
6115
5961
  return { specs, effects: [] };
@@ -6126,6 +5972,12 @@ function buildDielineRenderBundle(options) {
6126
5972
  id: ids.clipSource,
6127
5973
  type: "path",
6128
5974
  space: "screen",
5975
+ layout: {
5976
+ reference: "custom",
5977
+ referenceRect: cutFrameRect,
5978
+ alignX: "start",
5979
+ alignY: "start"
5980
+ },
6129
5981
  data: {
6130
5982
  id: ids.clipSource,
6131
5983
  type: "dieline-effect",