@pooder/kit 6.2.1 → 6.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1192,967 +1192,1097 @@ import {
1192
1192
  controlsUtils
1193
1193
  } from "fabric";
1194
1194
 
1195
- // src/extensions/geometry.ts
1196
- import paper from "paper";
1197
-
1198
- // src/extensions/bridgeSelection.ts
1199
- function pickExitIndex(hits) {
1200
- for (let i = 0; i < hits.length; i++) {
1201
- const h = hits[i];
1202
- if (h.insideBelow && !h.insideAbove) return i;
1203
- }
1204
- return -1;
1195
+ // src/shared/scene/frame.ts
1196
+ function emptyFrameRect() {
1197
+ return { left: 0, top: 0, width: 0, height: 0 };
1205
1198
  }
1206
- function scoreOutsideAbove(samples) {
1207
- let score = 0;
1208
- for (const s of samples) {
1209
- if (s.outsideAbove) score++;
1199
+ function resolveCutFrameRect(canvasService, configService) {
1200
+ if (!canvasService || !configService) {
1201
+ return emptyFrameRect();
1210
1202
  }
1211
- return score;
1212
- }
1213
-
1214
- // src/extensions/wrappedOffsets.ts
1215
- function wrappedDistance(total, start, end) {
1216
- if (!Number.isFinite(total) || total <= 0) return 0;
1217
- if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
1218
- const s = (start % total + total) % total;
1219
- const e = (end % total + total) % total;
1220
- return e >= s ? e - s : total - s + e;
1221
- }
1222
- function sampleWrappedOffsets(total, start, end, count) {
1223
- if (!Number.isFinite(total) || total <= 0) return [];
1224
- if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
1225
- const n = Math.max(0, Math.floor(count));
1226
- if (n <= 0) return [];
1227
- const dist = wrappedDistance(total, start, end);
1228
- if (n === 1) return [(start % total + total) % total];
1229
- const step = dist / (n - 1);
1230
- const offsets = [];
1231
- for (let i = 0; i < n; i++) {
1232
- const raw = start + step * i;
1233
- const wrapped = (raw % total + total) % total;
1234
- offsets.push(wrapped);
1203
+ const sizeState = readSizeState(configService);
1204
+ const layout = computeSceneLayout(canvasService, sizeState);
1205
+ if (!layout) {
1206
+ return emptyFrameRect();
1235
1207
  }
1236
- return offsets;
1208
+ return canvasService.toSceneRect({
1209
+ left: layout.cutRect.left,
1210
+ top: layout.cutRect.top,
1211
+ width: layout.cutRect.width,
1212
+ height: layout.cutRect.height
1213
+ });
1237
1214
  }
1238
-
1239
- // src/extensions/geometry.ts
1240
- function resolveFeaturePosition(feature, geometry) {
1241
- const { x, y, width, height } = geometry;
1242
- const left = x - width / 2;
1243
- const top = y - height / 2;
1215
+ function toLayoutSceneRect(rect) {
1244
1216
  return {
1245
- x: left + feature.x * width,
1246
- y: top + feature.y * height
1217
+ left: rect.left,
1218
+ top: rect.top,
1219
+ width: rect.width,
1220
+ height: rect.height,
1221
+ space: "scene"
1247
1222
  };
1248
1223
  }
1249
- function ensurePaper(width, height) {
1250
- if (!paper.project) {
1251
- paper.setup(new paper.Size(width, height));
1252
- } else {
1253
- paper.view.viewSize = new paper.Size(width, height);
1254
- }
1255
- }
1256
- var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
1257
- function normalizePathItem(shape) {
1258
- let result = shape;
1259
- if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
1260
- if (typeof result.reduce === "function") result = result.reduce({});
1261
- if (typeof result.reorient === "function") result = result.reorient(true, true);
1262
- if (typeof result.reduce === "function") result = result.reduce({});
1263
- return result;
1264
- }
1265
- function getBridgeDelta(itemBounds, overlap) {
1266
- return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
1267
- }
1268
- function getExitHit(args) {
1269
- const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
1270
- const ray = new paper.Path.Line({
1271
- from: [x, bridgeBottom],
1272
- to: [x, toY],
1273
- insert: false
1274
- });
1275
- const intersections = mainShape.getIntersections(ray) || [];
1276
- ray.remove();
1277
- const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
1278
- if (validHits.length === 0) return null;
1279
- validHits.sort((a, b) => b.point.y - a.point.y);
1280
- const flags = validHits.map((h) => {
1281
- const above = h.point.add(new paper.Point(0, -delta));
1282
- const below = h.point.add(new paper.Point(0, delta));
1283
- return {
1284
- insideAbove: mainShape.contains(above),
1285
- insideBelow: mainShape.contains(below)
1286
- };
1287
- });
1288
- const idx = pickExitIndex(flags);
1289
- if (idx < 0) return null;
1290
- if (isBridgeDebugEnabled()) {
1291
- console.debug("Geometry: Bridge ray", {
1292
- x,
1293
- validHits: validHits.length,
1294
- idx,
1295
- delta,
1296
- overlap,
1297
- op
1298
- });
1299
- }
1300
- const hit = validHits[idx];
1301
- return { point: hit.point, location: hit };
1224
+
1225
+ // src/shared/runtime/sessionState.ts
1226
+ function cloneWithJson(value) {
1227
+ return JSON.parse(JSON.stringify(value));
1302
1228
  }
1303
- function selectOuterChain(args) {
1304
- const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
1305
- const scoreA = scoreOutsideAbove(
1306
- pointsA.map((p) => ({
1307
- outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
1308
- }))
1309
- );
1310
- const scoreB = scoreOutsideAbove(
1311
- pointsB.map((p) => ({
1312
- outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
1313
- }))
1314
- );
1315
- const ratioA = scoreA / pointsA.length;
1316
- const ratioB = scoreB / pointsB.length;
1317
- if (isBridgeDebugEnabled()) {
1318
- console.debug("Geometry: Bridge chain", {
1319
- scoreA,
1320
- scoreB,
1321
- lenA: pointsA.length,
1322
- lenB: pointsB.length,
1323
- ratioA,
1324
- ratioB,
1325
- delta,
1326
- overlap,
1327
- op
1328
- });
1329
- }
1330
- const ratioEps = 1e-6;
1331
- if (Math.abs(ratioA - ratioB) > ratioEps) {
1332
- return ratioA > ratioB ? pointsA : pointsB;
1229
+ function applyCommittedSnapshot(session, nextCommitted, options) {
1230
+ const clone = options.clone;
1231
+ session.committed = clone(nextCommitted);
1232
+ const shouldPreserveDirtyWorking = options.toolActive && options.preserveDirtyWorking !== false && session.hasWorkingChanges;
1233
+ if (!shouldPreserveDirtyWorking) {
1234
+ session.working = clone(session.committed);
1235
+ session.hasWorkingChanges = false;
1333
1236
  }
1334
- if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
1335
- return pointsA.length <= pointsB.length ? pointsA : pointsB;
1336
1237
  }
1337
- function fitPathItemToRect(item, rect, fitMode) {
1338
- const { left, top, width, height } = rect;
1339
- const bounds = item.bounds;
1340
- if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
1341
- item.position = new paper.Point(left + width / 2, top + height / 2);
1342
- return item;
1343
- }
1344
- item.translate(new paper.Point(-bounds.left, -bounds.top));
1345
- if (fitMode === "stretch") {
1346
- item.scale(width / bounds.width, height / bounds.height, new paper.Point(0, 0));
1347
- item.translate(new paper.Point(left, top));
1348
- return item;
1238
+ function runDeferredConfigUpdate(state, action, cooldownMs = 0) {
1239
+ state.isUpdatingConfig = true;
1240
+ action();
1241
+ if (cooldownMs <= 0) {
1242
+ state.isUpdatingConfig = false;
1243
+ return;
1349
1244
  }
1350
- const uniformScale = Math.min(width / bounds.width, height / bounds.height);
1351
- item.scale(uniformScale, uniformScale, new paper.Point(0, 0));
1352
- const scaledWidth = bounds.width * uniformScale;
1353
- const scaledHeight = bounds.height * uniformScale;
1354
- item.translate(
1355
- new paper.Point(
1356
- left + (width - scaledWidth) / 2,
1357
- top + (height - scaledHeight) / 2
1358
- )
1359
- );
1360
- return item;
1361
- }
1362
- function createNormalizedHeartPath(params) {
1363
- const { lobeSpread, notchDepth, tipSharpness } = params;
1364
- const halfSpread = 0.22 + lobeSpread * 0.18;
1365
- const notchY = 0.06 + notchDepth * 0.2;
1366
- const shoulderY = 0.24 + notchDepth * 0.2;
1367
- const topLift = 0.12 + (1 - notchDepth) * 0.06;
1368
- const topY = notchY - topLift;
1369
- const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
1370
- const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
1371
- const tipCtrlX = 0.34 - tipSharpness * 0.2;
1372
- const notchCtrlX = 0.06 + lobeSpread * 0.06;
1373
- const lobeCtrlX = 0.1 + lobeSpread * 0.08;
1374
- const notchCtrlY = notchY - topLift * 0.45;
1375
- const xPeakL = 0.5 - halfSpread;
1376
- const xPeakR = 0.5 + halfSpread;
1377
- const heartPath = new paper.Path({ insert: false });
1378
- heartPath.moveTo(new paper.Point(0.5, notchY));
1379
- heartPath.cubicCurveTo(
1380
- new paper.Point(0.5 - notchCtrlX, notchCtrlY),
1381
- new paper.Point(xPeakL + lobeCtrlX, topY),
1382
- new paper.Point(xPeakL, topY)
1383
- );
1384
- heartPath.cubicCurveTo(
1385
- new paper.Point(xPeakL - lobeCtrlX, topY),
1386
- new paper.Point(0, sideCtrlY),
1387
- new paper.Point(0, shoulderY)
1388
- );
1389
- heartPath.cubicCurveTo(
1390
- new paper.Point(0, lowerCtrlY),
1391
- new paper.Point(tipCtrlX, 1),
1392
- new paper.Point(0.5, 1)
1393
- );
1394
- heartPath.cubicCurveTo(
1395
- new paper.Point(1 - tipCtrlX, 1),
1396
- new paper.Point(1, lowerCtrlY),
1397
- new paper.Point(1, shoulderY)
1398
- );
1399
- heartPath.cubicCurveTo(
1400
- new paper.Point(1, sideCtrlY),
1401
- new paper.Point(xPeakR + lobeCtrlX, topY),
1402
- new paper.Point(xPeakR, topY)
1403
- );
1404
- heartPath.cubicCurveTo(
1405
- new paper.Point(xPeakR - lobeCtrlX, topY),
1406
- new paper.Point(0.5 + notchCtrlX, notchCtrlY),
1407
- new paper.Point(0.5, notchY)
1408
- );
1409
- heartPath.closed = true;
1410
- return heartPath;
1245
+ setTimeout(() => {
1246
+ state.isUpdatingConfig = false;
1247
+ }, cooldownMs);
1411
1248
  }
1412
- function createHeartBaseShape(options) {
1413
- const { x, y, width, height } = options;
1414
- const w = Math.max(0, width);
1415
- const h = Math.max(0, height);
1416
- const left = x - w / 2;
1417
- const top = y - h / 2;
1418
- const fitMode = getShapeFitMode(options.shapeStyle);
1419
- const heartParams = getHeartShapeParams(options.shapeStyle);
1420
- const rawHeart = createNormalizedHeartPath(heartParams);
1421
- return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
1422
- }
1423
- var BUILTIN_SHAPE_BUILDERS = {
1424
- rect: (options) => {
1425
- const { x, y, width, height, radius } = options;
1426
- return new paper.Path.Rectangle({
1427
- point: [x - width / 2, y - height / 2],
1428
- size: [Math.max(0, width), Math.max(0, height)],
1429
- radius: Math.max(0, radius)
1430
- });
1431
- },
1432
- circle: (options) => {
1433
- const { x, y, width, height } = options;
1434
- const r = Math.min(width, height) / 2;
1435
- return new paper.Path.Circle({
1436
- center: new paper.Point(x, y),
1437
- radius: Math.max(0, r)
1438
- });
1439
- },
1440
- ellipse: (options) => {
1441
- const { x, y, width, height } = options;
1442
- return new paper.Path.Ellipse({
1443
- center: new paper.Point(x, y),
1444
- radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
1445
- });
1446
- },
1447
- heart: createHeartBaseShape
1448
- };
1449
- function createCustomBaseShape(options) {
1450
- var _a;
1451
- const {
1452
- pathData,
1453
- customSourceWidthPx,
1454
- customSourceHeightPx,
1455
- x,
1456
- y,
1457
- width,
1458
- height
1459
- } = options;
1460
- if (typeof pathData !== "string" || pathData.trim().length === 0) {
1461
- return null;
1462
- }
1463
- const center = new paper.Point(x, y);
1464
- const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
1465
- const path = hasMultipleSubPaths ? new paper.CompoundPath(pathData) : (() => {
1466
- const single = new paper.Path();
1467
- single.pathData = pathData;
1468
- return single;
1469
- })();
1470
- const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
1471
- const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
1472
- if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
1473
- const targetLeft = x - width / 2;
1474
- const targetTop = y - height / 2;
1475
- path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
1476
- path.translate(new paper.Point(targetLeft, targetTop));
1477
- return path;
1478
- }
1479
- if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
1480
- path.position = center;
1481
- path.scale(width / path.bounds.width, height / path.bounds.height);
1482
- return path;
1483
- }
1484
- path.position = center;
1485
- return path;
1486
- }
1487
- function createBaseShape(options) {
1488
- const { shape } = options;
1489
- if (shape === "custom") {
1490
- const customShape = createCustomBaseShape(options);
1491
- if (customShape) return customShape;
1492
- return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
1493
- }
1494
- return BUILTIN_SHAPE_BUILDERS[shape](options);
1495
- }
1496
- function resolveBridgeBasePath(shape, anchor) {
1497
- if (shape instanceof paper.Path) {
1498
- return shape;
1499
- }
1500
- if (shape instanceof paper.CompoundPath) {
1501
- const children = (shape.children || []).filter(
1502
- (child) => child instanceof paper.Path
1503
- );
1504
- if (!children.length) return null;
1505
- let best = children[0];
1506
- let bestDistance = Infinity;
1507
- for (const child of children) {
1508
- const location = child.getNearestLocation(anchor);
1509
- const point = location == null ? void 0 : location.point;
1510
- if (!point) continue;
1511
- const distance = point.getDistance(anchor);
1512
- if (distance < bestDistance) {
1513
- bestDistance = distance;
1514
- best = child;
1249
+
1250
+ // src/extensions/image/commands.ts
1251
+ function createImageCommands(tool) {
1252
+ return [
1253
+ {
1254
+ command: "addImage",
1255
+ id: "addImage",
1256
+ title: "Add Image",
1257
+ handler: async (url, options) => {
1258
+ const result = await tool.upsertImageEntry(url, {
1259
+ mode: "add",
1260
+ addOptions: options
1261
+ });
1262
+ return result.id;
1515
1263
  }
1516
- }
1517
- return best;
1518
- }
1519
- return null;
1520
- }
1521
- function createFeatureItem(feature, center) {
1522
- let item;
1523
- if (feature.shape === "rect") {
1524
- const w = feature.width || 10;
1525
- const h = feature.height || 10;
1526
- const r = feature.radius || 0;
1527
- item = new paper.Path.Rectangle({
1528
- point: [center.x - w / 2, center.y - h / 2],
1529
- size: [w, h],
1530
- radius: r
1531
- });
1532
- } else {
1533
- const r = feature.radius || 5;
1534
- item = new paper.Path.Circle({
1535
- center,
1536
- radius: r
1537
- });
1538
- }
1539
- if (feature.rotation) {
1540
- item.rotate(feature.rotation, center);
1541
- }
1542
- return item;
1543
- }
1544
- function getPerimeterShape(options) {
1545
- let mainShape = createBaseShape(options);
1546
- const { features } = options;
1547
- if (features && features.length > 0) {
1548
- const edgeFeatures = features.filter(
1549
- (f) => !f.renderBehavior || f.renderBehavior === "edge"
1550
- );
1551
- const adds = [];
1552
- const subtracts = [];
1553
- edgeFeatures.forEach((f) => {
1554
- const pos = resolveFeaturePosition(f, options);
1555
- const center = new paper.Point(pos.x, pos.y);
1556
- const item = createFeatureItem(f, center);
1557
- if (f.bridge && f.bridge.type === "vertical") {
1558
- const itemBounds = item.bounds;
1559
- const mainBounds = mainShape.bounds;
1560
- const bridgeTop = mainBounds.top;
1561
- const bridgeBottom = itemBounds.top;
1562
- if (bridgeBottom > bridgeTop) {
1563
- const overlap = 2;
1564
- const rayPadding = 10;
1565
- const eps = 0.1;
1566
- const delta = getBridgeDelta(itemBounds, overlap);
1567
- const toY = bridgeTop - rayPadding;
1568
- const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
1569
- const xLeft = itemBounds.left + inset;
1570
- const xRight = itemBounds.right - inset;
1571
- const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
1572
- const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
1573
- if (canBridge && bridgeBasePath) {
1574
- const leftHit = getExitHit({
1575
- mainShape: bridgeBasePath,
1576
- x: xLeft,
1577
- bridgeBottom,
1578
- toY,
1579
- eps,
1580
- delta,
1581
- overlap,
1582
- op: f.operation
1583
- });
1584
- const rightHit = getExitHit({
1585
- mainShape: bridgeBasePath,
1586
- x: xRight,
1587
- bridgeBottom,
1588
- toY,
1589
- eps,
1590
- delta,
1591
- overlap,
1592
- op: f.operation
1264
+ },
1265
+ {
1266
+ command: "upsertImage",
1267
+ id: "upsertImage",
1268
+ title: "Upsert Image",
1269
+ handler: async (url, options = {}) => {
1270
+ return await tool.upsertImageEntry(url, options);
1271
+ }
1272
+ },
1273
+ {
1274
+ command: "getWorkingImages",
1275
+ id: "getWorkingImages",
1276
+ title: "Get Working Images",
1277
+ handler: () => {
1278
+ return tool.cloneItems(tool.workingItems);
1279
+ }
1280
+ },
1281
+ {
1282
+ command: "setWorkingImage",
1283
+ id: "setWorkingImage",
1284
+ title: "Set Working Image",
1285
+ handler: (id, updates) => {
1286
+ tool.updateImageInWorking(id, updates);
1287
+ }
1288
+ },
1289
+ {
1290
+ command: "resetWorkingImages",
1291
+ id: "resetWorkingImages",
1292
+ title: "Reset Working Images",
1293
+ handler: () => {
1294
+ tool.workingItems = tool.cloneItems(tool.items);
1295
+ tool.hasWorkingChanges = false;
1296
+ tool.updateImages();
1297
+ tool.emitWorkingChange();
1298
+ }
1299
+ },
1300
+ {
1301
+ command: "completeImages",
1302
+ id: "completeImages",
1303
+ title: "Complete Images",
1304
+ handler: async () => {
1305
+ return await tool.commitWorkingImagesAsCropped();
1306
+ }
1307
+ },
1308
+ {
1309
+ command: "exportUserCroppedImage",
1310
+ id: "exportUserCroppedImage",
1311
+ title: "Export User Cropped Image",
1312
+ handler: async (options = {}) => {
1313
+ return await tool.exportUserCroppedImage(options);
1314
+ }
1315
+ },
1316
+ {
1317
+ command: "fitImageToArea",
1318
+ id: "fitImageToArea",
1319
+ title: "Fit Image to Area",
1320
+ handler: async (id, area) => {
1321
+ await tool.fitImageToArea(id, area);
1322
+ }
1323
+ },
1324
+ {
1325
+ command: "fitImageToDefaultArea",
1326
+ id: "fitImageToDefaultArea",
1327
+ title: "Fit Image to Default Area",
1328
+ handler: async (id) => {
1329
+ await tool.fitImageToDefaultArea(id);
1330
+ }
1331
+ },
1332
+ {
1333
+ command: "focusImage",
1334
+ id: "focusImage",
1335
+ title: "Focus Image",
1336
+ handler: (id, options = {}) => {
1337
+ return tool.setImageFocus(id, options);
1338
+ }
1339
+ },
1340
+ {
1341
+ command: "removeImage",
1342
+ id: "removeImage",
1343
+ title: "Remove Image",
1344
+ handler: (id) => {
1345
+ const removed = tool.items.find((item) => item.id === id);
1346
+ const next = tool.items.filter((item) => item.id !== id);
1347
+ if (next.length !== tool.items.length) {
1348
+ tool.purgeSourceSizeCacheForItem(removed);
1349
+ if (tool.focusedImageId === id) {
1350
+ tool.setImageFocus(null, {
1351
+ syncCanvasSelection: true,
1352
+ skipRender: true
1593
1353
  });
1594
- if (leftHit && rightHit) {
1595
- const pathLength = bridgeBasePath.length;
1596
- const leftOffset = leftHit.location.offset;
1597
- const rightOffset = rightHit.location.offset;
1598
- const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
1599
- const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
1600
- const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
1601
- const offsetsA = sampleWrappedOffsets(
1602
- pathLength,
1603
- leftOffset,
1604
- rightOffset,
1605
- countFor(distanceA)
1606
- );
1607
- const offsetsB = sampleWrappedOffsets(
1608
- pathLength,
1609
- rightOffset,
1610
- leftOffset,
1611
- countFor(distanceB)
1612
- );
1613
- const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1614
- const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1615
- if (pointsA.length >= 2 && pointsB.length >= 2) {
1616
- let topBase = selectOuterChain({
1617
- mainShape: bridgeBasePath,
1618
- pointsA,
1619
- pointsB,
1620
- delta,
1621
- overlap,
1622
- op: f.operation
1623
- });
1624
- const dist2 = (a, b) => {
1625
- const dx = a.x - b.x;
1626
- const dy = a.y - b.y;
1627
- return dx * dx + dy * dy;
1628
- };
1629
- if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
1630
- topBase = topBase.slice().reverse();
1631
- }
1632
- topBase = topBase.slice();
1633
- topBase[0] = leftHit.point;
1634
- topBase[topBase.length - 1] = rightHit.point;
1635
- const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
1636
- const topPoints = topBase.map(
1637
- (p) => p.add(new paper.Point(0, capShiftY))
1638
- );
1639
- const bridgeBottomY = bridgeBottom + overlap * 2;
1640
- const bridgePoly = new paper.Path({ insert: false });
1641
- for (const p of topPoints) bridgePoly.add(p);
1642
- bridgePoly.add(new paper.Point(xRight, bridgeBottomY));
1643
- bridgePoly.add(new paper.Point(xLeft, bridgeBottomY));
1644
- bridgePoly.closed = true;
1645
- const unitedItem = item.unite(bridgePoly);
1646
- item.remove();
1647
- bridgePoly.remove();
1648
- if (f.operation === "add") {
1649
- adds.push(unitedItem);
1650
- } else {
1651
- subtracts.push(unitedItem);
1652
- }
1653
- return;
1654
- }
1655
- }
1656
- }
1657
- if (f.operation === "add") {
1658
- adds.push(item);
1659
- } else {
1660
- subtracts.push(item);
1661
1354
  }
1662
- } else {
1663
- if (f.operation === "add") {
1664
- adds.push(item);
1665
- } else {
1666
- subtracts.push(item);
1667
- }
1668
- }
1669
- } else {
1670
- if (f.operation === "add") {
1671
- adds.push(item);
1672
- } else {
1673
- subtracts.push(item);
1355
+ tool.updateConfig(next);
1674
1356
  }
1675
1357
  }
1676
- });
1677
- if (adds.length > 0) {
1678
- for (const item of adds) {
1679
- try {
1680
- const temp = mainShape.unite(item);
1681
- mainShape.remove();
1682
- item.remove();
1683
- mainShape = normalizePathItem(temp);
1684
- } catch (e) {
1685
- console.error("Geometry: Failed to unite feature", e);
1686
- item.remove();
1358
+ },
1359
+ {
1360
+ command: "updateImage",
1361
+ id: "updateImage",
1362
+ title: "Update Image",
1363
+ handler: async (id, updates, options = {}) => {
1364
+ await tool.updateImage(id, updates, options);
1365
+ }
1366
+ },
1367
+ {
1368
+ command: "clearImages",
1369
+ id: "clearImages",
1370
+ title: "Clear Images",
1371
+ handler: () => {
1372
+ tool.sourceSizeCache.clear();
1373
+ tool.setImageFocus(null, {
1374
+ syncCanvasSelection: true,
1375
+ skipRender: true
1376
+ });
1377
+ tool.updateConfig([]);
1378
+ }
1379
+ },
1380
+ {
1381
+ command: "bringToFront",
1382
+ id: "bringToFront",
1383
+ title: "Bring Image to Front",
1384
+ handler: (id) => {
1385
+ const index = tool.items.findIndex((item) => item.id === id);
1386
+ if (index !== -1 && index < tool.items.length - 1) {
1387
+ const next = [...tool.items];
1388
+ const [item] = next.splice(index, 1);
1389
+ next.push(item);
1390
+ tool.updateConfig(next);
1687
1391
  }
1688
1392
  }
1689
- }
1690
- if (subtracts.length > 0) {
1691
- for (const item of subtracts) {
1692
- try {
1693
- const temp = mainShape.subtract(item);
1694
- mainShape.remove();
1695
- item.remove();
1696
- mainShape = normalizePathItem(temp);
1697
- } catch (e) {
1698
- console.error("Geometry: Failed to subtract feature", e);
1699
- item.remove();
1393
+ },
1394
+ {
1395
+ command: "sendToBack",
1396
+ id: "sendToBack",
1397
+ title: "Send Image to Back",
1398
+ handler: (id) => {
1399
+ const index = tool.items.findIndex((item) => item.id === id);
1400
+ if (index > 0) {
1401
+ const next = [...tool.items];
1402
+ const [item] = next.splice(index, 1);
1403
+ next.unshift(item);
1404
+ tool.updateConfig(next);
1700
1405
  }
1701
1406
  }
1702
1407
  }
1408
+ ];
1409
+ }
1410
+
1411
+ // src/extensions/image/config.ts
1412
+ function createImageConfigurations() {
1413
+ return [
1414
+ {
1415
+ id: "image.items",
1416
+ type: "array",
1417
+ label: "Images",
1418
+ default: []
1419
+ },
1420
+ {
1421
+ id: "image.debug",
1422
+ type: "boolean",
1423
+ label: "Image Debug Log",
1424
+ default: false
1425
+ },
1426
+ {
1427
+ id: "image.control.cornerSize",
1428
+ type: "number",
1429
+ label: "Image Control Corner Size",
1430
+ min: 4,
1431
+ max: 64,
1432
+ step: 1,
1433
+ default: 14
1434
+ },
1435
+ {
1436
+ id: "image.control.touchCornerSize",
1437
+ type: "number",
1438
+ label: "Image Control Touch Corner Size",
1439
+ min: 8,
1440
+ max: 96,
1441
+ step: 1,
1442
+ default: 24
1443
+ },
1444
+ {
1445
+ id: "image.control.cornerStyle",
1446
+ type: "select",
1447
+ label: "Image Control Corner Style",
1448
+ options: ["circle", "rect"],
1449
+ default: "circle"
1450
+ },
1451
+ {
1452
+ id: "image.control.cornerColor",
1453
+ type: "color",
1454
+ label: "Image Control Corner Color",
1455
+ default: "#ffffff"
1456
+ },
1457
+ {
1458
+ id: "image.control.cornerStrokeColor",
1459
+ type: "color",
1460
+ label: "Image Control Corner Stroke Color",
1461
+ default: "#1677ff"
1462
+ },
1463
+ {
1464
+ id: "image.control.transparentCorners",
1465
+ type: "boolean",
1466
+ label: "Image Control Transparent Corners",
1467
+ default: false
1468
+ },
1469
+ {
1470
+ id: "image.control.borderColor",
1471
+ type: "color",
1472
+ label: "Image Control Border Color",
1473
+ default: "#1677ff"
1474
+ },
1475
+ {
1476
+ id: "image.control.borderScaleFactor",
1477
+ type: "number",
1478
+ label: "Image Control Border Width",
1479
+ min: 0.5,
1480
+ max: 8,
1481
+ step: 0.1,
1482
+ default: 1.5
1483
+ },
1484
+ {
1485
+ id: "image.control.padding",
1486
+ type: "number",
1487
+ label: "Image Control Padding",
1488
+ min: 0,
1489
+ max: 64,
1490
+ step: 1,
1491
+ default: 0
1492
+ },
1493
+ {
1494
+ id: "image.frame.strokeColor",
1495
+ type: "color",
1496
+ label: "Image Frame Stroke Color",
1497
+ default: "#808080"
1498
+ },
1499
+ {
1500
+ id: "image.frame.strokeWidth",
1501
+ type: "number",
1502
+ label: "Image Frame Stroke Width",
1503
+ min: 0,
1504
+ max: 20,
1505
+ step: 0.5,
1506
+ default: 2
1507
+ },
1508
+ {
1509
+ id: "image.frame.strokeStyle",
1510
+ type: "select",
1511
+ label: "Image Frame Stroke Style",
1512
+ options: ["solid", "dashed", "hidden"],
1513
+ default: "dashed"
1514
+ },
1515
+ {
1516
+ id: "image.frame.dashLength",
1517
+ type: "number",
1518
+ label: "Image Frame Dash Length",
1519
+ min: 1,
1520
+ max: 40,
1521
+ step: 1,
1522
+ default: 8
1523
+ },
1524
+ {
1525
+ id: "image.frame.innerBackground",
1526
+ type: "color",
1527
+ label: "Image Frame Inner Background",
1528
+ default: "rgba(0,0,0,0)"
1529
+ },
1530
+ {
1531
+ id: "image.frame.outerBackground",
1532
+ type: "color",
1533
+ label: "Image Frame Outer Background",
1534
+ default: "#f5f5f5"
1535
+ }
1536
+ ];
1537
+ }
1538
+
1539
+ // src/extensions/geometry.ts
1540
+ import paper from "paper";
1541
+
1542
+ // src/extensions/bridgeSelection.ts
1543
+ function pickExitIndex(hits) {
1544
+ for (let i = 0; i < hits.length; i++) {
1545
+ const h = hits[i];
1546
+ if (h.insideBelow && !h.insideAbove) return i;
1703
1547
  }
1704
- return mainShape;
1548
+ return -1;
1705
1549
  }
1706
- function applySurfaceFeatures(shape, features, options) {
1707
- const surfaceFeatures = features.filter(
1708
- (f) => f.renderBehavior === "surface"
1709
- );
1710
- if (surfaceFeatures.length === 0) return shape;
1711
- let result = shape;
1712
- for (const f of surfaceFeatures) {
1713
- const pos = resolveFeaturePosition(f, options);
1714
- const center = new paper.Point(pos.x, pos.y);
1715
- const item = createFeatureItem(f, center);
1716
- try {
1717
- if (f.operation === "add") {
1718
- const temp = result.unite(item);
1719
- result.remove();
1720
- item.remove();
1721
- result = normalizePathItem(temp);
1722
- } else {
1723
- const temp = result.subtract(item);
1724
- result.remove();
1725
- item.remove();
1726
- result = normalizePathItem(temp);
1727
- }
1728
- } catch (e) {
1729
- console.error("Geometry: Failed to apply surface feature", e);
1730
- item.remove();
1731
- }
1550
+ function scoreOutsideAbove(samples) {
1551
+ let score = 0;
1552
+ for (const s of samples) {
1553
+ if (s.outsideAbove) score++;
1732
1554
  }
1733
- return result;
1555
+ return score;
1734
1556
  }
1735
- function generateDielinePath(options) {
1736
- const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
1737
- const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
1738
- ensurePaper(paperWidth, paperHeight);
1739
- paper.project.activeLayer.removeChildren();
1740
- const perimeter = getPerimeterShape(options);
1741
- const finalShape = applySurfaceFeatures(perimeter, options.features, options);
1742
- const pathData = finalShape.pathData;
1743
- finalShape.remove();
1744
- return pathData;
1557
+
1558
+ // src/extensions/wrappedOffsets.ts
1559
+ function wrappedDistance(total, start, end) {
1560
+ if (!Number.isFinite(total) || total <= 0) return 0;
1561
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
1562
+ const s = (start % total + total) % total;
1563
+ const e = (end % total + total) % total;
1564
+ return e >= s ? e - s : total - s + e;
1745
1565
  }
1746
- function generateBleedZonePath(originalOptions, offsetOptions, offset) {
1747
- const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
1748
- const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
1749
- ensurePaper(paperWidth, paperHeight);
1750
- paper.project.activeLayer.removeChildren();
1751
- const pOriginal = getPerimeterShape(originalOptions);
1752
- const shapeOriginal = applySurfaceFeatures(
1753
- pOriginal,
1754
- originalOptions.features,
1755
- originalOptions
1756
- );
1757
- const pOffset = getPerimeterShape(offsetOptions);
1758
- const shapeOffset = applySurfaceFeatures(
1759
- pOffset,
1760
- offsetOptions.features,
1761
- offsetOptions
1762
- );
1763
- let bleedZone;
1764
- if (offset > 0) {
1765
- bleedZone = shapeOffset.subtract(shapeOriginal);
1766
- } else {
1767
- bleedZone = shapeOriginal.subtract(shapeOffset);
1566
+ function sampleWrappedOffsets(total, start, end, count) {
1567
+ if (!Number.isFinite(total) || total <= 0) return [];
1568
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
1569
+ const n = Math.max(0, Math.floor(count));
1570
+ if (n <= 0) return [];
1571
+ const dist = wrappedDistance(total, start, end);
1572
+ if (n === 1) return [(start % total + total) % total];
1573
+ const step = dist / (n - 1);
1574
+ const offsets = [];
1575
+ for (let i = 0; i < n; i++) {
1576
+ const raw = start + step * i;
1577
+ const wrapped = (raw % total + total) % total;
1578
+ offsets.push(wrapped);
1768
1579
  }
1769
- const pathData = bleedZone.pathData;
1770
- shapeOriginal.remove();
1771
- shapeOffset.remove();
1772
- bleedZone.remove();
1773
- return pathData;
1580
+ return offsets;
1774
1581
  }
1775
- function getLowestPointOnDieline(options) {
1776
- ensurePaper(options.width * 2, options.height * 2);
1777
- paper.project.activeLayer.removeChildren();
1778
- const shape = createBaseShape(options);
1779
- const bounds = shape.bounds;
1780
- const result = {
1781
- x: bounds.center.x,
1782
- y: bounds.bottom
1582
+
1583
+ // src/extensions/geometry.ts
1584
+ function resolveFeaturePosition(feature, geometry) {
1585
+ const { x, y, width, height } = geometry;
1586
+ const left = x - width / 2;
1587
+ const top = y - height / 2;
1588
+ return {
1589
+ x: left + feature.x * width,
1590
+ y: top + feature.y * height
1783
1591
  };
1784
- shape.remove();
1785
- return result;
1786
1592
  }
1787
- function getNearestPointOnDieline(point, options) {
1788
- ensurePaper(options.width * 2, options.height * 2);
1789
- paper.project.activeLayer.removeChildren();
1790
- const shape = createBaseShape(options);
1791
- const p = new paper.Point(point.x, point.y);
1792
- const location = shape.getNearestLocation(p);
1793
- const result = {
1794
- x: location.point.x,
1795
- y: location.point.y,
1796
- normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
1797
- };
1798
- shape.remove();
1593
+ function ensurePaper(width, height) {
1594
+ if (!paper.project) {
1595
+ paper.setup(new paper.Size(width, height));
1596
+ } else {
1597
+ paper.view.viewSize = new paper.Size(width, height);
1598
+ }
1599
+ }
1600
+ var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
1601
+ function normalizePathItem(shape) {
1602
+ let result = shape;
1603
+ if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
1604
+ if (typeof result.reduce === "function") result = result.reduce({});
1605
+ if (typeof result.reorient === "function") result = result.reorient(true, true);
1606
+ if (typeof result.reduce === "function") result = result.reduce({});
1799
1607
  return result;
1800
1608
  }
1801
- function getPathBounds(pathData) {
1802
- const path = new paper.Path();
1803
- path.pathData = pathData;
1804
- const bounds = path.bounds;
1805
- path.remove();
1806
- return {
1807
- x: bounds.x,
1808
- y: bounds.y,
1809
- width: bounds.width,
1810
- height: bounds.height
1811
- };
1609
+ function getBridgeDelta(itemBounds, overlap) {
1610
+ return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
1812
1611
  }
1813
-
1814
- // src/shared/scene/frame.ts
1815
- function emptyFrameRect() {
1816
- return { left: 0, top: 0, width: 0, height: 0 };
1612
+ function getExitHit(args) {
1613
+ const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
1614
+ const ray = new paper.Path.Line({
1615
+ from: [x, bridgeBottom],
1616
+ to: [x, toY],
1617
+ insert: false
1618
+ });
1619
+ const intersections = mainShape.getIntersections(ray) || [];
1620
+ ray.remove();
1621
+ const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
1622
+ if (validHits.length === 0) return null;
1623
+ validHits.sort((a, b) => b.point.y - a.point.y);
1624
+ const flags = validHits.map((h) => {
1625
+ const above = h.point.add(new paper.Point(0, -delta));
1626
+ const below = h.point.add(new paper.Point(0, delta));
1627
+ return {
1628
+ insideAbove: mainShape.contains(above),
1629
+ insideBelow: mainShape.contains(below)
1630
+ };
1631
+ });
1632
+ const idx = pickExitIndex(flags);
1633
+ if (idx < 0) return null;
1634
+ if (isBridgeDebugEnabled()) {
1635
+ console.debug("Geometry: Bridge ray", {
1636
+ x,
1637
+ validHits: validHits.length,
1638
+ idx,
1639
+ delta,
1640
+ overlap,
1641
+ op
1642
+ });
1643
+ }
1644
+ const hit = validHits[idx];
1645
+ return { point: hit.point, location: hit };
1817
1646
  }
1818
- function resolveCutFrameRect(canvasService, configService) {
1819
- if (!canvasService || !configService) {
1820
- return emptyFrameRect();
1647
+ function selectOuterChain(args) {
1648
+ const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
1649
+ const scoreA = scoreOutsideAbove(
1650
+ pointsA.map((p) => ({
1651
+ outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
1652
+ }))
1653
+ );
1654
+ const scoreB = scoreOutsideAbove(
1655
+ pointsB.map((p) => ({
1656
+ outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
1657
+ }))
1658
+ );
1659
+ const ratioA = scoreA / pointsA.length;
1660
+ const ratioB = scoreB / pointsB.length;
1661
+ if (isBridgeDebugEnabled()) {
1662
+ console.debug("Geometry: Bridge chain", {
1663
+ scoreA,
1664
+ scoreB,
1665
+ lenA: pointsA.length,
1666
+ lenB: pointsB.length,
1667
+ ratioA,
1668
+ ratioB,
1669
+ delta,
1670
+ overlap,
1671
+ op
1672
+ });
1821
1673
  }
1822
- const sizeState = readSizeState(configService);
1823
- const layout = computeSceneLayout(canvasService, sizeState);
1824
- if (!layout) {
1825
- return emptyFrameRect();
1674
+ const ratioEps = 1e-6;
1675
+ if (Math.abs(ratioA - ratioB) > ratioEps) {
1676
+ return ratioA > ratioB ? pointsA : pointsB;
1826
1677
  }
1827
- return canvasService.toSceneRect({
1828
- left: layout.cutRect.left,
1829
- top: layout.cutRect.top,
1830
- width: layout.cutRect.width,
1831
- height: layout.cutRect.height
1832
- });
1678
+ if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
1679
+ return pointsA.length <= pointsB.length ? pointsA : pointsB;
1833
1680
  }
1834
- function toLayoutSceneRect(rect) {
1835
- return {
1836
- left: rect.left,
1837
- top: rect.top,
1838
- width: rect.width,
1839
- height: rect.height,
1840
- space: "scene"
1841
- };
1681
+ function fitPathItemToRect(item, rect, fitMode) {
1682
+ const { left, top, width, height } = rect;
1683
+ const bounds = item.bounds;
1684
+ if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
1685
+ item.position = new paper.Point(left + width / 2, top + height / 2);
1686
+ return item;
1687
+ }
1688
+ item.translate(new paper.Point(-bounds.left, -bounds.top));
1689
+ if (fitMode === "stretch") {
1690
+ item.scale(width / bounds.width, height / bounds.height, new paper.Point(0, 0));
1691
+ item.translate(new paper.Point(left, top));
1692
+ return item;
1693
+ }
1694
+ const uniformScale = Math.min(width / bounds.width, height / bounds.height);
1695
+ item.scale(uniformScale, uniformScale, new paper.Point(0, 0));
1696
+ const scaledWidth = bounds.width * uniformScale;
1697
+ const scaledHeight = bounds.height * uniformScale;
1698
+ item.translate(
1699
+ new paper.Point(
1700
+ left + (width - scaledWidth) / 2,
1701
+ top + (height - scaledHeight) / 2
1702
+ )
1703
+ );
1704
+ return item;
1842
1705
  }
1843
-
1844
- // src/shared/runtime/sessionState.ts
1845
- function cloneWithJson(value) {
1846
- return JSON.parse(JSON.stringify(value));
1706
+ function createNormalizedHeartPath(params) {
1707
+ const { lobeSpread, notchDepth, tipSharpness } = params;
1708
+ const halfSpread = 0.22 + lobeSpread * 0.18;
1709
+ const notchY = 0.06 + notchDepth * 0.2;
1710
+ const shoulderY = 0.24 + notchDepth * 0.2;
1711
+ const topLift = 0.12 + (1 - notchDepth) * 0.06;
1712
+ const topY = notchY - topLift;
1713
+ const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
1714
+ const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
1715
+ const tipCtrlX = 0.34 - tipSharpness * 0.2;
1716
+ const notchCtrlX = 0.06 + lobeSpread * 0.06;
1717
+ const lobeCtrlX = 0.1 + lobeSpread * 0.08;
1718
+ const notchCtrlY = notchY - topLift * 0.45;
1719
+ const xPeakL = 0.5 - halfSpread;
1720
+ const xPeakR = 0.5 + halfSpread;
1721
+ const heartPath = new paper.Path({ insert: false });
1722
+ heartPath.moveTo(new paper.Point(0.5, notchY));
1723
+ heartPath.cubicCurveTo(
1724
+ new paper.Point(0.5 - notchCtrlX, notchCtrlY),
1725
+ new paper.Point(xPeakL + lobeCtrlX, topY),
1726
+ new paper.Point(xPeakL, topY)
1727
+ );
1728
+ heartPath.cubicCurveTo(
1729
+ new paper.Point(xPeakL - lobeCtrlX, topY),
1730
+ new paper.Point(0, sideCtrlY),
1731
+ new paper.Point(0, shoulderY)
1732
+ );
1733
+ heartPath.cubicCurveTo(
1734
+ new paper.Point(0, lowerCtrlY),
1735
+ new paper.Point(tipCtrlX, 1),
1736
+ new paper.Point(0.5, 1)
1737
+ );
1738
+ heartPath.cubicCurveTo(
1739
+ new paper.Point(1 - tipCtrlX, 1),
1740
+ new paper.Point(1, lowerCtrlY),
1741
+ new paper.Point(1, shoulderY)
1742
+ );
1743
+ heartPath.cubicCurveTo(
1744
+ new paper.Point(1, sideCtrlY),
1745
+ new paper.Point(xPeakR + lobeCtrlX, topY),
1746
+ new paper.Point(xPeakR, topY)
1747
+ );
1748
+ heartPath.cubicCurveTo(
1749
+ new paper.Point(xPeakR - lobeCtrlX, topY),
1750
+ new paper.Point(0.5 + notchCtrlX, notchCtrlY),
1751
+ new paper.Point(0.5, notchY)
1752
+ );
1753
+ heartPath.closed = true;
1754
+ return heartPath;
1847
1755
  }
1848
- function applyCommittedSnapshot(session, nextCommitted, options) {
1849
- const clone = options.clone;
1850
- session.committed = clone(nextCommitted);
1851
- const shouldPreserveDirtyWorking = options.toolActive && options.preserveDirtyWorking !== false && session.hasWorkingChanges;
1852
- if (!shouldPreserveDirtyWorking) {
1853
- session.working = clone(session.committed);
1854
- session.hasWorkingChanges = false;
1855
- }
1756
+ function createHeartBaseShape(options) {
1757
+ const { x, y, width, height } = options;
1758
+ const w = Math.max(0, width);
1759
+ const h = Math.max(0, height);
1760
+ const left = x - w / 2;
1761
+ const top = y - h / 2;
1762
+ const fitMode = getShapeFitMode(options.shapeStyle);
1763
+ const heartParams = getHeartShapeParams(options.shapeStyle);
1764
+ const rawHeart = createNormalizedHeartPath(heartParams);
1765
+ return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
1856
1766
  }
1857
- function runDeferredConfigUpdate(state, action, cooldownMs = 0) {
1858
- state.isUpdatingConfig = true;
1859
- action();
1860
- if (cooldownMs <= 0) {
1861
- state.isUpdatingConfig = false;
1862
- return;
1767
+ var BUILTIN_SHAPE_BUILDERS = {
1768
+ rect: (options) => {
1769
+ const { x, y, width, height, radius } = options;
1770
+ return new paper.Path.Rectangle({
1771
+ point: [x - width / 2, y - height / 2],
1772
+ size: [Math.max(0, width), Math.max(0, height)],
1773
+ radius: Math.max(0, radius)
1774
+ });
1775
+ },
1776
+ circle: (options) => {
1777
+ const { x, y, width, height } = options;
1778
+ const r = Math.min(width, height) / 2;
1779
+ return new paper.Path.Circle({
1780
+ center: new paper.Point(x, y),
1781
+ radius: Math.max(0, r)
1782
+ });
1783
+ },
1784
+ ellipse: (options) => {
1785
+ const { x, y, width, height } = options;
1786
+ return new paper.Path.Ellipse({
1787
+ center: new paper.Point(x, y),
1788
+ radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
1789
+ });
1790
+ },
1791
+ heart: createHeartBaseShape
1792
+ };
1793
+ function createCustomBaseShape(options) {
1794
+ var _a;
1795
+ const {
1796
+ pathData,
1797
+ customSourceWidthPx,
1798
+ customSourceHeightPx,
1799
+ x,
1800
+ y,
1801
+ width,
1802
+ height
1803
+ } = options;
1804
+ if (typeof pathData !== "string" || pathData.trim().length === 0) {
1805
+ return null;
1863
1806
  }
1864
- setTimeout(() => {
1865
- state.isUpdatingConfig = false;
1866
- }, cooldownMs);
1867
- }
1868
-
1869
- // src/extensions/image/commands.ts
1870
- function createImageCommands(tool) {
1871
- return [
1872
- {
1873
- command: "addImage",
1874
- id: "addImage",
1875
- title: "Add Image",
1876
- handler: async (url, options) => {
1877
- const result = await tool.upsertImageEntry(url, {
1878
- mode: "add",
1879
- addOptions: options
1880
- });
1881
- return result.id;
1882
- }
1883
- },
1884
- {
1885
- command: "upsertImage",
1886
- id: "upsertImage",
1887
- title: "Upsert Image",
1888
- handler: async (url, options = {}) => {
1889
- return await tool.upsertImageEntry(url, options);
1890
- }
1891
- },
1892
- {
1893
- command: "getWorkingImages",
1894
- id: "getWorkingImages",
1895
- title: "Get Working Images",
1896
- handler: () => {
1897
- return tool.cloneItems(tool.workingItems);
1898
- }
1899
- },
1900
- {
1901
- command: "setWorkingImage",
1902
- id: "setWorkingImage",
1903
- title: "Set Working Image",
1904
- handler: (id, updates) => {
1905
- tool.updateImageInWorking(id, updates);
1906
- }
1907
- },
1908
- {
1909
- command: "resetWorkingImages",
1910
- id: "resetWorkingImages",
1911
- title: "Reset Working Images",
1912
- handler: () => {
1913
- tool.workingItems = tool.cloneItems(tool.items);
1914
- tool.hasWorkingChanges = false;
1915
- tool.updateImages();
1916
- tool.emitWorkingChange();
1917
- }
1918
- },
1919
- {
1920
- command: "completeImages",
1921
- id: "completeImages",
1922
- title: "Complete Images",
1923
- handler: async () => {
1924
- return await tool.commitWorkingImagesAsCropped();
1925
- }
1926
- },
1927
- {
1928
- command: "exportUserCroppedImage",
1929
- id: "exportUserCroppedImage",
1930
- title: "Export User Cropped Image",
1931
- handler: async (options = {}) => {
1932
- return await tool.exportUserCroppedImage(options);
1933
- }
1934
- },
1935
- {
1936
- command: "fitImageToArea",
1937
- id: "fitImageToArea",
1938
- title: "Fit Image to Area",
1939
- handler: async (id, area) => {
1940
- await tool.fitImageToArea(id, area);
1941
- }
1942
- },
1943
- {
1944
- command: "fitImageToDefaultArea",
1945
- id: "fitImageToDefaultArea",
1946
- title: "Fit Image to Default Area",
1947
- handler: async (id) => {
1948
- await tool.fitImageToDefaultArea(id);
1949
- }
1950
- },
1951
- {
1952
- command: "focusImage",
1953
- id: "focusImage",
1954
- title: "Focus Image",
1955
- handler: (id, options = {}) => {
1956
- return tool.setImageFocus(id, options);
1807
+ const center = new paper.Point(x, y);
1808
+ const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
1809
+ const path = hasMultipleSubPaths ? new paper.CompoundPath(pathData) : (() => {
1810
+ const single = new paper.Path();
1811
+ single.pathData = pathData;
1812
+ return single;
1813
+ })();
1814
+ const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
1815
+ const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
1816
+ if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
1817
+ const targetLeft = x - width / 2;
1818
+ const targetTop = y - height / 2;
1819
+ path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
1820
+ path.translate(new paper.Point(targetLeft, targetTop));
1821
+ return path;
1822
+ }
1823
+ if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
1824
+ path.position = center;
1825
+ path.scale(width / path.bounds.width, height / path.bounds.height);
1826
+ return path;
1827
+ }
1828
+ path.position = center;
1829
+ return path;
1830
+ }
1831
+ function createBaseShape(options) {
1832
+ const { shape } = options;
1833
+ if (shape === "custom") {
1834
+ const customShape = createCustomBaseShape(options);
1835
+ if (customShape) return customShape;
1836
+ return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
1837
+ }
1838
+ return BUILTIN_SHAPE_BUILDERS[shape](options);
1839
+ }
1840
+ function resolveBridgeBasePath(shape, anchor) {
1841
+ if (shape instanceof paper.Path) {
1842
+ return shape;
1843
+ }
1844
+ if (shape instanceof paper.CompoundPath) {
1845
+ const children = (shape.children || []).filter(
1846
+ (child) => child instanceof paper.Path
1847
+ );
1848
+ if (!children.length) return null;
1849
+ let best = children[0];
1850
+ let bestDistance = Infinity;
1851
+ for (const child of children) {
1852
+ const location = child.getNearestLocation(anchor);
1853
+ const point = location == null ? void 0 : location.point;
1854
+ if (!point) continue;
1855
+ const distance = point.getDistance(anchor);
1856
+ if (distance < bestDistance) {
1857
+ bestDistance = distance;
1858
+ best = child;
1957
1859
  }
1958
- },
1959
- {
1960
- command: "removeImage",
1961
- id: "removeImage",
1962
- title: "Remove Image",
1963
- handler: (id) => {
1964
- const removed = tool.items.find((item) => item.id === id);
1965
- const next = tool.items.filter((item) => item.id !== id);
1966
- if (next.length !== tool.items.length) {
1967
- tool.purgeSourceSizeCacheForItem(removed);
1968
- if (tool.focusedImageId === id) {
1969
- tool.setImageFocus(null, {
1970
- syncCanvasSelection: true,
1971
- skipRender: true
1860
+ }
1861
+ return best;
1862
+ }
1863
+ return null;
1864
+ }
1865
+ function createFeatureItem(feature, center) {
1866
+ let item;
1867
+ if (feature.shape === "rect") {
1868
+ const w = feature.width || 10;
1869
+ const h = feature.height || 10;
1870
+ const r = feature.radius || 0;
1871
+ item = new paper.Path.Rectangle({
1872
+ point: [center.x - w / 2, center.y - h / 2],
1873
+ size: [w, h],
1874
+ radius: r
1875
+ });
1876
+ } else {
1877
+ const r = feature.radius || 5;
1878
+ item = new paper.Path.Circle({
1879
+ center,
1880
+ radius: r
1881
+ });
1882
+ }
1883
+ if (feature.rotation) {
1884
+ item.rotate(feature.rotation, center);
1885
+ }
1886
+ return item;
1887
+ }
1888
+ function getPerimeterShape(options) {
1889
+ let mainShape = createBaseShape(options);
1890
+ const { features } = options;
1891
+ if (features && features.length > 0) {
1892
+ const edgeFeatures = features.filter(
1893
+ (f) => !f.renderBehavior || f.renderBehavior === "edge"
1894
+ );
1895
+ const adds = [];
1896
+ const subtracts = [];
1897
+ edgeFeatures.forEach((f) => {
1898
+ const pos = resolveFeaturePosition(f, options);
1899
+ const center = new paper.Point(pos.x, pos.y);
1900
+ const item = createFeatureItem(f, center);
1901
+ if (f.bridge && f.bridge.type === "vertical") {
1902
+ const itemBounds = item.bounds;
1903
+ const mainBounds = mainShape.bounds;
1904
+ const bridgeTop = mainBounds.top;
1905
+ const bridgeBottom = itemBounds.top;
1906
+ if (bridgeBottom > bridgeTop) {
1907
+ const overlap = 2;
1908
+ const rayPadding = 10;
1909
+ const eps = 0.1;
1910
+ const delta = getBridgeDelta(itemBounds, overlap);
1911
+ const toY = bridgeTop - rayPadding;
1912
+ const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
1913
+ const xLeft = itemBounds.left + inset;
1914
+ const xRight = itemBounds.right - inset;
1915
+ const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
1916
+ const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
1917
+ if (canBridge && bridgeBasePath) {
1918
+ const leftHit = getExitHit({
1919
+ mainShape: bridgeBasePath,
1920
+ x: xLeft,
1921
+ bridgeBottom,
1922
+ toY,
1923
+ eps,
1924
+ delta,
1925
+ overlap,
1926
+ op: f.operation
1927
+ });
1928
+ const rightHit = getExitHit({
1929
+ mainShape: bridgeBasePath,
1930
+ x: xRight,
1931
+ bridgeBottom,
1932
+ toY,
1933
+ eps,
1934
+ delta,
1935
+ overlap,
1936
+ op: f.operation
1972
1937
  });
1938
+ if (leftHit && rightHit) {
1939
+ const pathLength = bridgeBasePath.length;
1940
+ const leftOffset = leftHit.location.offset;
1941
+ const rightOffset = rightHit.location.offset;
1942
+ const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
1943
+ const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
1944
+ const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
1945
+ const offsetsA = sampleWrappedOffsets(
1946
+ pathLength,
1947
+ leftOffset,
1948
+ rightOffset,
1949
+ countFor(distanceA)
1950
+ );
1951
+ const offsetsB = sampleWrappedOffsets(
1952
+ pathLength,
1953
+ rightOffset,
1954
+ leftOffset,
1955
+ countFor(distanceB)
1956
+ );
1957
+ const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1958
+ const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1959
+ if (pointsA.length >= 2 && pointsB.length >= 2) {
1960
+ let topBase = selectOuterChain({
1961
+ mainShape: bridgeBasePath,
1962
+ pointsA,
1963
+ pointsB,
1964
+ delta,
1965
+ overlap,
1966
+ op: f.operation
1967
+ });
1968
+ const dist2 = (a, b) => {
1969
+ const dx = a.x - b.x;
1970
+ const dy = a.y - b.y;
1971
+ return dx * dx + dy * dy;
1972
+ };
1973
+ if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
1974
+ topBase = topBase.slice().reverse();
1975
+ }
1976
+ topBase = topBase.slice();
1977
+ topBase[0] = leftHit.point;
1978
+ topBase[topBase.length - 1] = rightHit.point;
1979
+ const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
1980
+ const topPoints = topBase.map(
1981
+ (p) => p.add(new paper.Point(0, capShiftY))
1982
+ );
1983
+ const bridgeBottomY = bridgeBottom + overlap * 2;
1984
+ const bridgePoly = new paper.Path({ insert: false });
1985
+ for (const p of topPoints) bridgePoly.add(p);
1986
+ bridgePoly.add(new paper.Point(xRight, bridgeBottomY));
1987
+ bridgePoly.add(new paper.Point(xLeft, bridgeBottomY));
1988
+ bridgePoly.closed = true;
1989
+ const unitedItem = item.unite(bridgePoly);
1990
+ item.remove();
1991
+ bridgePoly.remove();
1992
+ if (f.operation === "add") {
1993
+ adds.push(unitedItem);
1994
+ } else {
1995
+ subtracts.push(unitedItem);
1996
+ }
1997
+ return;
1998
+ }
1999
+ }
2000
+ }
2001
+ if (f.operation === "add") {
2002
+ adds.push(item);
2003
+ } else {
2004
+ subtracts.push(item);
2005
+ }
2006
+ } else {
2007
+ if (f.operation === "add") {
2008
+ adds.push(item);
2009
+ } else {
2010
+ subtracts.push(item);
1973
2011
  }
1974
- tool.updateConfig(next);
2012
+ }
2013
+ } else {
2014
+ if (f.operation === "add") {
2015
+ adds.push(item);
2016
+ } else {
2017
+ subtracts.push(item);
1975
2018
  }
1976
2019
  }
1977
- },
1978
- {
1979
- command: "updateImage",
1980
- id: "updateImage",
1981
- title: "Update Image",
1982
- handler: async (id, updates, options = {}) => {
1983
- await tool.updateImage(id, updates, options);
1984
- }
1985
- },
1986
- {
1987
- command: "clearImages",
1988
- id: "clearImages",
1989
- title: "Clear Images",
1990
- handler: () => {
1991
- tool.sourceSizeCache.clear();
1992
- tool.setImageFocus(null, {
1993
- syncCanvasSelection: true,
1994
- skipRender: true
1995
- });
1996
- tool.updateConfig([]);
1997
- }
1998
- },
1999
- {
2000
- command: "bringToFront",
2001
- id: "bringToFront",
2002
- title: "Bring Image to Front",
2003
- handler: (id) => {
2004
- const index = tool.items.findIndex((item) => item.id === id);
2005
- if (index !== -1 && index < tool.items.length - 1) {
2006
- const next = [...tool.items];
2007
- const [item] = next.splice(index, 1);
2008
- next.push(item);
2009
- tool.updateConfig(next);
2020
+ });
2021
+ if (adds.length > 0) {
2022
+ for (const item of adds) {
2023
+ try {
2024
+ const temp = mainShape.unite(item);
2025
+ mainShape.remove();
2026
+ item.remove();
2027
+ mainShape = normalizePathItem(temp);
2028
+ } catch (e) {
2029
+ console.error("Geometry: Failed to unite feature", e);
2030
+ item.remove();
2010
2031
  }
2011
2032
  }
2012
- },
2013
- {
2014
- command: "sendToBack",
2015
- id: "sendToBack",
2016
- title: "Send Image to Back",
2017
- handler: (id) => {
2018
- const index = tool.items.findIndex((item) => item.id === id);
2019
- if (index > 0) {
2020
- const next = [...tool.items];
2021
- const [item] = next.splice(index, 1);
2022
- next.unshift(item);
2023
- tool.updateConfig(next);
2033
+ }
2034
+ if (subtracts.length > 0) {
2035
+ for (const item of subtracts) {
2036
+ try {
2037
+ const temp = mainShape.subtract(item);
2038
+ mainShape.remove();
2039
+ item.remove();
2040
+ mainShape = normalizePathItem(temp);
2041
+ } catch (e) {
2042
+ console.error("Geometry: Failed to subtract feature", e);
2043
+ item.remove();
2024
2044
  }
2025
2045
  }
2026
2046
  }
2027
- ];
2047
+ }
2048
+ return mainShape;
2049
+ }
2050
+ function applySurfaceFeatures(shape, features, options) {
2051
+ const surfaceFeatures = features.filter(
2052
+ (f) => f.renderBehavior === "surface"
2053
+ );
2054
+ if (surfaceFeatures.length === 0) return shape;
2055
+ let result = shape;
2056
+ for (const f of surfaceFeatures) {
2057
+ const pos = resolveFeaturePosition(f, options);
2058
+ const center = new paper.Point(pos.x, pos.y);
2059
+ const item = createFeatureItem(f, center);
2060
+ try {
2061
+ if (f.operation === "add") {
2062
+ const temp = result.unite(item);
2063
+ result.remove();
2064
+ item.remove();
2065
+ result = normalizePathItem(temp);
2066
+ } else {
2067
+ const temp = result.subtract(item);
2068
+ result.remove();
2069
+ item.remove();
2070
+ result = normalizePathItem(temp);
2071
+ }
2072
+ } catch (e) {
2073
+ console.error("Geometry: Failed to apply surface feature", e);
2074
+ item.remove();
2075
+ }
2076
+ }
2077
+ return result;
2078
+ }
2079
+ function generateDielinePath(options) {
2080
+ const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
2081
+ const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
2082
+ ensurePaper(paperWidth, paperHeight);
2083
+ paper.project.activeLayer.removeChildren();
2084
+ const perimeter = getPerimeterShape(options);
2085
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
2086
+ const pathData = finalShape.pathData;
2087
+ finalShape.remove();
2088
+ return pathData;
2089
+ }
2090
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
2091
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
2092
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
2093
+ ensurePaper(paperWidth, paperHeight);
2094
+ paper.project.activeLayer.removeChildren();
2095
+ const pOriginal = getPerimeterShape(originalOptions);
2096
+ const shapeOriginal = applySurfaceFeatures(
2097
+ pOriginal,
2098
+ originalOptions.features,
2099
+ originalOptions
2100
+ );
2101
+ const pOffset = getPerimeterShape(offsetOptions);
2102
+ const shapeOffset = applySurfaceFeatures(
2103
+ pOffset,
2104
+ offsetOptions.features,
2105
+ offsetOptions
2106
+ );
2107
+ let bleedZone;
2108
+ if (offset > 0) {
2109
+ bleedZone = shapeOffset.subtract(shapeOriginal);
2110
+ } else {
2111
+ bleedZone = shapeOriginal.subtract(shapeOffset);
2112
+ }
2113
+ const pathData = bleedZone.pathData;
2114
+ shapeOriginal.remove();
2115
+ shapeOffset.remove();
2116
+ bleedZone.remove();
2117
+ return pathData;
2118
+ }
2119
+ function getLowestPointOnDieline(options) {
2120
+ ensurePaper(options.width * 2, options.height * 2);
2121
+ paper.project.activeLayer.removeChildren();
2122
+ const shape = createBaseShape(options);
2123
+ const bounds = shape.bounds;
2124
+ const result = {
2125
+ x: bounds.center.x,
2126
+ y: bounds.bottom
2127
+ };
2128
+ shape.remove();
2129
+ return result;
2130
+ }
2131
+ function getNearestPointOnDieline(point, options) {
2132
+ ensurePaper(options.width * 2, options.height * 2);
2133
+ paper.project.activeLayer.removeChildren();
2134
+ const shape = createBaseShape(options);
2135
+ const p = new paper.Point(point.x, point.y);
2136
+ const location = shape.getNearestLocation(p);
2137
+ const result = {
2138
+ x: location.point.x,
2139
+ y: location.point.y,
2140
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
2141
+ };
2142
+ shape.remove();
2143
+ return result;
2028
2144
  }
2029
2145
 
2030
- // src/extensions/image/config.ts
2031
- function createImageConfigurations() {
2146
+ // src/extensions/image/sessionOverlay.ts
2147
+ var EPSILON = 1e-4;
2148
+ var SHAPE_OUTLINE_COLOR = "rgba(255, 0, 0, 0.9)";
2149
+ var DEFAULT_HATCH_FILL = "rgba(255, 0, 0, 0.22)";
2150
+ function buildRectPath(width, height) {
2151
+ return `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`;
2152
+ }
2153
+ function buildViewportMaskPath(viewport, cutRect) {
2154
+ const cutLeft = cutRect.left - viewport.left;
2155
+ const cutTop = cutRect.top - viewport.top;
2032
2156
  return [
2033
- {
2034
- id: "image.items",
2035
- type: "array",
2036
- label: "Images",
2037
- default: []
2038
- },
2039
- {
2040
- id: "image.debug",
2041
- type: "boolean",
2042
- label: "Image Debug Log",
2043
- default: false
2044
- },
2045
- {
2046
- id: "image.control.cornerSize",
2047
- type: "number",
2048
- label: "Image Control Corner Size",
2049
- min: 4,
2050
- max: 64,
2051
- step: 1,
2052
- default: 14
2053
- },
2054
- {
2055
- id: "image.control.touchCornerSize",
2056
- type: "number",
2057
- label: "Image Control Touch Corner Size",
2058
- min: 8,
2059
- max: 96,
2060
- step: 1,
2061
- default: 24
2062
- },
2063
- {
2064
- id: "image.control.cornerStyle",
2065
- type: "select",
2066
- label: "Image Control Corner Style",
2067
- options: ["circle", "rect"],
2068
- default: "circle"
2069
- },
2070
- {
2071
- id: "image.control.cornerColor",
2072
- type: "color",
2073
- label: "Image Control Corner Color",
2074
- default: "#ffffff"
2075
- },
2076
- {
2077
- id: "image.control.cornerStrokeColor",
2078
- type: "color",
2079
- label: "Image Control Corner Stroke Color",
2080
- default: "#1677ff"
2081
- },
2082
- {
2083
- id: "image.control.transparentCorners",
2084
- type: "boolean",
2085
- label: "Image Control Transparent Corners",
2086
- default: false
2087
- },
2088
- {
2089
- id: "image.control.borderColor",
2090
- type: "color",
2091
- label: "Image Control Border Color",
2092
- default: "#1677ff"
2093
- },
2094
- {
2095
- id: "image.control.borderScaleFactor",
2096
- type: "number",
2097
- label: "Image Control Border Width",
2098
- min: 0.5,
2099
- max: 8,
2100
- step: 0.1,
2101
- default: 1.5
2102
- },
2103
- {
2104
- id: "image.control.padding",
2105
- type: "number",
2106
- label: "Image Control Padding",
2107
- min: 0,
2108
- max: 64,
2109
- step: 1,
2110
- default: 0
2111
- },
2112
- {
2113
- id: "image.frame.strokeColor",
2114
- type: "color",
2115
- label: "Image Frame Stroke Color",
2116
- default: "#808080"
2117
- },
2118
- {
2119
- id: "image.frame.strokeWidth",
2120
- type: "number",
2121
- label: "Image Frame Stroke Width",
2122
- min: 0,
2123
- max: 20,
2124
- step: 0.5,
2125
- default: 2
2126
- },
2127
- {
2128
- id: "image.frame.strokeStyle",
2129
- type: "select",
2130
- label: "Image Frame Stroke Style",
2131
- options: ["solid", "dashed", "hidden"],
2132
- default: "dashed"
2133
- },
2134
- {
2135
- id: "image.frame.dashLength",
2136
- type: "number",
2137
- label: "Image Frame Dash Length",
2138
- min: 1,
2139
- max: 40,
2140
- step: 1,
2141
- default: 8
2142
- },
2143
- {
2144
- id: "image.frame.innerBackground",
2145
- type: "color",
2146
- label: "Image Frame Inner Background",
2147
- default: "rgba(0,0,0,0)"
2148
- },
2149
- {
2150
- id: "image.frame.outerBackground",
2151
- type: "color",
2152
- label: "Image Frame Outer Background",
2153
- default: "#f5f5f5"
2157
+ buildRectPath(viewport.width, viewport.height),
2158
+ `M ${cutLeft} ${cutTop} L ${cutLeft + cutRect.width} ${cutTop} L ${cutLeft + cutRect.width} ${cutTop + cutRect.height} L ${cutLeft} ${cutTop + cutRect.height} Z`
2159
+ ].join(" ");
2160
+ }
2161
+ function resolveCutShapeRadiusPx(geometry, cutRect) {
2162
+ const visualRadius = Number.isFinite(geometry.radius) ? Math.max(0, geometry.radius) : 0;
2163
+ const visualOffset = Number.isFinite(geometry.offset) ? geometry.offset : 0;
2164
+ const rawCutRadius = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
2165
+ const maxRadius = Math.max(0, Math.min(cutRect.width, cutRect.height) / 2);
2166
+ return Math.max(0, Math.min(maxRadius, rawCutRadius));
2167
+ }
2168
+ function buildBuiltinShapeOverlayPaths(cutRect, geometry) {
2169
+ if (!geometry || geometry.shape === "custom") {
2170
+ return null;
2171
+ }
2172
+ const radius = resolveCutShapeRadiusPx(geometry, cutRect);
2173
+ if (geometry.shape === "rect" && radius <= EPSILON) {
2174
+ return null;
2175
+ }
2176
+ const shapePathData = generateDielinePath({
2177
+ shape: geometry.shape,
2178
+ shapeStyle: geometry.shapeStyle,
2179
+ width: Math.max(1, cutRect.width),
2180
+ height: Math.max(1, cutRect.height),
2181
+ radius,
2182
+ x: cutRect.width / 2,
2183
+ y: cutRect.height / 2,
2184
+ features: [],
2185
+ canvasWidth: Math.max(1, cutRect.width),
2186
+ canvasHeight: Math.max(1, cutRect.height)
2187
+ });
2188
+ if (!shapePathData) {
2189
+ return null;
2190
+ }
2191
+ return {
2192
+ shapePathData,
2193
+ hatchPathData: `${buildRectPath(cutRect.width, cutRect.height)} ${shapePathData}`
2194
+ };
2195
+ }
2196
+ function buildImageSessionOverlaySpecs(args) {
2197
+ const { viewport, layout, geometry, visual, hatchPattern } = args;
2198
+ const cutRect = layout.cutRect;
2199
+ const specs = [];
2200
+ specs.push({
2201
+ id: "image.cropMask.rect",
2202
+ type: "path",
2203
+ space: "screen",
2204
+ data: { id: "image.cropMask.rect", zIndex: 1 },
2205
+ props: {
2206
+ pathData: buildViewportMaskPath(viewport, cutRect),
2207
+ left: viewport.left,
2208
+ top: viewport.top,
2209
+ originX: "left",
2210
+ originY: "top",
2211
+ fill: visual.outerBackground,
2212
+ stroke: null,
2213
+ fillRule: "evenodd",
2214
+ selectable: false,
2215
+ evented: false,
2216
+ excludeFromExport: true,
2217
+ objectCaching: false
2154
2218
  }
2155
- ];
2219
+ });
2220
+ const shapeOverlay = buildBuiltinShapeOverlayPaths(cutRect, geometry);
2221
+ if (shapeOverlay) {
2222
+ specs.push({
2223
+ id: "image.cropShapeHatch",
2224
+ type: "path",
2225
+ space: "screen",
2226
+ data: { id: "image.cropShapeHatch", zIndex: 5 },
2227
+ props: {
2228
+ pathData: shapeOverlay.hatchPathData,
2229
+ left: cutRect.left,
2230
+ top: cutRect.top,
2231
+ originX: "left",
2232
+ originY: "top",
2233
+ fill: hatchPattern || DEFAULT_HATCH_FILL,
2234
+ opacity: hatchPattern ? 1 : 0.8,
2235
+ stroke: null,
2236
+ fillRule: "evenodd",
2237
+ selectable: false,
2238
+ evented: false,
2239
+ excludeFromExport: true,
2240
+ objectCaching: false
2241
+ }
2242
+ });
2243
+ specs.push({
2244
+ id: "image.cropShapeOutline",
2245
+ type: "path",
2246
+ space: "screen",
2247
+ data: { id: "image.cropShapeOutline", zIndex: 6 },
2248
+ props: {
2249
+ pathData: shapeOverlay.shapePathData,
2250
+ left: cutRect.left,
2251
+ top: cutRect.top,
2252
+ originX: "left",
2253
+ originY: "top",
2254
+ fill: "transparent",
2255
+ stroke: SHAPE_OUTLINE_COLOR,
2256
+ strokeWidth: 1,
2257
+ selectable: false,
2258
+ evented: false,
2259
+ excludeFromExport: true,
2260
+ objectCaching: false
2261
+ }
2262
+ });
2263
+ }
2264
+ specs.push({
2265
+ id: "image.cropFrame",
2266
+ type: "rect",
2267
+ space: "screen",
2268
+ data: { id: "image.cropFrame", zIndex: 7 },
2269
+ props: {
2270
+ left: cutRect.left,
2271
+ top: cutRect.top,
2272
+ width: cutRect.width,
2273
+ height: cutRect.height,
2274
+ originX: "left",
2275
+ originY: "top",
2276
+ fill: visual.innerBackground,
2277
+ stroke: visual.strokeStyle === "hidden" ? "rgba(0,0,0,0)" : visual.strokeColor,
2278
+ strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
2279
+ strokeDashArray: visual.strokeStyle === "dashed" ? [visual.dashLength, visual.dashLength] : void 0,
2280
+ selectable: false,
2281
+ evented: false,
2282
+ excludeFromExport: true
2283
+ }
2284
+ });
2285
+ return specs;
2156
2286
  }
2157
2287
 
2158
2288
  // src/extensions/image/ImageTool.ts
@@ -2614,11 +2744,21 @@ var ImageTool = class {
2614
2744
  (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2615
2745
  }
2616
2746
  }
2747
+ clearSnapGuideContext() {
2748
+ var _a;
2749
+ const topContext = (_a = this.canvasService) == null ? void 0 : _a.canvas.contextTop;
2750
+ if (!this.canvasService || !topContext) return;
2751
+ this.canvasService.canvas.clearContext(topContext);
2752
+ }
2617
2753
  clearSnapPreview() {
2618
2754
  var _a;
2755
+ const shouldClearCanvas = this.hasRenderedSnapGuides || !!this.activeSnapX || !!this.activeSnapY;
2619
2756
  this.activeSnapX = null;
2620
2757
  this.activeSnapY = null;
2621
2758
  this.hasRenderedSnapGuides = false;
2759
+ if (shouldClearCanvas) {
2760
+ this.clearSnapGuideContext();
2761
+ }
2622
2762
  (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2623
2763
  }
2624
2764
  endMoveSnapInteraction() {
@@ -3022,9 +3162,6 @@ var ImageTool = class {
3022
3162
  }
3023
3163
  return this.canvasService.toScreenRect(frame || this.getFrameRect());
3024
3164
  }
3025
- toLayoutSceneRect(rect) {
3026
- return toLayoutSceneRect(rect);
3027
- }
3028
3165
  async resolveDefaultFitArea() {
3029
3166
  if (!this.canvasService) return null;
3030
3167
  const frame = this.getFrameRect();
@@ -3151,74 +3288,37 @@ var ImageTool = class {
3151
3288
  outerBackground: this.getConfig("image.frame.outerBackground", "#f5f5f5") || "#f5f5f5"
3152
3289
  };
3153
3290
  }
3154
- toSceneGeometryLike(raw) {
3155
- const shape = raw == null ? void 0 : raw.shape;
3156
- if (!isDielineShape(shape)) {
3291
+ resolveSessionOverlayState() {
3292
+ if (!this.canvasService || !this.context) {
3157
3293
  return null;
3158
3294
  }
3159
- const radiusRaw = Number(raw == null ? void 0 : raw.radius);
3160
- const offsetRaw = Number(raw == null ? void 0 : raw.offset);
3161
- const unit = typeof (raw == null ? void 0 : raw.unit) === "string" ? raw.unit : "px";
3162
- const radius = unit === "scene" || !this.canvasService ? radiusRaw : this.canvasService.toSceneLength(radiusRaw);
3163
- const offset = unit === "scene" || !this.canvasService ? offsetRaw : this.canvasService.toSceneLength(offsetRaw);
3164
- return {
3165
- shape,
3166
- shapeStyle: normalizeShapeStyle(raw == null ? void 0 : raw.shapeStyle),
3167
- radius: Number.isFinite(radius) ? radius : 0,
3168
- offset: Number.isFinite(offset) ? offset : 0
3169
- };
3170
- }
3171
- async resolveSceneGeometryForOverlay() {
3172
- if (!this.context) return null;
3173
- const commandService = this.context.services.get("CommandService");
3174
- if (commandService) {
3175
- try {
3176
- const raw = await Promise.resolve(
3177
- commandService.executeCommand("getSceneGeometry")
3178
- );
3179
- const geometry2 = this.toSceneGeometryLike(raw);
3180
- if (geometry2) {
3181
- this.debug("overlay:sceneGeometry:command", geometry2);
3182
- return geometry2;
3183
- }
3184
- this.debug("overlay:sceneGeometry:command:invalid", { raw });
3185
- } catch (error) {
3186
- this.debug("overlay:sceneGeometry:command:error", {
3187
- error: error instanceof Error ? error.message : String(error)
3188
- });
3189
- }
3190
- }
3191
- if (!this.canvasService) return null;
3192
3295
  const configService = this.context.services.get(
3193
3296
  "ConfigurationService"
3194
3297
  );
3195
- if (!configService) return null;
3196
- const sizeState = readSizeState(configService);
3197
- const layout = computeSceneLayout(this.canvasService, sizeState);
3198
- if (!layout) {
3199
- this.debug("overlay:sceneGeometry:fallback:missing-layout");
3298
+ if (!configService) {
3200
3299
  return null;
3201
3300
  }
3202
- const geometry = this.toSceneGeometryLike(
3203
- buildSceneGeometry(configService, layout)
3301
+ const layout = computeSceneLayout(
3302
+ this.canvasService,
3303
+ readSizeState(configService)
3204
3304
  );
3205
- if (geometry) {
3206
- this.debug("overlay:sceneGeometry:fallback", geometry);
3305
+ if (!layout) {
3306
+ this.debug("overlay:layout:missing");
3307
+ return null;
3207
3308
  }
3208
- return geometry;
3209
- }
3210
- resolveCutShapeRadius(geometry, frame) {
3211
- const visualRadius = Number.isFinite(geometry.radius) ? Math.max(0, geometry.radius) : 0;
3212
- const visualOffset = Number.isFinite(geometry.offset) ? geometry.offset : 0;
3213
- const rawCutRadius = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
3214
- const maxRadius = Math.max(0, Math.min(frame.width, frame.height) / 2);
3215
- return Math.max(0, Math.min(maxRadius, rawCutRadius));
3309
+ const geometry = buildSceneGeometry(configService, layout);
3310
+ this.debug("overlay:state:resolved", {
3311
+ cutRect: layout.cutRect,
3312
+ shape: geometry.shape,
3313
+ shapeStyle: geometry.shapeStyle,
3314
+ radius: geometry.radius,
3315
+ offset: geometry.offset
3316
+ });
3317
+ return { layout, geometry };
3216
3318
  }
3217
3319
  getCropShapeHatchPattern(color = "rgba(255, 0, 0, 0.6)") {
3218
- var _a;
3219
3320
  if (typeof document === "undefined") return void 0;
3220
- const sceneScale = ((_a = this.canvasService) == null ? void 0 : _a.getSceneScale()) || 1;
3221
- const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
3321
+ const cacheKey = color;
3222
3322
  if (this.cropShapeHatchPattern && this.cropShapeHatchPatternColor === color && this.cropShapeHatchPatternKey === cacheKey) {
3223
3323
  return this.cropShapeHatchPattern;
3224
3324
  }
@@ -3248,138 +3348,11 @@ var ImageTool = class {
3248
3348
  // @ts-ignore: Fabric Pattern accepts canvas source here.
3249
3349
  repetition: "repeat"
3250
3350
  });
3251
- pattern.patternTransform = [
3252
- 1 / sceneScale,
3253
- 0,
3254
- 0,
3255
- 1 / sceneScale,
3256
- 0,
3257
- 0
3258
- ];
3259
3351
  this.cropShapeHatchPattern = pattern;
3260
3352
  this.cropShapeHatchPatternColor = color;
3261
3353
  this.cropShapeHatchPatternKey = cacheKey;
3262
3354
  return pattern;
3263
3355
  }
3264
- buildCropShapeOverlaySpecs(frame, sceneGeometry) {
3265
- var _a, _b;
3266
- if (!sceneGeometry) {
3267
- this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
3268
- return [];
3269
- }
3270
- if (sceneGeometry.shape === "custom") {
3271
- this.debug("overlay:shape:skip", { reason: "shape-custom" });
3272
- return [];
3273
- }
3274
- const shape = sceneGeometry.shape;
3275
- const shapeStyle = sceneGeometry.shapeStyle;
3276
- const inset = 0;
3277
- const shapeWidth = Math.max(1, frame.width);
3278
- const shapeHeight = Math.max(1, frame.height);
3279
- const radius = this.resolveCutShapeRadius(sceneGeometry, frame);
3280
- this.debug("overlay:shape:geometry", {
3281
- shape,
3282
- frameWidth: frame.width,
3283
- frameHeight: frame.height,
3284
- offset: sceneGeometry.offset,
3285
- shapeStyle,
3286
- inset,
3287
- shapeWidth,
3288
- shapeHeight,
3289
- baseRadius: sceneGeometry.radius,
3290
- radius
3291
- });
3292
- const isSameAsFrame = Math.abs(shapeWidth - frame.width) <= 1e-4 && Math.abs(shapeHeight - frame.height) <= 1e-4;
3293
- if (shape === "rect" && radius <= 1e-4 && isSameAsFrame) {
3294
- this.debug("overlay:shape:skip", {
3295
- reason: "shape-rect-no-radius"
3296
- });
3297
- return [];
3298
- }
3299
- const baseOptions = {
3300
- shape,
3301
- width: shapeWidth,
3302
- height: shapeHeight,
3303
- radius,
3304
- x: frame.width / 2,
3305
- y: frame.height / 2,
3306
- features: [],
3307
- shapeStyle,
3308
- canvasWidth: frame.width,
3309
- canvasHeight: frame.height
3310
- };
3311
- try {
3312
- const shapePathData = generateDielinePath(baseOptions);
3313
- const outerRectPathData = `M 0 0 L ${frame.width} 0 L ${frame.width} ${frame.height} L 0 ${frame.height} Z`;
3314
- const hatchPathData = `${outerRectPathData} ${shapePathData}`;
3315
- if (!shapePathData || !hatchPathData) {
3316
- this.debug("overlay:shape:skip", {
3317
- reason: "path-generation-empty",
3318
- shape,
3319
- radius
3320
- });
3321
- return [];
3322
- }
3323
- const patternFill = this.getCropShapeHatchPattern();
3324
- const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
3325
- const shapeBounds = getPathBounds(shapePathData);
3326
- const hatchBounds = getPathBounds(hatchPathData);
3327
- const frameRect = this.toLayoutSceneRect(frame);
3328
- const hatchPathLength = hatchPathData.length;
3329
- const shapePathLength = shapePathData.length;
3330
- const specs = [
3331
- {
3332
- id: "image.cropShapeHatch",
3333
- type: "path",
3334
- data: { id: "image.cropShapeHatch", zIndex: 5 },
3335
- layout: {
3336
- reference: "custom",
3337
- referenceRect: frameRect,
3338
- alignX: "start",
3339
- alignY: "start",
3340
- offsetX: hatchBounds.x,
3341
- offsetY: hatchBounds.y
3342
- },
3343
- props: {
3344
- pathData: hatchPathData,
3345
- originX: "left",
3346
- originY: "top",
3347
- fill: hatchFill,
3348
- opacity: patternFill ? 1 : 0.8,
3349
- stroke: "rgba(255, 0, 0, 0.9)",
3350
- strokeWidth: (_b = (_a = this.canvasService) == null ? void 0 : _a.toSceneLength(1)) != null ? _b : 1,
3351
- fillRule: "evenodd",
3352
- selectable: false,
3353
- evented: false,
3354
- excludeFromExport: true,
3355
- objectCaching: false
3356
- }
3357
- }
3358
- ];
3359
- this.debug("overlay:shape:built", {
3360
- shape,
3361
- radius,
3362
- inset,
3363
- shapeWidth,
3364
- shapeHeight,
3365
- fillRule: "evenodd",
3366
- shapePathLength,
3367
- hatchPathLength,
3368
- shapeBounds,
3369
- hatchBounds,
3370
- hatchFillType: hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
3371
- ids: specs.map((spec) => spec.id)
3372
- });
3373
- return specs;
3374
- } catch (error) {
3375
- this.debug("overlay:shape:error", {
3376
- shape,
3377
- radius,
3378
- error: error instanceof Error ? error.message : String(error)
3379
- });
3380
- return [];
3381
- }
3382
- }
3383
3356
  resolveRenderImageState(item) {
3384
3357
  var _a;
3385
3358
  const active = this.isToolActive;
@@ -3461,172 +3434,35 @@ var ImageTool = class {
3461
3434
  }
3462
3435
  return specs;
3463
3436
  }
3464
- buildOverlaySpecs(frame, sceneGeometry) {
3437
+ buildOverlaySpecs(overlayState) {
3465
3438
  const visible = this.isImageEditingVisible();
3466
- if (!visible || frame.width <= 0 || frame.height <= 0 || !this.canvasService) {
3439
+ if (!visible || !overlayState || !this.canvasService) {
3467
3440
  this.debug("overlay:hidden", {
3468
3441
  visible,
3469
- frame,
3442
+ cutRect: overlayState == null ? void 0 : overlayState.layout.cutRect,
3470
3443
  isToolActive: this.isToolActive,
3471
3444
  isImageSelectionActive: this.isImageSelectionActive,
3472
3445
  focusedImageId: this.focusedImageId
3473
3446
  });
3474
3447
  return [];
3475
3448
  }
3476
- const viewport = this.canvasService.getSceneViewportRect();
3477
- const canvasW = viewport.width || 0;
3478
- const canvasH = viewport.height || 0;
3479
- const canvasLeft = viewport.left || 0;
3480
- const canvasTop = viewport.top || 0;
3449
+ const viewport = this.canvasService.getScreenViewportRect();
3481
3450
  const visual = this.getFrameVisualConfig();
3482
- const strokeWidthScene = this.canvasService.toSceneLength(
3483
- visual.strokeWidth
3484
- );
3485
- const dashLengthScene = this.canvasService.toSceneLength(visual.dashLength);
3486
- const frameLeft = Math.max(
3487
- canvasLeft,
3488
- Math.min(canvasLeft + canvasW, frame.left)
3489
- );
3490
- const frameTop = Math.max(
3491
- canvasTop,
3492
- Math.min(canvasTop + canvasH, frame.top)
3493
- );
3494
- const frameRight = Math.max(
3495
- frameLeft,
3496
- Math.min(canvasLeft + canvasW, frame.left + frame.width)
3497
- );
3498
- const frameBottom = Math.max(
3499
- frameTop,
3500
- Math.min(canvasTop + canvasH, frame.top + frame.height)
3501
- );
3502
- const visibleFrameH = Math.max(0, frameBottom - frameTop);
3503
- const topH = Math.max(0, frameTop - canvasTop);
3504
- const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
3505
- const leftW = Math.max(0, frameLeft - canvasLeft);
3506
- const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
3507
- const viewportRect = this.toLayoutSceneRect({
3508
- left: canvasLeft,
3509
- top: canvasTop,
3510
- width: canvasW,
3511
- height: canvasH
3512
- });
3513
- const visibleFrameBandRect = this.toLayoutSceneRect({
3514
- left: canvasLeft,
3515
- top: frameTop,
3516
- width: canvasW,
3517
- height: visibleFrameH
3518
- });
3519
- const frameRect = this.toLayoutSceneRect(frame);
3520
- const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
3521
- const mask = [
3522
- {
3523
- id: "image.cropMask.top",
3524
- type: "rect",
3525
- data: { id: "image.cropMask.top", zIndex: 1 },
3526
- layout: {
3527
- reference: "custom",
3528
- referenceRect: viewportRect,
3529
- alignX: "start",
3530
- alignY: "start",
3531
- width: "100%",
3532
- height: topH
3533
- },
3534
- props: {
3535
- originX: "left",
3536
- originY: "top",
3537
- fill: visual.outerBackground,
3538
- selectable: false,
3539
- evented: false
3540
- }
3541
- },
3542
- {
3543
- id: "image.cropMask.bottom",
3544
- type: "rect",
3545
- data: { id: "image.cropMask.bottom", zIndex: 2 },
3546
- layout: {
3547
- reference: "custom",
3548
- referenceRect: viewportRect,
3549
- alignX: "start",
3550
- alignY: "end",
3551
- width: "100%",
3552
- height: bottomH
3553
- },
3554
- props: {
3555
- originX: "left",
3556
- originY: "top",
3557
- fill: visual.outerBackground,
3558
- selectable: false,
3559
- evented: false
3560
- }
3561
- },
3562
- {
3563
- id: "image.cropMask.left",
3564
- type: "rect",
3565
- data: { id: "image.cropMask.left", zIndex: 3 },
3566
- layout: {
3567
- reference: "custom",
3568
- referenceRect: visibleFrameBandRect,
3569
- alignX: "start",
3570
- alignY: "start",
3571
- width: leftW,
3572
- height: "100%"
3573
- },
3574
- props: {
3575
- originX: "left",
3576
- originY: "top",
3577
- fill: visual.outerBackground,
3578
- selectable: false,
3579
- evented: false
3580
- }
3581
- },
3582
- {
3583
- id: "image.cropMask.right",
3584
- type: "rect",
3585
- data: { id: "image.cropMask.right", zIndex: 4 },
3586
- layout: {
3587
- reference: "custom",
3588
- referenceRect: visibleFrameBandRect,
3589
- alignX: "end",
3590
- alignY: "start",
3591
- width: rightW,
3592
- height: "100%"
3593
- },
3594
- props: {
3595
- originX: "left",
3596
- originY: "top",
3597
- fill: visual.outerBackground,
3598
- selectable: false,
3599
- evented: false
3600
- }
3601
- }
3602
- ];
3603
- const frameSpec = {
3604
- id: "image.cropFrame",
3605
- type: "rect",
3606
- data: { id: "image.cropFrame", zIndex: 7 },
3607
- layout: {
3608
- reference: "custom",
3609
- referenceRect: frameRect,
3610
- alignX: "start",
3611
- alignY: "start",
3612
- width: "100%",
3613
- height: "100%"
3451
+ const specs = buildImageSessionOverlaySpecs({
3452
+ viewport: {
3453
+ left: viewport.left,
3454
+ top: viewport.top,
3455
+ width: viewport.width,
3456
+ height: viewport.height
3614
3457
  },
3615
- props: {
3616
- originX: "left",
3617
- originY: "top",
3618
- fill: visual.innerBackground,
3619
- stroke: visual.strokeStyle === "hidden" ? "rgba(0,0,0,0)" : visual.strokeColor,
3620
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
3621
- strokeDashArray: visual.strokeStyle === "dashed" ? [dashLengthScene, dashLengthScene] : void 0,
3622
- selectable: false,
3623
- evented: false
3624
- }
3625
- };
3626
- const specs = shapeOverlay.length > 0 ? [...mask, ...shapeOverlay] : [...mask, ...shapeOverlay, frameSpec];
3458
+ layout: overlayState.layout,
3459
+ geometry: overlayState.geometry,
3460
+ visual,
3461
+ hatchPattern: this.getCropShapeHatchPattern()
3462
+ });
3627
3463
  this.debug("overlay:built", {
3628
- frame,
3629
- shape: sceneGeometry == null ? void 0 : sceneGeometry.shape,
3464
+ cutRect: overlayState.layout.cutRect,
3465
+ shape: overlayState.geometry.shape,
3630
3466
  overlayIds: specs.map((spec) => {
3631
3467
  var _a;
3632
3468
  return {
@@ -3655,10 +3491,9 @@ var ImageTool = class {
3655
3491
  }
3656
3492
  const imageSpecs = await this.buildImageSpecs(renderItems, frame);
3657
3493
  if (seq !== this.renderSeq) return;
3658
- const sceneGeometry = await this.resolveSceneGeometryForOverlay();
3659
- if (seq !== this.renderSeq) return;
3494
+ const overlayState = this.resolveSessionOverlayState();
3660
3495
  this.imageSpecs = imageSpecs;
3661
- this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
3496
+ this.overlaySpecs = this.buildOverlaySpecs(overlayState);
3662
3497
  await this.canvasService.flushRenderFromProducers();
3663
3498
  if (seq !== this.renderSeq) return;
3664
3499
  this.refreshImageObjectInteractionState();
@@ -4912,6 +4747,13 @@ function buildDielineRenderBundle(options) {
4912
4747
  canvasWidth,
4913
4748
  canvasHeight
4914
4749
  };
4750
+ const cutFrameRect = {
4751
+ left: cx - cutW / 2,
4752
+ top: cy - cutH / 2,
4753
+ width: cutW,
4754
+ height: cutH,
4755
+ space: "screen"
4756
+ };
4915
4757
  const specs = [];
4916
4758
  if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)" && !hasImages) {
4917
4759
  specs.push({
@@ -5033,9 +4875,13 @@ function buildDielineRenderBundle(options) {
5033
4875
  width: cutW,
5034
4876
  height: cutH,
5035
4877
  radius: cutR,
5036
- x: cx,
5037
- y: cy,
5038
- features: cutFeatures
4878
+ // Build the clip path in the cut frame's local coordinates so Fabric
4879
+ // does not have to infer placement from the standalone path bounds.
4880
+ x: cutW / 2,
4881
+ y: cutH / 2,
4882
+ features: cutFeatures,
4883
+ canvasWidth: cutW,
4884
+ canvasHeight: cutH
5039
4885
  });
5040
4886
  if (!clipPathData) {
5041
4887
  return { specs, effects: [] };
@@ -5052,6 +4898,12 @@ function buildDielineRenderBundle(options) {
5052
4898
  id: ids.clipSource,
5053
4899
  type: "path",
5054
4900
  space: "screen",
4901
+ layout: {
4902
+ reference: "custom",
4903
+ referenceRect: cutFrameRect,
4904
+ alignX: "start",
4905
+ alignY: "start"
4906
+ },
5055
4907
  data: {
5056
4908
  id: ids.clipSource,
5057
4909
  type: "dieline-effect",