@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.
@@ -14,24 +14,15 @@ import {
14
14
  Point,
15
15
  controlsUtils,
16
16
  } from "fabric";
17
- import {
18
- CanvasService,
19
- RenderLayoutRect,
20
- RenderObjectSpec,
21
- } from "../../services";
22
- import { isDielineShape, normalizeShapeStyle } from "../dielineShape";
23
- import type { DielineShape, DielineShapeStyle } from "../dielineShape";
24
- import { generateDielinePath, getPathBounds } from "../geometry";
17
+ import { CanvasService, RenderObjectSpec } from "../../services";
25
18
  import {
26
19
  buildSceneGeometry,
27
20
  computeSceneLayout,
28
21
  readSizeState,
22
+ type SceneGeometrySnapshot,
23
+ type SceneLayoutSnapshot,
29
24
  } from "../../shared/scene/sceneLayoutModel";
30
- import {
31
- type FrameRect,
32
- resolveCutFrameRect,
33
- toLayoutSceneRect as toSceneLayoutRect,
34
- } from "../../shared/scene/frame";
25
+ import { type FrameRect, resolveCutFrameRect } from "../../shared/scene/frame";
35
26
  import {
36
27
  createSourceSizeCache,
37
28
  getCoverScale as getCoverScaleFromRect,
@@ -48,6 +39,7 @@ import {
48
39
  } from "../../shared/constants/layers";
49
40
  import { createImageCommands } from "./commands";
50
41
  import { createImageConfigurations } from "./config";
42
+ import { buildImageSessionOverlaySpecs } from "./sessionOverlay";
51
43
 
52
44
  export interface ImageItem {
53
45
  id: string;
@@ -91,13 +83,9 @@ interface ImageControlVisualConfig {
91
83
  padding: number;
92
84
  }
93
85
 
94
- type ShapeOverlayShape = Exclude<DielineShape, "custom">;
95
-
96
- interface SceneGeometryLike {
97
- shape: DielineShape;
98
- shapeStyle: DielineShapeStyle;
99
- radius: number;
100
- offset: number;
86
+ interface ImageSessionOverlayState {
87
+ layout: SceneLayoutSnapshot;
88
+ geometry: SceneGeometrySnapshot;
101
89
  }
102
90
 
103
91
  interface UpsertImageOptions {
@@ -663,10 +651,21 @@ export class ImageTool implements Extension {
663
651
  }
664
652
  }
665
653
 
654
+ private clearSnapGuideContext() {
655
+ const topContext = this.canvasService?.canvas.contextTop;
656
+ if (!this.canvasService || !topContext) return;
657
+ this.canvasService.canvas.clearContext(topContext);
658
+ }
659
+
666
660
  private clearSnapPreview() {
661
+ const shouldClearCanvas =
662
+ this.hasRenderedSnapGuides || !!this.activeSnapX || !!this.activeSnapY;
667
663
  this.activeSnapX = null;
668
664
  this.activeSnapY = null;
669
665
  this.hasRenderedSnapGuides = false;
666
+ if (shouldClearCanvas) {
667
+ this.clearSnapGuideContext();
668
+ }
670
669
  this.canvasService?.requestRenderAll();
671
670
  }
672
671
 
@@ -1166,10 +1165,6 @@ export class ImageTool implements Extension {
1166
1165
  return this.canvasService.toScreenRect(frame || this.getFrameRect());
1167
1166
  }
1168
1167
 
1169
- private toLayoutSceneRect(rect: FrameRect): RenderLayoutRect {
1170
- return toSceneLayoutRect(rect);
1171
- }
1172
-
1173
1168
  private async resolveDefaultFitArea(): Promise<DielineFitArea | null> {
1174
1169
  if (!this.canvasService) return null;
1175
1170
  const frame = this.getFrameRect();
@@ -1319,94 +1314,42 @@ export class ImageTool implements Extension {
1319
1314
  };
1320
1315
  }
1321
1316
 
1322
- private toSceneGeometryLike(raw: any): SceneGeometryLike | null {
1323
- const shape = raw?.shape;
1324
- if (!isDielineShape(shape)) {
1317
+ private resolveSessionOverlayState(): ImageSessionOverlayState | null {
1318
+ if (!this.canvasService || !this.context) {
1325
1319
  return null;
1326
1320
  }
1327
-
1328
- const radiusRaw = Number(raw?.radius);
1329
- const offsetRaw = Number(raw?.offset);
1330
- const unit = typeof raw?.unit === "string" ? raw.unit : "px";
1331
- const radius =
1332
- unit === "scene" || !this.canvasService
1333
- ? radiusRaw
1334
- : this.canvasService.toSceneLength(radiusRaw);
1335
- const offset =
1336
- unit === "scene" || !this.canvasService
1337
- ? offsetRaw
1338
- : this.canvasService.toSceneLength(offsetRaw);
1339
- return {
1340
- shape,
1341
- shapeStyle: normalizeShapeStyle(raw?.shapeStyle),
1342
- radius: Number.isFinite(radius) ? radius : 0,
1343
- offset: Number.isFinite(offset) ? offset : 0,
1344
- };
1345
- }
1346
-
1347
- private async resolveSceneGeometryForOverlay(): Promise<SceneGeometryLike | null> {
1348
- if (!this.context) return null;
1349
- const commandService = this.context.services.get<any>("CommandService");
1350
- if (commandService) {
1351
- try {
1352
- const raw = await Promise.resolve(
1353
- commandService.executeCommand("getSceneGeometry"),
1354
- );
1355
- const geometry = this.toSceneGeometryLike(raw);
1356
- if (geometry) {
1357
- this.debug("overlay:sceneGeometry:command", geometry);
1358
- return geometry;
1359
- }
1360
- this.debug("overlay:sceneGeometry:command:invalid", { raw });
1361
- } catch (error) {
1362
- this.debug("overlay:sceneGeometry:command:error", {
1363
- error: error instanceof Error ? error.message : String(error),
1364
- });
1365
- }
1366
- }
1367
-
1368
- if (!this.canvasService) return null;
1369
1321
  const configService = this.context.services.get<ConfigurationService>(
1370
1322
  "ConfigurationService",
1371
1323
  );
1372
- if (!configService) return null;
1373
-
1374
- const sizeState = readSizeState(configService);
1375
- const layout = computeSceneLayout(this.canvasService, sizeState);
1376
- if (!layout) {
1377
- this.debug("overlay:sceneGeometry:fallback:missing-layout");
1324
+ if (!configService) {
1378
1325
  return null;
1379
1326
  }
1380
1327
 
1381
- const geometry = this.toSceneGeometryLike(
1382
- buildSceneGeometry(configService, layout),
1328
+ const layout = computeSceneLayout(
1329
+ this.canvasService,
1330
+ readSizeState(configService),
1383
1331
  );
1384
- if (geometry) {
1385
- this.debug("overlay:sceneGeometry:fallback", geometry);
1332
+ if (!layout) {
1333
+ this.debug("overlay:layout:missing");
1334
+ return null;
1386
1335
  }
1387
- return geometry;
1388
- }
1389
1336
 
1390
- private resolveCutShapeRadius(
1391
- geometry: SceneGeometryLike,
1392
- frame: FrameRect,
1393
- ): number {
1394
- const visualRadius = Number.isFinite(geometry.radius)
1395
- ? Math.max(0, geometry.radius)
1396
- : 0;
1397
- const visualOffset = Number.isFinite(geometry.offset) ? geometry.offset : 0;
1398
- const rawCutRadius =
1399
- visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
1400
- const maxRadius = Math.max(0, Math.min(frame.width, frame.height) / 2);
1401
- return Math.max(0, Math.min(maxRadius, rawCutRadius));
1337
+ const geometry = buildSceneGeometry(configService, layout);
1338
+ this.debug("overlay:state:resolved", {
1339
+ cutRect: layout.cutRect,
1340
+ shape: geometry.shape,
1341
+ shapeStyle: geometry.shapeStyle,
1342
+ radius: geometry.radius,
1343
+ offset: geometry.offset,
1344
+ });
1345
+ return { layout, geometry };
1402
1346
  }
1403
1347
 
1404
1348
  private getCropShapeHatchPattern(
1405
1349
  color = "rgba(255, 0, 0, 0.6)",
1406
1350
  ): Pattern | undefined {
1407
1351
  if (typeof document === "undefined") return undefined;
1408
- const sceneScale = this.canvasService?.getSceneScale() || 1;
1409
- const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
1352
+ const cacheKey = color;
1410
1353
  if (
1411
1354
  this.cropShapeHatchPattern &&
1412
1355
  this.cropShapeHatchPatternColor === color &&
@@ -1443,152 +1386,12 @@ export class ImageTool implements Extension {
1443
1386
  // @ts-ignore: Fabric Pattern accepts canvas source here.
1444
1387
  repetition: "repeat",
1445
1388
  });
1446
- // Scene specs are scaled to screen by CanvasService; keep hatch density in screen pixels.
1447
- (pattern as any).patternTransform = [
1448
- 1 / sceneScale,
1449
- 0,
1450
- 0,
1451
- 1 / sceneScale,
1452
- 0,
1453
- 0,
1454
- ];
1455
1389
  this.cropShapeHatchPattern = pattern;
1456
1390
  this.cropShapeHatchPatternColor = color;
1457
1391
  this.cropShapeHatchPatternKey = cacheKey;
1458
1392
  return pattern;
1459
1393
  }
1460
1394
 
1461
- private buildCropShapeOverlaySpecs(
1462
- frame: FrameRect,
1463
- sceneGeometry: SceneGeometryLike | null,
1464
- ): RenderObjectSpec[] {
1465
- if (!sceneGeometry) {
1466
- this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
1467
- return [];
1468
- }
1469
- if (sceneGeometry.shape === "custom") {
1470
- this.debug("overlay:shape:skip", { reason: "shape-custom" });
1471
- return [];
1472
- }
1473
-
1474
- const shape = sceneGeometry.shape as ShapeOverlayShape;
1475
- const shapeStyle = sceneGeometry.shapeStyle;
1476
- const inset = 0;
1477
- const shapeWidth = Math.max(1, frame.width);
1478
- const shapeHeight = Math.max(1, frame.height);
1479
- const radius = this.resolveCutShapeRadius(sceneGeometry, frame);
1480
-
1481
- this.debug("overlay:shape:geometry", {
1482
- shape,
1483
- frameWidth: frame.width,
1484
- frameHeight: frame.height,
1485
- offset: sceneGeometry.offset,
1486
- shapeStyle,
1487
- inset,
1488
- shapeWidth,
1489
- shapeHeight,
1490
- baseRadius: sceneGeometry.radius,
1491
- radius,
1492
- });
1493
-
1494
- const isSameAsFrame =
1495
- Math.abs(shapeWidth - frame.width) <= 0.0001 &&
1496
- Math.abs(shapeHeight - frame.height) <= 0.0001;
1497
- if (shape === "rect" && radius <= 0.0001 && isSameAsFrame) {
1498
- this.debug("overlay:shape:skip", {
1499
- reason: "shape-rect-no-radius",
1500
- });
1501
- return [];
1502
- }
1503
-
1504
- const baseOptions = {
1505
- shape,
1506
- width: shapeWidth,
1507
- height: shapeHeight,
1508
- radius,
1509
- x: frame.width / 2,
1510
- y: frame.height / 2,
1511
- features: [],
1512
- shapeStyle,
1513
- canvasWidth: frame.width,
1514
- canvasHeight: frame.height,
1515
- };
1516
-
1517
- try {
1518
- const shapePathData = generateDielinePath(baseOptions);
1519
- const outerRectPathData = `M 0 0 L ${frame.width} 0 L ${frame.width} ${frame.height} L 0 ${frame.height} Z`;
1520
- const hatchPathData = `${outerRectPathData} ${shapePathData}`;
1521
- if (!shapePathData || !hatchPathData) {
1522
- this.debug("overlay:shape:skip", {
1523
- reason: "path-generation-empty",
1524
- shape,
1525
- radius,
1526
- });
1527
- return [];
1528
- }
1529
-
1530
- const patternFill = this.getCropShapeHatchPattern();
1531
- const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
1532
- const shapeBounds = getPathBounds(shapePathData);
1533
- const hatchBounds = getPathBounds(hatchPathData);
1534
- const frameRect = this.toLayoutSceneRect(frame);
1535
- const hatchPathLength = hatchPathData.length;
1536
- const shapePathLength = shapePathData.length;
1537
- const specs: RenderObjectSpec[] = [
1538
- {
1539
- id: "image.cropShapeHatch",
1540
- type: "path",
1541
- data: { id: "image.cropShapeHatch", zIndex: 5 },
1542
- layout: {
1543
- reference: "custom",
1544
- referenceRect: frameRect,
1545
- alignX: "start",
1546
- alignY: "start",
1547
- offsetX: hatchBounds.x,
1548
- offsetY: hatchBounds.y,
1549
- },
1550
- props: {
1551
- pathData: hatchPathData,
1552
- originX: "left",
1553
- originY: "top",
1554
- fill: hatchFill,
1555
- opacity: patternFill ? 1 : 0.8,
1556
- stroke: "rgba(255, 0, 0, 0.9)",
1557
- strokeWidth: this.canvasService?.toSceneLength(1) ?? 1,
1558
- fillRule: "evenodd",
1559
- selectable: false,
1560
- evented: false,
1561
- excludeFromExport: true,
1562
- objectCaching: false,
1563
- },
1564
- },
1565
- ];
1566
- this.debug("overlay:shape:built", {
1567
- shape,
1568
- radius,
1569
- inset,
1570
- shapeWidth,
1571
- shapeHeight,
1572
- fillRule: "evenodd",
1573
- shapePathLength,
1574
- hatchPathLength,
1575
- shapeBounds,
1576
- hatchBounds,
1577
- hatchFillType:
1578
- hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
1579
- ids: specs.map((spec) => spec.id),
1580
- });
1581
- return specs;
1582
- } catch (error) {
1583
- this.debug("overlay:shape:error", {
1584
- shape,
1585
- radius,
1586
- error: error instanceof Error ? error.message : String(error),
1587
- });
1588
- return [];
1589
- }
1590
- }
1591
-
1592
1395
  private resolveRenderImageState(item: ImageItem): RenderImageState {
1593
1396
  const active = this.isToolActive;
1594
1397
  const sourceUrl = item.sourceUrl || item.url;
@@ -1689,19 +1492,13 @@ export class ImageTool implements Extension {
1689
1492
  }
1690
1493
 
1691
1494
  private buildOverlaySpecs(
1692
- frame: FrameRect,
1693
- sceneGeometry: SceneGeometryLike | null,
1495
+ overlayState: ImageSessionOverlayState | null,
1694
1496
  ): RenderObjectSpec[] {
1695
1497
  const visible = this.isImageEditingVisible();
1696
- if (
1697
- !visible ||
1698
- frame.width <= 0 ||
1699
- frame.height <= 0 ||
1700
- !this.canvasService
1701
- ) {
1498
+ if (!visible || !overlayState || !this.canvasService) {
1702
1499
  this.debug("overlay:hidden", {
1703
1500
  visible,
1704
- frame,
1501
+ cutRect: overlayState?.layout.cutRect,
1705
1502
  isToolActive: this.isToolActive,
1706
1503
  isImageSelectionActive: this.isImageSelectionActive,
1707
1504
  focusedImageId: this.focusedImageId,
@@ -1709,174 +1506,23 @@ export class ImageTool implements Extension {
1709
1506
  return [];
1710
1507
  }
1711
1508
 
1712
- const viewport = this.canvasService.getSceneViewportRect();
1713
- const canvasW = viewport.width || 0;
1714
- const canvasH = viewport.height || 0;
1715
- const canvasLeft = viewport.left || 0;
1716
- const canvasTop = viewport.top || 0;
1509
+ const viewport = this.canvasService.getScreenViewportRect();
1717
1510
  const visual = this.getFrameVisualConfig();
1718
- const strokeWidthScene = this.canvasService.toSceneLength(
1719
- visual.strokeWidth,
1720
- );
1721
- const dashLengthScene = this.canvasService.toSceneLength(visual.dashLength);
1722
-
1723
- const frameLeft = Math.max(
1724
- canvasLeft,
1725
- Math.min(canvasLeft + canvasW, frame.left),
1726
- );
1727
- const frameTop = Math.max(
1728
- canvasTop,
1729
- Math.min(canvasTop + canvasH, frame.top),
1730
- );
1731
- const frameRight = Math.max(
1732
- frameLeft,
1733
- Math.min(canvasLeft + canvasW, frame.left + frame.width),
1734
- );
1735
- const frameBottom = Math.max(
1736
- frameTop,
1737
- Math.min(canvasTop + canvasH, frame.top + frame.height),
1738
- );
1739
- const visibleFrameH = Math.max(0, frameBottom - frameTop);
1740
-
1741
- const topH = Math.max(0, frameTop - canvasTop);
1742
- const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
1743
- const leftW = Math.max(0, frameLeft - canvasLeft);
1744
- const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
1745
- const viewportRect = this.toLayoutSceneRect({
1746
- left: canvasLeft,
1747
- top: canvasTop,
1748
- width: canvasW,
1749
- height: canvasH,
1750
- });
1751
- const visibleFrameBandRect = this.toLayoutSceneRect({
1752
- left: canvasLeft,
1753
- top: frameTop,
1754
- width: canvasW,
1755
- height: visibleFrameH,
1756
- });
1757
- const frameRect = this.toLayoutSceneRect(frame);
1758
- const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
1759
-
1760
- const mask: RenderObjectSpec[] = [
1761
- {
1762
- id: "image.cropMask.top",
1763
- type: "rect",
1764
- data: { id: "image.cropMask.top", zIndex: 1 },
1765
- layout: {
1766
- reference: "custom",
1767
- referenceRect: viewportRect,
1768
- alignX: "start",
1769
- alignY: "start",
1770
- width: "100%",
1771
- height: topH,
1772
- },
1773
- props: {
1774
- originX: "left",
1775
- originY: "top",
1776
- fill: visual.outerBackground,
1777
- selectable: false,
1778
- evented: false,
1779
- },
1511
+ const specs = buildImageSessionOverlaySpecs({
1512
+ viewport: {
1513
+ left: viewport.left,
1514
+ top: viewport.top,
1515
+ width: viewport.width,
1516
+ height: viewport.height,
1780
1517
  },
1781
- {
1782
- id: "image.cropMask.bottom",
1783
- type: "rect",
1784
- data: { id: "image.cropMask.bottom", zIndex: 2 },
1785
- layout: {
1786
- reference: "custom",
1787
- referenceRect: viewportRect,
1788
- alignX: "start",
1789
- alignY: "end",
1790
- width: "100%",
1791
- height: bottomH,
1792
- },
1793
- props: {
1794
- originX: "left",
1795
- originY: "top",
1796
- fill: visual.outerBackground,
1797
- selectable: false,
1798
- evented: false,
1799
- },
1800
- },
1801
- {
1802
- id: "image.cropMask.left",
1803
- type: "rect",
1804
- data: { id: "image.cropMask.left", zIndex: 3 },
1805
- layout: {
1806
- reference: "custom",
1807
- referenceRect: visibleFrameBandRect,
1808
- alignX: "start",
1809
- alignY: "start",
1810
- width: leftW,
1811
- height: "100%",
1812
- },
1813
- props: {
1814
- originX: "left",
1815
- originY: "top",
1816
- fill: visual.outerBackground,
1817
- selectable: false,
1818
- evented: false,
1819
- },
1820
- },
1821
- {
1822
- id: "image.cropMask.right",
1823
- type: "rect",
1824
- data: { id: "image.cropMask.right", zIndex: 4 },
1825
- layout: {
1826
- reference: "custom",
1827
- referenceRect: visibleFrameBandRect,
1828
- alignX: "end",
1829
- alignY: "start",
1830
- width: rightW,
1831
- height: "100%",
1832
- },
1833
- props: {
1834
- originX: "left",
1835
- originY: "top",
1836
- fill: visual.outerBackground,
1837
- selectable: false,
1838
- evented: false,
1839
- },
1840
- },
1841
- ];
1842
-
1843
- const frameSpec: RenderObjectSpec = {
1844
- id: "image.cropFrame",
1845
- type: "rect",
1846
- data: { id: "image.cropFrame", zIndex: 7 },
1847
- layout: {
1848
- reference: "custom",
1849
- referenceRect: frameRect,
1850
- alignX: "start",
1851
- alignY: "start",
1852
- width: "100%",
1853
- height: "100%",
1854
- },
1855
- props: {
1856
- originX: "left",
1857
- originY: "top",
1858
- fill: visual.innerBackground,
1859
- stroke:
1860
- visual.strokeStyle === "hidden"
1861
- ? "rgba(0,0,0,0)"
1862
- : visual.strokeColor,
1863
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
1864
- strokeDashArray:
1865
- visual.strokeStyle === "dashed"
1866
- ? [dashLengthScene, dashLengthScene]
1867
- : undefined,
1868
- selectable: false,
1869
- evented: false,
1870
- },
1871
- };
1872
-
1873
- const specs =
1874
- shapeOverlay.length > 0
1875
- ? [...mask, ...shapeOverlay]
1876
- : [...mask, ...shapeOverlay, frameSpec];
1518
+ layout: overlayState.layout,
1519
+ geometry: overlayState.geometry,
1520
+ visual,
1521
+ hatchPattern: this.getCropShapeHatchPattern(),
1522
+ });
1877
1523
  this.debug("overlay:built", {
1878
- frame,
1879
- shape: sceneGeometry?.shape,
1524
+ cutRect: overlayState.layout.cutRect,
1525
+ shape: overlayState.geometry.shape,
1880
1526
  overlayIds: specs.map((spec) => ({
1881
1527
  id: spec.id,
1882
1528
  zIndex: spec.data?.zIndex,
@@ -1907,11 +1553,10 @@ export class ImageTool implements Extension {
1907
1553
  const imageSpecs = await this.buildImageSpecs(renderItems, frame);
1908
1554
  if (seq !== this.renderSeq) return;
1909
1555
 
1910
- const sceneGeometry = await this.resolveSceneGeometryForOverlay();
1911
- if (seq !== this.renderSeq) return;
1556
+ const overlayState = this.resolveSessionOverlayState();
1912
1557
 
1913
1558
  this.imageSpecs = imageSpecs;
1914
- this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
1559
+ this.overlaySpecs = this.buildOverlaySpecs(overlayState);
1915
1560
  await this.canvasService.flushRenderFromProducers();
1916
1561
  if (seq !== this.renderSeq) return;
1917
1562
  this.refreshImageObjectInteractionState();