@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.js CHANGED
@@ -1074,6 +1074,7 @@ __export(index_exports, {
1074
1074
  ViewportSystem: () => ViewportSystem,
1075
1075
  WhiteInkTool: () => WhiteInkTool,
1076
1076
  computeImageCoverScale: () => getCoverScale,
1077
+ computeImageOperationUpdates: () => computeImageOperationUpdates,
1077
1078
  computeWhiteInkCoverScale: () => getCoverScale,
1078
1079
  createDefaultDielineState: () => createDefaultDielineState,
1079
1080
  createDielineCommands: () => createDielineCommands,
@@ -1083,7 +1084,9 @@ __export(index_exports, {
1083
1084
  createWhiteInkCommands: () => createWhiteInkCommands,
1084
1085
  createWhiteInkConfigurations: () => createWhiteInkConfigurations,
1085
1086
  evaluateVisibilityExpr: () => evaluateVisibilityExpr,
1086
- readDielineState: () => readDielineState
1087
+ hasAnyImageInViewState: () => hasAnyImageInViewState,
1088
+ readDielineState: () => readDielineState,
1089
+ resolveImageOperationArea: () => resolveImageOperationArea
1087
1090
  });
1088
1091
  module.exports = __toCommonJS(index_exports);
1089
1092
 
@@ -2349,30 +2352,43 @@ function createImageCommands(tool) {
2349
2352
  }
2350
2353
  },
2351
2354
  {
2352
- command: "getWorkingImages",
2353
- id: "getWorkingImages",
2354
- title: "Get Working Images",
2355
+ command: "applyImageOperation",
2356
+ id: "applyImageOperation",
2357
+ title: "Apply Image Operation",
2358
+ handler: async (id, operation, options = {}) => {
2359
+ await tool.applyImageOperation(id, operation, options);
2360
+ }
2361
+ },
2362
+ {
2363
+ command: "getImageViewState",
2364
+ id: "getImageViewState",
2365
+ title: "Get Image View State",
2355
2366
  handler: () => {
2356
- return tool.cloneItems(tool.workingItems);
2367
+ return tool.getImageViewState();
2357
2368
  }
2358
2369
  },
2359
2370
  {
2360
- command: "setWorkingImage",
2361
- id: "setWorkingImage",
2362
- title: "Set Working Image",
2363
- handler: (id, updates) => {
2364
- tool.updateImageInWorking(id, updates);
2371
+ command: "setImageTransform",
2372
+ id: "setImageTransform",
2373
+ title: "Set Image Transform",
2374
+ handler: async (id, updates, options = {}) => {
2375
+ await tool.setImageTransform(id, updates, options);
2365
2376
  }
2366
2377
  },
2367
2378
  {
2368
- command: "resetWorkingImages",
2369
- id: "resetWorkingImages",
2370
- title: "Reset Working Images",
2379
+ command: "imageSessionReset",
2380
+ id: "imageSessionReset",
2381
+ title: "Reset Image Session",
2371
2382
  handler: () => {
2372
- tool.workingItems = tool.cloneItems(tool.items);
2373
- tool.hasWorkingChanges = false;
2374
- tool.updateImages();
2375
- tool.emitWorkingChange();
2383
+ tool.resetImageSession();
2384
+ }
2385
+ },
2386
+ {
2387
+ command: "validateImageSession",
2388
+ id: "validateImageSession",
2389
+ title: "Validate Image Session",
2390
+ handler: async () => {
2391
+ return await tool.validateImageSession();
2376
2392
  }
2377
2393
  },
2378
2394
  {
@@ -2380,7 +2396,7 @@ function createImageCommands(tool) {
2380
2396
  id: "completeImages",
2381
2397
  title: "Complete Images",
2382
2398
  handler: async () => {
2383
- return await tool.commitWorkingImagesAsCropped();
2399
+ return await tool.completeImageSession();
2384
2400
  }
2385
2401
  },
2386
2402
  {
@@ -2391,22 +2407,6 @@ function createImageCommands(tool) {
2391
2407
  return await tool.exportUserCroppedImage(options);
2392
2408
  }
2393
2409
  },
2394
- {
2395
- command: "fitImageToArea",
2396
- id: "fitImageToArea",
2397
- title: "Fit Image to Area",
2398
- handler: async (id, area) => {
2399
- await tool.fitImageToArea(id, area);
2400
- }
2401
- },
2402
- {
2403
- command: "fitImageToDefaultArea",
2404
- id: "fitImageToDefaultArea",
2405
- title: "Fit Image to Default Area",
2406
- handler: async (id) => {
2407
- await tool.fitImageToDefaultArea(id);
2408
- }
2409
- },
2410
2410
  {
2411
2411
  command: "focusImage",
2412
2412
  id: "focusImage",
@@ -2420,9 +2420,10 @@ function createImageCommands(tool) {
2420
2420
  id: "removeImage",
2421
2421
  title: "Remove Image",
2422
2422
  handler: (id) => {
2423
- const removed = tool.items.find((item) => item.id === id);
2424
- const next = tool.items.filter((item) => item.id !== id);
2425
- if (next.length !== tool.items.length) {
2423
+ const sourceItems = tool.isToolActive ? tool.workingItems : tool.items;
2424
+ const removed = sourceItems.find((item) => item.id === id);
2425
+ const next = sourceItems.filter((item) => item.id !== id);
2426
+ if (next.length !== sourceItems.length) {
2426
2427
  tool.purgeSourceSizeCacheForItem(removed);
2427
2428
  if (tool.focusedImageId === id) {
2428
2429
  tool.setImageFocus(null, {
@@ -2430,6 +2431,13 @@ function createImageCommands(tool) {
2430
2431
  skipRender: true
2431
2432
  });
2432
2433
  }
2434
+ if (tool.isToolActive) {
2435
+ tool.workingItems = tool.cloneItems(next);
2436
+ tool.hasWorkingChanges = true;
2437
+ tool.updateImages();
2438
+ tool.emitWorkingChange(id);
2439
+ return;
2440
+ }
2433
2441
  tool.updateConfig(next);
2434
2442
  }
2435
2443
  }
@@ -2452,6 +2460,13 @@ function createImageCommands(tool) {
2452
2460
  syncCanvasSelection: true,
2453
2461
  skipRender: true
2454
2462
  });
2463
+ if (tool.isToolActive) {
2464
+ tool.workingItems = [];
2465
+ tool.hasWorkingChanges = true;
2466
+ tool.updateImages();
2467
+ tool.emitWorkingChange();
2468
+ return;
2469
+ }
2455
2470
  tool.updateConfig([]);
2456
2471
  }
2457
2472
  },
@@ -2460,11 +2475,19 @@ function createImageCommands(tool) {
2460
2475
  id: "bringToFront",
2461
2476
  title: "Bring Image to Front",
2462
2477
  handler: (id) => {
2463
- const index = tool.items.findIndex((item) => item.id === id);
2464
- if (index !== -1 && index < tool.items.length - 1) {
2465
- const next = [...tool.items];
2478
+ const sourceItems = tool.isToolActive ? tool.workingItems : tool.items;
2479
+ const index = sourceItems.findIndex((item) => item.id === id);
2480
+ if (index !== -1 && index < sourceItems.length - 1) {
2481
+ const next = [...sourceItems];
2466
2482
  const [item] = next.splice(index, 1);
2467
2483
  next.push(item);
2484
+ if (tool.isToolActive) {
2485
+ tool.workingItems = tool.cloneItems(next);
2486
+ tool.hasWorkingChanges = true;
2487
+ tool.updateImages();
2488
+ tool.emitWorkingChange(id);
2489
+ return;
2490
+ }
2468
2491
  tool.updateConfig(next);
2469
2492
  }
2470
2493
  }
@@ -2474,11 +2497,19 @@ function createImageCommands(tool) {
2474
2497
  id: "sendToBack",
2475
2498
  title: "Send Image to Back",
2476
2499
  handler: (id) => {
2477
- const index = tool.items.findIndex((item) => item.id === id);
2500
+ const sourceItems = tool.isToolActive ? tool.workingItems : tool.items;
2501
+ const index = sourceItems.findIndex((item) => item.id === id);
2478
2502
  if (index > 0) {
2479
- const next = [...tool.items];
2503
+ const next = [...sourceItems];
2480
2504
  const [item] = next.splice(index, 1);
2481
2505
  next.unshift(item);
2506
+ if (tool.isToolActive) {
2507
+ tool.workingItems = tool.cloneItems(next);
2508
+ tool.hasWorkingChanges = true;
2509
+ tool.updateImages();
2510
+ tool.emitWorkingChange(id);
2511
+ return;
2512
+ }
2482
2513
  tool.updateConfig(next);
2483
2514
  }
2484
2515
  }
@@ -2610,10 +2641,127 @@ function createImageConfigurations() {
2610
2641
  type: "color",
2611
2642
  label: "Image Frame Outer Background",
2612
2643
  default: "#f5f5f5"
2644
+ },
2645
+ {
2646
+ id: "image.session.placementPolicy",
2647
+ type: "select",
2648
+ label: "Image Session Placement Policy",
2649
+ options: ["free", "warn", "strict"],
2650
+ default: "free"
2613
2651
  }
2614
2652
  ];
2615
2653
  }
2616
2654
 
2655
+ // src/extensions/image/imageOperations.ts
2656
+ function clampNormalizedAnchor(value) {
2657
+ return Math.max(-1, Math.min(2, value));
2658
+ }
2659
+ function toNormalizedAnchor(center, start, size) {
2660
+ return clampNormalizedAnchor((center - start) / Math.max(1, size));
2661
+ }
2662
+ function resolveAbsoluteScale(operation, area, source) {
2663
+ const widthScale = Math.max(1, area.width) / Math.max(1, source.width);
2664
+ const heightScale = Math.max(1, area.height) / Math.max(1, source.height);
2665
+ switch (operation.type) {
2666
+ case "cover":
2667
+ return Math.max(widthScale, heightScale);
2668
+ case "contain":
2669
+ return Math.min(widthScale, heightScale);
2670
+ case "maximizeWidth":
2671
+ return widthScale;
2672
+ case "maximizeHeight":
2673
+ return heightScale;
2674
+ default:
2675
+ return null;
2676
+ }
2677
+ }
2678
+ function resolveImageOperationArea(args) {
2679
+ const spec = args.area || { type: "frame" };
2680
+ if (spec.type === "custom") {
2681
+ return {
2682
+ width: Math.max(1, spec.width),
2683
+ height: Math.max(1, spec.height),
2684
+ centerX: spec.centerX,
2685
+ centerY: spec.centerY
2686
+ };
2687
+ }
2688
+ if (spec.type === "viewport") {
2689
+ return {
2690
+ width: Math.max(1, args.viewport.width),
2691
+ height: Math.max(1, args.viewport.height),
2692
+ centerX: args.viewport.left + args.viewport.width / 2,
2693
+ centerY: args.viewport.top + args.viewport.height / 2
2694
+ };
2695
+ }
2696
+ return {
2697
+ width: Math.max(1, args.frame.width),
2698
+ height: Math.max(1, args.frame.height),
2699
+ centerX: args.frame.left + args.frame.width / 2,
2700
+ centerY: args.frame.top + args.frame.height / 2
2701
+ };
2702
+ }
2703
+ function computeImageOperationUpdates(args) {
2704
+ const { frame, source, operation, area } = args;
2705
+ if (operation.type === "resetTransform") {
2706
+ return {
2707
+ scale: 1,
2708
+ left: 0.5,
2709
+ top: 0.5,
2710
+ angle: 0
2711
+ };
2712
+ }
2713
+ const left = toNormalizedAnchor(area.centerX, frame.left, frame.width);
2714
+ const top = toNormalizedAnchor(area.centerY, frame.top, frame.height);
2715
+ if (operation.type === "center") {
2716
+ return { left, top };
2717
+ }
2718
+ const absoluteScale = resolveAbsoluteScale(operation, area, source);
2719
+ const coverScale = getCoverScale(frame, source);
2720
+ return {
2721
+ scale: Math.max(0.05, (absoluteScale || coverScale) / coverScale),
2722
+ left,
2723
+ top
2724
+ };
2725
+ }
2726
+
2727
+ // src/extensions/image/imagePlacement.ts
2728
+ function toRadians(angle) {
2729
+ return angle * Math.PI / 180;
2730
+ }
2731
+ function validateImagePlacement(args) {
2732
+ const { frame, source, placement } = args;
2733
+ if (frame.width <= 0 || frame.height <= 0 || source.width <= 0 || source.height <= 0) {
2734
+ return { ok: true };
2735
+ }
2736
+ const coverScale = getCoverScale(frame, source);
2737
+ const imageWidth = source.width * coverScale * Math.max(0.05, Number(placement.scale || 1));
2738
+ const imageHeight = source.height * coverScale * Math.max(0.05, Number(placement.scale || 1));
2739
+ if (imageWidth <= 0 || imageHeight <= 0) {
2740
+ return { ok: true };
2741
+ }
2742
+ const centerX = frame.left + placement.left * frame.width;
2743
+ const centerY = frame.top + placement.top * frame.height;
2744
+ const halfWidth = imageWidth / 2;
2745
+ const halfHeight = imageHeight / 2;
2746
+ const radians = toRadians(placement.angle || 0);
2747
+ const cos = Math.cos(radians);
2748
+ const sin = Math.sin(radians);
2749
+ const frameCorners = [
2750
+ { x: frame.left, y: frame.top },
2751
+ { x: frame.left + frame.width, y: frame.top },
2752
+ { x: frame.left + frame.width, y: frame.top + frame.height },
2753
+ { x: frame.left, y: frame.top + frame.height }
2754
+ ];
2755
+ const coversFrame = frameCorners.every((corner) => {
2756
+ const dx = corner.x - centerX;
2757
+ const dy = corner.y - centerY;
2758
+ const localX = dx * cos + dy * sin;
2759
+ const localY = -dx * sin + dy * cos;
2760
+ return Math.abs(localX) <= halfWidth + 1e-6 && Math.abs(localY) <= halfHeight + 1e-6;
2761
+ });
2762
+ return { ok: coversFrame };
2763
+ }
2764
+
2617
2765
  // src/extensions/geometry.ts
2618
2766
  var import_paper = __toESM(require("paper"));
2619
2767
 
@@ -3416,6 +3564,7 @@ var ImageTool = class {
3416
3564
  this.activeSnapX = null;
3417
3565
  this.activeSnapY = null;
3418
3566
  this.movingImageId = null;
3567
+ this.sessionNotice = null;
3419
3568
  this.hasRenderedSnapGuides = false;
3420
3569
  this.subscriptions = new SubscriptionBag();
3421
3570
  this.imageControlsByCapabilityKey = /* @__PURE__ */ new Map();
@@ -3615,7 +3764,10 @@ var ImageTool = class {
3615
3764
  this.updateImages();
3616
3765
  return;
3617
3766
  }
3618
- if (e.key.startsWith("size.") || e.key.startsWith("image.frame.") || e.key.startsWith("image.control.")) {
3767
+ if (e.key.startsWith("size.") || e.key.startsWith("image.frame.") || e.key.startsWith("image.session.") || e.key.startsWith("image.control.")) {
3768
+ if (e.key === "image.session.placementPolicy") {
3769
+ this.clearSessionNotice();
3770
+ }
3619
3771
  if (e.key.startsWith("image.control.")) {
3620
3772
  this.imageControlsByCapabilityKey.clear();
3621
3773
  }
@@ -3648,6 +3800,7 @@ var ImageTool = class {
3648
3800
  this.clearRenderedImages();
3649
3801
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
3650
3802
  this.renderProducerDisposable = void 0;
3803
+ this.emitImageStateChange();
3651
3804
  if (this.canvasService) {
3652
3805
  void this.canvasService.flushRenderFromProducers();
3653
3806
  this.canvasService = void 0;
@@ -4057,9 +4210,10 @@ var ImageTool = class {
4057
4210
  name: "Image",
4058
4211
  interaction: "session",
4059
4212
  commands: {
4060
- begin: "resetWorkingImages",
4213
+ begin: "imageSessionReset",
4214
+ validate: "validateImageSession",
4061
4215
  commit: "completeImages",
4062
- rollback: "resetWorkingImages"
4216
+ rollback: "imageSessionReset"
4063
4217
  },
4064
4218
  session: {
4065
4219
  autoBegin: true,
@@ -4093,6 +4247,56 @@ var ImageTool = class {
4093
4247
  cloneItems(items) {
4094
4248
  return this.normalizeItems((items || []).map((i) => ({ ...i })));
4095
4249
  }
4250
+ getViewItems() {
4251
+ return this.isToolActive ? this.workingItems : this.items;
4252
+ }
4253
+ getPlacementPolicy() {
4254
+ const policy = this.getConfig(
4255
+ "image.session.placementPolicy",
4256
+ "free"
4257
+ );
4258
+ return policy === "warn" || policy === "strict" ? policy : "free";
4259
+ }
4260
+ areSessionNoticesEqual(a, b) {
4261
+ if (!a && !b) return true;
4262
+ if (!a || !b) return false;
4263
+ 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);
4264
+ }
4265
+ setSessionNotice(notice, options = {}) {
4266
+ var _a;
4267
+ if (this.areSessionNoticesEqual(this.sessionNotice, notice)) {
4268
+ return;
4269
+ }
4270
+ this.sessionNotice = notice;
4271
+ if (options.emit !== false) {
4272
+ (_a = this.context) == null ? void 0 : _a.eventBus.emit("image:session:notice", this.sessionNotice);
4273
+ this.emitImageStateChange();
4274
+ }
4275
+ }
4276
+ clearSessionNotice(options = {}) {
4277
+ this.setSessionNotice(null, options);
4278
+ }
4279
+ getImageViewState() {
4280
+ this.syncToolActiveFromWorkbench();
4281
+ const items = this.cloneItems(this.getViewItems());
4282
+ const focusedItem = this.focusedImageId == null ? null : items.find((item) => item.id === this.focusedImageId) || null;
4283
+ return {
4284
+ items,
4285
+ hasAnyImage: items.length > 0,
4286
+ focusedId: this.focusedImageId,
4287
+ focusedItem,
4288
+ isToolActive: this.isToolActive,
4289
+ isImageSelectionActive: this.isImageSelectionActive,
4290
+ hasWorkingChanges: this.hasWorkingChanges,
4291
+ source: this.isToolActive ? "working" : "committed",
4292
+ placementPolicy: this.getPlacementPolicy(),
4293
+ sessionNotice: this.sessionNotice
4294
+ };
4295
+ }
4296
+ emitImageStateChange() {
4297
+ var _a;
4298
+ (_a = this.context) == null ? void 0 : _a.eventBus.emit("image:state:change", this.getImageViewState());
4299
+ }
4096
4300
  emitWorkingChange(changedId = null) {
4097
4301
  var _a;
4098
4302
  (_a = this.context) == null ? void 0 : _a.eventBus.emit("image:working:change", {
@@ -4128,10 +4332,14 @@ var ImageTool = class {
4128
4332
  }
4129
4333
  if (!options.skipRender) {
4130
4334
  this.updateImages();
4335
+ } else {
4336
+ this.emitImageStateChange();
4131
4337
  }
4132
4338
  return { ok: true, id };
4133
4339
  }
4134
- async addImageEntry(url, options, fitOnAdd = true) {
4340
+ async addImageEntry(url, options, operation) {
4341
+ this.syncToolActiveFromWorkbench();
4342
+ this.clearSessionNotice({ emit: false });
4135
4343
  const id = this.generateId();
4136
4344
  const newItem = this.normalizeItem({
4137
4345
  id,
@@ -4139,13 +4347,20 @@ var ImageTool = class {
4139
4347
  opacity: 1,
4140
4348
  ...options
4141
4349
  });
4142
- const sessionDirtyBeforeAdd = this.isToolActive && this.hasWorkingChanges;
4143
4350
  const waitLoaded = this.waitImageLoaded(id, true);
4144
- this.updateConfig([...this.items, newItem]);
4145
- this.addItemToWorkingSessionIfNeeded(newItem, sessionDirtyBeforeAdd);
4351
+ if (this.isToolActive) {
4352
+ this.workingItems = this.cloneItems([...this.workingItems, newItem]);
4353
+ this.hasWorkingChanges = true;
4354
+ this.updateImages();
4355
+ this.emitWorkingChange(id);
4356
+ } else {
4357
+ this.updateConfig([...this.items, newItem]);
4358
+ }
4146
4359
  const loaded = await waitLoaded;
4147
- if (loaded && fitOnAdd) {
4148
- await this.fitImageToDefaultArea(id);
4360
+ if (loaded && operation) {
4361
+ await this.applyImageOperation(id, operation, {
4362
+ target: this.isToolActive ? "working" : "config"
4363
+ });
4149
4364
  }
4150
4365
  if (loaded) {
4151
4366
  this.setImageFocus(id);
@@ -4153,8 +4368,8 @@ var ImageTool = class {
4153
4368
  return id;
4154
4369
  }
4155
4370
  async upsertImageEntry(url, options = {}) {
4371
+ this.syncToolActiveFromWorkbench();
4156
4372
  const mode = options.mode || (options.id ? "replace" : "add");
4157
- const fitOnAdd = options.fitOnAdd !== false;
4158
4373
  if (mode === "replace") {
4159
4374
  if (!options.id) {
4160
4375
  throw new Error("replace-target-id-required");
@@ -4163,19 +4378,35 @@ var ImageTool = class {
4163
4378
  if (!this.hasImageItem(targetId)) {
4164
4379
  throw new Error("replace-target-not-found");
4165
4380
  }
4166
- await this.updateImageInConfig(targetId, { url });
4381
+ if (this.isToolActive) {
4382
+ const current = this.workingItems.find((item) => item.id === targetId) || this.items.find((item) => item.id === targetId);
4383
+ this.purgeSourceSizeCacheForItem(current);
4384
+ this.updateImageInWorking(targetId, {
4385
+ url,
4386
+ sourceUrl: url,
4387
+ committedUrl: void 0
4388
+ });
4389
+ } else {
4390
+ await this.updateImageInConfig(targetId, { url });
4391
+ }
4392
+ const loaded = await this.waitImageLoaded(targetId, true);
4393
+ if (loaded && options.operation) {
4394
+ await this.applyImageOperation(targetId, options.operation, {
4395
+ target: this.isToolActive ? "working" : "config"
4396
+ });
4397
+ }
4398
+ if (loaded) {
4399
+ this.setImageFocus(targetId);
4400
+ }
4167
4401
  return { id: targetId, mode: "replace" };
4168
4402
  }
4169
- const id = await this.addImageEntry(url, options.addOptions, fitOnAdd);
4403
+ const id = await this.addImageEntry(
4404
+ url,
4405
+ options.addOptions,
4406
+ options.operation
4407
+ );
4170
4408
  return { id, mode: "add" };
4171
4409
  }
4172
- addItemToWorkingSessionIfNeeded(item, sessionDirtyBeforeAdd) {
4173
- if (!sessionDirtyBeforeAdd || !this.isToolActive) return;
4174
- if (this.workingItems.some((existing) => existing.id === item.id)) return;
4175
- this.workingItems = this.cloneItems([...this.workingItems, item]);
4176
- this.updateImages();
4177
- this.emitWorkingChange(item.id);
4178
- }
4179
4410
  async updateImage(id, updates, options = {}) {
4180
4411
  this.syncToolActiveFromWorkbench();
4181
4412
  const target = options.target || "auto";
@@ -4211,6 +4442,7 @@ var ImageTool = class {
4211
4442
  }
4212
4443
  updateConfig(newItems, skipCanvasUpdate = false) {
4213
4444
  if (!this.context) return;
4445
+ this.clearSessionNotice({ emit: false });
4214
4446
  this.applyCommittedItems(newItems);
4215
4447
  runDeferredConfigUpdate(
4216
4448
  this,
@@ -4240,34 +4472,6 @@ var ImageTool = class {
4240
4472
  }
4241
4473
  return this.canvasService.toScreenRect(frame || this.getFrameRect());
4242
4474
  }
4243
- async resolveDefaultFitArea() {
4244
- if (!this.canvasService) return null;
4245
- const frame = this.getFrameRect();
4246
- if (frame.width <= 0 || frame.height <= 0) return null;
4247
- return {
4248
- width: Math.max(1, frame.width),
4249
- height: Math.max(1, frame.height),
4250
- left: frame.left + frame.width / 2,
4251
- top: frame.top + frame.height / 2
4252
- };
4253
- }
4254
- async fitImageToDefaultArea(id) {
4255
- if (!this.canvasService) return;
4256
- const area = await this.resolveDefaultFitArea();
4257
- if (area) {
4258
- await this.fitImageToArea(id, area);
4259
- return;
4260
- }
4261
- const viewport = this.canvasService.getSceneViewportRect();
4262
- const canvasW = Math.max(1, viewport.width || 0);
4263
- const canvasH = Math.max(1, viewport.height || 0);
4264
- await this.fitImageToArea(id, {
4265
- width: canvasW,
4266
- height: canvasH,
4267
- left: viewport.left + canvasW / 2,
4268
- top: viewport.top + canvasH / 2
4269
- });
4270
- }
4271
4475
  getImageObjects() {
4272
4476
  if (!this.canvasService) return [];
4273
4477
  return this.canvasService.canvas.getObjects().filter((obj) => {
@@ -4341,6 +4545,71 @@ var ImageTool = class {
4341
4545
  getCoverScale(frame, size) {
4342
4546
  return getCoverScale(frame, size);
4343
4547
  }
4548
+ resolvePlacementState(item) {
4549
+ var _a;
4550
+ return {
4551
+ left: Number.isFinite(item.left) ? item.left : 0.5,
4552
+ top: Number.isFinite(item.top) ? item.top : 0.5,
4553
+ scale: Math.max(0.05, (_a = item.scale) != null ? _a : 1),
4554
+ angle: Number.isFinite(item.angle) ? item.angle : 0
4555
+ };
4556
+ }
4557
+ async validatePlacementForItem(item) {
4558
+ const frame = this.getFrameRect();
4559
+ if (!frame.width || !frame.height) {
4560
+ return true;
4561
+ }
4562
+ const src = item.sourceUrl || item.url;
4563
+ if (!src) {
4564
+ return true;
4565
+ }
4566
+ const source = await this.resolveImageSourceSize(item.id, src);
4567
+ if (!source) {
4568
+ return true;
4569
+ }
4570
+ return validateImagePlacement({
4571
+ frame,
4572
+ source,
4573
+ placement: this.resolvePlacementState(item)
4574
+ }).ok;
4575
+ }
4576
+ async validateImageSession() {
4577
+ const policy = this.getPlacementPolicy();
4578
+ if (policy === "free") {
4579
+ this.clearSessionNotice();
4580
+ return { ok: true, policy };
4581
+ }
4582
+ const invalidImageIds = [];
4583
+ for (const item of this.workingItems) {
4584
+ const valid = await this.validatePlacementForItem(item);
4585
+ if (!valid) {
4586
+ invalidImageIds.push(item.id);
4587
+ }
4588
+ }
4589
+ if (!invalidImageIds.length) {
4590
+ this.clearSessionNotice();
4591
+ return { ok: true, policy };
4592
+ }
4593
+ const notice = {
4594
+ code: "image-outside-frame",
4595
+ level: policy === "strict" ? "error" : "warning",
4596
+ 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",
4597
+ imageIds: invalidImageIds,
4598
+ policy
4599
+ };
4600
+ this.setSessionNotice(notice);
4601
+ this.setImageFocus(invalidImageIds[0], {
4602
+ syncCanvasSelection: true,
4603
+ skipRender: true
4604
+ });
4605
+ return {
4606
+ ok: policy !== "strict",
4607
+ reason: notice.code,
4608
+ message: notice.message,
4609
+ imageIds: notice.imageIds,
4610
+ policy: notice.policy
4611
+ };
4612
+ }
4344
4613
  getFrameVisualConfig() {
4345
4614
  var _a, _b;
4346
4615
  const strokeStyleRaw = this.getConfig(
@@ -4598,14 +4867,43 @@ var ImageTool = class {
4598
4867
  isImageSelectionActive: this.isImageSelectionActive,
4599
4868
  focusedImageId: this.focusedImageId
4600
4869
  });
4870
+ this.emitImageStateChange();
4601
4871
  this.canvasService.requestRenderAll();
4602
4872
  }
4603
4873
  clampNormalized(value) {
4604
4874
  return Math.max(-1, Math.min(2, value));
4605
4875
  }
4876
+ async setImageTransform(id, updates, options = {}) {
4877
+ const next = {};
4878
+ if (Number.isFinite(updates.scale)) {
4879
+ next.scale = Math.max(0.05, Number(updates.scale));
4880
+ }
4881
+ if (Number.isFinite(updates.angle)) {
4882
+ next.angle = Number(updates.angle);
4883
+ }
4884
+ if (Number.isFinite(updates.left)) {
4885
+ next.left = this.clampNormalized(Number(updates.left));
4886
+ }
4887
+ if (Number.isFinite(updates.top)) {
4888
+ next.top = this.clampNormalized(Number(updates.top));
4889
+ }
4890
+ if (Number.isFinite(updates.opacity)) {
4891
+ next.opacity = Math.max(0, Math.min(1, Number(updates.opacity)));
4892
+ }
4893
+ if (!Object.keys(next).length) return;
4894
+ await this.updateImage(id, next, options);
4895
+ }
4896
+ resetImageSession() {
4897
+ this.clearSessionNotice({ emit: false });
4898
+ this.workingItems = this.cloneItems(this.items);
4899
+ this.hasWorkingChanges = false;
4900
+ this.updateImages();
4901
+ this.emitWorkingChange();
4902
+ }
4606
4903
  updateImageInWorking(id, updates) {
4607
4904
  const index = this.workingItems.findIndex((item) => item.id === id);
4608
4905
  if (index < 0) return;
4906
+ this.clearSessionNotice({ emit: false });
4609
4907
  const next = [...this.workingItems];
4610
4908
  next[index] = this.normalizeItem({ ...next[index], ...updates });
4611
4909
  this.workingItems = next;
@@ -4620,9 +4918,9 @@ var ImageTool = class {
4620
4918
  this.emitWorkingChange(id);
4621
4919
  }
4622
4920
  async updateImageInConfig(id, updates) {
4623
- var _a, _b, _c, _d;
4624
4921
  const index = this.items.findIndex((item) => item.id === id);
4625
4922
  if (index < 0) return;
4923
+ this.clearSessionNotice({ emit: false });
4626
4924
  const replacingSource = typeof updates.url === "string" && updates.url.length > 0;
4627
4925
  const next = [...this.items];
4628
4926
  const base = next[index];
@@ -4633,23 +4931,12 @@ var ImageTool = class {
4633
4931
  ...replacingSource ? {
4634
4932
  url: replacingUrl,
4635
4933
  sourceUrl: replacingUrl,
4636
- committedUrl: void 0,
4637
- scale: (_a = updates.scale) != null ? _a : 1,
4638
- angle: (_b = updates.angle) != null ? _b : 0,
4639
- left: (_c = updates.left) != null ? _c : 0.5,
4640
- top: (_d = updates.top) != null ? _d : 0.5
4934
+ committedUrl: void 0
4641
4935
  } : {}
4642
4936
  });
4643
4937
  this.updateConfig(next);
4644
4938
  if (replacingSource) {
4645
- this.debug("replace:image:begin", { id, replacingUrl });
4646
4939
  this.purgeSourceSizeCacheForItem(base);
4647
- const loaded = await this.waitImageLoaded(id, true);
4648
- this.debug("replace:image:loaded", { id, loaded });
4649
- if (loaded) {
4650
- await this.refitImageToFrame(id);
4651
- this.setImageFocus(id);
4652
- }
4653
4940
  }
4654
4941
  }
4655
4942
  waitImageLoaded(id, forceWait = false) {
@@ -4667,70 +4954,43 @@ var ImageTool = class {
4667
4954
  });
4668
4955
  });
4669
4956
  }
4670
- async refitImageToFrame(id) {
4957
+ async resolveImageSourceSize(id, src) {
4671
4958
  const obj = this.getImageObject(id);
4672
- if (!obj || !this.canvasService) return;
4673
- const current = this.items.find((item) => item.id === id);
4674
- if (!current) return;
4675
- const render = this.resolveRenderImageState(current);
4676
- this.rememberSourceSize(render.src, obj);
4677
- const source = this.getSourceSize(render.src, obj);
4678
- const frame = this.getFrameRect();
4679
- const coverScale = this.getCoverScale(frame, source);
4680
- const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
4681
- const zoom = Math.max(0.05, currentScale / coverScale);
4682
- const updated = {
4683
- scale: Number.isFinite(zoom) ? zoom : 1,
4684
- angle: 0,
4685
- left: 0.5,
4686
- top: 0.5
4687
- };
4688
- const index = this.items.findIndex((item) => item.id === id);
4689
- if (index < 0) return;
4690
- const next = [...this.items];
4691
- next[index] = this.normalizeItem({ ...next[index], ...updated });
4692
- this.updateConfig(next);
4693
- this.workingItems = this.cloneItems(next);
4694
- this.hasWorkingChanges = false;
4695
- this.updateImages();
4696
- this.emitWorkingChange(id);
4959
+ if (obj) {
4960
+ this.rememberSourceSize(src, obj);
4961
+ }
4962
+ const ensured = await this.ensureSourceSize(src);
4963
+ if (ensured) return ensured;
4964
+ if (!obj) return null;
4965
+ const width = Number((obj == null ? void 0 : obj.width) || 0);
4966
+ const height = Number((obj == null ? void 0 : obj.height) || 0);
4967
+ if (width <= 0 || height <= 0) return null;
4968
+ return { width, height };
4697
4969
  }
4698
- async fitImageToArea(id, area) {
4699
- var _a, _b;
4970
+ async applyImageOperation(id, operation, options = {}) {
4700
4971
  if (!this.canvasService) return;
4701
- const loaded = await this.waitImageLoaded(id, false);
4702
- if (!loaded) return;
4703
- const obj = this.getImageObject(id);
4704
- if (!obj) return;
4705
- const renderItems = this.isToolActive ? this.workingItems : this.items;
4972
+ this.syncToolActiveFromWorkbench();
4973
+ const target = options.target || "auto";
4974
+ const renderItems = target === "working" || target === "auto" && this.isToolActive ? this.workingItems : this.items;
4706
4975
  const current = renderItems.find((item) => item.id === id);
4707
4976
  if (!current) return;
4708
4977
  const render = this.resolveRenderImageState(current);
4709
- this.rememberSourceSize(render.src, obj);
4710
- const source = this.getSourceSize(render.src, obj);
4978
+ const source = await this.resolveImageSourceSize(id, render.src);
4979
+ if (!source) return;
4711
4980
  const frame = this.getFrameRect();
4712
- const baseCover = this.getCoverScale(frame, source);
4713
- const desiredScale = Math.max(
4714
- Math.max(1, area.width) / Math.max(1, source.width),
4715
- Math.max(1, area.height) / Math.max(1, source.height)
4716
- );
4717
4981
  const viewport = this.canvasService.getSceneViewportRect();
4718
- const canvasW = viewport.width || 1;
4719
- const canvasH = viewport.height || 1;
4720
- const areaLeftInput = (_a = area.left) != null ? _a : 0.5;
4721
- const areaTopInput = (_b = area.top) != null ? _b : 0.5;
4722
- const areaLeftPx = areaLeftInput <= 1.5 ? viewport.left + areaLeftInput * canvasW : areaLeftInput;
4723
- const areaTopPx = areaTopInput <= 1.5 ? viewport.top + areaTopInput * canvasH : areaTopInput;
4724
- const updates = {
4725
- scale: Math.max(0.05, desiredScale / baseCover),
4726
- left: this.clampNormalized(
4727
- (areaLeftPx - frame.left) / Math.max(1, frame.width)
4728
- ),
4729
- top: this.clampNormalized(
4730
- (areaTopPx - frame.top) / Math.max(1, frame.height)
4731
- )
4732
- };
4733
- if (this.isToolActive) {
4982
+ const area = operation.type === "resetTransform" ? resolveImageOperationArea({ frame, viewport }) : resolveImageOperationArea({
4983
+ frame,
4984
+ viewport,
4985
+ area: operation.area
4986
+ });
4987
+ const updates = computeImageOperationUpdates({
4988
+ frame,
4989
+ source,
4990
+ operation,
4991
+ area
4992
+ });
4993
+ if (target === "working" || target === "auto" && this.isToolActive) {
4734
4994
  this.updateImageInWorking(id, updates);
4735
4995
  return;
4736
4996
  }
@@ -4769,11 +5029,42 @@ var ImageTool = class {
4769
5029
  }
4770
5030
  }
4771
5031
  this.hasWorkingChanges = false;
5032
+ this.clearSessionNotice({ emit: false });
4772
5033
  this.workingItems = this.cloneItems(next);
4773
5034
  this.updateConfig(next);
4774
5035
  this.emitWorkingChange(this.focusedImageId);
4775
5036
  return { ok: true };
4776
5037
  }
5038
+ async completeImageSession() {
5039
+ var _a, _b, _c;
5040
+ const sessionState = (_a = this.context) == null ? void 0 : _a.services.get("ToolSessionService");
5041
+ const workbench = (_b = this.context) == null ? void 0 : _b.services.get("WorkbenchService");
5042
+ console.info("[ImageTool] completeImageSession:start", {
5043
+ activeToolId: (_c = workbench == null ? void 0 : workbench.activeToolId) != null ? _c : null,
5044
+ isToolActive: this.isToolActive,
5045
+ dirtyBeforeComplete: this.hasWorkingChanges,
5046
+ workingCount: this.workingItems.length,
5047
+ committedCount: this.items.length,
5048
+ sessionDirty: sessionState == null ? void 0 : sessionState.isDirty(this.id)
5049
+ });
5050
+ const validation = await this.validateImageSession();
5051
+ if (!validation.ok) {
5052
+ console.warn("[ImageTool] completeImageSession:validation-failed", {
5053
+ validation,
5054
+ dirtyAfterValidation: this.hasWorkingChanges
5055
+ });
5056
+ return validation;
5057
+ }
5058
+ const result = await this.commitWorkingImagesAsCropped();
5059
+ console.info("[ImageTool] completeImageSession:done", {
5060
+ result,
5061
+ dirtyAfterComplete: this.hasWorkingChanges,
5062
+ workingCount: this.workingItems.length,
5063
+ committedCount: this.items.length,
5064
+ sessionDirty: sessionState == null ? void 0 : sessionState.isDirty(this.id)
5065
+ });
5066
+ return result;
5067
+ }
4777
5068
  async exportCroppedImageByIds(imageIds, options) {
4778
5069
  var _a, _b, _c;
4779
5070
  if (!this.canvasService) {
@@ -4861,6 +5152,11 @@ var ImageTool = class {
4861
5152
  }
4862
5153
  };
4863
5154
 
5155
+ // src/extensions/image/model.ts
5156
+ function hasAnyImageInViewState(state) {
5157
+ return Boolean(state == null ? void 0 : state.hasAnyImage);
5158
+ }
5159
+
4864
5160
  // src/extensions/size/SizeTool.ts
4865
5161
  var import_core3 = require("@pooder/core");
4866
5162
  var SizeTool = class {
@@ -10787,6 +11083,7 @@ var CanvasService = class {
10787
11083
  ViewportSystem,
10788
11084
  WhiteInkTool,
10789
11085
  computeImageCoverScale,
11086
+ computeImageOperationUpdates,
10790
11087
  computeWhiteInkCoverScale,
10791
11088
  createDefaultDielineState,
10792
11089
  createDielineCommands,
@@ -10796,5 +11093,7 @@ var CanvasService = class {
10796
11093
  createWhiteInkCommands,
10797
11094
  createWhiteInkConfigurations,
10798
11095
  evaluateVisibilityExpr,
10799
- readDielineState
11096
+ hasAnyImageInViewState,
11097
+ readDielineState,
11098
+ resolveImageOperationArea
10800
11099
  });