@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.
package/dist/index.mjs CHANGED
@@ -1271,30 +1271,43 @@ function createImageCommands(tool) {
1271
1271
  }
1272
1272
  },
1273
1273
  {
1274
- command: "getWorkingImages",
1275
- id: "getWorkingImages",
1276
- title: "Get Working Images",
1274
+ command: "applyImageOperation",
1275
+ id: "applyImageOperation",
1276
+ title: "Apply Image Operation",
1277
+ handler: async (id, operation, options = {}) => {
1278
+ await tool.applyImageOperation(id, operation, options);
1279
+ }
1280
+ },
1281
+ {
1282
+ command: "getImageViewState",
1283
+ id: "getImageViewState",
1284
+ title: "Get Image View State",
1277
1285
  handler: () => {
1278
- return tool.cloneItems(tool.workingItems);
1286
+ return tool.getImageViewState();
1279
1287
  }
1280
1288
  },
1281
1289
  {
1282
- command: "setWorkingImage",
1283
- id: "setWorkingImage",
1284
- title: "Set Working Image",
1285
- handler: (id, updates) => {
1286
- tool.updateImageInWorking(id, updates);
1290
+ command: "setImageTransform",
1291
+ id: "setImageTransform",
1292
+ title: "Set Image Transform",
1293
+ handler: async (id, updates, options = {}) => {
1294
+ await tool.setImageTransform(id, updates, options);
1287
1295
  }
1288
1296
  },
1289
1297
  {
1290
- command: "resetWorkingImages",
1291
- id: "resetWorkingImages",
1292
- title: "Reset Working Images",
1298
+ command: "imageSessionReset",
1299
+ id: "imageSessionReset",
1300
+ title: "Reset Image Session",
1293
1301
  handler: () => {
1294
- tool.workingItems = tool.cloneItems(tool.items);
1295
- tool.hasWorkingChanges = false;
1296
- tool.updateImages();
1297
- tool.emitWorkingChange();
1302
+ tool.resetImageSession();
1303
+ }
1304
+ },
1305
+ {
1306
+ command: "validateImageSession",
1307
+ id: "validateImageSession",
1308
+ title: "Validate Image Session",
1309
+ handler: async () => {
1310
+ return await tool.validateImageSession();
1298
1311
  }
1299
1312
  },
1300
1313
  {
@@ -1302,7 +1315,7 @@ function createImageCommands(tool) {
1302
1315
  id: "completeImages",
1303
1316
  title: "Complete Images",
1304
1317
  handler: async () => {
1305
- return await tool.commitWorkingImagesAsCropped();
1318
+ return await tool.completeImageSession();
1306
1319
  }
1307
1320
  },
1308
1321
  {
@@ -1313,22 +1326,6 @@ function createImageCommands(tool) {
1313
1326
  return await tool.exportUserCroppedImage(options);
1314
1327
  }
1315
1328
  },
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
1329
  {
1333
1330
  command: "focusImage",
1334
1331
  id: "focusImage",
@@ -1342,9 +1339,10 @@ function createImageCommands(tool) {
1342
1339
  id: "removeImage",
1343
1340
  title: "Remove Image",
1344
1341
  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) {
1342
+ const sourceItems = tool.isToolActive ? tool.workingItems : tool.items;
1343
+ const removed = sourceItems.find((item) => item.id === id);
1344
+ const next = sourceItems.filter((item) => item.id !== id);
1345
+ if (next.length !== sourceItems.length) {
1348
1346
  tool.purgeSourceSizeCacheForItem(removed);
1349
1347
  if (tool.focusedImageId === id) {
1350
1348
  tool.setImageFocus(null, {
@@ -1352,6 +1350,13 @@ function createImageCommands(tool) {
1352
1350
  skipRender: true
1353
1351
  });
1354
1352
  }
1353
+ if (tool.isToolActive) {
1354
+ tool.workingItems = tool.cloneItems(next);
1355
+ tool.hasWorkingChanges = true;
1356
+ tool.updateImages();
1357
+ tool.emitWorkingChange(id);
1358
+ return;
1359
+ }
1355
1360
  tool.updateConfig(next);
1356
1361
  }
1357
1362
  }
@@ -1374,6 +1379,13 @@ function createImageCommands(tool) {
1374
1379
  syncCanvasSelection: true,
1375
1380
  skipRender: true
1376
1381
  });
1382
+ if (tool.isToolActive) {
1383
+ tool.workingItems = [];
1384
+ tool.hasWorkingChanges = true;
1385
+ tool.updateImages();
1386
+ tool.emitWorkingChange();
1387
+ return;
1388
+ }
1377
1389
  tool.updateConfig([]);
1378
1390
  }
1379
1391
  },
@@ -1382,11 +1394,19 @@ function createImageCommands(tool) {
1382
1394
  id: "bringToFront",
1383
1395
  title: "Bring Image to Front",
1384
1396
  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];
1397
+ const sourceItems = tool.isToolActive ? tool.workingItems : tool.items;
1398
+ const index = sourceItems.findIndex((item) => item.id === id);
1399
+ if (index !== -1 && index < sourceItems.length - 1) {
1400
+ const next = [...sourceItems];
1388
1401
  const [item] = next.splice(index, 1);
1389
1402
  next.push(item);
1403
+ if (tool.isToolActive) {
1404
+ tool.workingItems = tool.cloneItems(next);
1405
+ tool.hasWorkingChanges = true;
1406
+ tool.updateImages();
1407
+ tool.emitWorkingChange(id);
1408
+ return;
1409
+ }
1390
1410
  tool.updateConfig(next);
1391
1411
  }
1392
1412
  }
@@ -1396,11 +1416,19 @@ function createImageCommands(tool) {
1396
1416
  id: "sendToBack",
1397
1417
  title: "Send Image to Back",
1398
1418
  handler: (id) => {
1399
- const index = tool.items.findIndex((item) => item.id === id);
1419
+ const sourceItems = tool.isToolActive ? tool.workingItems : tool.items;
1420
+ const index = sourceItems.findIndex((item) => item.id === id);
1400
1421
  if (index > 0) {
1401
- const next = [...tool.items];
1422
+ const next = [...sourceItems];
1402
1423
  const [item] = next.splice(index, 1);
1403
1424
  next.unshift(item);
1425
+ if (tool.isToolActive) {
1426
+ tool.workingItems = tool.cloneItems(next);
1427
+ tool.hasWorkingChanges = true;
1428
+ tool.updateImages();
1429
+ tool.emitWorkingChange(id);
1430
+ return;
1431
+ }
1404
1432
  tool.updateConfig(next);
1405
1433
  }
1406
1434
  }
@@ -1532,10 +1560,127 @@ function createImageConfigurations() {
1532
1560
  type: "color",
1533
1561
  label: "Image Frame Outer Background",
1534
1562
  default: "#f5f5f5"
1563
+ },
1564
+ {
1565
+ id: "image.session.placementPolicy",
1566
+ type: "select",
1567
+ label: "Image Session Placement Policy",
1568
+ options: ["free", "warn", "strict"],
1569
+ default: "free"
1535
1570
  }
1536
1571
  ];
1537
1572
  }
1538
1573
 
1574
+ // src/extensions/image/imageOperations.ts
1575
+ function clampNormalizedAnchor(value) {
1576
+ return Math.max(-1, Math.min(2, value));
1577
+ }
1578
+ function toNormalizedAnchor(center, start, size) {
1579
+ return clampNormalizedAnchor((center - start) / Math.max(1, size));
1580
+ }
1581
+ function resolveAbsoluteScale(operation, area, source) {
1582
+ const widthScale = Math.max(1, area.width) / Math.max(1, source.width);
1583
+ const heightScale = Math.max(1, area.height) / Math.max(1, source.height);
1584
+ switch (operation.type) {
1585
+ case "cover":
1586
+ return Math.max(widthScale, heightScale);
1587
+ case "contain":
1588
+ return Math.min(widthScale, heightScale);
1589
+ case "maximizeWidth":
1590
+ return widthScale;
1591
+ case "maximizeHeight":
1592
+ return heightScale;
1593
+ default:
1594
+ return null;
1595
+ }
1596
+ }
1597
+ function resolveImageOperationArea(args) {
1598
+ const spec = args.area || { type: "frame" };
1599
+ if (spec.type === "custom") {
1600
+ return {
1601
+ width: Math.max(1, spec.width),
1602
+ height: Math.max(1, spec.height),
1603
+ centerX: spec.centerX,
1604
+ centerY: spec.centerY
1605
+ };
1606
+ }
1607
+ if (spec.type === "viewport") {
1608
+ return {
1609
+ width: Math.max(1, args.viewport.width),
1610
+ height: Math.max(1, args.viewport.height),
1611
+ centerX: args.viewport.left + args.viewport.width / 2,
1612
+ centerY: args.viewport.top + args.viewport.height / 2
1613
+ };
1614
+ }
1615
+ return {
1616
+ width: Math.max(1, args.frame.width),
1617
+ height: Math.max(1, args.frame.height),
1618
+ centerX: args.frame.left + args.frame.width / 2,
1619
+ centerY: args.frame.top + args.frame.height / 2
1620
+ };
1621
+ }
1622
+ function computeImageOperationUpdates(args) {
1623
+ const { frame, source, operation, area } = args;
1624
+ if (operation.type === "resetTransform") {
1625
+ return {
1626
+ scale: 1,
1627
+ left: 0.5,
1628
+ top: 0.5,
1629
+ angle: 0
1630
+ };
1631
+ }
1632
+ const left = toNormalizedAnchor(area.centerX, frame.left, frame.width);
1633
+ const top = toNormalizedAnchor(area.centerY, frame.top, frame.height);
1634
+ if (operation.type === "center") {
1635
+ return { left, top };
1636
+ }
1637
+ const absoluteScale = resolveAbsoluteScale(operation, area, source);
1638
+ const coverScale = getCoverScale(frame, source);
1639
+ return {
1640
+ scale: Math.max(0.05, (absoluteScale || coverScale) / coverScale),
1641
+ left,
1642
+ top
1643
+ };
1644
+ }
1645
+
1646
+ // src/extensions/image/imagePlacement.ts
1647
+ function toRadians(angle) {
1648
+ return angle * Math.PI / 180;
1649
+ }
1650
+ function validateImagePlacement(args) {
1651
+ const { frame, source, placement } = args;
1652
+ if (frame.width <= 0 || frame.height <= 0 || source.width <= 0 || source.height <= 0) {
1653
+ return { ok: true };
1654
+ }
1655
+ const coverScale = getCoverScale(frame, source);
1656
+ const imageWidth = source.width * coverScale * Math.max(0.05, Number(placement.scale || 1));
1657
+ const imageHeight = source.height * coverScale * Math.max(0.05, Number(placement.scale || 1));
1658
+ if (imageWidth <= 0 || imageHeight <= 0) {
1659
+ return { ok: true };
1660
+ }
1661
+ const centerX = frame.left + placement.left * frame.width;
1662
+ const centerY = frame.top + placement.top * frame.height;
1663
+ const halfWidth = imageWidth / 2;
1664
+ const halfHeight = imageHeight / 2;
1665
+ const radians = toRadians(placement.angle || 0);
1666
+ const cos = Math.cos(radians);
1667
+ const sin = Math.sin(radians);
1668
+ const frameCorners = [
1669
+ { x: frame.left, y: frame.top },
1670
+ { x: frame.left + frame.width, y: frame.top },
1671
+ { x: frame.left + frame.width, y: frame.top + frame.height },
1672
+ { x: frame.left, y: frame.top + frame.height }
1673
+ ];
1674
+ const coversFrame = frameCorners.every((corner) => {
1675
+ const dx = corner.x - centerX;
1676
+ const dy = corner.y - centerY;
1677
+ const localX = dx * cos + dy * sin;
1678
+ const localY = -dx * sin + dy * cos;
1679
+ return Math.abs(localX) <= halfWidth + 1e-6 && Math.abs(localY) <= halfHeight + 1e-6;
1680
+ });
1681
+ return { ok: coversFrame };
1682
+ }
1683
+
1539
1684
  // src/extensions/geometry.ts
1540
1685
  import paper from "paper";
1541
1686
 
@@ -2338,6 +2483,7 @@ var ImageTool = class {
2338
2483
  this.activeSnapX = null;
2339
2484
  this.activeSnapY = null;
2340
2485
  this.movingImageId = null;
2486
+ this.sessionNotice = null;
2341
2487
  this.hasRenderedSnapGuides = false;
2342
2488
  this.subscriptions = new SubscriptionBag();
2343
2489
  this.imageControlsByCapabilityKey = /* @__PURE__ */ new Map();
@@ -2537,7 +2683,10 @@ var ImageTool = class {
2537
2683
  this.updateImages();
2538
2684
  return;
2539
2685
  }
2540
- if (e.key.startsWith("size.") || e.key.startsWith("image.frame.") || e.key.startsWith("image.control.")) {
2686
+ if (e.key.startsWith("size.") || e.key.startsWith("image.frame.") || e.key.startsWith("image.session.") || e.key.startsWith("image.control.")) {
2687
+ if (e.key === "image.session.placementPolicy") {
2688
+ this.clearSessionNotice();
2689
+ }
2541
2690
  if (e.key.startsWith("image.control.")) {
2542
2691
  this.imageControlsByCapabilityKey.clear();
2543
2692
  }
@@ -2570,6 +2719,7 @@ var ImageTool = class {
2570
2719
  this.clearRenderedImages();
2571
2720
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
2572
2721
  this.renderProducerDisposable = void 0;
2722
+ this.emitImageStateChange();
2573
2723
  if (this.canvasService) {
2574
2724
  void this.canvasService.flushRenderFromProducers();
2575
2725
  this.canvasService = void 0;
@@ -2979,9 +3129,10 @@ var ImageTool = class {
2979
3129
  name: "Image",
2980
3130
  interaction: "session",
2981
3131
  commands: {
2982
- begin: "resetWorkingImages",
3132
+ begin: "imageSessionReset",
3133
+ validate: "validateImageSession",
2983
3134
  commit: "completeImages",
2984
- rollback: "resetWorkingImages"
3135
+ rollback: "imageSessionReset"
2985
3136
  },
2986
3137
  session: {
2987
3138
  autoBegin: true,
@@ -3015,6 +3166,56 @@ var ImageTool = class {
3015
3166
  cloneItems(items) {
3016
3167
  return this.normalizeItems((items || []).map((i) => ({ ...i })));
3017
3168
  }
3169
+ getViewItems() {
3170
+ return this.isToolActive ? this.workingItems : this.items;
3171
+ }
3172
+ getPlacementPolicy() {
3173
+ const policy = this.getConfig(
3174
+ "image.session.placementPolicy",
3175
+ "free"
3176
+ );
3177
+ return policy === "warn" || policy === "strict" ? policy : "free";
3178
+ }
3179
+ areSessionNoticesEqual(a, b) {
3180
+ if (!a && !b) return true;
3181
+ if (!a || !b) return false;
3182
+ return a.code === b.code && a.level === b.level && a.message === b.message && a.policy === b.policy && JSON.stringify(a.imageIds) === JSON.stringify(b.imageIds);
3183
+ }
3184
+ setSessionNotice(notice, options = {}) {
3185
+ var _a;
3186
+ if (this.areSessionNoticesEqual(this.sessionNotice, notice)) {
3187
+ return;
3188
+ }
3189
+ this.sessionNotice = notice;
3190
+ if (options.emit !== false) {
3191
+ (_a = this.context) == null ? void 0 : _a.eventBus.emit("image:session:notice", this.sessionNotice);
3192
+ this.emitImageStateChange();
3193
+ }
3194
+ }
3195
+ clearSessionNotice(options = {}) {
3196
+ this.setSessionNotice(null, options);
3197
+ }
3198
+ getImageViewState() {
3199
+ this.syncToolActiveFromWorkbench();
3200
+ const items = this.cloneItems(this.getViewItems());
3201
+ const focusedItem = this.focusedImageId == null ? null : items.find((item) => item.id === this.focusedImageId) || null;
3202
+ return {
3203
+ items,
3204
+ hasAnyImage: items.length > 0,
3205
+ focusedId: this.focusedImageId,
3206
+ focusedItem,
3207
+ isToolActive: this.isToolActive,
3208
+ isImageSelectionActive: this.isImageSelectionActive,
3209
+ hasWorkingChanges: this.hasWorkingChanges,
3210
+ source: this.isToolActive ? "working" : "committed",
3211
+ placementPolicy: this.getPlacementPolicy(),
3212
+ sessionNotice: this.sessionNotice
3213
+ };
3214
+ }
3215
+ emitImageStateChange() {
3216
+ var _a;
3217
+ (_a = this.context) == null ? void 0 : _a.eventBus.emit("image:state:change", this.getImageViewState());
3218
+ }
3018
3219
  emitWorkingChange(changedId = null) {
3019
3220
  var _a;
3020
3221
  (_a = this.context) == null ? void 0 : _a.eventBus.emit("image:working:change", {
@@ -3050,10 +3251,14 @@ var ImageTool = class {
3050
3251
  }
3051
3252
  if (!options.skipRender) {
3052
3253
  this.updateImages();
3254
+ } else {
3255
+ this.emitImageStateChange();
3053
3256
  }
3054
3257
  return { ok: true, id };
3055
3258
  }
3056
- async addImageEntry(url, options, fitOnAdd = true) {
3259
+ async addImageEntry(url, options, operation) {
3260
+ this.syncToolActiveFromWorkbench();
3261
+ this.clearSessionNotice({ emit: false });
3057
3262
  const id = this.generateId();
3058
3263
  const newItem = this.normalizeItem({
3059
3264
  id,
@@ -3061,13 +3266,20 @@ var ImageTool = class {
3061
3266
  opacity: 1,
3062
3267
  ...options
3063
3268
  });
3064
- const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
3065
3269
  const waitLoaded = this.waitImageLoaded(id, true);
3066
- this.updateConfig([...this.items, newItem]);
3067
- this.addItemToWorkingSessionIfNeeded(newItem, sessionDirtyBeforeAdd);
3270
+ if (this.isToolActive) {
3271
+ this.workingItems = this.cloneItems([...this.workingItems, newItem]);
3272
+ this.hasWorkingChanges = true;
3273
+ this.updateImages();
3274
+ this.emitWorkingChange(id);
3275
+ } else {
3276
+ this.updateConfig([...this.items, newItem]);
3277
+ }
3068
3278
  const loaded = await waitLoaded;
3069
- if (loaded && fitOnAdd) {
3070
- await this.fitImageToDefaultArea(id);
3279
+ if (loaded && operation) {
3280
+ await this.applyImageOperation(id, operation, {
3281
+ target: this.isToolActive ? "working" : "config"
3282
+ });
3071
3283
  }
3072
3284
  if (loaded) {
3073
3285
  this.setImageFocus(id);
@@ -3075,8 +3287,8 @@ var ImageTool = class {
3075
3287
  return id;
3076
3288
  }
3077
3289
  async upsertImageEntry(url, options = {}) {
3290
+ this.syncToolActiveFromWorkbench();
3078
3291
  const mode = options.mode || (options.id ? "replace" : "add");
3079
- const fitOnAdd = options.fitOnAdd !== false;
3080
3292
  if (mode === "replace") {
3081
3293
  if (!options.id) {
3082
3294
  throw new Error("replace-target-id-required");
@@ -3085,19 +3297,35 @@ var ImageTool = class {
3085
3297
  if (!this.hasImageItem(targetId)) {
3086
3298
  throw new Error("replace-target-not-found");
3087
3299
  }
3088
- await this.updateImageInConfig(targetId, { url });
3300
+ if (this.isToolActive) {
3301
+ const current = this.workingItems.find((item) => item.id === targetId) || this.items.find((item) => item.id === targetId);
3302
+ this.purgeSourceSizeCacheForItem(current);
3303
+ this.updateImageInWorking(targetId, {
3304
+ url,
3305
+ sourceUrl: url,
3306
+ committedUrl: void 0
3307
+ });
3308
+ } else {
3309
+ await this.updateImageInConfig(targetId, { url });
3310
+ }
3311
+ const loaded = await this.waitImageLoaded(targetId, true);
3312
+ if (loaded && options.operation) {
3313
+ await this.applyImageOperation(targetId, options.operation, {
3314
+ target: this.isToolActive ? "working" : "config"
3315
+ });
3316
+ }
3317
+ if (loaded) {
3318
+ this.setImageFocus(targetId);
3319
+ }
3089
3320
  return { id: targetId, mode: "replace" };
3090
3321
  }
3091
- const id = await this.addImageEntry(url, options.addOptions, fitOnAdd);
3322
+ const id = await this.addImageEntry(
3323
+ url,
3324
+ options.addOptions,
3325
+ options.operation
3326
+ );
3092
3327
  return { id, mode: "add" };
3093
3328
  }
3094
- addItemToWorkingSessionIfNeeded(item, sessionDirtyBeforeAdd) {
3095
- if (!sessionDirtyBeforeAdd || !this.isToolActive) return;
3096
- if (this.workingItems.some((existing) => existing.id === item.id)) return;
3097
- this.workingItems = this.cloneItems([...this.workingItems, item]);
3098
- this.updateImages();
3099
- this.emitWorkingChange(item.id);
3100
- }
3101
3329
  async updateImage(id, updates, options = {}) {
3102
3330
  this.syncToolActiveFromWorkbench();
3103
3331
  const target = options.target || "auto";
@@ -3133,6 +3361,7 @@ var ImageTool = class {
3133
3361
  }
3134
3362
  updateConfig(newItems, skipCanvasUpdate = false) {
3135
3363
  if (!this.context) return;
3364
+ this.clearSessionNotice({ emit: false });
3136
3365
  this.applyCommittedItems(newItems);
3137
3366
  runDeferredConfigUpdate(
3138
3367
  this,
@@ -3162,34 +3391,6 @@ var ImageTool = class {
3162
3391
  }
3163
3392
  return this.canvasService.toScreenRect(frame || this.getFrameRect());
3164
3393
  }
3165
- async resolveDefaultFitArea() {
3166
- if (!this.canvasService) return null;
3167
- const frame = this.getFrameRect();
3168
- if (frame.width <= 0 || frame.height <= 0) return null;
3169
- return {
3170
- width: Math.max(1, frame.width),
3171
- height: Math.max(1, frame.height),
3172
- left: frame.left + frame.width / 2,
3173
- top: frame.top + frame.height / 2
3174
- };
3175
- }
3176
- async fitImageToDefaultArea(id) {
3177
- if (!this.canvasService) return;
3178
- const area = await this.resolveDefaultFitArea();
3179
- if (area) {
3180
- await this.fitImageToArea(id, area);
3181
- return;
3182
- }
3183
- const viewport = this.canvasService.getSceneViewportRect();
3184
- const canvasW = Math.max(1, viewport.width || 0);
3185
- const canvasH = Math.max(1, viewport.height || 0);
3186
- await this.fitImageToArea(id, {
3187
- width: canvasW,
3188
- height: canvasH,
3189
- left: viewport.left + canvasW / 2,
3190
- top: viewport.top + canvasH / 2
3191
- });
3192
- }
3193
3394
  getImageObjects() {
3194
3395
  if (!this.canvasService) return [];
3195
3396
  return this.canvasService.canvas.getObjects().filter((obj) => {
@@ -3263,6 +3464,71 @@ var ImageTool = class {
3263
3464
  getCoverScale(frame, size) {
3264
3465
  return getCoverScale(frame, size);
3265
3466
  }
3467
+ resolvePlacementState(item) {
3468
+ var _a;
3469
+ return {
3470
+ left: Number.isFinite(item.left) ? item.left : 0.5,
3471
+ top: Number.isFinite(item.top) ? item.top : 0.5,
3472
+ scale: Math.max(0.05, (_a = item.scale) != null ? _a : 1),
3473
+ angle: Number.isFinite(item.angle) ? item.angle : 0
3474
+ };
3475
+ }
3476
+ async validatePlacementForItem(item) {
3477
+ const frame = this.getFrameRect();
3478
+ if (!frame.width || !frame.height) {
3479
+ return true;
3480
+ }
3481
+ const src = item.sourceUrl || item.url;
3482
+ if (!src) {
3483
+ return true;
3484
+ }
3485
+ const source = await this.resolveImageSourceSize(item.id, src);
3486
+ if (!source) {
3487
+ return true;
3488
+ }
3489
+ return validateImagePlacement({
3490
+ frame,
3491
+ source,
3492
+ placement: this.resolvePlacementState(item)
3493
+ }).ok;
3494
+ }
3495
+ async validateImageSession() {
3496
+ const policy = this.getPlacementPolicy();
3497
+ if (policy === "free") {
3498
+ this.clearSessionNotice();
3499
+ return { ok: true, policy };
3500
+ }
3501
+ const invalidImageIds = [];
3502
+ for (const item of this.workingItems) {
3503
+ const valid = await this.validatePlacementForItem(item);
3504
+ if (!valid) {
3505
+ invalidImageIds.push(item.id);
3506
+ }
3507
+ }
3508
+ if (!invalidImageIds.length) {
3509
+ this.clearSessionNotice();
3510
+ return { ok: true, policy };
3511
+ }
3512
+ const notice = {
3513
+ code: "image-outside-frame",
3514
+ level: policy === "strict" ? "error" : "warning",
3515
+ message: policy === "strict" ? "\u56FE\u7247\u4F4D\u7F6E\u4E0D\u80FD\u8D85\u51FA frame\uFF0C\u8BF7\u8C03\u6574\u540E\u518D\u63D0\u4EA4\u3002" : "\u56FE\u7247\u4F4D\u7F6E\u5DF2\u8D85\u51FA frame\uFF0C\u5EFA\u8BAE\u8C03\u6574\u540E\u518D\u63D0\u4EA4\u3002",
3516
+ imageIds: invalidImageIds,
3517
+ policy
3518
+ };
3519
+ this.setSessionNotice(notice);
3520
+ this.setImageFocus(invalidImageIds[0], {
3521
+ syncCanvasSelection: true,
3522
+ skipRender: true
3523
+ });
3524
+ return {
3525
+ ok: policy !== "strict",
3526
+ reason: notice.code,
3527
+ message: notice.message,
3528
+ imageIds: notice.imageIds,
3529
+ policy: notice.policy
3530
+ };
3531
+ }
3266
3532
  getFrameVisualConfig() {
3267
3533
  var _a, _b;
3268
3534
  const strokeStyleRaw = this.getConfig(
@@ -3520,14 +3786,43 @@ var ImageTool = class {
3520
3786
  isImageSelectionActive: this.isImageSelectionActive,
3521
3787
  focusedImageId: this.focusedImageId
3522
3788
  });
3789
+ this.emitImageStateChange();
3523
3790
  this.canvasService.requestRenderAll();
3524
3791
  }
3525
3792
  clampNormalized(value) {
3526
3793
  return Math.max(-1, Math.min(2, value));
3527
3794
  }
3795
+ async setImageTransform(id, updates, options = {}) {
3796
+ const next = {};
3797
+ if (Number.isFinite(updates.scale)) {
3798
+ next.scale = Math.max(0.05, Number(updates.scale));
3799
+ }
3800
+ if (Number.isFinite(updates.angle)) {
3801
+ next.angle = Number(updates.angle);
3802
+ }
3803
+ if (Number.isFinite(updates.left)) {
3804
+ next.left = this.clampNormalized(Number(updates.left));
3805
+ }
3806
+ if (Number.isFinite(updates.top)) {
3807
+ next.top = this.clampNormalized(Number(updates.top));
3808
+ }
3809
+ if (Number.isFinite(updates.opacity)) {
3810
+ next.opacity = Math.max(0, Math.min(1, Number(updates.opacity)));
3811
+ }
3812
+ if (!Object.keys(next).length) return;
3813
+ await this.updateImage(id, next, options);
3814
+ }
3815
+ resetImageSession() {
3816
+ this.clearSessionNotice({ emit: false });
3817
+ this.workingItems = this.cloneItems(this.items);
3818
+ this.hasWorkingChanges = false;
3819
+ this.updateImages();
3820
+ this.emitWorkingChange();
3821
+ }
3528
3822
  updateImageInWorking(id, updates) {
3529
3823
  const index = this.workingItems.findIndex((item) => item.id === id);
3530
3824
  if (index < 0) return;
3825
+ this.clearSessionNotice({ emit: false });
3531
3826
  const next = [...this.workingItems];
3532
3827
  next[index] = this.normalizeItem({ ...next[index], ...updates });
3533
3828
  this.workingItems = next;
@@ -3542,9 +3837,9 @@ var ImageTool = class {
3542
3837
  this.emitWorkingChange(id);
3543
3838
  }
3544
3839
  async updateImageInConfig(id, updates) {
3545
- var _a, _b, _c, _d;
3546
3840
  const index = this.items.findIndex((item) => item.id === id);
3547
3841
  if (index < 0) return;
3842
+ this.clearSessionNotice({ emit: false });
3548
3843
  const replacingSource = typeof updates.url === "string" && updates.url.length > 0;
3549
3844
  const next = [...this.items];
3550
3845
  const base = next[index];
@@ -3555,23 +3850,12 @@ var ImageTool = class {
3555
3850
  ...replacingSource ? {
3556
3851
  url: replacingUrl,
3557
3852
  sourceUrl: replacingUrl,
3558
- committedUrl: void 0,
3559
- scale: (_a = updates.scale) != null ? _a : 1,
3560
- angle: (_b = updates.angle) != null ? _b : 0,
3561
- left: (_c = updates.left) != null ? _c : 0.5,
3562
- top: (_d = updates.top) != null ? _d : 0.5
3853
+ committedUrl: void 0
3563
3854
  } : {}
3564
3855
  });
3565
3856
  this.updateConfig(next);
3566
3857
  if (replacingSource) {
3567
- this.debug("replace:image:begin", { id, replacingUrl });
3568
3858
  this.purgeSourceSizeCacheForItem(base);
3569
- const loaded = await this.waitImageLoaded(id, true);
3570
- this.debug("replace:image:loaded", { id, loaded });
3571
- if (loaded) {
3572
- await this.refitImageToFrame(id);
3573
- this.setImageFocus(id);
3574
- }
3575
3859
  }
3576
3860
  }
3577
3861
  waitImageLoaded(id, forceWait = false) {
@@ -3589,70 +3873,43 @@ var ImageTool = class {
3589
3873
  });
3590
3874
  });
3591
3875
  }
3592
- async refitImageToFrame(id) {
3876
+ async resolveImageSourceSize(id, src) {
3593
3877
  const obj = this.getImageObject(id);
3594
- if (!obj || !this.canvasService) return;
3595
- const current = this.items.find((item) => item.id === id);
3596
- if (!current) return;
3597
- const render = this.resolveRenderImageState(current);
3598
- this.rememberSourceSize(render.src, obj);
3599
- const source = this.getSourceSize(render.src, obj);
3600
- const frame = this.getFrameRect();
3601
- const coverScale = this.getCoverScale(frame, source);
3602
- const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
3603
- const zoom = Math.max(0.05, currentScale / coverScale);
3604
- const updated = {
3605
- scale: Number.isFinite(zoom) ? zoom : 1,
3606
- angle: 0,
3607
- left: 0.5,
3608
- top: 0.5
3609
- };
3610
- const index = this.items.findIndex((item) => item.id === id);
3611
- if (index < 0) return;
3612
- const next = [...this.items];
3613
- next[index] = this.normalizeItem({ ...next[index], ...updated });
3614
- this.updateConfig(next);
3615
- this.workingItems = this.cloneItems(next);
3616
- this.hasWorkingChanges = false;
3617
- this.updateImages();
3618
- this.emitWorkingChange(id);
3878
+ if (obj) {
3879
+ this.rememberSourceSize(src, obj);
3880
+ }
3881
+ const ensured = await this.ensureSourceSize(src);
3882
+ if (ensured) return ensured;
3883
+ if (!obj) return null;
3884
+ const width = Number((obj == null ? void 0 : obj.width) || 0);
3885
+ const height = Number((obj == null ? void 0 : obj.height) || 0);
3886
+ if (width <= 0 || height <= 0) return null;
3887
+ return { width, height };
3619
3888
  }
3620
- async fitImageToArea(id, area) {
3621
- var _a, _b;
3889
+ async applyImageOperation(id, operation, options = {}) {
3622
3890
  if (!this.canvasService) return;
3623
- const loaded = await this.waitImageLoaded(id, false);
3624
- if (!loaded) return;
3625
- const obj = this.getImageObject(id);
3626
- if (!obj) return;
3627
- const renderItems = this.isToolActive ? this.workingItems : this.items;
3891
+ this.syncToolActiveFromWorkbench();
3892
+ const target = options.target || "auto";
3893
+ const renderItems = target === "working" || target === "auto" && this.isToolActive ? this.workingItems : this.items;
3628
3894
  const current = renderItems.find((item) => item.id === id);
3629
3895
  if (!current) return;
3630
3896
  const render = this.resolveRenderImageState(current);
3631
- this.rememberSourceSize(render.src, obj);
3632
- const source = this.getSourceSize(render.src, obj);
3897
+ const source = await this.resolveImageSourceSize(id, render.src);
3898
+ if (!source) return;
3633
3899
  const frame = this.getFrameRect();
3634
- const baseCover = this.getCoverScale(frame, source);
3635
- const desiredScale = Math.max(
3636
- Math.max(1, area.width) / Math.max(1, source.width),
3637
- Math.max(1, area.height) / Math.max(1, source.height)
3638
- );
3639
3900
  const viewport = this.canvasService.getSceneViewportRect();
3640
- const canvasW = viewport.width || 1;
3641
- const canvasH = viewport.height || 1;
3642
- const areaLeftInput = (_a = area.left) != null ? _a : 0.5;
3643
- const areaTopInput = (_b = area.top) != null ? _b : 0.5;
3644
- const areaLeftPx = areaLeftInput <= 1.5 ? viewport.left + areaLeftInput * canvasW : areaLeftInput;
3645
- const areaTopPx = areaTopInput <= 1.5 ? viewport.top + areaTopInput * canvasH : areaTopInput;
3646
- const updates = {
3647
- scale: Math.max(0.05, desiredScale / baseCover),
3648
- left: this.clampNormalized(
3649
- (areaLeftPx - frame.left) / Math.max(1, frame.width)
3650
- ),
3651
- top: this.clampNormalized(
3652
- (areaTopPx - frame.top) / Math.max(1, frame.height)
3653
- )
3654
- };
3655
- if (this.isToolActive) {
3901
+ const area = operation.type === "resetTransform" ? resolveImageOperationArea({ frame, viewport }) : resolveImageOperationArea({
3902
+ frame,
3903
+ viewport,
3904
+ area: operation.area
3905
+ });
3906
+ const updates = computeImageOperationUpdates({
3907
+ frame,
3908
+ source,
3909
+ operation,
3910
+ area
3911
+ });
3912
+ if (target === "working" || target === "auto" && this.isToolActive) {
3656
3913
  this.updateImageInWorking(id, updates);
3657
3914
  return;
3658
3915
  }
@@ -3691,11 +3948,42 @@ var ImageTool = class {
3691
3948
  }
3692
3949
  }
3693
3950
  this.hasWorkingChanges = false;
3951
+ this.clearSessionNotice({ emit: false });
3694
3952
  this.workingItems = this.cloneItems(next);
3695
3953
  this.updateConfig(next);
3696
3954
  this.emitWorkingChange(this.focusedImageId);
3697
3955
  return { ok: true };
3698
3956
  }
3957
+ async completeImageSession() {
3958
+ var _a, _b, _c;
3959
+ const sessionState = (_a = this.context) == null ? void 0 : _a.services.get("ToolSessionService");
3960
+ const workbench = (_b = this.context) == null ? void 0 : _b.services.get("WorkbenchService");
3961
+ console.info("[ImageTool] completeImageSession:start", {
3962
+ activeToolId: (_c = workbench == null ? void 0 : workbench.activeToolId) != null ? _c : null,
3963
+ isToolActive: this.isToolActive,
3964
+ dirtyBeforeComplete: this.hasWorkingChanges,
3965
+ workingCount: this.workingItems.length,
3966
+ committedCount: this.items.length,
3967
+ sessionDirty: sessionState == null ? void 0 : sessionState.isDirty(this.id)
3968
+ });
3969
+ const validation = await this.validateImageSession();
3970
+ if (!validation.ok) {
3971
+ console.warn("[ImageTool] completeImageSession:validation-failed", {
3972
+ validation,
3973
+ dirtyAfterValidation: this.hasWorkingChanges
3974
+ });
3975
+ return validation;
3976
+ }
3977
+ const result = await this.commitWorkingImagesAsCropped();
3978
+ console.info("[ImageTool] completeImageSession:done", {
3979
+ result,
3980
+ dirtyAfterComplete: this.hasWorkingChanges,
3981
+ workingCount: this.workingItems.length,
3982
+ committedCount: this.items.length,
3983
+ sessionDirty: sessionState == null ? void 0 : sessionState.isDirty(this.id)
3984
+ });
3985
+ return result;
3986
+ }
3699
3987
  async exportCroppedImageByIds(imageIds, options) {
3700
3988
  var _a, _b, _c;
3701
3989
  if (!this.canvasService) {
@@ -3783,6 +4071,11 @@ var ImageTool = class {
3783
4071
  }
3784
4072
  };
3785
4073
 
4074
+ // src/extensions/image/model.ts
4075
+ function hasAnyImageInViewState(state) {
4076
+ return Boolean(state == null ? void 0 : state.hasAnyImage);
4077
+ }
4078
+
3786
4079
  // src/extensions/size/SizeTool.ts
3787
4080
  import {
3788
4081
  ContributionPointIds as ContributionPointIds3
@@ -9728,6 +10021,7 @@ export {
9728
10021
  ViewportSystem,
9729
10022
  WhiteInkTool,
9730
10023
  getCoverScale as computeImageCoverScale,
10024
+ computeImageOperationUpdates,
9731
10025
  getCoverScale as computeWhiteInkCoverScale,
9732
10026
  createDefaultDielineState,
9733
10027
  createDielineCommands,
@@ -9737,5 +10031,7 @@ export {
9737
10031
  createWhiteInkCommands,
9738
10032
  createWhiteInkConfigurations,
9739
10033
  evaluateVisibilityExpr,
9740
- readDielineState
10034
+ hasAnyImageInViewState,
10035
+ readDielineState,
10036
+ resolveImageOperationArea
9741
10037
  };