@pooder/kit 6.2.1 → 6.3.0
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/.test-dist/src/extensions/dieline/renderBuilder.js +19 -2
- package/.test-dist/src/extensions/image/ImageTool.js +180 -467
- package/.test-dist/src/extensions/image/commands.js +60 -40
- package/.test-dist/src/extensions/image/imageOperations.js +75 -0
- package/.test-dist/src/extensions/image/index.js +1 -0
- package/.test-dist/src/extensions/image/model.js +4 -0
- package/.test-dist/src/extensions/image/sessionOverlay.js +148 -0
- package/.test-dist/src/extensions/ruler/RulerTool.js +1 -1
- package/.test-dist/tests/run.js +39 -5
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +252 -168
- package/dist/index.d.ts +252 -168
- package/dist/index.js +806 -846
- package/dist/index.mjs +802 -845
- package/package.json +1 -1
- package/src/extensions/dieline/renderBuilder.ts +26 -4
- package/src/extensions/image/ImageTool.ts +229 -557
- package/src/extensions/image/commands.ts +69 -48
- package/src/extensions/image/imageOperations.ts +135 -0
- package/src/extensions/image/index.ts +1 -0
- package/src/extensions/image/model.ts +13 -1
- package/src/extensions/image/sessionOverlay.ts +206 -0
- package/tests/run.ts +49 -8
|
@@ -3,8 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ImageTool = void 0;
|
|
4
4
|
const core_1 = require("@pooder/core");
|
|
5
5
|
const fabric_1 = require("fabric");
|
|
6
|
-
const dielineShape_1 = require("../dielineShape");
|
|
7
|
-
const geometry_1 = require("../geometry");
|
|
8
6
|
const sceneLayoutModel_1 = require("../../shared/scene/sceneLayoutModel");
|
|
9
7
|
const frame_1 = require("../../shared/scene/frame");
|
|
10
8
|
const sourceSizeCache_1 = require("../../shared/imaging/sourceSizeCache");
|
|
@@ -13,6 +11,8 @@ const sessionState_1 = require("../../shared/runtime/sessionState");
|
|
|
13
11
|
const layers_1 = require("../../shared/constants/layers");
|
|
14
12
|
const commands_1 = require("./commands");
|
|
15
13
|
const config_1 = require("./config");
|
|
14
|
+
const imageOperations_1 = require("./imageOperations");
|
|
15
|
+
const sessionOverlay_1 = require("./sessionOverlay");
|
|
16
16
|
const IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
|
|
17
17
|
"rotate",
|
|
18
18
|
"scale",
|
|
@@ -256,6 +256,7 @@ class ImageTool {
|
|
|
256
256
|
this.clearRenderedImages();
|
|
257
257
|
this.renderProducerDisposable?.dispose();
|
|
258
258
|
this.renderProducerDisposable = undefined;
|
|
259
|
+
this.emitImageStateChange();
|
|
259
260
|
if (this.canvasService) {
|
|
260
261
|
void this.canvasService.flushRenderFromProducers();
|
|
261
262
|
this.canvasService = undefined;
|
|
@@ -427,10 +428,20 @@ class ImageTool {
|
|
|
427
428
|
this.canvasService?.requestRenderAll();
|
|
428
429
|
}
|
|
429
430
|
}
|
|
431
|
+
clearSnapGuideContext() {
|
|
432
|
+
const topContext = this.canvasService?.canvas.contextTop;
|
|
433
|
+
if (!this.canvasService || !topContext)
|
|
434
|
+
return;
|
|
435
|
+
this.canvasService.canvas.clearContext(topContext);
|
|
436
|
+
}
|
|
430
437
|
clearSnapPreview() {
|
|
438
|
+
const shouldClearCanvas = this.hasRenderedSnapGuides || !!this.activeSnapX || !!this.activeSnapY;
|
|
431
439
|
this.activeSnapX = null;
|
|
432
440
|
this.activeSnapY = null;
|
|
433
441
|
this.hasRenderedSnapGuides = false;
|
|
442
|
+
if (shouldClearCanvas) {
|
|
443
|
+
this.clearSnapGuideContext();
|
|
444
|
+
}
|
|
434
445
|
this.canvasService?.requestRenderAll();
|
|
435
446
|
}
|
|
436
447
|
endMoveSnapInteraction() {
|
|
@@ -636,9 +647,9 @@ class ImageTool {
|
|
|
636
647
|
name: "Image",
|
|
637
648
|
interaction: "session",
|
|
638
649
|
commands: {
|
|
639
|
-
begin: "
|
|
650
|
+
begin: "imageSessionReset",
|
|
640
651
|
commit: "completeImages",
|
|
641
|
-
rollback: "
|
|
652
|
+
rollback: "imageSessionReset",
|
|
642
653
|
},
|
|
643
654
|
session: {
|
|
644
655
|
autoBegin: true,
|
|
@@ -676,6 +687,29 @@ class ImageTool {
|
|
|
676
687
|
cloneItems(items) {
|
|
677
688
|
return this.normalizeItems((items || []).map((i) => ({ ...i })));
|
|
678
689
|
}
|
|
690
|
+
getViewItems() {
|
|
691
|
+
return this.isToolActive ? this.workingItems : this.items;
|
|
692
|
+
}
|
|
693
|
+
getImageViewState() {
|
|
694
|
+
this.syncToolActiveFromWorkbench();
|
|
695
|
+
const items = this.cloneItems(this.getViewItems());
|
|
696
|
+
const focusedItem = this.focusedImageId == null
|
|
697
|
+
? null
|
|
698
|
+
: items.find((item) => item.id === this.focusedImageId) || null;
|
|
699
|
+
return {
|
|
700
|
+
items,
|
|
701
|
+
hasAnyImage: items.length > 0,
|
|
702
|
+
focusedId: this.focusedImageId,
|
|
703
|
+
focusedItem,
|
|
704
|
+
isToolActive: this.isToolActive,
|
|
705
|
+
isImageSelectionActive: this.isImageSelectionActive,
|
|
706
|
+
hasWorkingChanges: this.hasWorkingChanges,
|
|
707
|
+
source: this.isToolActive ? "working" : "committed",
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
emitImageStateChange() {
|
|
711
|
+
this.context?.eventBus.emit("image:state:change", this.getImageViewState());
|
|
712
|
+
}
|
|
679
713
|
emitWorkingChange(changedId = null) {
|
|
680
714
|
this.context?.eventBus.emit("image:working:change", {
|
|
681
715
|
changedId,
|
|
@@ -713,9 +747,13 @@ class ImageTool {
|
|
|
713
747
|
if (!options.skipRender) {
|
|
714
748
|
this.updateImages();
|
|
715
749
|
}
|
|
750
|
+
else {
|
|
751
|
+
this.emitImageStateChange();
|
|
752
|
+
}
|
|
716
753
|
return { ok: true, id };
|
|
717
754
|
}
|
|
718
|
-
async addImageEntry(url, options,
|
|
755
|
+
async addImageEntry(url, options, operation) {
|
|
756
|
+
this.syncToolActiveFromWorkbench();
|
|
719
757
|
const id = this.generateId();
|
|
720
758
|
const newItem = this.normalizeItem({
|
|
721
759
|
id,
|
|
@@ -723,13 +761,21 @@ class ImageTool {
|
|
|
723
761
|
opacity: 1,
|
|
724
762
|
...options,
|
|
725
763
|
});
|
|
726
|
-
const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
|
|
727
764
|
const waitLoaded = this.waitImageLoaded(id, true);
|
|
728
|
-
|
|
729
|
-
|
|
765
|
+
if (this.isToolActive) {
|
|
766
|
+
this.workingItems = this.cloneItems([...this.workingItems, newItem]);
|
|
767
|
+
this.hasWorkingChanges = true;
|
|
768
|
+
this.updateImages();
|
|
769
|
+
this.emitWorkingChange(id);
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
this.updateConfig([...this.items, newItem]);
|
|
773
|
+
}
|
|
730
774
|
const loaded = await waitLoaded;
|
|
731
|
-
if (loaded &&
|
|
732
|
-
await this.
|
|
775
|
+
if (loaded && operation) {
|
|
776
|
+
await this.applyImageOperation(id, operation, {
|
|
777
|
+
target: this.isToolActive ? "working" : "config",
|
|
778
|
+
});
|
|
733
779
|
}
|
|
734
780
|
if (loaded) {
|
|
735
781
|
this.setImageFocus(id);
|
|
@@ -737,8 +783,8 @@ class ImageTool {
|
|
|
737
783
|
return id;
|
|
738
784
|
}
|
|
739
785
|
async upsertImageEntry(url, options = {}) {
|
|
786
|
+
this.syncToolActiveFromWorkbench();
|
|
740
787
|
const mode = options.mode || (options.id ? "replace" : "add");
|
|
741
|
-
const fitOnAdd = options.fitOnAdd !== false;
|
|
742
788
|
if (mode === "replace") {
|
|
743
789
|
if (!options.id) {
|
|
744
790
|
throw new Error("replace-target-id-required");
|
|
@@ -747,21 +793,33 @@ class ImageTool {
|
|
|
747
793
|
if (!this.hasImageItem(targetId)) {
|
|
748
794
|
throw new Error("replace-target-not-found");
|
|
749
795
|
}
|
|
750
|
-
|
|
796
|
+
if (this.isToolActive) {
|
|
797
|
+
const current = this.workingItems.find((item) => item.id === targetId) ||
|
|
798
|
+
this.items.find((item) => item.id === targetId);
|
|
799
|
+
this.purgeSourceSizeCacheForItem(current);
|
|
800
|
+
this.updateImageInWorking(targetId, {
|
|
801
|
+
url,
|
|
802
|
+
sourceUrl: url,
|
|
803
|
+
committedUrl: undefined,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
await this.updateImageInConfig(targetId, { url });
|
|
808
|
+
}
|
|
809
|
+
const loaded = await this.waitImageLoaded(targetId, true);
|
|
810
|
+
if (loaded && options.operation) {
|
|
811
|
+
await this.applyImageOperation(targetId, options.operation, {
|
|
812
|
+
target: this.isToolActive ? "working" : "config",
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
if (loaded) {
|
|
816
|
+
this.setImageFocus(targetId);
|
|
817
|
+
}
|
|
751
818
|
return { id: targetId, mode: "replace" };
|
|
752
819
|
}
|
|
753
|
-
const id = await this.addImageEntry(url, options.addOptions,
|
|
820
|
+
const id = await this.addImageEntry(url, options.addOptions, options.operation);
|
|
754
821
|
return { id, mode: "add" };
|
|
755
822
|
}
|
|
756
|
-
addItemToWorkingSessionIfNeeded(item, sessionDirtyBeforeAdd) {
|
|
757
|
-
if (!sessionDirtyBeforeAdd || !this.isToolActive)
|
|
758
|
-
return;
|
|
759
|
-
if (this.workingItems.some((existing) => existing.id === item.id))
|
|
760
|
-
return;
|
|
761
|
-
this.workingItems = this.cloneItems([...this.workingItems, item]);
|
|
762
|
-
this.updateImages();
|
|
763
|
-
this.emitWorkingChange(item.id);
|
|
764
|
-
}
|
|
765
823
|
async updateImage(id, updates, options = {}) {
|
|
766
824
|
this.syncToolActiveFromWorkbench();
|
|
767
825
|
const target = options.target || "auto";
|
|
@@ -816,40 +874,6 @@ class ImageTool {
|
|
|
816
874
|
}
|
|
817
875
|
return this.canvasService.toScreenRect(frame || this.getFrameRect());
|
|
818
876
|
}
|
|
819
|
-
toLayoutSceneRect(rect) {
|
|
820
|
-
return (0, frame_1.toLayoutSceneRect)(rect);
|
|
821
|
-
}
|
|
822
|
-
async resolveDefaultFitArea() {
|
|
823
|
-
if (!this.canvasService)
|
|
824
|
-
return null;
|
|
825
|
-
const frame = this.getFrameRect();
|
|
826
|
-
if (frame.width <= 0 || frame.height <= 0)
|
|
827
|
-
return null;
|
|
828
|
-
return {
|
|
829
|
-
width: Math.max(1, frame.width),
|
|
830
|
-
height: Math.max(1, frame.height),
|
|
831
|
-
left: frame.left + frame.width / 2,
|
|
832
|
-
top: frame.top + frame.height / 2,
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
async fitImageToDefaultArea(id) {
|
|
836
|
-
if (!this.canvasService)
|
|
837
|
-
return;
|
|
838
|
-
const area = await this.resolveDefaultFitArea();
|
|
839
|
-
if (area) {
|
|
840
|
-
await this.fitImageToArea(id, area);
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
const viewport = this.canvasService.getSceneViewportRect();
|
|
844
|
-
const canvasW = Math.max(1, viewport.width || 0);
|
|
845
|
-
const canvasH = Math.max(1, viewport.height || 0);
|
|
846
|
-
await this.fitImageToArea(id, {
|
|
847
|
-
width: canvasW,
|
|
848
|
-
height: canvasH,
|
|
849
|
-
left: viewport.left + canvasW / 2,
|
|
850
|
-
top: viewport.top + canvasH / 2,
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
877
|
getImageObjects() {
|
|
854
878
|
if (!this.canvasService)
|
|
855
879
|
return [];
|
|
@@ -941,78 +965,33 @@ class ImageTool {
|
|
|
941
965
|
"#f5f5f5",
|
|
942
966
|
};
|
|
943
967
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
if (!(0, dielineShape_1.isDielineShape)(shape)) {
|
|
968
|
+
resolveSessionOverlayState() {
|
|
969
|
+
if (!this.canvasService || !this.context) {
|
|
947
970
|
return null;
|
|
948
971
|
}
|
|
949
|
-
const radiusRaw = Number(raw?.radius);
|
|
950
|
-
const offsetRaw = Number(raw?.offset);
|
|
951
|
-
const unit = typeof raw?.unit === "string" ? raw.unit : "px";
|
|
952
|
-
const radius = unit === "scene" || !this.canvasService
|
|
953
|
-
? radiusRaw
|
|
954
|
-
: this.canvasService.toSceneLength(radiusRaw);
|
|
955
|
-
const offset = unit === "scene" || !this.canvasService
|
|
956
|
-
? offsetRaw
|
|
957
|
-
: this.canvasService.toSceneLength(offsetRaw);
|
|
958
|
-
return {
|
|
959
|
-
shape,
|
|
960
|
-
shapeStyle: (0, dielineShape_1.normalizeShapeStyle)(raw?.shapeStyle),
|
|
961
|
-
radius: Number.isFinite(radius) ? radius : 0,
|
|
962
|
-
offset: Number.isFinite(offset) ? offset : 0,
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
async resolveSceneGeometryForOverlay() {
|
|
966
|
-
if (!this.context)
|
|
967
|
-
return null;
|
|
968
|
-
const commandService = this.context.services.get("CommandService");
|
|
969
|
-
if (commandService) {
|
|
970
|
-
try {
|
|
971
|
-
const raw = await Promise.resolve(commandService.executeCommand("getSceneGeometry"));
|
|
972
|
-
const geometry = this.toSceneGeometryLike(raw);
|
|
973
|
-
if (geometry) {
|
|
974
|
-
this.debug("overlay:sceneGeometry:command", geometry);
|
|
975
|
-
return geometry;
|
|
976
|
-
}
|
|
977
|
-
this.debug("overlay:sceneGeometry:command:invalid", { raw });
|
|
978
|
-
}
|
|
979
|
-
catch (error) {
|
|
980
|
-
this.debug("overlay:sceneGeometry:command:error", {
|
|
981
|
-
error: error instanceof Error ? error.message : String(error),
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
if (!this.canvasService)
|
|
986
|
-
return null;
|
|
987
972
|
const configService = this.context.services.get("ConfigurationService");
|
|
988
|
-
if (!configService)
|
|
973
|
+
if (!configService) {
|
|
989
974
|
return null;
|
|
990
|
-
|
|
991
|
-
const layout = (0, sceneLayoutModel_1.computeSceneLayout)(this.canvasService,
|
|
975
|
+
}
|
|
976
|
+
const layout = (0, sceneLayoutModel_1.computeSceneLayout)(this.canvasService, (0, sceneLayoutModel_1.readSizeState)(configService));
|
|
992
977
|
if (!layout) {
|
|
993
|
-
this.debug("overlay:
|
|
978
|
+
this.debug("overlay:layout:missing");
|
|
994
979
|
return null;
|
|
995
980
|
}
|
|
996
|
-
const geometry =
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
: 0;
|
|
1006
|
-
const visualOffset = Number.isFinite(geometry.offset) ? geometry.offset : 0;
|
|
1007
|
-
const rawCutRadius = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
|
|
1008
|
-
const maxRadius = Math.max(0, Math.min(frame.width, frame.height) / 2);
|
|
1009
|
-
return Math.max(0, Math.min(maxRadius, rawCutRadius));
|
|
981
|
+
const geometry = (0, sceneLayoutModel_1.buildSceneGeometry)(configService, layout);
|
|
982
|
+
this.debug("overlay:state:resolved", {
|
|
983
|
+
cutRect: layout.cutRect,
|
|
984
|
+
shape: geometry.shape,
|
|
985
|
+
shapeStyle: geometry.shapeStyle,
|
|
986
|
+
radius: geometry.radius,
|
|
987
|
+
offset: geometry.offset,
|
|
988
|
+
});
|
|
989
|
+
return { layout, geometry };
|
|
1010
990
|
}
|
|
1011
991
|
getCropShapeHatchPattern(color = "rgba(255, 0, 0, 0.6)") {
|
|
1012
992
|
if (typeof document === "undefined")
|
|
1013
993
|
return undefined;
|
|
1014
|
-
const
|
|
1015
|
-
const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
|
|
994
|
+
const cacheKey = color;
|
|
1016
995
|
if (this.cropShapeHatchPattern &&
|
|
1017
996
|
this.cropShapeHatchPatternColor === color &&
|
|
1018
997
|
this.cropShapeHatchPatternKey === cacheKey) {
|
|
@@ -1045,140 +1024,11 @@ class ImageTool {
|
|
|
1045
1024
|
// @ts-ignore: Fabric Pattern accepts canvas source here.
|
|
1046
1025
|
repetition: "repeat",
|
|
1047
1026
|
});
|
|
1048
|
-
// Scene specs are scaled to screen by CanvasService; keep hatch density in screen pixels.
|
|
1049
|
-
pattern.patternTransform = [
|
|
1050
|
-
1 / sceneScale,
|
|
1051
|
-
0,
|
|
1052
|
-
0,
|
|
1053
|
-
1 / sceneScale,
|
|
1054
|
-
0,
|
|
1055
|
-
0,
|
|
1056
|
-
];
|
|
1057
1027
|
this.cropShapeHatchPattern = pattern;
|
|
1058
1028
|
this.cropShapeHatchPatternColor = color;
|
|
1059
1029
|
this.cropShapeHatchPatternKey = cacheKey;
|
|
1060
1030
|
return pattern;
|
|
1061
1031
|
}
|
|
1062
|
-
buildCropShapeOverlaySpecs(frame, sceneGeometry) {
|
|
1063
|
-
if (!sceneGeometry) {
|
|
1064
|
-
this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
|
|
1065
|
-
return [];
|
|
1066
|
-
}
|
|
1067
|
-
if (sceneGeometry.shape === "custom") {
|
|
1068
|
-
this.debug("overlay:shape:skip", { reason: "shape-custom" });
|
|
1069
|
-
return [];
|
|
1070
|
-
}
|
|
1071
|
-
const shape = sceneGeometry.shape;
|
|
1072
|
-
const shapeStyle = sceneGeometry.shapeStyle;
|
|
1073
|
-
const inset = 0;
|
|
1074
|
-
const shapeWidth = Math.max(1, frame.width);
|
|
1075
|
-
const shapeHeight = Math.max(1, frame.height);
|
|
1076
|
-
const radius = this.resolveCutShapeRadius(sceneGeometry, frame);
|
|
1077
|
-
this.debug("overlay:shape:geometry", {
|
|
1078
|
-
shape,
|
|
1079
|
-
frameWidth: frame.width,
|
|
1080
|
-
frameHeight: frame.height,
|
|
1081
|
-
offset: sceneGeometry.offset,
|
|
1082
|
-
shapeStyle,
|
|
1083
|
-
inset,
|
|
1084
|
-
shapeWidth,
|
|
1085
|
-
shapeHeight,
|
|
1086
|
-
baseRadius: sceneGeometry.radius,
|
|
1087
|
-
radius,
|
|
1088
|
-
});
|
|
1089
|
-
const isSameAsFrame = Math.abs(shapeWidth - frame.width) <= 0.0001 &&
|
|
1090
|
-
Math.abs(shapeHeight - frame.height) <= 0.0001;
|
|
1091
|
-
if (shape === "rect" && radius <= 0.0001 && isSameAsFrame) {
|
|
1092
|
-
this.debug("overlay:shape:skip", {
|
|
1093
|
-
reason: "shape-rect-no-radius",
|
|
1094
|
-
});
|
|
1095
|
-
return [];
|
|
1096
|
-
}
|
|
1097
|
-
const baseOptions = {
|
|
1098
|
-
shape,
|
|
1099
|
-
width: shapeWidth,
|
|
1100
|
-
height: shapeHeight,
|
|
1101
|
-
radius,
|
|
1102
|
-
x: frame.width / 2,
|
|
1103
|
-
y: frame.height / 2,
|
|
1104
|
-
features: [],
|
|
1105
|
-
shapeStyle,
|
|
1106
|
-
canvasWidth: frame.width,
|
|
1107
|
-
canvasHeight: frame.height,
|
|
1108
|
-
};
|
|
1109
|
-
try {
|
|
1110
|
-
const shapePathData = (0, geometry_1.generateDielinePath)(baseOptions);
|
|
1111
|
-
const outerRectPathData = `M 0 0 L ${frame.width} 0 L ${frame.width} ${frame.height} L 0 ${frame.height} Z`;
|
|
1112
|
-
const hatchPathData = `${outerRectPathData} ${shapePathData}`;
|
|
1113
|
-
if (!shapePathData || !hatchPathData) {
|
|
1114
|
-
this.debug("overlay:shape:skip", {
|
|
1115
|
-
reason: "path-generation-empty",
|
|
1116
|
-
shape,
|
|
1117
|
-
radius,
|
|
1118
|
-
});
|
|
1119
|
-
return [];
|
|
1120
|
-
}
|
|
1121
|
-
const patternFill = this.getCropShapeHatchPattern();
|
|
1122
|
-
const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
|
|
1123
|
-
const shapeBounds = (0, geometry_1.getPathBounds)(shapePathData);
|
|
1124
|
-
const hatchBounds = (0, geometry_1.getPathBounds)(hatchPathData);
|
|
1125
|
-
const frameRect = this.toLayoutSceneRect(frame);
|
|
1126
|
-
const hatchPathLength = hatchPathData.length;
|
|
1127
|
-
const shapePathLength = shapePathData.length;
|
|
1128
|
-
const specs = [
|
|
1129
|
-
{
|
|
1130
|
-
id: "image.cropShapeHatch",
|
|
1131
|
-
type: "path",
|
|
1132
|
-
data: { id: "image.cropShapeHatch", zIndex: 5 },
|
|
1133
|
-
layout: {
|
|
1134
|
-
reference: "custom",
|
|
1135
|
-
referenceRect: frameRect,
|
|
1136
|
-
alignX: "start",
|
|
1137
|
-
alignY: "start",
|
|
1138
|
-
offsetX: hatchBounds.x,
|
|
1139
|
-
offsetY: hatchBounds.y,
|
|
1140
|
-
},
|
|
1141
|
-
props: {
|
|
1142
|
-
pathData: hatchPathData,
|
|
1143
|
-
originX: "left",
|
|
1144
|
-
originY: "top",
|
|
1145
|
-
fill: hatchFill,
|
|
1146
|
-
opacity: patternFill ? 1 : 0.8,
|
|
1147
|
-
stroke: "rgba(255, 0, 0, 0.9)",
|
|
1148
|
-
strokeWidth: this.canvasService?.toSceneLength(1) ?? 1,
|
|
1149
|
-
fillRule: "evenodd",
|
|
1150
|
-
selectable: false,
|
|
1151
|
-
evented: false,
|
|
1152
|
-
excludeFromExport: true,
|
|
1153
|
-
objectCaching: false,
|
|
1154
|
-
},
|
|
1155
|
-
},
|
|
1156
|
-
];
|
|
1157
|
-
this.debug("overlay:shape:built", {
|
|
1158
|
-
shape,
|
|
1159
|
-
radius,
|
|
1160
|
-
inset,
|
|
1161
|
-
shapeWidth,
|
|
1162
|
-
shapeHeight,
|
|
1163
|
-
fillRule: "evenodd",
|
|
1164
|
-
shapePathLength,
|
|
1165
|
-
hatchPathLength,
|
|
1166
|
-
shapeBounds,
|
|
1167
|
-
hatchBounds,
|
|
1168
|
-
hatchFillType: hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
|
|
1169
|
-
ids: specs.map((spec) => spec.id),
|
|
1170
|
-
});
|
|
1171
|
-
return specs;
|
|
1172
|
-
}
|
|
1173
|
-
catch (error) {
|
|
1174
|
-
this.debug("overlay:shape:error", {
|
|
1175
|
-
shape,
|
|
1176
|
-
radius,
|
|
1177
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1178
|
-
});
|
|
1179
|
-
return [];
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
1032
|
resolveRenderImageState(item) {
|
|
1183
1033
|
const active = this.isToolActive;
|
|
1184
1034
|
const sourceUrl = item.sourceUrl || item.url;
|
|
@@ -1262,167 +1112,35 @@ class ImageTool {
|
|
|
1262
1112
|
}
|
|
1263
1113
|
return specs;
|
|
1264
1114
|
}
|
|
1265
|
-
buildOverlaySpecs(
|
|
1115
|
+
buildOverlaySpecs(overlayState) {
|
|
1266
1116
|
const visible = this.isImageEditingVisible();
|
|
1267
|
-
if (!visible ||
|
|
1268
|
-
frame.width <= 0 ||
|
|
1269
|
-
frame.height <= 0 ||
|
|
1270
|
-
!this.canvasService) {
|
|
1117
|
+
if (!visible || !overlayState || !this.canvasService) {
|
|
1271
1118
|
this.debug("overlay:hidden", {
|
|
1272
1119
|
visible,
|
|
1273
|
-
|
|
1120
|
+
cutRect: overlayState?.layout.cutRect,
|
|
1274
1121
|
isToolActive: this.isToolActive,
|
|
1275
1122
|
isImageSelectionActive: this.isImageSelectionActive,
|
|
1276
1123
|
focusedImageId: this.focusedImageId,
|
|
1277
1124
|
});
|
|
1278
1125
|
return [];
|
|
1279
1126
|
}
|
|
1280
|
-
const viewport = this.canvasService.
|
|
1281
|
-
const canvasW = viewport.width || 0;
|
|
1282
|
-
const canvasH = viewport.height || 0;
|
|
1283
|
-
const canvasLeft = viewport.left || 0;
|
|
1284
|
-
const canvasTop = viewport.top || 0;
|
|
1127
|
+
const viewport = this.canvasService.getScreenViewportRect();
|
|
1285
1128
|
const visual = this.getFrameVisualConfig();
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
const visibleFrameH = Math.max(0, frameBottom - frameTop);
|
|
1293
|
-
const topH = Math.max(0, frameTop - canvasTop);
|
|
1294
|
-
const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
|
|
1295
|
-
const leftW = Math.max(0, frameLeft - canvasLeft);
|
|
1296
|
-
const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
|
|
1297
|
-
const viewportRect = this.toLayoutSceneRect({
|
|
1298
|
-
left: canvasLeft,
|
|
1299
|
-
top: canvasTop,
|
|
1300
|
-
width: canvasW,
|
|
1301
|
-
height: canvasH,
|
|
1302
|
-
});
|
|
1303
|
-
const visibleFrameBandRect = this.toLayoutSceneRect({
|
|
1304
|
-
left: canvasLeft,
|
|
1305
|
-
top: frameTop,
|
|
1306
|
-
width: canvasW,
|
|
1307
|
-
height: visibleFrameH,
|
|
1308
|
-
});
|
|
1309
|
-
const frameRect = this.toLayoutSceneRect(frame);
|
|
1310
|
-
const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
|
|
1311
|
-
const mask = [
|
|
1312
|
-
{
|
|
1313
|
-
id: "image.cropMask.top",
|
|
1314
|
-
type: "rect",
|
|
1315
|
-
data: { id: "image.cropMask.top", zIndex: 1 },
|
|
1316
|
-
layout: {
|
|
1317
|
-
reference: "custom",
|
|
1318
|
-
referenceRect: viewportRect,
|
|
1319
|
-
alignX: "start",
|
|
1320
|
-
alignY: "start",
|
|
1321
|
-
width: "100%",
|
|
1322
|
-
height: topH,
|
|
1323
|
-
},
|
|
1324
|
-
props: {
|
|
1325
|
-
originX: "left",
|
|
1326
|
-
originY: "top",
|
|
1327
|
-
fill: visual.outerBackground,
|
|
1328
|
-
selectable: false,
|
|
1329
|
-
evented: false,
|
|
1330
|
-
},
|
|
1129
|
+
const specs = (0, sessionOverlay_1.buildImageSessionOverlaySpecs)({
|
|
1130
|
+
viewport: {
|
|
1131
|
+
left: viewport.left,
|
|
1132
|
+
top: viewport.top,
|
|
1133
|
+
width: viewport.width,
|
|
1134
|
+
height: viewport.height,
|
|
1331
1135
|
},
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
reference: "custom",
|
|
1338
|
-
referenceRect: viewportRect,
|
|
1339
|
-
alignX: "start",
|
|
1340
|
-
alignY: "end",
|
|
1341
|
-
width: "100%",
|
|
1342
|
-
height: bottomH,
|
|
1343
|
-
},
|
|
1344
|
-
props: {
|
|
1345
|
-
originX: "left",
|
|
1346
|
-
originY: "top",
|
|
1347
|
-
fill: visual.outerBackground,
|
|
1348
|
-
selectable: false,
|
|
1349
|
-
evented: false,
|
|
1350
|
-
},
|
|
1351
|
-
},
|
|
1352
|
-
{
|
|
1353
|
-
id: "image.cropMask.left",
|
|
1354
|
-
type: "rect",
|
|
1355
|
-
data: { id: "image.cropMask.left", zIndex: 3 },
|
|
1356
|
-
layout: {
|
|
1357
|
-
reference: "custom",
|
|
1358
|
-
referenceRect: visibleFrameBandRect,
|
|
1359
|
-
alignX: "start",
|
|
1360
|
-
alignY: "start",
|
|
1361
|
-
width: leftW,
|
|
1362
|
-
height: "100%",
|
|
1363
|
-
},
|
|
1364
|
-
props: {
|
|
1365
|
-
originX: "left",
|
|
1366
|
-
originY: "top",
|
|
1367
|
-
fill: visual.outerBackground,
|
|
1368
|
-
selectable: false,
|
|
1369
|
-
evented: false,
|
|
1370
|
-
},
|
|
1371
|
-
},
|
|
1372
|
-
{
|
|
1373
|
-
id: "image.cropMask.right",
|
|
1374
|
-
type: "rect",
|
|
1375
|
-
data: { id: "image.cropMask.right", zIndex: 4 },
|
|
1376
|
-
layout: {
|
|
1377
|
-
reference: "custom",
|
|
1378
|
-
referenceRect: visibleFrameBandRect,
|
|
1379
|
-
alignX: "end",
|
|
1380
|
-
alignY: "start",
|
|
1381
|
-
width: rightW,
|
|
1382
|
-
height: "100%",
|
|
1383
|
-
},
|
|
1384
|
-
props: {
|
|
1385
|
-
originX: "left",
|
|
1386
|
-
originY: "top",
|
|
1387
|
-
fill: visual.outerBackground,
|
|
1388
|
-
selectable: false,
|
|
1389
|
-
evented: false,
|
|
1390
|
-
},
|
|
1391
|
-
},
|
|
1392
|
-
];
|
|
1393
|
-
const frameSpec = {
|
|
1394
|
-
id: "image.cropFrame",
|
|
1395
|
-
type: "rect",
|
|
1396
|
-
data: { id: "image.cropFrame", zIndex: 7 },
|
|
1397
|
-
layout: {
|
|
1398
|
-
reference: "custom",
|
|
1399
|
-
referenceRect: frameRect,
|
|
1400
|
-
alignX: "start",
|
|
1401
|
-
alignY: "start",
|
|
1402
|
-
width: "100%",
|
|
1403
|
-
height: "100%",
|
|
1404
|
-
},
|
|
1405
|
-
props: {
|
|
1406
|
-
originX: "left",
|
|
1407
|
-
originY: "top",
|
|
1408
|
-
fill: visual.innerBackground,
|
|
1409
|
-
stroke: visual.strokeStyle === "hidden"
|
|
1410
|
-
? "rgba(0,0,0,0)"
|
|
1411
|
-
: visual.strokeColor,
|
|
1412
|
-
strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
|
|
1413
|
-
strokeDashArray: visual.strokeStyle === "dashed"
|
|
1414
|
-
? [dashLengthScene, dashLengthScene]
|
|
1415
|
-
: undefined,
|
|
1416
|
-
selectable: false,
|
|
1417
|
-
evented: false,
|
|
1418
|
-
},
|
|
1419
|
-
};
|
|
1420
|
-
const specs = shapeOverlay.length > 0
|
|
1421
|
-
? [...mask, ...shapeOverlay]
|
|
1422
|
-
: [...mask, ...shapeOverlay, frameSpec];
|
|
1136
|
+
layout: overlayState.layout,
|
|
1137
|
+
geometry: overlayState.geometry,
|
|
1138
|
+
visual,
|
|
1139
|
+
hatchPattern: this.getCropShapeHatchPattern(),
|
|
1140
|
+
});
|
|
1423
1141
|
this.debug("overlay:built", {
|
|
1424
|
-
|
|
1425
|
-
shape:
|
|
1142
|
+
cutRect: overlayState.layout.cutRect,
|
|
1143
|
+
shape: overlayState.geometry.shape,
|
|
1426
1144
|
overlayIds: specs.map((spec) => ({
|
|
1427
1145
|
id: spec.id,
|
|
1428
1146
|
zIndex: spec.data?.zIndex,
|
|
@@ -1450,11 +1168,9 @@ class ImageTool {
|
|
|
1450
1168
|
const imageSpecs = await this.buildImageSpecs(renderItems, frame);
|
|
1451
1169
|
if (seq !== this.renderSeq)
|
|
1452
1170
|
return;
|
|
1453
|
-
const
|
|
1454
|
-
if (seq !== this.renderSeq)
|
|
1455
|
-
return;
|
|
1171
|
+
const overlayState = this.resolveSessionOverlayState();
|
|
1456
1172
|
this.imageSpecs = imageSpecs;
|
|
1457
|
-
this.overlaySpecs = this.buildOverlaySpecs(
|
|
1173
|
+
this.overlaySpecs = this.buildOverlaySpecs(overlayState);
|
|
1458
1174
|
await this.canvasService.flushRenderFromProducers();
|
|
1459
1175
|
if (seq !== this.renderSeq)
|
|
1460
1176
|
return;
|
|
@@ -1484,11 +1200,39 @@ class ImageTool {
|
|
|
1484
1200
|
isImageSelectionActive: this.isImageSelectionActive,
|
|
1485
1201
|
focusedImageId: this.focusedImageId,
|
|
1486
1202
|
});
|
|
1203
|
+
this.emitImageStateChange();
|
|
1487
1204
|
this.canvasService.requestRenderAll();
|
|
1488
1205
|
}
|
|
1489
1206
|
clampNormalized(value) {
|
|
1490
1207
|
return Math.max(-1, Math.min(2, value));
|
|
1491
1208
|
}
|
|
1209
|
+
async setImageTransform(id, updates, options = {}) {
|
|
1210
|
+
const next = {};
|
|
1211
|
+
if (Number.isFinite(updates.scale)) {
|
|
1212
|
+
next.scale = Math.max(0.05, Number(updates.scale));
|
|
1213
|
+
}
|
|
1214
|
+
if (Number.isFinite(updates.angle)) {
|
|
1215
|
+
next.angle = Number(updates.angle);
|
|
1216
|
+
}
|
|
1217
|
+
if (Number.isFinite(updates.left)) {
|
|
1218
|
+
next.left = this.clampNormalized(Number(updates.left));
|
|
1219
|
+
}
|
|
1220
|
+
if (Number.isFinite(updates.top)) {
|
|
1221
|
+
next.top = this.clampNormalized(Number(updates.top));
|
|
1222
|
+
}
|
|
1223
|
+
if (Number.isFinite(updates.opacity)) {
|
|
1224
|
+
next.opacity = Math.max(0, Math.min(1, Number(updates.opacity)));
|
|
1225
|
+
}
|
|
1226
|
+
if (!Object.keys(next).length)
|
|
1227
|
+
return;
|
|
1228
|
+
await this.updateImage(id, next, options);
|
|
1229
|
+
}
|
|
1230
|
+
resetImageSession() {
|
|
1231
|
+
this.workingItems = this.cloneItems(this.items);
|
|
1232
|
+
this.hasWorkingChanges = false;
|
|
1233
|
+
this.updateImages();
|
|
1234
|
+
this.emitWorkingChange();
|
|
1235
|
+
}
|
|
1492
1236
|
updateImageInWorking(id, updates) {
|
|
1493
1237
|
const index = this.workingItems.findIndex((item) => item.id === id);
|
|
1494
1238
|
if (index < 0)
|
|
@@ -1522,23 +1266,12 @@ class ImageTool {
|
|
|
1522
1266
|
url: replacingUrl,
|
|
1523
1267
|
sourceUrl: replacingUrl,
|
|
1524
1268
|
committedUrl: undefined,
|
|
1525
|
-
scale: updates.scale ?? 1,
|
|
1526
|
-
angle: updates.angle ?? 0,
|
|
1527
|
-
left: updates.left ?? 0.5,
|
|
1528
|
-
top: updates.top ?? 0.5,
|
|
1529
1269
|
}
|
|
1530
1270
|
: {}),
|
|
1531
1271
|
});
|
|
1532
1272
|
this.updateConfig(next);
|
|
1533
1273
|
if (replacingSource) {
|
|
1534
|
-
this.debug("replace:image:begin", { id, replacingUrl });
|
|
1535
1274
|
this.purgeSourceSizeCacheForItem(base);
|
|
1536
|
-
const loaded = await this.waitImageLoaded(id, true);
|
|
1537
|
-
this.debug("replace:image:loaded", { id, loaded });
|
|
1538
|
-
if (loaded) {
|
|
1539
|
-
await this.refitImageToFrame(id);
|
|
1540
|
-
this.setImageFocus(id);
|
|
1541
|
-
}
|
|
1542
1275
|
}
|
|
1543
1276
|
}
|
|
1544
1277
|
waitImageLoaded(id, forceWait = false) {
|
|
@@ -1556,73 +1289,53 @@ class ImageTool {
|
|
|
1556
1289
|
});
|
|
1557
1290
|
});
|
|
1558
1291
|
}
|
|
1559
|
-
async
|
|
1292
|
+
async resolveImageSourceSize(id, src) {
|
|
1560
1293
|
const obj = this.getImageObject(id);
|
|
1561
|
-
if (
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
scale: Number.isFinite(zoom) ? zoom : 1,
|
|
1575
|
-
angle: 0,
|
|
1576
|
-
left: 0.5,
|
|
1577
|
-
top: 0.5,
|
|
1578
|
-
};
|
|
1579
|
-
const index = this.items.findIndex((item) => item.id === id);
|
|
1580
|
-
if (index < 0)
|
|
1581
|
-
return;
|
|
1582
|
-
const next = [...this.items];
|
|
1583
|
-
next[index] = this.normalizeItem({ ...next[index], ...updated });
|
|
1584
|
-
this.updateConfig(next);
|
|
1585
|
-
this.workingItems = this.cloneItems(next);
|
|
1586
|
-
this.hasWorkingChanges = false;
|
|
1587
|
-
this.updateImages();
|
|
1588
|
-
this.emitWorkingChange(id);
|
|
1294
|
+
if (obj) {
|
|
1295
|
+
this.rememberSourceSize(src, obj);
|
|
1296
|
+
}
|
|
1297
|
+
const ensured = await this.ensureSourceSize(src);
|
|
1298
|
+
if (ensured)
|
|
1299
|
+
return ensured;
|
|
1300
|
+
if (!obj)
|
|
1301
|
+
return null;
|
|
1302
|
+
const width = Number(obj?.width || 0);
|
|
1303
|
+
const height = Number(obj?.height || 0);
|
|
1304
|
+
if (width <= 0 || height <= 0)
|
|
1305
|
+
return null;
|
|
1306
|
+
return { width, height };
|
|
1589
1307
|
}
|
|
1590
|
-
async
|
|
1308
|
+
async applyImageOperation(id, operation, options = {}) {
|
|
1591
1309
|
if (!this.canvasService)
|
|
1592
1310
|
return;
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
return;
|
|
1599
|
-
const renderItems = this.isToolActive ? this.workingItems : this.items;
|
|
1311
|
+
this.syncToolActiveFromWorkbench();
|
|
1312
|
+
const target = options.target || "auto";
|
|
1313
|
+
const renderItems = target === "working" || (target === "auto" && this.isToolActive)
|
|
1314
|
+
? this.workingItems
|
|
1315
|
+
: this.items;
|
|
1600
1316
|
const current = renderItems.find((item) => item.id === id);
|
|
1601
1317
|
if (!current)
|
|
1602
1318
|
return;
|
|
1603
1319
|
const render = this.resolveRenderImageState(current);
|
|
1604
|
-
this.
|
|
1605
|
-
|
|
1320
|
+
const source = await this.resolveImageSourceSize(id, render.src);
|
|
1321
|
+
if (!source)
|
|
1322
|
+
return;
|
|
1606
1323
|
const frame = this.getFrameRect();
|
|
1607
|
-
const baseCover = this.getCoverScale(frame, source);
|
|
1608
|
-
const desiredScale = Math.max(Math.max(1, area.width) / Math.max(1, source.width), Math.max(1, area.height) / Math.max(1, source.height));
|
|
1609
1324
|
const viewport = this.canvasService.getSceneViewportRect();
|
|
1610
|
-
const
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
};
|
|
1625
|
-
if (this.isToolActive) {
|
|
1325
|
+
const area = operation.type === "resetTransform"
|
|
1326
|
+
? (0, imageOperations_1.resolveImageOperationArea)({ frame, viewport })
|
|
1327
|
+
: (0, imageOperations_1.resolveImageOperationArea)({
|
|
1328
|
+
frame,
|
|
1329
|
+
viewport,
|
|
1330
|
+
area: operation.area,
|
|
1331
|
+
});
|
|
1332
|
+
const updates = (0, imageOperations_1.computeImageOperationUpdates)({
|
|
1333
|
+
frame,
|
|
1334
|
+
source,
|
|
1335
|
+
operation,
|
|
1336
|
+
area,
|
|
1337
|
+
});
|
|
1338
|
+
if (target === "working" || (target === "auto" && this.isToolActive)) {
|
|
1626
1339
|
this.updateImageInWorking(id, updates);
|
|
1627
1340
|
return;
|
|
1628
1341
|
}
|