@pooder/kit 6.2.2 → 6.3.1

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.
@@ -11,6 +11,8 @@ const sessionState_1 = require("../../shared/runtime/sessionState");
11
11
  const layers_1 = require("../../shared/constants/layers");
12
12
  const commands_1 = require("./commands");
13
13
  const config_1 = require("./config");
14
+ const imageOperations_1 = require("./imageOperations");
15
+ const imagePlacement_1 = require("./imagePlacement");
14
16
  const sessionOverlay_1 = require("./sessionOverlay");
15
17
  const IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
16
18
  "rotate",
@@ -62,6 +64,7 @@ class ImageTool {
62
64
  this.activeSnapX = null;
63
65
  this.activeSnapY = null;
64
66
  this.movingImageId = null;
67
+ this.sessionNotice = null;
65
68
  this.hasRenderedSnapGuides = false;
66
69
  this.subscriptions = new subscriptions_1.SubscriptionBag();
67
70
  this.imageControlsByCapabilityKey = new Map();
@@ -227,7 +230,11 @@ class ImageTool {
227
230
  }
228
231
  if (e.key.startsWith("size.") ||
229
232
  e.key.startsWith("image.frame.") ||
233
+ e.key.startsWith("image.session.") ||
230
234
  e.key.startsWith("image.control.")) {
235
+ if (e.key === "image.session.placementPolicy") {
236
+ this.clearSessionNotice();
237
+ }
231
238
  if (e.key.startsWith("image.control.")) {
232
239
  this.imageControlsByCapabilityKey.clear();
233
240
  }
@@ -255,6 +262,7 @@ class ImageTool {
255
262
  this.clearRenderedImages();
256
263
  this.renderProducerDisposable?.dispose();
257
264
  this.renderProducerDisposable = undefined;
265
+ this.emitImageStateChange();
258
266
  if (this.canvasService) {
259
267
  void this.canvasService.flushRenderFromProducers();
260
268
  this.canvasService = undefined;
@@ -645,9 +653,10 @@ class ImageTool {
645
653
  name: "Image",
646
654
  interaction: "session",
647
655
  commands: {
648
- begin: "resetWorkingImages",
656
+ begin: "imageSessionReset",
657
+ validate: "validateImageSession",
649
658
  commit: "completeImages",
650
- rollback: "resetWorkingImages",
659
+ rollback: "imageSessionReset",
651
660
  },
652
661
  session: {
653
662
  autoBegin: true,
@@ -685,6 +694,59 @@ class ImageTool {
685
694
  cloneItems(items) {
686
695
  return this.normalizeItems((items || []).map((i) => ({ ...i })));
687
696
  }
697
+ getViewItems() {
698
+ return this.isToolActive ? this.workingItems : this.items;
699
+ }
700
+ getPlacementPolicy() {
701
+ const policy = this.getConfig("image.session.placementPolicy", "free");
702
+ return policy === "warn" || policy === "strict" ? policy : "free";
703
+ }
704
+ areSessionNoticesEqual(a, b) {
705
+ if (!a && !b)
706
+ return true;
707
+ if (!a || !b)
708
+ return false;
709
+ return (a.code === b.code &&
710
+ a.level === b.level &&
711
+ a.message === b.message &&
712
+ a.policy === b.policy &&
713
+ JSON.stringify(a.imageIds) === JSON.stringify(b.imageIds));
714
+ }
715
+ setSessionNotice(notice, options = {}) {
716
+ if (this.areSessionNoticesEqual(this.sessionNotice, notice)) {
717
+ return;
718
+ }
719
+ this.sessionNotice = notice;
720
+ if (options.emit !== false) {
721
+ this.context?.eventBus.emit("image:session:notice", this.sessionNotice);
722
+ this.emitImageStateChange();
723
+ }
724
+ }
725
+ clearSessionNotice(options = {}) {
726
+ this.setSessionNotice(null, options);
727
+ }
728
+ getImageViewState() {
729
+ this.syncToolActiveFromWorkbench();
730
+ const items = this.cloneItems(this.getViewItems());
731
+ const focusedItem = this.focusedImageId == null
732
+ ? null
733
+ : items.find((item) => item.id === this.focusedImageId) || null;
734
+ return {
735
+ items,
736
+ hasAnyImage: items.length > 0,
737
+ focusedId: this.focusedImageId,
738
+ focusedItem,
739
+ isToolActive: this.isToolActive,
740
+ isImageSelectionActive: this.isImageSelectionActive,
741
+ hasWorkingChanges: this.hasWorkingChanges,
742
+ source: this.isToolActive ? "working" : "committed",
743
+ placementPolicy: this.getPlacementPolicy(),
744
+ sessionNotice: this.sessionNotice,
745
+ };
746
+ }
747
+ emitImageStateChange() {
748
+ this.context?.eventBus.emit("image:state:change", this.getImageViewState());
749
+ }
688
750
  emitWorkingChange(changedId = null) {
689
751
  this.context?.eventBus.emit("image:working:change", {
690
752
  changedId,
@@ -722,9 +784,14 @@ class ImageTool {
722
784
  if (!options.skipRender) {
723
785
  this.updateImages();
724
786
  }
787
+ else {
788
+ this.emitImageStateChange();
789
+ }
725
790
  return { ok: true, id };
726
791
  }
727
- async addImageEntry(url, options, fitOnAdd = true) {
792
+ async addImageEntry(url, options, operation) {
793
+ this.syncToolActiveFromWorkbench();
794
+ this.clearSessionNotice({ emit: false });
728
795
  const id = this.generateId();
729
796
  const newItem = this.normalizeItem({
730
797
  id,
@@ -732,13 +799,21 @@ class ImageTool {
732
799
  opacity: 1,
733
800
  ...options,
734
801
  });
735
- const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
736
802
  const waitLoaded = this.waitImageLoaded(id, true);
737
- this.updateConfig([...this.items, newItem]);
738
- this.addItemToWorkingSessionIfNeeded(newItem, sessionDirtyBeforeAdd);
803
+ if (this.isToolActive) {
804
+ this.workingItems = this.cloneItems([...this.workingItems, newItem]);
805
+ this.hasWorkingChanges = true;
806
+ this.updateImages();
807
+ this.emitWorkingChange(id);
808
+ }
809
+ else {
810
+ this.updateConfig([...this.items, newItem]);
811
+ }
739
812
  const loaded = await waitLoaded;
740
- if (loaded && fitOnAdd) {
741
- await this.fitImageToDefaultArea(id);
813
+ if (loaded && operation) {
814
+ await this.applyImageOperation(id, operation, {
815
+ target: this.isToolActive ? "working" : "config",
816
+ });
742
817
  }
743
818
  if (loaded) {
744
819
  this.setImageFocus(id);
@@ -746,8 +821,8 @@ class ImageTool {
746
821
  return id;
747
822
  }
748
823
  async upsertImageEntry(url, options = {}) {
824
+ this.syncToolActiveFromWorkbench();
749
825
  const mode = options.mode || (options.id ? "replace" : "add");
750
- const fitOnAdd = options.fitOnAdd !== false;
751
826
  if (mode === "replace") {
752
827
  if (!options.id) {
753
828
  throw new Error("replace-target-id-required");
@@ -756,21 +831,33 @@ class ImageTool {
756
831
  if (!this.hasImageItem(targetId)) {
757
832
  throw new Error("replace-target-not-found");
758
833
  }
759
- await this.updateImageInConfig(targetId, { url });
834
+ if (this.isToolActive) {
835
+ const current = this.workingItems.find((item) => item.id === targetId) ||
836
+ this.items.find((item) => item.id === targetId);
837
+ this.purgeSourceSizeCacheForItem(current);
838
+ this.updateImageInWorking(targetId, {
839
+ url,
840
+ sourceUrl: url,
841
+ committedUrl: undefined,
842
+ });
843
+ }
844
+ else {
845
+ await this.updateImageInConfig(targetId, { url });
846
+ }
847
+ const loaded = await this.waitImageLoaded(targetId, true);
848
+ if (loaded && options.operation) {
849
+ await this.applyImageOperation(targetId, options.operation, {
850
+ target: this.isToolActive ? "working" : "config",
851
+ });
852
+ }
853
+ if (loaded) {
854
+ this.setImageFocus(targetId);
855
+ }
760
856
  return { id: targetId, mode: "replace" };
761
857
  }
762
- const id = await this.addImageEntry(url, options.addOptions, fitOnAdd);
858
+ const id = await this.addImageEntry(url, options.addOptions, options.operation);
763
859
  return { id, mode: "add" };
764
860
  }
765
- addItemToWorkingSessionIfNeeded(item, sessionDirtyBeforeAdd) {
766
- if (!sessionDirtyBeforeAdd || !this.isToolActive)
767
- return;
768
- if (this.workingItems.some((existing) => existing.id === item.id))
769
- return;
770
- this.workingItems = this.cloneItems([...this.workingItems, item]);
771
- this.updateImages();
772
- this.emitWorkingChange(item.id);
773
- }
774
861
  async updateImage(id, updates, options = {}) {
775
862
  this.syncToolActiveFromWorkbench();
776
863
  const target = options.target || "auto";
@@ -806,6 +893,7 @@ class ImageTool {
806
893
  updateConfig(newItems, skipCanvasUpdate = false) {
807
894
  if (!this.context)
808
895
  return;
896
+ this.clearSessionNotice({ emit: false });
809
897
  this.applyCommittedItems(newItems);
810
898
  (0, sessionState_1.runDeferredConfigUpdate)(this, () => {
811
899
  const configService = this.context?.services.get("ConfigurationService");
@@ -825,37 +913,6 @@ class ImageTool {
825
913
  }
826
914
  return this.canvasService.toScreenRect(frame || this.getFrameRect());
827
915
  }
828
- async resolveDefaultFitArea() {
829
- if (!this.canvasService)
830
- return null;
831
- const frame = this.getFrameRect();
832
- if (frame.width <= 0 || frame.height <= 0)
833
- return null;
834
- return {
835
- width: Math.max(1, frame.width),
836
- height: Math.max(1, frame.height),
837
- left: frame.left + frame.width / 2,
838
- top: frame.top + frame.height / 2,
839
- };
840
- }
841
- async fitImageToDefaultArea(id) {
842
- if (!this.canvasService)
843
- return;
844
- const area = await this.resolveDefaultFitArea();
845
- if (area) {
846
- await this.fitImageToArea(id, area);
847
- return;
848
- }
849
- const viewport = this.canvasService.getSceneViewportRect();
850
- const canvasW = Math.max(1, viewport.width || 0);
851
- const canvasH = Math.max(1, viewport.height || 0);
852
- await this.fitImageToArea(id, {
853
- width: canvasW,
854
- height: canvasH,
855
- left: viewport.left + canvasW / 2,
856
- top: viewport.top + canvasH / 2,
857
- });
858
- }
859
916
  getImageObjects() {
860
917
  if (!this.canvasService)
861
918
  return [];
@@ -929,6 +986,72 @@ class ImageTool {
929
986
  getCoverScale(frame, size) {
930
987
  return (0, sourceSizeCache_1.getCoverScale)(frame, size);
931
988
  }
989
+ resolvePlacementState(item) {
990
+ return {
991
+ left: Number.isFinite(item.left) ? item.left : 0.5,
992
+ top: Number.isFinite(item.top) ? item.top : 0.5,
993
+ scale: Math.max(0.05, item.scale ?? 1),
994
+ angle: Number.isFinite(item.angle) ? item.angle : 0,
995
+ };
996
+ }
997
+ async validatePlacementForItem(item) {
998
+ const frame = this.getFrameRect();
999
+ if (!frame.width || !frame.height) {
1000
+ return true;
1001
+ }
1002
+ const src = item.sourceUrl || item.url;
1003
+ if (!src) {
1004
+ return true;
1005
+ }
1006
+ const source = await this.resolveImageSourceSize(item.id, src);
1007
+ if (!source) {
1008
+ return true;
1009
+ }
1010
+ return (0, imagePlacement_1.validateImagePlacement)({
1011
+ frame,
1012
+ source,
1013
+ placement: this.resolvePlacementState(item),
1014
+ }).ok;
1015
+ }
1016
+ async validateImageSession() {
1017
+ const policy = this.getPlacementPolicy();
1018
+ if (policy === "free") {
1019
+ this.clearSessionNotice();
1020
+ return { ok: true, policy };
1021
+ }
1022
+ const invalidImageIds = [];
1023
+ for (const item of this.workingItems) {
1024
+ const valid = await this.validatePlacementForItem(item);
1025
+ if (!valid) {
1026
+ invalidImageIds.push(item.id);
1027
+ }
1028
+ }
1029
+ if (!invalidImageIds.length) {
1030
+ this.clearSessionNotice();
1031
+ return { ok: true, policy };
1032
+ }
1033
+ const notice = {
1034
+ code: "image-outside-frame",
1035
+ level: policy === "strict" ? "error" : "warning",
1036
+ message: policy === "strict"
1037
+ ? "图片位置不能超出 frame,请调整后再提交。"
1038
+ : "图片位置已超出 frame,建议调整后再提交。",
1039
+ imageIds: invalidImageIds,
1040
+ policy,
1041
+ };
1042
+ this.setSessionNotice(notice);
1043
+ this.setImageFocus(invalidImageIds[0], {
1044
+ syncCanvasSelection: true,
1045
+ skipRender: true,
1046
+ });
1047
+ return {
1048
+ ok: policy !== "strict",
1049
+ reason: notice.code,
1050
+ message: notice.message,
1051
+ imageIds: notice.imageIds,
1052
+ policy: notice.policy,
1053
+ };
1054
+ }
932
1055
  getFrameVisualConfig() {
933
1056
  const strokeStyleRaw = (this.getConfig("image.frame.strokeStyle", "dashed") || "dashed");
934
1057
  const strokeStyle = strokeStyleRaw === "dashed" || strokeStyleRaw === "hidden"
@@ -1182,15 +1305,45 @@ class ImageTool {
1182
1305
  isImageSelectionActive: this.isImageSelectionActive,
1183
1306
  focusedImageId: this.focusedImageId,
1184
1307
  });
1308
+ this.emitImageStateChange();
1185
1309
  this.canvasService.requestRenderAll();
1186
1310
  }
1187
1311
  clampNormalized(value) {
1188
1312
  return Math.max(-1, Math.min(2, value));
1189
1313
  }
1314
+ async setImageTransform(id, updates, options = {}) {
1315
+ const next = {};
1316
+ if (Number.isFinite(updates.scale)) {
1317
+ next.scale = Math.max(0.05, Number(updates.scale));
1318
+ }
1319
+ if (Number.isFinite(updates.angle)) {
1320
+ next.angle = Number(updates.angle);
1321
+ }
1322
+ if (Number.isFinite(updates.left)) {
1323
+ next.left = this.clampNormalized(Number(updates.left));
1324
+ }
1325
+ if (Number.isFinite(updates.top)) {
1326
+ next.top = this.clampNormalized(Number(updates.top));
1327
+ }
1328
+ if (Number.isFinite(updates.opacity)) {
1329
+ next.opacity = Math.max(0, Math.min(1, Number(updates.opacity)));
1330
+ }
1331
+ if (!Object.keys(next).length)
1332
+ return;
1333
+ await this.updateImage(id, next, options);
1334
+ }
1335
+ resetImageSession() {
1336
+ this.clearSessionNotice({ emit: false });
1337
+ this.workingItems = this.cloneItems(this.items);
1338
+ this.hasWorkingChanges = false;
1339
+ this.updateImages();
1340
+ this.emitWorkingChange();
1341
+ }
1190
1342
  updateImageInWorking(id, updates) {
1191
1343
  const index = this.workingItems.findIndex((item) => item.id === id);
1192
1344
  if (index < 0)
1193
1345
  return;
1346
+ this.clearSessionNotice({ emit: false });
1194
1347
  const next = [...this.workingItems];
1195
1348
  next[index] = this.normalizeItem({ ...next[index], ...updates });
1196
1349
  this.workingItems = next;
@@ -1208,6 +1361,7 @@ class ImageTool {
1208
1361
  const index = this.items.findIndex((item) => item.id === id);
1209
1362
  if (index < 0)
1210
1363
  return;
1364
+ this.clearSessionNotice({ emit: false });
1211
1365
  const replacingSource = typeof updates.url === "string" && updates.url.length > 0;
1212
1366
  const next = [...this.items];
1213
1367
  const base = next[index];
@@ -1220,23 +1374,12 @@ class ImageTool {
1220
1374
  url: replacingUrl,
1221
1375
  sourceUrl: replacingUrl,
1222
1376
  committedUrl: undefined,
1223
- scale: updates.scale ?? 1,
1224
- angle: updates.angle ?? 0,
1225
- left: updates.left ?? 0.5,
1226
- top: updates.top ?? 0.5,
1227
1377
  }
1228
1378
  : {}),
1229
1379
  });
1230
1380
  this.updateConfig(next);
1231
1381
  if (replacingSource) {
1232
- this.debug("replace:image:begin", { id, replacingUrl });
1233
1382
  this.purgeSourceSizeCacheForItem(base);
1234
- const loaded = await this.waitImageLoaded(id, true);
1235
- this.debug("replace:image:loaded", { id, loaded });
1236
- if (loaded) {
1237
- await this.refitImageToFrame(id);
1238
- this.setImageFocus(id);
1239
- }
1240
1383
  }
1241
1384
  }
1242
1385
  waitImageLoaded(id, forceWait = false) {
@@ -1254,73 +1397,53 @@ class ImageTool {
1254
1397
  });
1255
1398
  });
1256
1399
  }
1257
- async refitImageToFrame(id) {
1400
+ async resolveImageSourceSize(id, src) {
1258
1401
  const obj = this.getImageObject(id);
1259
- if (!obj || !this.canvasService)
1260
- return;
1261
- const current = this.items.find((item) => item.id === id);
1262
- if (!current)
1263
- return;
1264
- const render = this.resolveRenderImageState(current);
1265
- this.rememberSourceSize(render.src, obj);
1266
- const source = this.getSourceSize(render.src, obj);
1267
- const frame = this.getFrameRect();
1268
- const coverScale = this.getCoverScale(frame, source);
1269
- const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
1270
- const zoom = Math.max(0.05, currentScale / coverScale);
1271
- const updated = {
1272
- scale: Number.isFinite(zoom) ? zoom : 1,
1273
- angle: 0,
1274
- left: 0.5,
1275
- top: 0.5,
1276
- };
1277
- const index = this.items.findIndex((item) => item.id === id);
1278
- if (index < 0)
1279
- return;
1280
- const next = [...this.items];
1281
- next[index] = this.normalizeItem({ ...next[index], ...updated });
1282
- this.updateConfig(next);
1283
- this.workingItems = this.cloneItems(next);
1284
- this.hasWorkingChanges = false;
1285
- this.updateImages();
1286
- this.emitWorkingChange(id);
1402
+ if (obj) {
1403
+ this.rememberSourceSize(src, obj);
1404
+ }
1405
+ const ensured = await this.ensureSourceSize(src);
1406
+ if (ensured)
1407
+ return ensured;
1408
+ if (!obj)
1409
+ return null;
1410
+ const width = Number(obj?.width || 0);
1411
+ const height = Number(obj?.height || 0);
1412
+ if (width <= 0 || height <= 0)
1413
+ return null;
1414
+ return { width, height };
1287
1415
  }
1288
- async fitImageToArea(id, area) {
1416
+ async applyImageOperation(id, operation, options = {}) {
1289
1417
  if (!this.canvasService)
1290
1418
  return;
1291
- const loaded = await this.waitImageLoaded(id, false);
1292
- if (!loaded)
1293
- return;
1294
- const obj = this.getImageObject(id);
1295
- if (!obj)
1296
- return;
1297
- const renderItems = this.isToolActive ? this.workingItems : this.items;
1419
+ this.syncToolActiveFromWorkbench();
1420
+ const target = options.target || "auto";
1421
+ const renderItems = target === "working" || (target === "auto" && this.isToolActive)
1422
+ ? this.workingItems
1423
+ : this.items;
1298
1424
  const current = renderItems.find((item) => item.id === id);
1299
1425
  if (!current)
1300
1426
  return;
1301
1427
  const render = this.resolveRenderImageState(current);
1302
- this.rememberSourceSize(render.src, obj);
1303
- const source = this.getSourceSize(render.src, obj);
1428
+ const source = await this.resolveImageSourceSize(id, render.src);
1429
+ if (!source)
1430
+ return;
1304
1431
  const frame = this.getFrameRect();
1305
- const baseCover = this.getCoverScale(frame, source);
1306
- const desiredScale = Math.max(Math.max(1, area.width) / Math.max(1, source.width), Math.max(1, area.height) / Math.max(1, source.height));
1307
1432
  const viewport = this.canvasService.getSceneViewportRect();
1308
- const canvasW = viewport.width || 1;
1309
- const canvasH = viewport.height || 1;
1310
- const areaLeftInput = area.left ?? 0.5;
1311
- const areaTopInput = area.top ?? 0.5;
1312
- const areaLeftPx = areaLeftInput <= 1.5
1313
- ? viewport.left + areaLeftInput * canvasW
1314
- : areaLeftInput;
1315
- const areaTopPx = areaTopInput <= 1.5
1316
- ? viewport.top + areaTopInput * canvasH
1317
- : areaTopInput;
1318
- const updates = {
1319
- scale: Math.max(0.05, desiredScale / baseCover),
1320
- left: this.clampNormalized((areaLeftPx - frame.left) / Math.max(1, frame.width)),
1321
- top: this.clampNormalized((areaTopPx - frame.top) / Math.max(1, frame.height)),
1322
- };
1323
- if (this.isToolActive) {
1433
+ const area = operation.type === "resetTransform"
1434
+ ? (0, imageOperations_1.resolveImageOperationArea)({ frame, viewport })
1435
+ : (0, imageOperations_1.resolveImageOperationArea)({
1436
+ frame,
1437
+ viewport,
1438
+ area: operation.area,
1439
+ });
1440
+ const updates = (0, imageOperations_1.computeImageOperationUpdates)({
1441
+ frame,
1442
+ source,
1443
+ operation,
1444
+ area,
1445
+ });
1446
+ if (target === "working" || (target === "auto" && this.isToolActive)) {
1324
1447
  this.updateImageInWorking(id, updates);
1325
1448
  return;
1326
1449
  }
@@ -1357,11 +1480,19 @@ class ImageTool {
1357
1480
  }
1358
1481
  }
1359
1482
  this.hasWorkingChanges = false;
1483
+ this.clearSessionNotice({ emit: false });
1360
1484
  this.workingItems = this.cloneItems(next);
1361
1485
  this.updateConfig(next);
1362
1486
  this.emitWorkingChange(this.focusedImageId);
1363
1487
  return { ok: true };
1364
1488
  }
1489
+ async completeImageSession() {
1490
+ const validation = await this.validateImageSession();
1491
+ if (!validation.ok) {
1492
+ return validation;
1493
+ }
1494
+ return await this.commitWorkingImagesAsCropped();
1495
+ }
1365
1496
  async exportCroppedImageByIds(imageIds, options) {
1366
1497
  if (!this.canvasService) {
1367
1498
  throw new Error("CanvasService not initialized");