@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/.test-dist/src/extensions/image/ImageTool.js +250 -119
- package/.test-dist/src/extensions/image/commands.js +69 -41
- package/.test-dist/src/extensions/image/config.js +7 -0
- package/.test-dist/src/extensions/image/imageOperations.js +75 -0
- package/.test-dist/src/extensions/image/imagePlacement.js +44 -0
- package/.test-dist/src/extensions/image/index.js +1 -0
- package/.test-dist/src/extensions/image/model.js +4 -0
- package/.test-dist/tests/run.js +51 -9
- package/CHANGELOG.md +14 -0
- package/dist/index.d.mts +270 -163
- package/dist/index.d.ts +270 -163
- package/dist/index.js +458 -159
- package/dist/index.mjs +454 -158
- package/package.json +2 -2
- package/src/extensions/image/ImageTool.ts +351 -145
- package/src/extensions/image/commands.ts +78 -49
- package/src/extensions/image/config.ts +7 -0
- package/src/extensions/image/imageOperations.ts +135 -0
- package/src/extensions/image/imagePlacement.ts +78 -0
- package/src/extensions/image/index.ts +1 -0
- package/src/extensions/image/model.ts +13 -1
- package/tests/run.ts +89 -15
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CommandContribution } from "@pooder/core";
|
|
2
|
+
import type { ImageOperation } from "./imageOperations";
|
|
2
3
|
|
|
3
4
|
export function createImageCommands(tool: any): CommandContribution[] {
|
|
4
5
|
return [
|
|
@@ -23,30 +24,51 @@ export function createImageCommands(tool: any): CommandContribution[] {
|
|
|
23
24
|
},
|
|
24
25
|
},
|
|
25
26
|
{
|
|
26
|
-
command: "
|
|
27
|
-
id: "
|
|
28
|
-
title: "
|
|
27
|
+
command: "applyImageOperation",
|
|
28
|
+
id: "applyImageOperation",
|
|
29
|
+
title: "Apply Image Operation",
|
|
30
|
+
handler: async (
|
|
31
|
+
id: string,
|
|
32
|
+
operation: ImageOperation,
|
|
33
|
+
options: Record<string, any> = {},
|
|
34
|
+
) => {
|
|
35
|
+
await tool.applyImageOperation(id, operation, options);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
command: "getImageViewState",
|
|
40
|
+
id: "getImageViewState",
|
|
41
|
+
title: "Get Image View State",
|
|
29
42
|
handler: () => {
|
|
30
|
-
return tool.
|
|
43
|
+
return tool.getImageViewState();
|
|
31
44
|
},
|
|
32
45
|
},
|
|
33
46
|
{
|
|
34
|
-
command: "
|
|
35
|
-
id: "
|
|
36
|
-
title: "Set
|
|
37
|
-
handler: (
|
|
38
|
-
|
|
47
|
+
command: "setImageTransform",
|
|
48
|
+
id: "setImageTransform",
|
|
49
|
+
title: "Set Image Transform",
|
|
50
|
+
handler: async (
|
|
51
|
+
id: string,
|
|
52
|
+
updates: Record<string, any>,
|
|
53
|
+
options: Record<string, any> = {},
|
|
54
|
+
) => {
|
|
55
|
+
await tool.setImageTransform(id, updates, options);
|
|
39
56
|
},
|
|
40
57
|
},
|
|
41
58
|
{
|
|
42
|
-
command: "
|
|
43
|
-
id: "
|
|
44
|
-
title: "Reset
|
|
59
|
+
command: "imageSessionReset",
|
|
60
|
+
id: "imageSessionReset",
|
|
61
|
+
title: "Reset Image Session",
|
|
45
62
|
handler: () => {
|
|
46
|
-
tool.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
tool.resetImageSession();
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
command: "validateImageSession",
|
|
68
|
+
id: "validateImageSession",
|
|
69
|
+
title: "Validate Image Session",
|
|
70
|
+
handler: async () => {
|
|
71
|
+
return await tool.validateImageSession();
|
|
50
72
|
},
|
|
51
73
|
},
|
|
52
74
|
{
|
|
@@ -54,7 +76,7 @@ export function createImageCommands(tool: any): CommandContribution[] {
|
|
|
54
76
|
id: "completeImages",
|
|
55
77
|
title: "Complete Images",
|
|
56
78
|
handler: async () => {
|
|
57
|
-
return await tool.
|
|
79
|
+
return await tool.completeImageSession();
|
|
58
80
|
},
|
|
59
81
|
},
|
|
60
82
|
{
|
|
@@ -65,30 +87,6 @@ export function createImageCommands(tool: any): CommandContribution[] {
|
|
|
65
87
|
return await tool.exportUserCroppedImage(options);
|
|
66
88
|
},
|
|
67
89
|
},
|
|
68
|
-
{
|
|
69
|
-
command: "fitImageToArea",
|
|
70
|
-
id: "fitImageToArea",
|
|
71
|
-
title: "Fit Image to Area",
|
|
72
|
-
handler: async (
|
|
73
|
-
id: string,
|
|
74
|
-
area: {
|
|
75
|
-
width: number;
|
|
76
|
-
height: number;
|
|
77
|
-
left?: number;
|
|
78
|
-
top?: number;
|
|
79
|
-
},
|
|
80
|
-
) => {
|
|
81
|
-
await tool.fitImageToArea(id, area);
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
command: "fitImageToDefaultArea",
|
|
86
|
-
id: "fitImageToDefaultArea",
|
|
87
|
-
title: "Fit Image to Default Area",
|
|
88
|
-
handler: async (id: string) => {
|
|
89
|
-
await tool.fitImageToDefaultArea(id);
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
90
|
{
|
|
93
91
|
command: "focusImage",
|
|
94
92
|
id: "focusImage",
|
|
@@ -105,9 +103,10 @@ export function createImageCommands(tool: any): CommandContribution[] {
|
|
|
105
103
|
id: "removeImage",
|
|
106
104
|
title: "Remove Image",
|
|
107
105
|
handler: (id: string) => {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
106
|
+
const sourceItems = tool.isToolActive ? tool.workingItems : tool.items;
|
|
107
|
+
const removed = sourceItems.find((item: any) => item.id === id);
|
|
108
|
+
const next = sourceItems.filter((item: any) => item.id !== id);
|
|
109
|
+
if (next.length !== sourceItems.length) {
|
|
111
110
|
tool.purgeSourceSizeCacheForItem(removed);
|
|
112
111
|
if (tool.focusedImageId === id) {
|
|
113
112
|
tool.setImageFocus(null, {
|
|
@@ -115,6 +114,13 @@ export function createImageCommands(tool: any): CommandContribution[] {
|
|
|
115
114
|
skipRender: true,
|
|
116
115
|
});
|
|
117
116
|
}
|
|
117
|
+
if (tool.isToolActive) {
|
|
118
|
+
tool.workingItems = tool.cloneItems(next);
|
|
119
|
+
tool.hasWorkingChanges = true;
|
|
120
|
+
tool.updateImages();
|
|
121
|
+
tool.emitWorkingChange(id);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
118
124
|
tool.updateConfig(next);
|
|
119
125
|
}
|
|
120
126
|
},
|
|
@@ -141,6 +147,13 @@ export function createImageCommands(tool: any): CommandContribution[] {
|
|
|
141
147
|
syncCanvasSelection: true,
|
|
142
148
|
skipRender: true,
|
|
143
149
|
});
|
|
150
|
+
if (tool.isToolActive) {
|
|
151
|
+
tool.workingItems = [];
|
|
152
|
+
tool.hasWorkingChanges = true;
|
|
153
|
+
tool.updateImages();
|
|
154
|
+
tool.emitWorkingChange();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
144
157
|
tool.updateConfig([]);
|
|
145
158
|
},
|
|
146
159
|
},
|
|
@@ -149,11 +162,19 @@ export function createImageCommands(tool: any): CommandContribution[] {
|
|
|
149
162
|
id: "bringToFront",
|
|
150
163
|
title: "Bring Image to Front",
|
|
151
164
|
handler: (id: string) => {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
const sourceItems = tool.isToolActive ? tool.workingItems : tool.items;
|
|
166
|
+
const index = sourceItems.findIndex((item: any) => item.id === id);
|
|
167
|
+
if (index !== -1 && index < sourceItems.length - 1) {
|
|
168
|
+
const next = [...sourceItems];
|
|
155
169
|
const [item] = next.splice(index, 1);
|
|
156
170
|
next.push(item);
|
|
171
|
+
if (tool.isToolActive) {
|
|
172
|
+
tool.workingItems = tool.cloneItems(next);
|
|
173
|
+
tool.hasWorkingChanges = true;
|
|
174
|
+
tool.updateImages();
|
|
175
|
+
tool.emitWorkingChange(id);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
157
178
|
tool.updateConfig(next);
|
|
158
179
|
}
|
|
159
180
|
},
|
|
@@ -163,11 +184,19 @@ export function createImageCommands(tool: any): CommandContribution[] {
|
|
|
163
184
|
id: "sendToBack",
|
|
164
185
|
title: "Send Image to Back",
|
|
165
186
|
handler: (id: string) => {
|
|
166
|
-
const
|
|
187
|
+
const sourceItems = tool.isToolActive ? tool.workingItems : tool.items;
|
|
188
|
+
const index = sourceItems.findIndex((item: any) => item.id === id);
|
|
167
189
|
if (index > 0) {
|
|
168
|
-
const next = [...
|
|
190
|
+
const next = [...sourceItems];
|
|
169
191
|
const [item] = next.splice(index, 1);
|
|
170
192
|
next.unshift(item);
|
|
193
|
+
if (tool.isToolActive) {
|
|
194
|
+
tool.workingItems = tool.cloneItems(next);
|
|
195
|
+
tool.hasWorkingChanges = true;
|
|
196
|
+
tool.updateImages();
|
|
197
|
+
tool.emitWorkingChange(id);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
171
200
|
tool.updateConfig(next);
|
|
172
201
|
}
|
|
173
202
|
},
|
|
@@ -124,5 +124,12 @@ export function createImageConfigurations(): ConfigurationContribution[] {
|
|
|
124
124
|
label: "Image Frame Outer Background",
|
|
125
125
|
default: "#f5f5f5",
|
|
126
126
|
},
|
|
127
|
+
{
|
|
128
|
+
id: "image.session.placementPolicy",
|
|
129
|
+
type: "select",
|
|
130
|
+
label: "Image Session Placement Policy",
|
|
131
|
+
options: ["free", "warn", "strict"],
|
|
132
|
+
default: "free",
|
|
133
|
+
},
|
|
127
134
|
];
|
|
128
135
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 ImagePlacementState {
|
|
8
|
+
left: number;
|
|
9
|
+
top: number;
|
|
10
|
+
scale: number;
|
|
11
|
+
angle: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ImagePlacementValidationArgs {
|
|
15
|
+
frame: FrameRect;
|
|
16
|
+
source: SourceSize;
|
|
17
|
+
placement: ImagePlacementState;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ImagePlacementValidationResult {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toRadians(angle: number): number {
|
|
25
|
+
return (angle * Math.PI) / 180;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validateImagePlacement(
|
|
29
|
+
args: ImagePlacementValidationArgs,
|
|
30
|
+
): ImagePlacementValidationResult {
|
|
31
|
+
const { frame, source, placement } = args;
|
|
32
|
+
if (
|
|
33
|
+
frame.width <= 0 ||
|
|
34
|
+
frame.height <= 0 ||
|
|
35
|
+
source.width <= 0 ||
|
|
36
|
+
source.height <= 0
|
|
37
|
+
) {
|
|
38
|
+
return { ok: true };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const coverScale = getCoverScaleFromRect(frame, source);
|
|
42
|
+
const imageWidth =
|
|
43
|
+
source.width * coverScale * Math.max(0.05, Number(placement.scale || 1));
|
|
44
|
+
const imageHeight =
|
|
45
|
+
source.height * coverScale * Math.max(0.05, Number(placement.scale || 1));
|
|
46
|
+
|
|
47
|
+
if (imageWidth <= 0 || imageHeight <= 0) {
|
|
48
|
+
return { ok: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const centerX = frame.left + placement.left * frame.width;
|
|
52
|
+
const centerY = frame.top + placement.top * frame.height;
|
|
53
|
+
const halfWidth = imageWidth / 2;
|
|
54
|
+
const halfHeight = imageHeight / 2;
|
|
55
|
+
const radians = toRadians(placement.angle || 0);
|
|
56
|
+
const cos = Math.cos(radians);
|
|
57
|
+
const sin = Math.sin(radians);
|
|
58
|
+
|
|
59
|
+
const frameCorners = [
|
|
60
|
+
{ x: frame.left, y: frame.top },
|
|
61
|
+
{ x: frame.left + frame.width, y: frame.top },
|
|
62
|
+
{ x: frame.left + frame.width, y: frame.top + frame.height },
|
|
63
|
+
{ x: frame.left, y: frame.top + frame.height },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const coversFrame = frameCorners.every((corner) => {
|
|
67
|
+
const dx = corner.x - centerX;
|
|
68
|
+
const dy = corner.y - centerY;
|
|
69
|
+
const localX = dx * cos + dy * sin;
|
|
70
|
+
const localY = -dx * sin + dy * cos;
|
|
71
|
+
return (
|
|
72
|
+
Math.abs(localX) <= halfWidth + 1e-6 &&
|
|
73
|
+
Math.abs(localY) <= halfHeight + 1e-6
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return { ok: coversFrame };
|
|
78
|
+
}
|
|
@@ -1 +1,13 @@
|
|
|
1
|
-
export type {
|
|
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);
|
|
@@ -32,7 +33,10 @@ function assert(condition: unknown, message: string) {
|
|
|
32
33
|
|
|
33
34
|
function testWrappedOffsets() {
|
|
34
35
|
assert(wrappedDistance(100, 10, 30) === 20, "distance 10->30 should be 20");
|
|
35
|
-
assert(
|
|
36
|
+
assert(
|
|
37
|
+
wrappedDistance(100, 90, 10) === 20,
|
|
38
|
+
"distance 90->10 should wrap to 20",
|
|
39
|
+
);
|
|
36
40
|
|
|
37
41
|
const a = sampleWrappedOffsets(100, 10, 30, 5);
|
|
38
42
|
assert(
|
|
@@ -75,9 +79,18 @@ function testMaskOps() {
|
|
|
75
79
|
|
|
76
80
|
const r = findMinimalConnectRadius(mask, width, height, 20);
|
|
77
81
|
const closed = circularMorphology(mask, width, height, r, "closing");
|
|
78
|
-
assert(
|
|
82
|
+
assert(
|
|
83
|
+
isMaskConnected8(closed, width, height),
|
|
84
|
+
`closed mask should be connected (r=${r})`,
|
|
85
|
+
);
|
|
79
86
|
if (r > 0) {
|
|
80
|
-
const closedPrev = circularMorphology(
|
|
87
|
+
const closedPrev = circularMorphology(
|
|
88
|
+
mask,
|
|
89
|
+
width,
|
|
90
|
+
height,
|
|
91
|
+
r - 1,
|
|
92
|
+
"closing",
|
|
93
|
+
);
|
|
81
94
|
assert(
|
|
82
95
|
!isMaskConnected8(closedPrev, width, height),
|
|
83
96
|
`r should be minimal (r=${r})`,
|
|
@@ -96,10 +109,12 @@ function testMaskOps() {
|
|
|
96
109
|
|
|
97
110
|
const imgW = 2;
|
|
98
111
|
const imgH = 1;
|
|
99
|
-
const rgba = new Uint8ClampedArray([
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
const rgba = new Uint8ClampedArray([255, 255, 255, 255, 10, 10, 10, 254]);
|
|
113
|
+
const imageData = {
|
|
114
|
+
width: imgW,
|
|
115
|
+
height: imgH,
|
|
116
|
+
data: rgba,
|
|
117
|
+
} as unknown as ImageData;
|
|
103
118
|
const paddedWidth = imgW + 4;
|
|
104
119
|
const paddedHeight = imgH + 4;
|
|
105
120
|
const created = createMask(imageData, {
|
|
@@ -110,15 +125,25 @@ function testMaskOps() {
|
|
|
110
125
|
maskMode: "auto",
|
|
111
126
|
alphaOpaqueCutoff: 250,
|
|
112
127
|
});
|
|
113
|
-
assert(
|
|
114
|
-
|
|
128
|
+
assert(
|
|
129
|
+
created[2 * paddedWidth + 2] === 0,
|
|
130
|
+
"white pixel should be background",
|
|
131
|
+
);
|
|
132
|
+
assert(
|
|
133
|
+
created[2 * paddedWidth + 3] === 1,
|
|
134
|
+
"non-white pixel should be foreground",
|
|
135
|
+
);
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
function testEdgeScale() {
|
|
118
139
|
const currentMax = 100;
|
|
119
140
|
const baseBounds = { width: 50, height: 20 };
|
|
120
141
|
const expandedBounds = { width: 70, height: 40 };
|
|
121
|
-
const { width, height, scale } = computeDetectEdgeSize(
|
|
142
|
+
const { width, height, scale } = computeDetectEdgeSize(
|
|
143
|
+
currentMax,
|
|
144
|
+
baseBounds,
|
|
145
|
+
expandedBounds,
|
|
146
|
+
);
|
|
122
147
|
assert(scale === 2, `expected scale 2, got ${scale}`);
|
|
123
148
|
assert(width === 140, `expected width 140, got ${width}`);
|
|
124
149
|
assert(height === 80, `expected height 80, got ${height}`);
|
|
@@ -288,6 +313,53 @@ function testVisibilityDsl() {
|
|
|
288
313
|
);
|
|
289
314
|
}
|
|
290
315
|
|
|
316
|
+
function testImageViewStateHelper() {
|
|
317
|
+
assert(
|
|
318
|
+
hasAnyImageInViewState(null) === false,
|
|
319
|
+
"null image state should be empty",
|
|
320
|
+
);
|
|
321
|
+
assert(
|
|
322
|
+
hasAnyImageInViewState({
|
|
323
|
+
items: [],
|
|
324
|
+
hasAnyImage: false,
|
|
325
|
+
focusedId: null,
|
|
326
|
+
focusedItem: null,
|
|
327
|
+
isToolActive: false,
|
|
328
|
+
isImageSelectionActive: false,
|
|
329
|
+
hasWorkingChanges: false,
|
|
330
|
+
source: "committed",
|
|
331
|
+
placementPolicy: "free",
|
|
332
|
+
sessionNotice: null,
|
|
333
|
+
}) === false,
|
|
334
|
+
"empty image state should report false",
|
|
335
|
+
);
|
|
336
|
+
assert(
|
|
337
|
+
hasAnyImageInViewState({
|
|
338
|
+
items: [
|
|
339
|
+
{
|
|
340
|
+
id: "img-1",
|
|
341
|
+
url: "blob:test",
|
|
342
|
+
opacity: 1,
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
hasAnyImage: true,
|
|
346
|
+
focusedId: "img-1",
|
|
347
|
+
focusedItem: {
|
|
348
|
+
id: "img-1",
|
|
349
|
+
url: "blob:test",
|
|
350
|
+
opacity: 1,
|
|
351
|
+
},
|
|
352
|
+
isToolActive: true,
|
|
353
|
+
isImageSelectionActive: true,
|
|
354
|
+
hasWorkingChanges: true,
|
|
355
|
+
source: "working",
|
|
356
|
+
placementPolicy: "free",
|
|
357
|
+
sessionNotice: null,
|
|
358
|
+
}) === true,
|
|
359
|
+
"non-empty image state should report true",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
291
363
|
function testContributionCompatibility() {
|
|
292
364
|
const imageCommandNames = createImageCommands({} as any).map(
|
|
293
365
|
(entry) => entry.command,
|
|
@@ -303,13 +375,13 @@ function testContributionCompatibility() {
|
|
|
303
375
|
const expectedImageCommands = [
|
|
304
376
|
"addImage",
|
|
305
377
|
"upsertImage",
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
"
|
|
378
|
+
"applyImageOperation",
|
|
379
|
+
"getImageViewState",
|
|
380
|
+
"setImageTransform",
|
|
381
|
+
"imageSessionReset",
|
|
382
|
+
"validateImageSession",
|
|
309
383
|
"completeImages",
|
|
310
384
|
"exportUserCroppedImage",
|
|
311
|
-
"fitImageToArea",
|
|
312
|
-
"fitImageToDefaultArea",
|
|
313
385
|
"focusImage",
|
|
314
386
|
"removeImage",
|
|
315
387
|
"updateImage",
|
|
@@ -387,6 +459,7 @@ function testContributionCompatibility() {
|
|
|
387
459
|
"image.frame.dashLength",
|
|
388
460
|
"image.frame.innerBackground",
|
|
389
461
|
"image.frame.outerBackground",
|
|
462
|
+
"image.session.placementPolicy",
|
|
390
463
|
];
|
|
391
464
|
const expectedWhiteInkConfigKeys = [
|
|
392
465
|
"whiteInk.items",
|
|
@@ -434,6 +507,7 @@ function main() {
|
|
|
434
507
|
testEdgeScale();
|
|
435
508
|
testFeaturePlacementProjection();
|
|
436
509
|
testVisibilityDsl();
|
|
510
|
+
testImageViewStateHelper();
|
|
437
511
|
testContributionCompatibility();
|
|
438
512
|
console.log("ok");
|
|
439
513
|
}
|