@pooder/kit 6.2.0 → 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
2671
- });
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);
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
2431
+ });
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 };
2895
2724
  }
2896
- function resolveCutFrameRect(canvasService, configService) {
2897
- if (!canvasService || !configService) {
2898
- return emptyFrameRect();
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
+ });
2899
2751
  }
2900
- const sizeState = readSizeState(configService);
2901
- const layout = computeSceneLayout(canvasService, sizeState);
2902
- if (!layout) {
2903
- return emptyFrameRect();
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;
2758
+ }
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;
2765
+ }
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
- });
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;
2911
2783
  }
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
- };
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;
2920
2833
  }
2921
-
2922
- // src/shared/runtime/sessionState.ts
2923
- function cloneWithJson(value) {
2924
- return JSON.parse(JSON.stringify(value));
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);
2925
2844
  }
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;
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;
2933
2884
  }
2934
- }
2935
- function runDeferredConfigUpdate(state, action, cooldownMs = 0) {
2936
- state.isUpdatingConfig = true;
2937
- action();
2938
- if (cooldownMs <= 0) {
2939
- state.isUpdatingConfig = false;
2940
- return;
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;
2941
2900
  }
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);
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([]);
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();
3109
+ }
3075
3110
  }
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);
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();
3088
3122
  }
3089
3123
  }
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);
3102
- }
3124
+ }
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);
3103
3149
  }
3150
+ } catch (e) {
3151
+ console.error("Geometry: Failed to apply surface feature", e);
3152
+ item.remove();
3104
3153
  }
3105
- ];
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
3296
+ }
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
3232
3361
  }
3233
- ];
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;
@@ -4503,208 +4476,71 @@ var ImageTool = class {
4503
4476
  selectable: this.isImageEditingVisible(),
4504
4477
  evented: this.isImageEditingVisible(),
4505
4478
  hasControls: this.isImageEditingVisible(),
4506
- hasBorders: this.isImageEditingVisible(),
4507
- opacity: render.opacity
4508
- };
4509
- }
4510
- toSceneObjectScale(value) {
4511
- if (!this.canvasService) return value;
4512
- return value / this.canvasService.getSceneScale();
4513
- }
4514
- getCurrentSrc(obj) {
4515
- var _a;
4516
- if (!obj) return void 0;
4517
- if (typeof obj.getSrc === "function") return obj.getSrc();
4518
- return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
4519
- }
4520
- async buildImageSpecs(items, frame) {
4521
- const specs = [];
4522
- for (const item of items) {
4523
- const render = this.resolveRenderImageState(item);
4524
- if (!render.src) continue;
4525
- const ensured = await this.ensureSourceSize(render.src);
4526
- const sourceSize = ensured || this.getSourceSize(render.src);
4527
- const props = this.computeCanvasProps(render, sourceSize, frame);
4528
- specs.push({
4529
- id: item.id,
4530
- type: "image",
4531
- src: render.src,
4532
- data: {
4533
- id: item.id,
4534
- layerId: IMAGE_OBJECT_LAYER_ID,
4535
- type: "image-item"
4536
- },
4537
- props
4538
- });
4539
- }
4540
- return specs;
4541
- }
4542
- buildOverlaySpecs(frame, sceneGeometry) {
4543
- const visible = this.isImageEditingVisible();
4544
- if (!visible || frame.width <= 0 || frame.height <= 0 || !this.canvasService) {
4545
- this.debug("overlay:hidden", {
4546
- visible,
4547
- frame,
4548
- isToolActive: this.isToolActive,
4549
- isImageSelectionActive: this.isImageSelectionActive,
4550
- focusedImageId: this.focusedImageId
4551
- });
4552
- return [];
4553
- }
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;
4559
- 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%"
4479
+ hasBorders: this.isImageEditingVisible(),
4480
+ opacity: render.opacity
4481
+ };
4482
+ }
4483
+ toSceneObjectScale(value) {
4484
+ if (!this.canvasService) return value;
4485
+ return value / this.canvasService.getSceneScale();
4486
+ }
4487
+ getCurrentSrc(obj) {
4488
+ var _a;
4489
+ if (!obj) return void 0;
4490
+ if (typeof obj.getSrc === "function") return obj.getSrc();
4491
+ return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
4492
+ }
4493
+ async buildImageSpecs(items, frame) {
4494
+ const specs = [];
4495
+ for (const item of items) {
4496
+ const render = this.resolveRenderImageState(item);
4497
+ if (!render.src) continue;
4498
+ const ensured = await this.ensureSourceSize(render.src);
4499
+ const sourceSize = ensured || this.getSourceSize(render.src);
4500
+ const props = this.computeCanvasProps(render, sourceSize, frame);
4501
+ specs.push({
4502
+ id: item.id,
4503
+ type: "image",
4504
+ src: render.src,
4505
+ data: {
4506
+ id: item.id,
4507
+ layerId: IMAGE_OBJECT_LAYER_ID,
4508
+ type: "image-item"
4671
4509
  },
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%"
4510
+ props
4511
+ });
4512
+ }
4513
+ return specs;
4514
+ }
4515
+ buildOverlaySpecs(overlayState) {
4516
+ const visible = this.isImageEditingVisible();
4517
+ if (!visible || !overlayState || !this.canvasService) {
4518
+ this.debug("overlay:hidden", {
4519
+ visible,
4520
+ cutRect: overlayState == null ? void 0 : overlayState.layout.cutRect,
4521
+ isToolActive: this.isToolActive,
4522
+ isImageSelectionActive: this.isImageSelectionActive,
4523
+ focusedImageId: this.focusedImageId
4524
+ });
4525
+ return [];
4526
+ }
4527
+ const viewport = this.canvasService.getScreenViewportRect();
4528
+ const visual = this.getFrameVisualConfig();
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();
@@ -5656,6 +5491,259 @@ function readDielineState(configService, fallback) {
5656
5491
  };
5657
5492
  }
5658
5493
 
5494
+ // src/extensions/constraints.ts
5495
+ var ConstraintRegistry = class {
5496
+ static register(type, handler) {
5497
+ this.handlers.set(type, handler);
5498
+ }
5499
+ static apply(x, y, feature, context, constraints) {
5500
+ const list = constraints || feature.constraints;
5501
+ if (!list || list.length === 0) {
5502
+ return { x, y };
5503
+ }
5504
+ let currentX = x;
5505
+ let currentY = y;
5506
+ for (const constraint of list) {
5507
+ const handler = this.handlers.get(constraint.type);
5508
+ if (handler) {
5509
+ const result = handler(currentX, currentY, feature, context, constraint.params || {});
5510
+ currentX = result.x;
5511
+ currentY = result.y;
5512
+ }
5513
+ }
5514
+ return { x: currentX, y: currentY };
5515
+ }
5516
+ };
5517
+ ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
5518
+ var pathConstraint = (x, y, feature, context, params) => {
5519
+ const { dielineWidth, dielineHeight, geometry } = context;
5520
+ if (!geometry) return { x, y };
5521
+ const minX = geometry.x - geometry.width / 2;
5522
+ const minY = geometry.y - geometry.height / 2;
5523
+ const absX = minX + x * geometry.width;
5524
+ const absY = minY + y * geometry.height;
5525
+ const nearest = getNearestPointOnDieline(
5526
+ { x: absX, y: absY },
5527
+ geometry
5528
+ );
5529
+ let finalX = nearest.x;
5530
+ let finalY = nearest.y;
5531
+ const hasOffsetParams = params.minOffset !== void 0 || params.maxOffset !== void 0;
5532
+ if (hasOffsetParams && nearest.normal) {
5533
+ const dx = absX - nearest.x;
5534
+ const dy = absY - nearest.y;
5535
+ const nx2 = nearest.normal.x;
5536
+ const ny2 = nearest.normal.y;
5537
+ const dist = dx * nx2 + dy * ny2;
5538
+ const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
5539
+ const rawMin = params.minOffset !== void 0 ? params.minOffset : 0;
5540
+ const rawMax = params.maxOffset !== void 0 ? params.maxOffset : 0;
5541
+ const minOffset = rawMin * scale;
5542
+ const maxOffset = rawMax * scale;
5543
+ const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
5544
+ finalX = nearest.x + nx2 * clampedDist;
5545
+ finalY = nearest.y + ny2 * clampedDist;
5546
+ }
5547
+ const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
5548
+ const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
5549
+ return { x: nx, y: ny };
5550
+ };
5551
+ var edgeConstraint = (x, y, feature, context, params) => {
5552
+ const { dielineWidth, dielineHeight } = context;
5553
+ const allowedEdges = params.allowedEdges || [
5554
+ "top",
5555
+ "bottom",
5556
+ "left",
5557
+ "right"
5558
+ ];
5559
+ const confine = params.confine || false;
5560
+ const offset = params.offset || 0;
5561
+ const distances = [];
5562
+ if (allowedEdges.includes("top"))
5563
+ distances.push({ edge: "top", dist: y * dielineHeight });
5564
+ if (allowedEdges.includes("bottom"))
5565
+ distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
5566
+ if (allowedEdges.includes("left"))
5567
+ distances.push({ edge: "left", dist: x * dielineWidth });
5568
+ if (allowedEdges.includes("right"))
5569
+ distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
5570
+ if (distances.length === 0) return { x, y };
5571
+ distances.sort((a, b) => a.dist - b.dist);
5572
+ const nearest = distances[0].edge;
5573
+ let newX = x;
5574
+ let newY = y;
5575
+ const fw = feature.width || 0;
5576
+ const fh = feature.height || 0;
5577
+ switch (nearest) {
5578
+ case "top":
5579
+ newY = 0 + offset / dielineHeight;
5580
+ if (confine) {
5581
+ const minX = fw / 2 / dielineWidth;
5582
+ const maxX = 1 - minX;
5583
+ newX = Math.max(minX, Math.min(newX, maxX));
5584
+ }
5585
+ break;
5586
+ case "bottom":
5587
+ newY = 1 - offset / dielineHeight;
5588
+ if (confine) {
5589
+ const minX = fw / 2 / dielineWidth;
5590
+ const maxX = 1 - minX;
5591
+ newX = Math.max(minX, Math.min(newX, maxX));
5592
+ }
5593
+ break;
5594
+ case "left":
5595
+ newX = 0 + offset / dielineWidth;
5596
+ if (confine) {
5597
+ const minY = fh / 2 / dielineHeight;
5598
+ const maxY = 1 - minY;
5599
+ newY = Math.max(minY, Math.min(newY, maxY));
5600
+ }
5601
+ break;
5602
+ case "right":
5603
+ newX = 1 - offset / dielineWidth;
5604
+ if (confine) {
5605
+ const minY = fh / 2 / dielineHeight;
5606
+ const maxY = 1 - minY;
5607
+ newY = Math.max(minY, Math.min(newY, maxY));
5608
+ }
5609
+ break;
5610
+ }
5611
+ return { x: newX, y: newY };
5612
+ };
5613
+ var internalConstraint = (x, y, feature, context, params) => {
5614
+ const { dielineWidth, dielineHeight } = context;
5615
+ const margin = params.margin || 0;
5616
+ const fw = feature.width || 0;
5617
+ const fh = feature.height || 0;
5618
+ const minX = (margin + fw / 2) / dielineWidth;
5619
+ const maxX = 1 - (margin + fw / 2) / dielineWidth;
5620
+ const minY = (margin + fh / 2) / dielineHeight;
5621
+ const maxY = 1 - (margin + fh / 2) / dielineHeight;
5622
+ const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
5623
+ const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
5624
+ return { x: clampedX, y: clampedY };
5625
+ };
5626
+ var tangentBottomConstraint = (x, y, feature, context, params) => {
5627
+ const { dielineWidth, dielineHeight } = context;
5628
+ const gap = params.gap || 0;
5629
+ const confineX = params.confineX !== false;
5630
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
5631
+ const newY = 1 + (extentY + gap) / dielineHeight;
5632
+ let newX = x;
5633
+ if (confineX) {
5634
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
5635
+ const minX = extentX / dielineWidth;
5636
+ const maxX = 1 - extentX / dielineWidth;
5637
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
5638
+ }
5639
+ return { x: newX, y: newY };
5640
+ };
5641
+ var lowestTangentConstraint = (x, y, feature, context, params) => {
5642
+ const { dielineWidth, dielineHeight, geometry } = context;
5643
+ if (!geometry) return { x, y };
5644
+ const lowest = getLowestPointOnDieline(geometry);
5645
+ const minY = geometry.y - geometry.height / 2;
5646
+ const normY = (lowest.y - minY) / geometry.height;
5647
+ const gap = params.gap || 0;
5648
+ const confineX = params.confineX !== false;
5649
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
5650
+ const newY = normY + (extentY + gap) / dielineHeight;
5651
+ let newX = x;
5652
+ if (confineX) {
5653
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
5654
+ const minX = extentX / dielineWidth;
5655
+ const maxX = 1 - extentX / dielineWidth;
5656
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
5657
+ }
5658
+ return { x: newX, y: newY };
5659
+ };
5660
+ ConstraintRegistry.register("path", pathConstraint);
5661
+ ConstraintRegistry.register("edge", edgeConstraint);
5662
+ ConstraintRegistry.register("internal", internalConstraint);
5663
+ ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
5664
+ ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
5665
+
5666
+ // src/extensions/featureCoordinates.ts
5667
+ function resolveFeaturePosition2(feature, geometry) {
5668
+ const { x, y, width, height } = geometry;
5669
+ const left = x - width / 2;
5670
+ const top = y - height / 2;
5671
+ return {
5672
+ x: left + feature.x * width,
5673
+ y: top + feature.y * height
5674
+ };
5675
+ }
5676
+ function normalizePointInGeometry(point, geometry) {
5677
+ const left = geometry.x - geometry.width / 2;
5678
+ const top = geometry.y - geometry.height / 2;
5679
+ return {
5680
+ x: geometry.width > 0 ? (point.x - left) / geometry.width : 0.5,
5681
+ y: geometry.height > 0 ? (point.y - top) / geometry.height : 0.5
5682
+ };
5683
+ }
5684
+
5685
+ // src/extensions/featurePlacement.ts
5686
+ function scaleFeatureForRender(feature, scale, x, y) {
5687
+ return {
5688
+ ...feature,
5689
+ x,
5690
+ y,
5691
+ width: feature.width !== void 0 ? feature.width * scale : void 0,
5692
+ height: feature.height !== void 0 ? feature.height * scale : void 0,
5693
+ radius: feature.radius !== void 0 ? feature.radius * scale : void 0
5694
+ };
5695
+ }
5696
+ function resolveFeaturePlacements(features, geometry) {
5697
+ const dielineWidth = geometry.scale > 0 ? geometry.width / geometry.scale : geometry.width;
5698
+ const dielineHeight = geometry.scale > 0 ? geometry.height / geometry.scale : geometry.height;
5699
+ return (features || []).map((feature) => {
5700
+ var _a;
5701
+ const activeConstraints = (_a = feature.constraints) == null ? void 0 : _a.filter(
5702
+ (constraint) => !constraint.validateOnly
5703
+ );
5704
+ const constrained = ConstraintRegistry.apply(
5705
+ feature.x,
5706
+ feature.y,
5707
+ feature,
5708
+ {
5709
+ dielineWidth,
5710
+ dielineHeight,
5711
+ geometry
5712
+ },
5713
+ activeConstraints
5714
+ );
5715
+ const center = resolveFeaturePosition2(
5716
+ {
5717
+ ...feature,
5718
+ x: constrained.x,
5719
+ y: constrained.y
5720
+ },
5721
+ geometry
5722
+ );
5723
+ return {
5724
+ feature,
5725
+ normalizedX: constrained.x,
5726
+ normalizedY: constrained.y,
5727
+ centerX: center.x,
5728
+ centerY: center.y
5729
+ };
5730
+ });
5731
+ }
5732
+ function projectPlacedFeatures(placements, geometry, scale) {
5733
+ return placements.map((placement) => {
5734
+ const normalized = normalizePointInGeometry(
5735
+ { x: placement.centerX, y: placement.centerY },
5736
+ geometry
5737
+ );
5738
+ return scaleFeatureForRender(
5739
+ placement.feature,
5740
+ scale,
5741
+ normalized.x,
5742
+ normalized.y
5743
+ );
5744
+ });
5745
+ }
5746
+
5659
5747
  // src/extensions/dieline/renderBuilder.ts
5660
5748
  var DEFAULT_IDS = {
5661
5749
  inside: "dieline.inside",
@@ -5665,16 +5753,6 @@ var DEFAULT_IDS = {
5665
5753
  clip: "dieline.clip.image",
5666
5754
  clipSource: "dieline.effect.clip-path"
5667
5755
  };
5668
- function scaleFeatures(state, scale) {
5669
- return (state.features || []).map((feature) => ({
5670
- ...feature,
5671
- x: feature.x,
5672
- y: feature.y,
5673
- width: (feature.width || 0) * scale,
5674
- height: (feature.height || 0) * scale,
5675
- radius: (feature.radius || 0) * scale
5676
- }));
5677
- }
5678
5756
  function buildDielineRenderBundle(options) {
5679
5757
  const ids = { ...DEFAULT_IDS, ...options.ids || {} };
5680
5758
  const {
@@ -5699,8 +5777,41 @@ function buildDielineRenderBundle(options) {
5699
5777
  const cutH = sceneLayout.cutRect.height;
5700
5778
  const visualOffset = (cutW - visualWidth) / 2;
5701
5779
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
5702
- const absoluteFeatures = scaleFeatures(state, scale);
5703
- const cutFeatures = absoluteFeatures.filter((feature) => !feature.skipCut);
5780
+ const placements = resolveFeaturePlacements(state.features || [], {
5781
+ shape,
5782
+ shapeStyle,
5783
+ pathData: state.pathData,
5784
+ customSourceWidthPx: state.customSourceWidthPx,
5785
+ customSourceHeightPx: state.customSourceHeightPx,
5786
+ canvasWidth,
5787
+ canvasHeight,
5788
+ x: cx,
5789
+ y: cy,
5790
+ width: visualWidth,
5791
+ height: visualHeight,
5792
+ radius: visualRadius,
5793
+ scale
5794
+ });
5795
+ const absoluteFeatures = projectPlacedFeatures(
5796
+ placements,
5797
+ {
5798
+ x: cx,
5799
+ y: cy,
5800
+ width: visualWidth,
5801
+ height: visualHeight
5802
+ },
5803
+ scale
5804
+ );
5805
+ const cutFeatures = projectPlacedFeatures(
5806
+ placements.filter((placement) => !placement.feature.skipCut),
5807
+ {
5808
+ x: cx,
5809
+ y: cy,
5810
+ width: cutW,
5811
+ height: cutH
5812
+ },
5813
+ scale
5814
+ );
5704
5815
  const common = {
5705
5816
  shape,
5706
5817
  shapeStyle,
@@ -5710,6 +5821,13 @@ function buildDielineRenderBundle(options) {
5710
5821
  canvasWidth,
5711
5822
  canvasHeight
5712
5823
  };
5824
+ const cutFrameRect = {
5825
+ left: cx - cutW / 2,
5826
+ top: cy - cutH / 2,
5827
+ width: cutW,
5828
+ height: cutH,
5829
+ space: "screen"
5830
+ };
5713
5831
  const specs = [];
5714
5832
  if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)" && !hasImages) {
5715
5833
  specs.push({
@@ -5831,9 +5949,13 @@ function buildDielineRenderBundle(options) {
5831
5949
  width: cutW,
5832
5950
  height: cutH,
5833
5951
  radius: cutR,
5834
- x: cx,
5835
- y: cy,
5836
- 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
5837
5959
  });
5838
5960
  if (!clipPathData) {
5839
5961
  return { specs, effects: [] };
@@ -5850,6 +5972,12 @@ function buildDielineRenderBundle(options) {
5850
5972
  id: ids.clipSource,
5851
5973
  type: "path",
5852
5974
  space: "screen",
5975
+ layout: {
5976
+ reference: "custom",
5977
+ referenceRect: cutFrameRect,
5978
+ alignX: "start",
5979
+ alignY: "start"
5980
+ },
5853
5981
  data: {
5854
5982
  id: ids.clipSource,
5855
5983
  type: "dieline-effect",
@@ -6129,15 +6257,31 @@ var DielineTool = class {
6129
6257
  const visualRadius = radius * scale;
6130
6258
  const visualOffset = (cutW - sceneLayout.trimRect.width) / 2;
6131
6259
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
6132
- const absoluteFeatures = (features || []).map((f) => ({
6133
- ...f,
6134
- x: f.x,
6135
- y: f.y,
6136
- width: (f.width || 0) * scale,
6137
- height: (f.height || 0) * scale,
6138
- radius: (f.radius || 0) * scale
6139
- }));
6140
- const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
6260
+ const placements = resolveFeaturePlacements(features || [], {
6261
+ shape,
6262
+ shapeStyle,
6263
+ pathData,
6264
+ customSourceWidthPx: this.state.customSourceWidthPx,
6265
+ customSourceHeightPx: this.state.customSourceHeightPx,
6266
+ canvasWidth: canvasW,
6267
+ canvasHeight: canvasH,
6268
+ x: cx,
6269
+ y: cy,
6270
+ width: sceneLayout.trimRect.width,
6271
+ height: sceneLayout.trimRect.height,
6272
+ radius: visualRadius,
6273
+ scale
6274
+ });
6275
+ const cutFeatures = projectPlacedFeatures(
6276
+ placements.filter((placement) => !placement.feature.skipCut),
6277
+ {
6278
+ x: cx,
6279
+ y: cy,
6280
+ width: cutW,
6281
+ height: cutH
6282
+ },
6283
+ scale
6284
+ );
6141
6285
  const generatedPathData = generateDielinePath({
6142
6286
  shape,
6143
6287
  width: cutW,
@@ -6261,178 +6405,6 @@ var DielineTool = class {
6261
6405
  var import_core5 = require("@pooder/core");
6262
6406
  var import_fabric4 = require("fabric");
6263
6407
 
6264
- // src/extensions/constraints.ts
6265
- var ConstraintRegistry = class {
6266
- static register(type, handler) {
6267
- this.handlers.set(type, handler);
6268
- }
6269
- static apply(x, y, feature, context, constraints) {
6270
- const list = constraints || feature.constraints;
6271
- if (!list || list.length === 0) {
6272
- return { x, y };
6273
- }
6274
- let currentX = x;
6275
- let currentY = y;
6276
- for (const constraint of list) {
6277
- const handler = this.handlers.get(constraint.type);
6278
- if (handler) {
6279
- const result = handler(currentX, currentY, feature, context, constraint.params || {});
6280
- currentX = result.x;
6281
- currentY = result.y;
6282
- }
6283
- }
6284
- return { x: currentX, y: currentY };
6285
- }
6286
- };
6287
- ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
6288
- var pathConstraint = (x, y, feature, context, params) => {
6289
- const { dielineWidth, dielineHeight, geometry } = context;
6290
- if (!geometry) return { x, y };
6291
- const minX = geometry.x - geometry.width / 2;
6292
- const minY = geometry.y - geometry.height / 2;
6293
- const absX = minX + x * geometry.width;
6294
- const absY = minY + y * geometry.height;
6295
- const nearest = getNearestPointOnDieline(
6296
- { x: absX, y: absY },
6297
- geometry
6298
- );
6299
- let finalX = nearest.x;
6300
- let finalY = nearest.y;
6301
- const hasOffsetParams = params.minOffset !== void 0 || params.maxOffset !== void 0;
6302
- if (hasOffsetParams && nearest.normal) {
6303
- const dx = absX - nearest.x;
6304
- const dy = absY - nearest.y;
6305
- const nx2 = nearest.normal.x;
6306
- const ny2 = nearest.normal.y;
6307
- const dist = dx * nx2 + dy * ny2;
6308
- const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
6309
- const rawMin = params.minOffset !== void 0 ? params.minOffset : 0;
6310
- const rawMax = params.maxOffset !== void 0 ? params.maxOffset : 0;
6311
- const minOffset = rawMin * scale;
6312
- const maxOffset = rawMax * scale;
6313
- const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
6314
- finalX = nearest.x + nx2 * clampedDist;
6315
- finalY = nearest.y + ny2 * clampedDist;
6316
- }
6317
- const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
6318
- const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
6319
- return { x: nx, y: ny };
6320
- };
6321
- var edgeConstraint = (x, y, feature, context, params) => {
6322
- const { dielineWidth, dielineHeight } = context;
6323
- const allowedEdges = params.allowedEdges || [
6324
- "top",
6325
- "bottom",
6326
- "left",
6327
- "right"
6328
- ];
6329
- const confine = params.confine || false;
6330
- const offset = params.offset || 0;
6331
- const distances = [];
6332
- if (allowedEdges.includes("top"))
6333
- distances.push({ edge: "top", dist: y * dielineHeight });
6334
- if (allowedEdges.includes("bottom"))
6335
- distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
6336
- if (allowedEdges.includes("left"))
6337
- distances.push({ edge: "left", dist: x * dielineWidth });
6338
- if (allowedEdges.includes("right"))
6339
- distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
6340
- if (distances.length === 0) return { x, y };
6341
- distances.sort((a, b) => a.dist - b.dist);
6342
- const nearest = distances[0].edge;
6343
- let newX = x;
6344
- let newY = y;
6345
- const fw = feature.width || 0;
6346
- const fh = feature.height || 0;
6347
- switch (nearest) {
6348
- case "top":
6349
- newY = 0 + offset / dielineHeight;
6350
- if (confine) {
6351
- const minX = fw / 2 / dielineWidth;
6352
- const maxX = 1 - minX;
6353
- newX = Math.max(minX, Math.min(newX, maxX));
6354
- }
6355
- break;
6356
- case "bottom":
6357
- newY = 1 - offset / dielineHeight;
6358
- if (confine) {
6359
- const minX = fw / 2 / dielineWidth;
6360
- const maxX = 1 - minX;
6361
- newX = Math.max(minX, Math.min(newX, maxX));
6362
- }
6363
- break;
6364
- case "left":
6365
- newX = 0 + offset / dielineWidth;
6366
- if (confine) {
6367
- const minY = fh / 2 / dielineHeight;
6368
- const maxY = 1 - minY;
6369
- newY = Math.max(minY, Math.min(newY, maxY));
6370
- }
6371
- break;
6372
- case "right":
6373
- newX = 1 - offset / dielineWidth;
6374
- if (confine) {
6375
- const minY = fh / 2 / dielineHeight;
6376
- const maxY = 1 - minY;
6377
- newY = Math.max(minY, Math.min(newY, maxY));
6378
- }
6379
- break;
6380
- }
6381
- return { x: newX, y: newY };
6382
- };
6383
- var internalConstraint = (x, y, feature, context, params) => {
6384
- const { dielineWidth, dielineHeight } = context;
6385
- const margin = params.margin || 0;
6386
- const fw = feature.width || 0;
6387
- const fh = feature.height || 0;
6388
- const minX = (margin + fw / 2) / dielineWidth;
6389
- const maxX = 1 - (margin + fw / 2) / dielineWidth;
6390
- const minY = (margin + fh / 2) / dielineHeight;
6391
- const maxY = 1 - (margin + fh / 2) / dielineHeight;
6392
- const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
6393
- const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
6394
- return { x: clampedX, y: clampedY };
6395
- };
6396
- var tangentBottomConstraint = (x, y, feature, context, params) => {
6397
- const { dielineWidth, dielineHeight } = context;
6398
- const gap = params.gap || 0;
6399
- const confineX = params.confineX !== false;
6400
- const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
6401
- const newY = 1 + (extentY + gap) / dielineHeight;
6402
- let newX = x;
6403
- if (confineX) {
6404
- const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
6405
- const minX = extentX / dielineWidth;
6406
- const maxX = 1 - extentX / dielineWidth;
6407
- newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
6408
- }
6409
- return { x: newX, y: newY };
6410
- };
6411
- var lowestTangentConstraint = (x, y, feature, context, params) => {
6412
- const { dielineWidth, dielineHeight, geometry } = context;
6413
- if (!geometry) return { x, y };
6414
- const lowest = getLowestPointOnDieline(geometry);
6415
- const minY = geometry.y - geometry.height / 2;
6416
- const normY = (lowest.y - minY) / geometry.height;
6417
- const gap = params.gap || 0;
6418
- const confineX = params.confineX !== false;
6419
- const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
6420
- const newY = normY + (extentY + gap) / dielineHeight;
6421
- let newX = x;
6422
- if (confineX) {
6423
- const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
6424
- const minX = extentX / dielineWidth;
6425
- const maxX = 1 - extentX / dielineWidth;
6426
- newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
6427
- }
6428
- return { x: newX, y: newY };
6429
- };
6430
- ConstraintRegistry.register("path", pathConstraint);
6431
- ConstraintRegistry.register("edge", edgeConstraint);
6432
- ConstraintRegistry.register("internal", internalConstraint);
6433
- ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
6434
- ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
6435
-
6436
6408
  // src/extensions/featureComplete.ts
6437
6409
  function validateFeaturesStrict(features, context) {
6438
6410
  const eps = 1e-6;
@@ -7208,9 +7180,29 @@ var FeatureTool = class {
7208
7180
  }
7209
7181
  const groups = /* @__PURE__ */ new Map();
7210
7182
  const singles = [];
7211
- this.workingFeatures.forEach((feature, index) => {
7183
+ const placements = resolveFeaturePlacements(
7184
+ this.workingFeatures,
7185
+ {
7186
+ shape: this.currentGeometry.shape,
7187
+ shapeStyle: this.currentGeometry.shapeStyle,
7188
+ pathData: this.currentGeometry.pathData,
7189
+ customSourceWidthPx: this.currentGeometry.customSourceWidthPx,
7190
+ customSourceHeightPx: this.currentGeometry.customSourceHeightPx,
7191
+ x: this.currentGeometry.x,
7192
+ y: this.currentGeometry.y,
7193
+ width: this.currentGeometry.width,
7194
+ height: this.currentGeometry.height,
7195
+ radius: this.currentGeometry.radius,
7196
+ scale: this.currentGeometry.scale || 1
7197
+ }
7198
+ );
7199
+ placements.forEach((placement, index) => {
7200
+ const feature = placement.feature;
7212
7201
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
7213
- const position = resolveFeaturePosition(feature, geometry);
7202
+ const position = {
7203
+ x: placement.centerX,
7204
+ y: placement.centerY
7205
+ };
7214
7206
  const scale = geometry.scale || 1;
7215
7207
  const marker = {
7216
7208
  feature,
@@ -7781,11 +7773,12 @@ var EXTENSION_LINE_LENGTH = 5;
7781
7773
  var MIN_ARROW_SIZE = 4;
7782
7774
  var THICKNESS_TO_STROKE_WIDTH_RATIO = 20;
7783
7775
  var DEFAULT_THICKNESS = 20;
7784
- var DEFAULT_GAP = 45;
7776
+ var DEFAULT_GAP = 65;
7785
7777
  var DEFAULT_FONT_SIZE = 10;
7786
7778
  var DEFAULT_BACKGROUND_COLOR = "#f0f0f0";
7787
7779
  var DEFAULT_TEXT_COLOR = "#333333";
7788
7780
  var DEFAULT_LINE_COLOR = "#999999";
7781
+ var RULER_DEBUG_KEY = "ruler.debug";
7789
7782
  var RULER_THICKNESS_MIN = 10;
7790
7783
  var RULER_THICKNESS_MAX = 100;
7791
7784
  var RULER_GAP_MIN = 0;
@@ -7804,6 +7797,7 @@ var RulerTool = class {
7804
7797
  this.textColor = DEFAULT_TEXT_COLOR;
7805
7798
  this.lineColor = DEFAULT_LINE_COLOR;
7806
7799
  this.fontSize = DEFAULT_FONT_SIZE;
7800
+ this.debugEnabled = false;
7807
7801
  this.renderSeq = 0;
7808
7802
  this.numericProps = /* @__PURE__ */ new Set(["thickness", "gap", "fontSize"]);
7809
7803
  this.specs = [];
@@ -7852,7 +7846,14 @@ var RulerTool = class {
7852
7846
  this.syncConfig(configService);
7853
7847
  configService.onAnyChange((e) => {
7854
7848
  let shouldUpdate = false;
7855
- if (e.key.startsWith("ruler.")) {
7849
+ if (e.key === RULER_DEBUG_KEY) {
7850
+ this.debugEnabled = e.value === true;
7851
+ this.log("config:update", {
7852
+ key: e.key,
7853
+ raw: e.value,
7854
+ normalized: this.debugEnabled
7855
+ });
7856
+ } else if (e.key.startsWith("ruler.")) {
7856
7857
  const prop = e.key.split(".")[1];
7857
7858
  if (prop && prop in this) {
7858
7859
  if (this.numericProps.has(prop)) {
@@ -7939,6 +7940,12 @@ var RulerTool = class {
7939
7940
  min: RULER_FONT_SIZE_MIN,
7940
7941
  max: RULER_FONT_SIZE_MAX,
7941
7942
  default: DEFAULT_FONT_SIZE
7943
+ },
7944
+ {
7945
+ id: RULER_DEBUG_KEY,
7946
+ type: "boolean",
7947
+ label: "Ruler Debug Log",
7948
+ default: false
7942
7949
  }
7943
7950
  ],
7944
7951
  [import_core8.ContributionPointIds.COMMANDS]: [
@@ -7975,7 +7982,11 @@ var RulerTool = class {
7975
7982
  ]
7976
7983
  };
7977
7984
  }
7985
+ isDebugEnabled() {
7986
+ return this.debugEnabled;
7987
+ }
7978
7988
  log(step, payload) {
7989
+ if (!this.isDebugEnabled()) return;
7979
7990
  if (payload) {
7980
7991
  console.debug(`[RulerTool] ${step}`, payload);
7981
7992
  return;
@@ -8004,6 +8015,7 @@ var RulerTool = class {
8004
8015
  configService.get("ruler.fontSize", this.fontSize),
8005
8016
  DEFAULT_FONT_SIZE
8006
8017
  );
8018
+ this.debugEnabled = configService.get(RULER_DEBUG_KEY, this.debugEnabled) === true;
8007
8019
  this.log("config:loaded", {
8008
8020
  thickness: this.thickness,
8009
8021
  gap: this.gap,