@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.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
1593
- });
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
- }
1662
- } else {
1663
- if (f.operation === "add") {
1664
- adds.push(item);
1665
- } else {
1666
- subtracts.push(item);
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
1353
+ });
1667
1354
  }
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;
1677
+ }
1678
+ if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
1679
+ return pointsA.length <= pointsB.length ? pointsA : pointsB;
1680
+ }
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;
1826
1693
  }
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
- });
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;
1833
1705
  }
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
- };
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;
1842
1755
  }
1843
-
1844
- // src/shared/runtime/sessionState.ts
1845
- function cloneWithJson(value) {
1846
- return JSON.parse(JSON.stringify(value));
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);
1847
1766
  }
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;
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;
1855
1806
  }
1856
- }
1857
- function runDeferredConfigUpdate(state, action, cooldownMs = 0) {
1858
- state.isUpdatingConfig = true;
1859
- action();
1860
- if (cooldownMs <= 0) {
1861
- state.isUpdatingConfig = false;
1862
- return;
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;
1863
1822
  }
1864
- setTimeout(() => {
1865
- state.isUpdatingConfig = false;
1866
- }, cooldownMs);
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;
1867
1830
  }
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);
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
2218
+ }
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
2154
2283
  }
2155
- ];
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;
@@ -3424,209 +3397,72 @@ var ImageTool = class {
3424
3397
  lockScalingFlip: true,
3425
3398
  selectable: this.isImageEditingVisible(),
3426
3399
  evented: this.isImageEditingVisible(),
3427
- hasControls: this.isImageEditingVisible(),
3428
- hasBorders: this.isImageEditingVisible(),
3429
- opacity: render.opacity
3430
- };
3431
- }
3432
- toSceneObjectScale(value) {
3433
- if (!this.canvasService) return value;
3434
- return value / this.canvasService.getSceneScale();
3435
- }
3436
- getCurrentSrc(obj) {
3437
- var _a;
3438
- if (!obj) return void 0;
3439
- if (typeof obj.getSrc === "function") return obj.getSrc();
3440
- return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
3441
- }
3442
- async buildImageSpecs(items, frame) {
3443
- const specs = [];
3444
- for (const item of items) {
3445
- const render = this.resolveRenderImageState(item);
3446
- if (!render.src) continue;
3447
- const ensured = await this.ensureSourceSize(render.src);
3448
- const sourceSize = ensured || this.getSourceSize(render.src);
3449
- const props = this.computeCanvasProps(render, sourceSize, frame);
3450
- specs.push({
3451
- id: item.id,
3452
- type: "image",
3453
- src: render.src,
3454
- data: {
3455
- id: item.id,
3456
- layerId: IMAGE_OBJECT_LAYER_ID,
3457
- type: "image-item"
3458
- },
3459
- props
3460
- });
3461
- }
3462
- return specs;
3463
- }
3464
- buildOverlaySpecs(frame, sceneGeometry) {
3465
- const visible = this.isImageEditingVisible();
3466
- if (!visible || frame.width <= 0 || frame.height <= 0 || !this.canvasService) {
3467
- this.debug("overlay:hidden", {
3468
- visible,
3469
- frame,
3470
- isToolActive: this.isToolActive,
3471
- isImageSelectionActive: this.isImageSelectionActive,
3472
- focusedImageId: this.focusedImageId
3473
- });
3474
- return [];
3475
- }
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;
3481
- 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%"
3400
+ hasControls: this.isImageEditingVisible(),
3401
+ hasBorders: this.isImageEditingVisible(),
3402
+ opacity: render.opacity
3403
+ };
3404
+ }
3405
+ toSceneObjectScale(value) {
3406
+ if (!this.canvasService) return value;
3407
+ return value / this.canvasService.getSceneScale();
3408
+ }
3409
+ getCurrentSrc(obj) {
3410
+ var _a;
3411
+ if (!obj) return void 0;
3412
+ if (typeof obj.getSrc === "function") return obj.getSrc();
3413
+ return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
3414
+ }
3415
+ async buildImageSpecs(items, frame) {
3416
+ const specs = [];
3417
+ for (const item of items) {
3418
+ const render = this.resolveRenderImageState(item);
3419
+ if (!render.src) continue;
3420
+ const ensured = await this.ensureSourceSize(render.src);
3421
+ const sourceSize = ensured || this.getSourceSize(render.src);
3422
+ const props = this.computeCanvasProps(render, sourceSize, frame);
3423
+ specs.push({
3424
+ id: item.id,
3425
+ type: "image",
3426
+ src: render.src,
3427
+ data: {
3428
+ id: item.id,
3429
+ layerId: IMAGE_OBJECT_LAYER_ID,
3430
+ type: "image-item"
3593
3431
  },
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%"
3432
+ props
3433
+ });
3434
+ }
3435
+ return specs;
3436
+ }
3437
+ buildOverlaySpecs(overlayState) {
3438
+ const visible = this.isImageEditingVisible();
3439
+ if (!visible || !overlayState || !this.canvasService) {
3440
+ this.debug("overlay:hidden", {
3441
+ visible,
3442
+ cutRect: overlayState == null ? void 0 : overlayState.layout.cutRect,
3443
+ isToolActive: this.isToolActive,
3444
+ isImageSelectionActive: this.isImageSelectionActive,
3445
+ focusedImageId: this.focusedImageId
3446
+ });
3447
+ return [];
3448
+ }
3449
+ const viewport = this.canvasService.getScreenViewportRect();
3450
+ const visual = this.getFrameVisualConfig();
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();
@@ -4582,6 +4417,259 @@ function readDielineState(configService, fallback) {
4582
4417
  };
4583
4418
  }
4584
4419
 
4420
+ // src/extensions/constraints.ts
4421
+ var ConstraintRegistry = class {
4422
+ static register(type, handler) {
4423
+ this.handlers.set(type, handler);
4424
+ }
4425
+ static apply(x, y, feature, context, constraints) {
4426
+ const list = constraints || feature.constraints;
4427
+ if (!list || list.length === 0) {
4428
+ return { x, y };
4429
+ }
4430
+ let currentX = x;
4431
+ let currentY = y;
4432
+ for (const constraint of list) {
4433
+ const handler = this.handlers.get(constraint.type);
4434
+ if (handler) {
4435
+ const result = handler(currentX, currentY, feature, context, constraint.params || {});
4436
+ currentX = result.x;
4437
+ currentY = result.y;
4438
+ }
4439
+ }
4440
+ return { x: currentX, y: currentY };
4441
+ }
4442
+ };
4443
+ ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
4444
+ var pathConstraint = (x, y, feature, context, params) => {
4445
+ const { dielineWidth, dielineHeight, geometry } = context;
4446
+ if (!geometry) return { x, y };
4447
+ const minX = geometry.x - geometry.width / 2;
4448
+ const minY = geometry.y - geometry.height / 2;
4449
+ const absX = minX + x * geometry.width;
4450
+ const absY = minY + y * geometry.height;
4451
+ const nearest = getNearestPointOnDieline(
4452
+ { x: absX, y: absY },
4453
+ geometry
4454
+ );
4455
+ let finalX = nearest.x;
4456
+ let finalY = nearest.y;
4457
+ const hasOffsetParams = params.minOffset !== void 0 || params.maxOffset !== void 0;
4458
+ if (hasOffsetParams && nearest.normal) {
4459
+ const dx = absX - nearest.x;
4460
+ const dy = absY - nearest.y;
4461
+ const nx2 = nearest.normal.x;
4462
+ const ny2 = nearest.normal.y;
4463
+ const dist = dx * nx2 + dy * ny2;
4464
+ const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
4465
+ const rawMin = params.minOffset !== void 0 ? params.minOffset : 0;
4466
+ const rawMax = params.maxOffset !== void 0 ? params.maxOffset : 0;
4467
+ const minOffset = rawMin * scale;
4468
+ const maxOffset = rawMax * scale;
4469
+ const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
4470
+ finalX = nearest.x + nx2 * clampedDist;
4471
+ finalY = nearest.y + ny2 * clampedDist;
4472
+ }
4473
+ const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
4474
+ const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
4475
+ return { x: nx, y: ny };
4476
+ };
4477
+ var edgeConstraint = (x, y, feature, context, params) => {
4478
+ const { dielineWidth, dielineHeight } = context;
4479
+ const allowedEdges = params.allowedEdges || [
4480
+ "top",
4481
+ "bottom",
4482
+ "left",
4483
+ "right"
4484
+ ];
4485
+ const confine = params.confine || false;
4486
+ const offset = params.offset || 0;
4487
+ const distances = [];
4488
+ if (allowedEdges.includes("top"))
4489
+ distances.push({ edge: "top", dist: y * dielineHeight });
4490
+ if (allowedEdges.includes("bottom"))
4491
+ distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
4492
+ if (allowedEdges.includes("left"))
4493
+ distances.push({ edge: "left", dist: x * dielineWidth });
4494
+ if (allowedEdges.includes("right"))
4495
+ distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
4496
+ if (distances.length === 0) return { x, y };
4497
+ distances.sort((a, b) => a.dist - b.dist);
4498
+ const nearest = distances[0].edge;
4499
+ let newX = x;
4500
+ let newY = y;
4501
+ const fw = feature.width || 0;
4502
+ const fh = feature.height || 0;
4503
+ switch (nearest) {
4504
+ case "top":
4505
+ newY = 0 + offset / dielineHeight;
4506
+ if (confine) {
4507
+ const minX = fw / 2 / dielineWidth;
4508
+ const maxX = 1 - minX;
4509
+ newX = Math.max(minX, Math.min(newX, maxX));
4510
+ }
4511
+ break;
4512
+ case "bottom":
4513
+ newY = 1 - offset / dielineHeight;
4514
+ if (confine) {
4515
+ const minX = fw / 2 / dielineWidth;
4516
+ const maxX = 1 - minX;
4517
+ newX = Math.max(minX, Math.min(newX, maxX));
4518
+ }
4519
+ break;
4520
+ case "left":
4521
+ newX = 0 + offset / dielineWidth;
4522
+ if (confine) {
4523
+ const minY = fh / 2 / dielineHeight;
4524
+ const maxY = 1 - minY;
4525
+ newY = Math.max(minY, Math.min(newY, maxY));
4526
+ }
4527
+ break;
4528
+ case "right":
4529
+ newX = 1 - offset / dielineWidth;
4530
+ if (confine) {
4531
+ const minY = fh / 2 / dielineHeight;
4532
+ const maxY = 1 - minY;
4533
+ newY = Math.max(minY, Math.min(newY, maxY));
4534
+ }
4535
+ break;
4536
+ }
4537
+ return { x: newX, y: newY };
4538
+ };
4539
+ var internalConstraint = (x, y, feature, context, params) => {
4540
+ const { dielineWidth, dielineHeight } = context;
4541
+ const margin = params.margin || 0;
4542
+ const fw = feature.width || 0;
4543
+ const fh = feature.height || 0;
4544
+ const minX = (margin + fw / 2) / dielineWidth;
4545
+ const maxX = 1 - (margin + fw / 2) / dielineWidth;
4546
+ const minY = (margin + fh / 2) / dielineHeight;
4547
+ const maxY = 1 - (margin + fh / 2) / dielineHeight;
4548
+ const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
4549
+ const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
4550
+ return { x: clampedX, y: clampedY };
4551
+ };
4552
+ var tangentBottomConstraint = (x, y, feature, context, params) => {
4553
+ const { dielineWidth, dielineHeight } = context;
4554
+ const gap = params.gap || 0;
4555
+ const confineX = params.confineX !== false;
4556
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
4557
+ const newY = 1 + (extentY + gap) / dielineHeight;
4558
+ let newX = x;
4559
+ if (confineX) {
4560
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
4561
+ const minX = extentX / dielineWidth;
4562
+ const maxX = 1 - extentX / dielineWidth;
4563
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
4564
+ }
4565
+ return { x: newX, y: newY };
4566
+ };
4567
+ var lowestTangentConstraint = (x, y, feature, context, params) => {
4568
+ const { dielineWidth, dielineHeight, geometry } = context;
4569
+ if (!geometry) return { x, y };
4570
+ const lowest = getLowestPointOnDieline(geometry);
4571
+ const minY = geometry.y - geometry.height / 2;
4572
+ const normY = (lowest.y - minY) / geometry.height;
4573
+ const gap = params.gap || 0;
4574
+ const confineX = params.confineX !== false;
4575
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
4576
+ const newY = normY + (extentY + gap) / dielineHeight;
4577
+ let newX = x;
4578
+ if (confineX) {
4579
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
4580
+ const minX = extentX / dielineWidth;
4581
+ const maxX = 1 - extentX / dielineWidth;
4582
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
4583
+ }
4584
+ return { x: newX, y: newY };
4585
+ };
4586
+ ConstraintRegistry.register("path", pathConstraint);
4587
+ ConstraintRegistry.register("edge", edgeConstraint);
4588
+ ConstraintRegistry.register("internal", internalConstraint);
4589
+ ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
4590
+ ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
4591
+
4592
+ // src/extensions/featureCoordinates.ts
4593
+ function resolveFeaturePosition2(feature, geometry) {
4594
+ const { x, y, width, height } = geometry;
4595
+ const left = x - width / 2;
4596
+ const top = y - height / 2;
4597
+ return {
4598
+ x: left + feature.x * width,
4599
+ y: top + feature.y * height
4600
+ };
4601
+ }
4602
+ function normalizePointInGeometry(point, geometry) {
4603
+ const left = geometry.x - geometry.width / 2;
4604
+ const top = geometry.y - geometry.height / 2;
4605
+ return {
4606
+ x: geometry.width > 0 ? (point.x - left) / geometry.width : 0.5,
4607
+ y: geometry.height > 0 ? (point.y - top) / geometry.height : 0.5
4608
+ };
4609
+ }
4610
+
4611
+ // src/extensions/featurePlacement.ts
4612
+ function scaleFeatureForRender(feature, scale, x, y) {
4613
+ return {
4614
+ ...feature,
4615
+ x,
4616
+ y,
4617
+ width: feature.width !== void 0 ? feature.width * scale : void 0,
4618
+ height: feature.height !== void 0 ? feature.height * scale : void 0,
4619
+ radius: feature.radius !== void 0 ? feature.radius * scale : void 0
4620
+ };
4621
+ }
4622
+ function resolveFeaturePlacements(features, geometry) {
4623
+ const dielineWidth = geometry.scale > 0 ? geometry.width / geometry.scale : geometry.width;
4624
+ const dielineHeight = geometry.scale > 0 ? geometry.height / geometry.scale : geometry.height;
4625
+ return (features || []).map((feature) => {
4626
+ var _a;
4627
+ const activeConstraints = (_a = feature.constraints) == null ? void 0 : _a.filter(
4628
+ (constraint) => !constraint.validateOnly
4629
+ );
4630
+ const constrained = ConstraintRegistry.apply(
4631
+ feature.x,
4632
+ feature.y,
4633
+ feature,
4634
+ {
4635
+ dielineWidth,
4636
+ dielineHeight,
4637
+ geometry
4638
+ },
4639
+ activeConstraints
4640
+ );
4641
+ const center = resolveFeaturePosition2(
4642
+ {
4643
+ ...feature,
4644
+ x: constrained.x,
4645
+ y: constrained.y
4646
+ },
4647
+ geometry
4648
+ );
4649
+ return {
4650
+ feature,
4651
+ normalizedX: constrained.x,
4652
+ normalizedY: constrained.y,
4653
+ centerX: center.x,
4654
+ centerY: center.y
4655
+ };
4656
+ });
4657
+ }
4658
+ function projectPlacedFeatures(placements, geometry, scale) {
4659
+ return placements.map((placement) => {
4660
+ const normalized = normalizePointInGeometry(
4661
+ { x: placement.centerX, y: placement.centerY },
4662
+ geometry
4663
+ );
4664
+ return scaleFeatureForRender(
4665
+ placement.feature,
4666
+ scale,
4667
+ normalized.x,
4668
+ normalized.y
4669
+ );
4670
+ });
4671
+ }
4672
+
4585
4673
  // src/extensions/dieline/renderBuilder.ts
4586
4674
  var DEFAULT_IDS = {
4587
4675
  inside: "dieline.inside",
@@ -4591,16 +4679,6 @@ var DEFAULT_IDS = {
4591
4679
  clip: "dieline.clip.image",
4592
4680
  clipSource: "dieline.effect.clip-path"
4593
4681
  };
4594
- function scaleFeatures(state, scale) {
4595
- return (state.features || []).map((feature) => ({
4596
- ...feature,
4597
- x: feature.x,
4598
- y: feature.y,
4599
- width: (feature.width || 0) * scale,
4600
- height: (feature.height || 0) * scale,
4601
- radius: (feature.radius || 0) * scale
4602
- }));
4603
- }
4604
4682
  function buildDielineRenderBundle(options) {
4605
4683
  const ids = { ...DEFAULT_IDS, ...options.ids || {} };
4606
4684
  const {
@@ -4625,8 +4703,41 @@ function buildDielineRenderBundle(options) {
4625
4703
  const cutH = sceneLayout.cutRect.height;
4626
4704
  const visualOffset = (cutW - visualWidth) / 2;
4627
4705
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
4628
- const absoluteFeatures = scaleFeatures(state, scale);
4629
- const cutFeatures = absoluteFeatures.filter((feature) => !feature.skipCut);
4706
+ const placements = resolveFeaturePlacements(state.features || [], {
4707
+ shape,
4708
+ shapeStyle,
4709
+ pathData: state.pathData,
4710
+ customSourceWidthPx: state.customSourceWidthPx,
4711
+ customSourceHeightPx: state.customSourceHeightPx,
4712
+ canvasWidth,
4713
+ canvasHeight,
4714
+ x: cx,
4715
+ y: cy,
4716
+ width: visualWidth,
4717
+ height: visualHeight,
4718
+ radius: visualRadius,
4719
+ scale
4720
+ });
4721
+ const absoluteFeatures = projectPlacedFeatures(
4722
+ placements,
4723
+ {
4724
+ x: cx,
4725
+ y: cy,
4726
+ width: visualWidth,
4727
+ height: visualHeight
4728
+ },
4729
+ scale
4730
+ );
4731
+ const cutFeatures = projectPlacedFeatures(
4732
+ placements.filter((placement) => !placement.feature.skipCut),
4733
+ {
4734
+ x: cx,
4735
+ y: cy,
4736
+ width: cutW,
4737
+ height: cutH
4738
+ },
4739
+ scale
4740
+ );
4630
4741
  const common = {
4631
4742
  shape,
4632
4743
  shapeStyle,
@@ -4636,6 +4747,13 @@ function buildDielineRenderBundle(options) {
4636
4747
  canvasWidth,
4637
4748
  canvasHeight
4638
4749
  };
4750
+ const cutFrameRect = {
4751
+ left: cx - cutW / 2,
4752
+ top: cy - cutH / 2,
4753
+ width: cutW,
4754
+ height: cutH,
4755
+ space: "screen"
4756
+ };
4639
4757
  const specs = [];
4640
4758
  if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)" && !hasImages) {
4641
4759
  specs.push({
@@ -4757,9 +4875,13 @@ function buildDielineRenderBundle(options) {
4757
4875
  width: cutW,
4758
4876
  height: cutH,
4759
4877
  radius: cutR,
4760
- x: cx,
4761
- y: cy,
4762
- 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
4763
4885
  });
4764
4886
  if (!clipPathData) {
4765
4887
  return { specs, effects: [] };
@@ -4776,6 +4898,12 @@ function buildDielineRenderBundle(options) {
4776
4898
  id: ids.clipSource,
4777
4899
  type: "path",
4778
4900
  space: "screen",
4901
+ layout: {
4902
+ reference: "custom",
4903
+ referenceRect: cutFrameRect,
4904
+ alignX: "start",
4905
+ alignY: "start"
4906
+ },
4779
4907
  data: {
4780
4908
  id: ids.clipSource,
4781
4909
  type: "dieline-effect",
@@ -5055,15 +5183,31 @@ var DielineTool = class {
5055
5183
  const visualRadius = radius * scale;
5056
5184
  const visualOffset = (cutW - sceneLayout.trimRect.width) / 2;
5057
5185
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
5058
- const absoluteFeatures = (features || []).map((f) => ({
5059
- ...f,
5060
- x: f.x,
5061
- y: f.y,
5062
- width: (f.width || 0) * scale,
5063
- height: (f.height || 0) * scale,
5064
- radius: (f.radius || 0) * scale
5065
- }));
5066
- const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
5186
+ const placements = resolveFeaturePlacements(features || [], {
5187
+ shape,
5188
+ shapeStyle,
5189
+ pathData,
5190
+ customSourceWidthPx: this.state.customSourceWidthPx,
5191
+ customSourceHeightPx: this.state.customSourceHeightPx,
5192
+ canvasWidth: canvasW,
5193
+ canvasHeight: canvasH,
5194
+ x: cx,
5195
+ y: cy,
5196
+ width: sceneLayout.trimRect.width,
5197
+ height: sceneLayout.trimRect.height,
5198
+ radius: visualRadius,
5199
+ scale
5200
+ });
5201
+ const cutFeatures = projectPlacedFeatures(
5202
+ placements.filter((placement) => !placement.feature.skipCut),
5203
+ {
5204
+ x: cx,
5205
+ y: cy,
5206
+ width: cutW,
5207
+ height: cutH
5208
+ },
5209
+ scale
5210
+ );
5067
5211
  const generatedPathData = generateDielinePath({
5068
5212
  shape,
5069
5213
  width: cutW,
@@ -5189,178 +5333,6 @@ import {
5189
5333
  } from "@pooder/core";
5190
5334
  import { Pattern as Pattern3 } from "fabric";
5191
5335
 
5192
- // src/extensions/constraints.ts
5193
- var ConstraintRegistry = class {
5194
- static register(type, handler) {
5195
- this.handlers.set(type, handler);
5196
- }
5197
- static apply(x, y, feature, context, constraints) {
5198
- const list = constraints || feature.constraints;
5199
- if (!list || list.length === 0) {
5200
- return { x, y };
5201
- }
5202
- let currentX = x;
5203
- let currentY = y;
5204
- for (const constraint of list) {
5205
- const handler = this.handlers.get(constraint.type);
5206
- if (handler) {
5207
- const result = handler(currentX, currentY, feature, context, constraint.params || {});
5208
- currentX = result.x;
5209
- currentY = result.y;
5210
- }
5211
- }
5212
- return { x: currentX, y: currentY };
5213
- }
5214
- };
5215
- ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
5216
- var pathConstraint = (x, y, feature, context, params) => {
5217
- const { dielineWidth, dielineHeight, geometry } = context;
5218
- if (!geometry) return { x, y };
5219
- const minX = geometry.x - geometry.width / 2;
5220
- const minY = geometry.y - geometry.height / 2;
5221
- const absX = minX + x * geometry.width;
5222
- const absY = minY + y * geometry.height;
5223
- const nearest = getNearestPointOnDieline(
5224
- { x: absX, y: absY },
5225
- geometry
5226
- );
5227
- let finalX = nearest.x;
5228
- let finalY = nearest.y;
5229
- const hasOffsetParams = params.minOffset !== void 0 || params.maxOffset !== void 0;
5230
- if (hasOffsetParams && nearest.normal) {
5231
- const dx = absX - nearest.x;
5232
- const dy = absY - nearest.y;
5233
- const nx2 = nearest.normal.x;
5234
- const ny2 = nearest.normal.y;
5235
- const dist = dx * nx2 + dy * ny2;
5236
- const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
5237
- const rawMin = params.minOffset !== void 0 ? params.minOffset : 0;
5238
- const rawMax = params.maxOffset !== void 0 ? params.maxOffset : 0;
5239
- const minOffset = rawMin * scale;
5240
- const maxOffset = rawMax * scale;
5241
- const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
5242
- finalX = nearest.x + nx2 * clampedDist;
5243
- finalY = nearest.y + ny2 * clampedDist;
5244
- }
5245
- const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
5246
- const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
5247
- return { x: nx, y: ny };
5248
- };
5249
- var edgeConstraint = (x, y, feature, context, params) => {
5250
- const { dielineWidth, dielineHeight } = context;
5251
- const allowedEdges = params.allowedEdges || [
5252
- "top",
5253
- "bottom",
5254
- "left",
5255
- "right"
5256
- ];
5257
- const confine = params.confine || false;
5258
- const offset = params.offset || 0;
5259
- const distances = [];
5260
- if (allowedEdges.includes("top"))
5261
- distances.push({ edge: "top", dist: y * dielineHeight });
5262
- if (allowedEdges.includes("bottom"))
5263
- distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
5264
- if (allowedEdges.includes("left"))
5265
- distances.push({ edge: "left", dist: x * dielineWidth });
5266
- if (allowedEdges.includes("right"))
5267
- distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
5268
- if (distances.length === 0) return { x, y };
5269
- distances.sort((a, b) => a.dist - b.dist);
5270
- const nearest = distances[0].edge;
5271
- let newX = x;
5272
- let newY = y;
5273
- const fw = feature.width || 0;
5274
- const fh = feature.height || 0;
5275
- switch (nearest) {
5276
- case "top":
5277
- newY = 0 + offset / dielineHeight;
5278
- if (confine) {
5279
- const minX = fw / 2 / dielineWidth;
5280
- const maxX = 1 - minX;
5281
- newX = Math.max(minX, Math.min(newX, maxX));
5282
- }
5283
- break;
5284
- case "bottom":
5285
- newY = 1 - offset / dielineHeight;
5286
- if (confine) {
5287
- const minX = fw / 2 / dielineWidth;
5288
- const maxX = 1 - minX;
5289
- newX = Math.max(minX, Math.min(newX, maxX));
5290
- }
5291
- break;
5292
- case "left":
5293
- newX = 0 + offset / dielineWidth;
5294
- if (confine) {
5295
- const minY = fh / 2 / dielineHeight;
5296
- const maxY = 1 - minY;
5297
- newY = Math.max(minY, Math.min(newY, maxY));
5298
- }
5299
- break;
5300
- case "right":
5301
- newX = 1 - offset / dielineWidth;
5302
- if (confine) {
5303
- const minY = fh / 2 / dielineHeight;
5304
- const maxY = 1 - minY;
5305
- newY = Math.max(minY, Math.min(newY, maxY));
5306
- }
5307
- break;
5308
- }
5309
- return { x: newX, y: newY };
5310
- };
5311
- var internalConstraint = (x, y, feature, context, params) => {
5312
- const { dielineWidth, dielineHeight } = context;
5313
- const margin = params.margin || 0;
5314
- const fw = feature.width || 0;
5315
- const fh = feature.height || 0;
5316
- const minX = (margin + fw / 2) / dielineWidth;
5317
- const maxX = 1 - (margin + fw / 2) / dielineWidth;
5318
- const minY = (margin + fh / 2) / dielineHeight;
5319
- const maxY = 1 - (margin + fh / 2) / dielineHeight;
5320
- const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
5321
- const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
5322
- return { x: clampedX, y: clampedY };
5323
- };
5324
- var tangentBottomConstraint = (x, y, feature, context, params) => {
5325
- const { dielineWidth, dielineHeight } = context;
5326
- const gap = params.gap || 0;
5327
- const confineX = params.confineX !== false;
5328
- const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
5329
- const newY = 1 + (extentY + gap) / dielineHeight;
5330
- let newX = x;
5331
- if (confineX) {
5332
- const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
5333
- const minX = extentX / dielineWidth;
5334
- const maxX = 1 - extentX / dielineWidth;
5335
- newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
5336
- }
5337
- return { x: newX, y: newY };
5338
- };
5339
- var lowestTangentConstraint = (x, y, feature, context, params) => {
5340
- const { dielineWidth, dielineHeight, geometry } = context;
5341
- if (!geometry) return { x, y };
5342
- const lowest = getLowestPointOnDieline(geometry);
5343
- const minY = geometry.y - geometry.height / 2;
5344
- const normY = (lowest.y - minY) / geometry.height;
5345
- const gap = params.gap || 0;
5346
- const confineX = params.confineX !== false;
5347
- const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
5348
- const newY = normY + (extentY + gap) / dielineHeight;
5349
- let newX = x;
5350
- if (confineX) {
5351
- const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
5352
- const minX = extentX / dielineWidth;
5353
- const maxX = 1 - extentX / dielineWidth;
5354
- newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
5355
- }
5356
- return { x: newX, y: newY };
5357
- };
5358
- ConstraintRegistry.register("path", pathConstraint);
5359
- ConstraintRegistry.register("edge", edgeConstraint);
5360
- ConstraintRegistry.register("internal", internalConstraint);
5361
- ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
5362
- ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
5363
-
5364
5336
  // src/extensions/featureComplete.ts
5365
5337
  function validateFeaturesStrict(features, context) {
5366
5338
  const eps = 1e-6;
@@ -6136,9 +6108,29 @@ var FeatureTool = class {
6136
6108
  }
6137
6109
  const groups = /* @__PURE__ */ new Map();
6138
6110
  const singles = [];
6139
- this.workingFeatures.forEach((feature, index) => {
6111
+ const placements = resolveFeaturePlacements(
6112
+ this.workingFeatures,
6113
+ {
6114
+ shape: this.currentGeometry.shape,
6115
+ shapeStyle: this.currentGeometry.shapeStyle,
6116
+ pathData: this.currentGeometry.pathData,
6117
+ customSourceWidthPx: this.currentGeometry.customSourceWidthPx,
6118
+ customSourceHeightPx: this.currentGeometry.customSourceHeightPx,
6119
+ x: this.currentGeometry.x,
6120
+ y: this.currentGeometry.y,
6121
+ width: this.currentGeometry.width,
6122
+ height: this.currentGeometry.height,
6123
+ radius: this.currentGeometry.radius,
6124
+ scale: this.currentGeometry.scale || 1
6125
+ }
6126
+ );
6127
+ placements.forEach((placement, index) => {
6128
+ const feature = placement.feature;
6140
6129
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
6141
- const position = resolveFeaturePosition(feature, geometry);
6130
+ const position = {
6131
+ x: placement.centerX,
6132
+ y: placement.centerY
6133
+ };
6142
6134
  const scale = geometry.scale || 1;
6143
6135
  const marker = {
6144
6136
  feature,
@@ -6715,11 +6707,12 @@ var EXTENSION_LINE_LENGTH = 5;
6715
6707
  var MIN_ARROW_SIZE = 4;
6716
6708
  var THICKNESS_TO_STROKE_WIDTH_RATIO = 20;
6717
6709
  var DEFAULT_THICKNESS = 20;
6718
- var DEFAULT_GAP = 45;
6710
+ var DEFAULT_GAP = 65;
6719
6711
  var DEFAULT_FONT_SIZE = 10;
6720
6712
  var DEFAULT_BACKGROUND_COLOR = "#f0f0f0";
6721
6713
  var DEFAULT_TEXT_COLOR = "#333333";
6722
6714
  var DEFAULT_LINE_COLOR = "#999999";
6715
+ var RULER_DEBUG_KEY = "ruler.debug";
6723
6716
  var RULER_THICKNESS_MIN = 10;
6724
6717
  var RULER_THICKNESS_MAX = 100;
6725
6718
  var RULER_GAP_MIN = 0;
@@ -6738,6 +6731,7 @@ var RulerTool = class {
6738
6731
  this.textColor = DEFAULT_TEXT_COLOR;
6739
6732
  this.lineColor = DEFAULT_LINE_COLOR;
6740
6733
  this.fontSize = DEFAULT_FONT_SIZE;
6734
+ this.debugEnabled = false;
6741
6735
  this.renderSeq = 0;
6742
6736
  this.numericProps = /* @__PURE__ */ new Set(["thickness", "gap", "fontSize"]);
6743
6737
  this.specs = [];
@@ -6786,7 +6780,14 @@ var RulerTool = class {
6786
6780
  this.syncConfig(configService);
6787
6781
  configService.onAnyChange((e) => {
6788
6782
  let shouldUpdate = false;
6789
- if (e.key.startsWith("ruler.")) {
6783
+ if (e.key === RULER_DEBUG_KEY) {
6784
+ this.debugEnabled = e.value === true;
6785
+ this.log("config:update", {
6786
+ key: e.key,
6787
+ raw: e.value,
6788
+ normalized: this.debugEnabled
6789
+ });
6790
+ } else if (e.key.startsWith("ruler.")) {
6790
6791
  const prop = e.key.split(".")[1];
6791
6792
  if (prop && prop in this) {
6792
6793
  if (this.numericProps.has(prop)) {
@@ -6873,6 +6874,12 @@ var RulerTool = class {
6873
6874
  min: RULER_FONT_SIZE_MIN,
6874
6875
  max: RULER_FONT_SIZE_MAX,
6875
6876
  default: DEFAULT_FONT_SIZE
6877
+ },
6878
+ {
6879
+ id: RULER_DEBUG_KEY,
6880
+ type: "boolean",
6881
+ label: "Ruler Debug Log",
6882
+ default: false
6876
6883
  }
6877
6884
  ],
6878
6885
  [ContributionPointIds8.COMMANDS]: [
@@ -6909,7 +6916,11 @@ var RulerTool = class {
6909
6916
  ]
6910
6917
  };
6911
6918
  }
6919
+ isDebugEnabled() {
6920
+ return this.debugEnabled;
6921
+ }
6912
6922
  log(step, payload) {
6923
+ if (!this.isDebugEnabled()) return;
6913
6924
  if (payload) {
6914
6925
  console.debug(`[RulerTool] ${step}`, payload);
6915
6926
  return;
@@ -6938,6 +6949,7 @@ var RulerTool = class {
6938
6949
  configService.get("ruler.fontSize", this.fontSize),
6939
6950
  DEFAULT_FONT_SIZE
6940
6951
  );
6952
+ this.debugEnabled = configService.get(RULER_DEBUG_KEY, this.debugEnabled) === true;
6941
6953
  this.log("config:loaded", {
6942
6954
  thickness: this.thickness,
6943
6955
  gap: this.gap,