@pooder/kit 6.2.2 → 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.
@@ -0,0 +1,135 @@
1
+ import type { FrameRect } from "../../shared/scene/frame";
2
+ import {
3
+ getCoverScale as getCoverScaleFromRect,
4
+ type SourceSize,
5
+ } from "../../shared/imaging/sourceSizeCache";
6
+
7
+ export interface ImageOperationArea {
8
+ width: number;
9
+ height: number;
10
+ centerX: number;
11
+ centerY: number;
12
+ }
13
+
14
+ export interface ImageOperationViewport {
15
+ left: number;
16
+ top: number;
17
+ width: number;
18
+ height: number;
19
+ }
20
+
21
+ export type ImageOperationAreaSpec =
22
+ | { type: "frame" }
23
+ | { type: "viewport" }
24
+ | ({
25
+ type: "custom";
26
+ } & ImageOperationArea);
27
+
28
+ export type ImageOperation =
29
+ | { type: "cover"; area?: ImageOperationAreaSpec }
30
+ | { type: "contain"; area?: ImageOperationAreaSpec }
31
+ | { type: "maximizeWidth"; area?: ImageOperationAreaSpec }
32
+ | { type: "maximizeHeight"; area?: ImageOperationAreaSpec }
33
+ | { type: "center"; area?: ImageOperationAreaSpec }
34
+ | { type: "resetTransform" };
35
+
36
+ export interface ComputeImageOperationArgs {
37
+ frame: FrameRect;
38
+ source: SourceSize;
39
+ operation: ImageOperation;
40
+ area: ImageOperationArea;
41
+ }
42
+
43
+ function clampNormalizedAnchor(value: number): number {
44
+ return Math.max(-1, Math.min(2, value));
45
+ }
46
+
47
+ function toNormalizedAnchor(center: number, start: number, size: number): number {
48
+ return clampNormalizedAnchor((center - start) / Math.max(1, size));
49
+ }
50
+
51
+ function resolveAbsoluteScale(
52
+ operation: ImageOperation,
53
+ area: ImageOperationArea,
54
+ source: SourceSize,
55
+ ): number | null {
56
+ const widthScale = Math.max(1, area.width) / Math.max(1, source.width);
57
+ const heightScale = Math.max(1, area.height) / Math.max(1, source.height);
58
+
59
+ switch (operation.type) {
60
+ case "cover":
61
+ return Math.max(widthScale, heightScale);
62
+ case "contain":
63
+ return Math.min(widthScale, heightScale);
64
+ case "maximizeWidth":
65
+ return widthScale;
66
+ case "maximizeHeight":
67
+ return heightScale;
68
+ default:
69
+ return null;
70
+ }
71
+ }
72
+
73
+ export function resolveImageOperationArea(args: {
74
+ frame: FrameRect;
75
+ viewport: ImageOperationViewport;
76
+ area?: ImageOperationAreaSpec;
77
+ }): ImageOperationArea {
78
+ const spec = args.area || { type: "frame" };
79
+
80
+ if (spec.type === "custom") {
81
+ return {
82
+ width: Math.max(1, spec.width),
83
+ height: Math.max(1, spec.height),
84
+ centerX: spec.centerX,
85
+ centerY: spec.centerY,
86
+ };
87
+ }
88
+
89
+ if (spec.type === "viewport") {
90
+ return {
91
+ width: Math.max(1, args.viewport.width),
92
+ height: Math.max(1, args.viewport.height),
93
+ centerX: args.viewport.left + args.viewport.width / 2,
94
+ centerY: args.viewport.top + args.viewport.height / 2,
95
+ };
96
+ }
97
+
98
+ return {
99
+ width: Math.max(1, args.frame.width),
100
+ height: Math.max(1, args.frame.height),
101
+ centerX: args.frame.left + args.frame.width / 2,
102
+ centerY: args.frame.top + args.frame.height / 2,
103
+ };
104
+ }
105
+
106
+ export function computeImageOperationUpdates(
107
+ args: ComputeImageOperationArgs,
108
+ ): { scale?: number; left?: number; top?: number; angle?: number } {
109
+ const { frame, source, operation, area } = args;
110
+
111
+ if (operation.type === "resetTransform") {
112
+ return {
113
+ scale: 1,
114
+ left: 0.5,
115
+ top: 0.5,
116
+ angle: 0,
117
+ };
118
+ }
119
+
120
+ const left = toNormalizedAnchor(area.centerX, frame.left, frame.width);
121
+ const top = toNormalizedAnchor(area.centerY, frame.top, frame.height);
122
+
123
+ if (operation.type === "center") {
124
+ return { left, top };
125
+ }
126
+
127
+ const absoluteScale = resolveAbsoluteScale(operation, area, source);
128
+ const coverScale = getCoverScaleFromRect(frame, source);
129
+
130
+ return {
131
+ scale: Math.max(0.05, (absoluteScale || coverScale) / coverScale),
132
+ left,
133
+ top,
134
+ };
135
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./ImageTool";
2
2
  export * from "./commands";
3
3
  export * from "./config";
4
+ export * from "./imageOperations";
4
5
  export * from "./model";
5
6
  export * from "./renderer";
@@ -1 +1,13 @@
1
- export type { ImageItem } from "./ImageTool";
1
+ export type {
2
+ ImageItem,
3
+ ImageTransformUpdates,
4
+ ImageViewState,
5
+ } from "./ImageTool";
6
+
7
+ import type { ImageViewState } from "./ImageTool";
8
+
9
+ export function hasAnyImageInViewState(
10
+ state: ImageViewState | null | undefined,
11
+ ): boolean {
12
+ return Boolean(state?.hasAnyImage);
13
+ }
package/tests/run.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  normalizePointInGeometry,
26
26
  resolveFeaturePosition,
27
27
  } from "../src/extensions/featureCoordinates";
28
+ import { hasAnyImageInViewState } from "../src/extensions/image/model";
28
29
 
29
30
  function assert(condition: unknown, message: string) {
30
31
  if (!condition) throw new Error(message);
@@ -288,6 +289,46 @@ function testVisibilityDsl() {
288
289
  );
289
290
  }
290
291
 
292
+ function testImageViewStateHelper() {
293
+ assert(hasAnyImageInViewState(null) === false, "null image state should be empty");
294
+ assert(
295
+ hasAnyImageInViewState({
296
+ items: [],
297
+ hasAnyImage: false,
298
+ focusedId: null,
299
+ focusedItem: null,
300
+ isToolActive: false,
301
+ isImageSelectionActive: false,
302
+ hasWorkingChanges: false,
303
+ source: "committed",
304
+ }) === false,
305
+ "empty image state should report false",
306
+ );
307
+ assert(
308
+ hasAnyImageInViewState({
309
+ items: [
310
+ {
311
+ id: "img-1",
312
+ url: "blob:test",
313
+ opacity: 1,
314
+ },
315
+ ],
316
+ hasAnyImage: true,
317
+ focusedId: "img-1",
318
+ focusedItem: {
319
+ id: "img-1",
320
+ url: "blob:test",
321
+ opacity: 1,
322
+ },
323
+ isToolActive: true,
324
+ isImageSelectionActive: true,
325
+ hasWorkingChanges: true,
326
+ source: "working",
327
+ }) === true,
328
+ "non-empty image state should report true",
329
+ );
330
+ }
331
+
291
332
  function testContributionCompatibility() {
292
333
  const imageCommandNames = createImageCommands({} as any).map(
293
334
  (entry) => entry.command,
@@ -303,13 +344,12 @@ function testContributionCompatibility() {
303
344
  const expectedImageCommands = [
304
345
  "addImage",
305
346
  "upsertImage",
306
- "getWorkingImages",
307
- "setWorkingImage",
308
- "resetWorkingImages",
347
+ "applyImageOperation",
348
+ "getImageViewState",
349
+ "setImageTransform",
350
+ "imageSessionReset",
309
351
  "completeImages",
310
352
  "exportUserCroppedImage",
311
- "fitImageToArea",
312
- "fitImageToDefaultArea",
313
353
  "focusImage",
314
354
  "removeImage",
315
355
  "updateImage",
@@ -432,9 +472,10 @@ function main() {
432
472
  testBridgeSelection();
433
473
  testMaskOps();
434
474
  testEdgeScale();
435
- testFeaturePlacementProjection();
436
- testVisibilityDsl();
437
- testContributionCompatibility();
475
+ testFeaturePlacementProjection();
476
+ testVisibilityDsl();
477
+ testImageViewStateHelper();
478
+ testContributionCompatibility();
438
479
  console.log("ok");
439
480
  }
440
481