@pooder/kit 6.3.0 → 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 +116 -0
- package/.test-dist/src/extensions/image/commands.js +9 -1
- package/.test-dist/src/extensions/image/config.js +7 -0
- package/.test-dist/src/extensions/image/imagePlacement.js +44 -0
- package/.test-dist/tests/run.js +12 -4
- package/CHANGELOG.md +8 -0
- package/dist/index.d.mts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +195 -4
- package/dist/index.mjs +195 -4
- package/package.json +2 -2
- package/src/extensions/image/ImageTool.ts +180 -1
- package/src/extensions/image/commands.ts +9 -1
- package/src/extensions/image/config.ts +7 -0
- package/src/extensions/image/imagePlacement.ts +78 -0
- package/tests/run.ts +48 -15
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
resolveImageOperationArea,
|
|
45
45
|
type ImageOperation,
|
|
46
46
|
} from "./imageOperations";
|
|
47
|
+
import { validateImagePlacement } from "./imagePlacement";
|
|
47
48
|
import { buildImageSessionOverlaySpecs } from "./sessionOverlay";
|
|
48
49
|
|
|
49
50
|
export interface ImageItem {
|
|
@@ -75,6 +76,18 @@ export interface ImageViewState {
|
|
|
75
76
|
isImageSelectionActive: boolean;
|
|
76
77
|
hasWorkingChanges: boolean;
|
|
77
78
|
source: "working" | "committed";
|
|
79
|
+
placementPolicy: ImageSessionPlacementPolicy;
|
|
80
|
+
sessionNotice: ImageSessionNotice | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ImageSessionPlacementPolicy = "free" | "warn" | "strict";
|
|
84
|
+
|
|
85
|
+
export interface ImageSessionNotice {
|
|
86
|
+
code: "image-outside-frame";
|
|
87
|
+
level: "warning" | "error";
|
|
88
|
+
message: string;
|
|
89
|
+
imageIds: string[];
|
|
90
|
+
policy: ImageSessionPlacementPolicy;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
interface RenderImageState {
|
|
@@ -238,6 +251,7 @@ export class ImageTool implements Extension {
|
|
|
238
251
|
private activeSnapX: SnapMatch | null = null;
|
|
239
252
|
private activeSnapY: SnapMatch | null = null;
|
|
240
253
|
private movingImageId: string | null = null;
|
|
254
|
+
private sessionNotice: ImageSessionNotice | null = null;
|
|
241
255
|
private hasRenderedSnapGuides = false;
|
|
242
256
|
private canvasObjectMovingHandler?: (e: any) => void;
|
|
243
257
|
private canvasMouseUpHandler?: (e: any) => void;
|
|
@@ -349,8 +363,12 @@ export class ImageTool implements Extension {
|
|
|
349
363
|
if (
|
|
350
364
|
e.key.startsWith("size.") ||
|
|
351
365
|
e.key.startsWith("image.frame.") ||
|
|
366
|
+
e.key.startsWith("image.session.") ||
|
|
352
367
|
e.key.startsWith("image.control.")
|
|
353
368
|
) {
|
|
369
|
+
if (e.key === "image.session.placementPolicy") {
|
|
370
|
+
this.clearSessionNotice();
|
|
371
|
+
}
|
|
354
372
|
if (e.key.startsWith("image.control.")) {
|
|
355
373
|
this.imageControlsByCapabilityKey.clear();
|
|
356
374
|
}
|
|
@@ -951,6 +969,7 @@ export class ImageTool implements Extension {
|
|
|
951
969
|
interaction: "session",
|
|
952
970
|
commands: {
|
|
953
971
|
begin: "imageSessionReset",
|
|
972
|
+
validate: "validateImageSession",
|
|
954
973
|
commit: "completeImages",
|
|
955
974
|
rollback: "imageSessionReset",
|
|
956
975
|
},
|
|
@@ -1001,6 +1020,47 @@ export class ImageTool implements Extension {
|
|
|
1001
1020
|
return this.isToolActive ? this.workingItems : this.items;
|
|
1002
1021
|
}
|
|
1003
1022
|
|
|
1023
|
+
private getPlacementPolicy(): ImageSessionPlacementPolicy {
|
|
1024
|
+
const policy = this.getConfig<ImageSessionPlacementPolicy>(
|
|
1025
|
+
"image.session.placementPolicy",
|
|
1026
|
+
"free",
|
|
1027
|
+
);
|
|
1028
|
+
return policy === "warn" || policy === "strict" ? policy : "free";
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private areSessionNoticesEqual(
|
|
1032
|
+
a: ImageSessionNotice | null,
|
|
1033
|
+
b: ImageSessionNotice | null,
|
|
1034
|
+
): boolean {
|
|
1035
|
+
if (!a && !b) return true;
|
|
1036
|
+
if (!a || !b) return false;
|
|
1037
|
+
return (
|
|
1038
|
+
a.code === b.code &&
|
|
1039
|
+
a.level === b.level &&
|
|
1040
|
+
a.message === b.message &&
|
|
1041
|
+
a.policy === b.policy &&
|
|
1042
|
+
JSON.stringify(a.imageIds) === JSON.stringify(b.imageIds)
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
private setSessionNotice(
|
|
1047
|
+
notice: ImageSessionNotice | null,
|
|
1048
|
+
options: { emit?: boolean } = {},
|
|
1049
|
+
) {
|
|
1050
|
+
if (this.areSessionNoticesEqual(this.sessionNotice, notice)) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
this.sessionNotice = notice;
|
|
1054
|
+
if (options.emit !== false) {
|
|
1055
|
+
this.context?.eventBus.emit("image:session:notice", this.sessionNotice);
|
|
1056
|
+
this.emitImageStateChange();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
private clearSessionNotice(options: { emit?: boolean } = {}) {
|
|
1061
|
+
this.setSessionNotice(null, options);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1004
1064
|
private getImageViewState(): ImageViewState {
|
|
1005
1065
|
this.syncToolActiveFromWorkbench();
|
|
1006
1066
|
const items = this.cloneItems(this.getViewItems());
|
|
@@ -1018,6 +1078,8 @@ export class ImageTool implements Extension {
|
|
|
1018
1078
|
isImageSelectionActive: this.isImageSelectionActive,
|
|
1019
1079
|
hasWorkingChanges: this.hasWorkingChanges,
|
|
1020
1080
|
source: this.isToolActive ? "working" : "committed",
|
|
1081
|
+
placementPolicy: this.getPlacementPolicy(),
|
|
1082
|
+
sessionNotice: this.sessionNotice,
|
|
1021
1083
|
};
|
|
1022
1084
|
}
|
|
1023
1085
|
|
|
@@ -1085,6 +1147,7 @@ export class ImageTool implements Extension {
|
|
|
1085
1147
|
operation?: ImageOperation,
|
|
1086
1148
|
): Promise<string> {
|
|
1087
1149
|
this.syncToolActiveFromWorkbench();
|
|
1150
|
+
this.clearSessionNotice({ emit: false });
|
|
1088
1151
|
const id = this.generateId();
|
|
1089
1152
|
const newItem = this.normalizeItem({
|
|
1090
1153
|
id,
|
|
@@ -1153,7 +1216,11 @@ export class ImageTool implements Extension {
|
|
|
1153
1216
|
return { id: targetId, mode: "replace" };
|
|
1154
1217
|
}
|
|
1155
1218
|
|
|
1156
|
-
const id = await this.addImageEntry(
|
|
1219
|
+
const id = await this.addImageEntry(
|
|
1220
|
+
url,
|
|
1221
|
+
options.addOptions,
|
|
1222
|
+
options.operation,
|
|
1223
|
+
);
|
|
1157
1224
|
return { id, mode: "add" };
|
|
1158
1225
|
}
|
|
1159
1226
|
|
|
@@ -1200,6 +1267,7 @@ export class ImageTool implements Extension {
|
|
|
1200
1267
|
|
|
1201
1268
|
private updateConfig(newItems: ImageItem[], skipCanvasUpdate = false) {
|
|
1202
1269
|
if (!this.context) return;
|
|
1270
|
+
this.clearSessionNotice({ emit: false });
|
|
1203
1271
|
this.applyCommittedItems(newItems);
|
|
1204
1272
|
runDeferredConfigUpdate(
|
|
1205
1273
|
this,
|
|
@@ -1313,6 +1381,82 @@ export class ImageTool implements Extension {
|
|
|
1313
1381
|
return getCoverScaleFromRect(frame, size);
|
|
1314
1382
|
}
|
|
1315
1383
|
|
|
1384
|
+
private resolvePlacementState(item: ImageItem) {
|
|
1385
|
+
return {
|
|
1386
|
+
left: Number.isFinite(item.left as any) ? (item.left as number) : 0.5,
|
|
1387
|
+
top: Number.isFinite(item.top as any) ? (item.top as number) : 0.5,
|
|
1388
|
+
scale: Math.max(0.05, item.scale ?? 1),
|
|
1389
|
+
angle: Number.isFinite(item.angle as any) ? (item.angle as number) : 0,
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
private async validatePlacementForItem(item: ImageItem): Promise<boolean> {
|
|
1394
|
+
const frame = this.getFrameRect();
|
|
1395
|
+
if (!frame.width || !frame.height) {
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const src = item.sourceUrl || item.url;
|
|
1400
|
+
if (!src) {
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const source = await this.resolveImageSourceSize(item.id, src);
|
|
1405
|
+
if (!source) {
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
return validateImagePlacement({
|
|
1410
|
+
frame,
|
|
1411
|
+
source,
|
|
1412
|
+
placement: this.resolvePlacementState(item),
|
|
1413
|
+
}).ok;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
private async validateImageSession() {
|
|
1417
|
+
const policy = this.getPlacementPolicy();
|
|
1418
|
+
if (policy === "free") {
|
|
1419
|
+
this.clearSessionNotice();
|
|
1420
|
+
return { ok: true, policy };
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const invalidImageIds: string[] = [];
|
|
1424
|
+
for (const item of this.workingItems) {
|
|
1425
|
+
const valid = await this.validatePlacementForItem(item);
|
|
1426
|
+
if (!valid) {
|
|
1427
|
+
invalidImageIds.push(item.id);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
if (!invalidImageIds.length) {
|
|
1432
|
+
this.clearSessionNotice();
|
|
1433
|
+
return { ok: true, policy };
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const notice: ImageSessionNotice = {
|
|
1437
|
+
code: "image-outside-frame",
|
|
1438
|
+
level: policy === "strict" ? "error" : "warning",
|
|
1439
|
+
message:
|
|
1440
|
+
policy === "strict"
|
|
1441
|
+
? "图片位置不能超出 frame,请调整后再提交。"
|
|
1442
|
+
: "图片位置已超出 frame,建议调整后再提交。",
|
|
1443
|
+
imageIds: invalidImageIds,
|
|
1444
|
+
policy,
|
|
1445
|
+
};
|
|
1446
|
+
this.setSessionNotice(notice);
|
|
1447
|
+
this.setImageFocus(invalidImageIds[0], {
|
|
1448
|
+
syncCanvasSelection: true,
|
|
1449
|
+
skipRender: true,
|
|
1450
|
+
});
|
|
1451
|
+
return {
|
|
1452
|
+
ok: policy !== "strict",
|
|
1453
|
+
reason: notice.code,
|
|
1454
|
+
message: notice.message,
|
|
1455
|
+
imageIds: notice.imageIds,
|
|
1456
|
+
policy: notice.policy,
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1316
1460
|
private getFrameVisualConfig(): FrameVisualConfig {
|
|
1317
1461
|
const strokeStyleRaw = (this.getConfig<string>(
|
|
1318
1462
|
"image.frame.strokeStyle",
|
|
@@ -1657,6 +1801,7 @@ export class ImageTool implements Extension {
|
|
|
1657
1801
|
}
|
|
1658
1802
|
|
|
1659
1803
|
private resetImageSession() {
|
|
1804
|
+
this.clearSessionNotice({ emit: false });
|
|
1660
1805
|
this.workingItems = this.cloneItems(this.items);
|
|
1661
1806
|
this.hasWorkingChanges = false;
|
|
1662
1807
|
this.updateImages();
|
|
@@ -1706,6 +1851,7 @@ export class ImageTool implements Extension {
|
|
|
1706
1851
|
const index = this.workingItems.findIndex((item) => item.id === id);
|
|
1707
1852
|
if (index < 0) return;
|
|
1708
1853
|
|
|
1854
|
+
this.clearSessionNotice({ emit: false });
|
|
1709
1855
|
const next = [...this.workingItems];
|
|
1710
1856
|
next[index] = this.normalizeItem({ ...next[index], ...updates });
|
|
1711
1857
|
this.workingItems = next;
|
|
@@ -1724,6 +1870,7 @@ export class ImageTool implements Extension {
|
|
|
1724
1870
|
const index = this.items.findIndex((item) => item.id === id);
|
|
1725
1871
|
if (index < 0) return;
|
|
1726
1872
|
|
|
1873
|
+
this.clearSessionNotice({ emit: false });
|
|
1727
1874
|
const replacingSource =
|
|
1728
1875
|
typeof updates.url === "string" && updates.url.length > 0;
|
|
1729
1876
|
const next = [...this.items];
|
|
@@ -1868,12 +2015,44 @@ export class ImageTool implements Extension {
|
|
|
1868
2015
|
}
|
|
1869
2016
|
|
|
1870
2017
|
this.hasWorkingChanges = false;
|
|
2018
|
+
this.clearSessionNotice({ emit: false });
|
|
1871
2019
|
this.workingItems = this.cloneItems(next);
|
|
1872
2020
|
this.updateConfig(next);
|
|
1873
2021
|
this.emitWorkingChange(this.focusedImageId);
|
|
1874
2022
|
return { ok: true };
|
|
1875
2023
|
}
|
|
1876
2024
|
|
|
2025
|
+
private async completeImageSession() {
|
|
2026
|
+
const sessionState =
|
|
2027
|
+
this.context?.services.get<ToolSessionService>("ToolSessionService");
|
|
2028
|
+
const workbench = this.context?.services.get<any>("WorkbenchService");
|
|
2029
|
+
console.info("[ImageTool] completeImageSession:start", {
|
|
2030
|
+
activeToolId: workbench?.activeToolId ?? null,
|
|
2031
|
+
isToolActive: this.isToolActive,
|
|
2032
|
+
dirtyBeforeComplete: this.hasWorkingChanges,
|
|
2033
|
+
workingCount: this.workingItems.length,
|
|
2034
|
+
committedCount: this.items.length,
|
|
2035
|
+
sessionDirty: sessionState?.isDirty(this.id),
|
|
2036
|
+
});
|
|
2037
|
+
const validation = await this.validateImageSession();
|
|
2038
|
+
if (!validation.ok) {
|
|
2039
|
+
console.warn("[ImageTool] completeImageSession:validation-failed", {
|
|
2040
|
+
validation,
|
|
2041
|
+
dirtyAfterValidation: this.hasWorkingChanges,
|
|
2042
|
+
});
|
|
2043
|
+
return validation;
|
|
2044
|
+
}
|
|
2045
|
+
const result = await this.commitWorkingImagesAsCropped();
|
|
2046
|
+
console.info("[ImageTool] completeImageSession:done", {
|
|
2047
|
+
result,
|
|
2048
|
+
dirtyAfterComplete: this.hasWorkingChanges,
|
|
2049
|
+
workingCount: this.workingItems.length,
|
|
2050
|
+
committedCount: this.items.length,
|
|
2051
|
+
sessionDirty: sessionState?.isDirty(this.id),
|
|
2052
|
+
});
|
|
2053
|
+
return result;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1877
2056
|
private async exportCroppedImageByIds(
|
|
1878
2057
|
imageIds: string[],
|
|
1879
2058
|
options: ExportCroppedImageOptions,
|
|
@@ -63,12 +63,20 @@ export function createImageCommands(tool: any): CommandContribution[] {
|
|
|
63
63
|
tool.resetImageSession();
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
command: "validateImageSession",
|
|
68
|
+
id: "validateImageSession",
|
|
69
|
+
title: "Validate Image Session",
|
|
70
|
+
handler: async () => {
|
|
71
|
+
return await tool.validateImageSession();
|
|
72
|
+
},
|
|
73
|
+
},
|
|
66
74
|
{
|
|
67
75
|
command: "completeImages",
|
|
68
76
|
id: "completeImages",
|
|
69
77
|
title: "Complete Images",
|
|
70
78
|
handler: async () => {
|
|
71
|
-
return await tool.
|
|
79
|
+
return await tool.completeImageSession();
|
|
72
80
|
},
|
|
73
81
|
},
|
|
74
82
|
{
|
|
@@ -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,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
|
+
}
|
package/tests/run.ts
CHANGED
|
@@ -33,7 +33,10 @@ function assert(condition: unknown, message: string) {
|
|
|
33
33
|
|
|
34
34
|
function testWrappedOffsets() {
|
|
35
35
|
assert(wrappedDistance(100, 10, 30) === 20, "distance 10->30 should be 20");
|
|
36
|
-
assert(
|
|
36
|
+
assert(
|
|
37
|
+
wrappedDistance(100, 90, 10) === 20,
|
|
38
|
+
"distance 90->10 should wrap to 20",
|
|
39
|
+
);
|
|
37
40
|
|
|
38
41
|
const a = sampleWrappedOffsets(100, 10, 30, 5);
|
|
39
42
|
assert(
|
|
@@ -76,9 +79,18 @@ function testMaskOps() {
|
|
|
76
79
|
|
|
77
80
|
const r = findMinimalConnectRadius(mask, width, height, 20);
|
|
78
81
|
const closed = circularMorphology(mask, width, height, r, "closing");
|
|
79
|
-
assert(
|
|
82
|
+
assert(
|
|
83
|
+
isMaskConnected8(closed, width, height),
|
|
84
|
+
`closed mask should be connected (r=${r})`,
|
|
85
|
+
);
|
|
80
86
|
if (r > 0) {
|
|
81
|
-
const closedPrev = circularMorphology(
|
|
87
|
+
const closedPrev = circularMorphology(
|
|
88
|
+
mask,
|
|
89
|
+
width,
|
|
90
|
+
height,
|
|
91
|
+
r - 1,
|
|
92
|
+
"closing",
|
|
93
|
+
);
|
|
82
94
|
assert(
|
|
83
95
|
!isMaskConnected8(closedPrev, width, height),
|
|
84
96
|
`r should be minimal (r=${r})`,
|
|
@@ -97,10 +109,12 @@ function testMaskOps() {
|
|
|
97
109
|
|
|
98
110
|
const imgW = 2;
|
|
99
111
|
const imgH = 1;
|
|
100
|
-
const rgba = new Uint8ClampedArray([
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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;
|
|
104
118
|
const paddedWidth = imgW + 4;
|
|
105
119
|
const paddedHeight = imgH + 4;
|
|
106
120
|
const created = createMask(imageData, {
|
|
@@ -111,15 +125,25 @@ function testMaskOps() {
|
|
|
111
125
|
maskMode: "auto",
|
|
112
126
|
alphaOpaqueCutoff: 250,
|
|
113
127
|
});
|
|
114
|
-
assert(
|
|
115
|
-
|
|
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
|
+
);
|
|
116
136
|
}
|
|
117
137
|
|
|
118
138
|
function testEdgeScale() {
|
|
119
139
|
const currentMax = 100;
|
|
120
140
|
const baseBounds = { width: 50, height: 20 };
|
|
121
141
|
const expandedBounds = { width: 70, height: 40 };
|
|
122
|
-
const { width, height, scale } = computeDetectEdgeSize(
|
|
142
|
+
const { width, height, scale } = computeDetectEdgeSize(
|
|
143
|
+
currentMax,
|
|
144
|
+
baseBounds,
|
|
145
|
+
expandedBounds,
|
|
146
|
+
);
|
|
123
147
|
assert(scale === 2, `expected scale 2, got ${scale}`);
|
|
124
148
|
assert(width === 140, `expected width 140, got ${width}`);
|
|
125
149
|
assert(height === 80, `expected height 80, got ${height}`);
|
|
@@ -290,7 +314,10 @@ function testVisibilityDsl() {
|
|
|
290
314
|
}
|
|
291
315
|
|
|
292
316
|
function testImageViewStateHelper() {
|
|
293
|
-
assert(
|
|
317
|
+
assert(
|
|
318
|
+
hasAnyImageInViewState(null) === false,
|
|
319
|
+
"null image state should be empty",
|
|
320
|
+
);
|
|
294
321
|
assert(
|
|
295
322
|
hasAnyImageInViewState({
|
|
296
323
|
items: [],
|
|
@@ -301,6 +328,8 @@ function testImageViewStateHelper() {
|
|
|
301
328
|
isImageSelectionActive: false,
|
|
302
329
|
hasWorkingChanges: false,
|
|
303
330
|
source: "committed",
|
|
331
|
+
placementPolicy: "free",
|
|
332
|
+
sessionNotice: null,
|
|
304
333
|
}) === false,
|
|
305
334
|
"empty image state should report false",
|
|
306
335
|
);
|
|
@@ -324,6 +353,8 @@ function testImageViewStateHelper() {
|
|
|
324
353
|
isImageSelectionActive: true,
|
|
325
354
|
hasWorkingChanges: true,
|
|
326
355
|
source: "working",
|
|
356
|
+
placementPolicy: "free",
|
|
357
|
+
sessionNotice: null,
|
|
327
358
|
}) === true,
|
|
328
359
|
"non-empty image state should report true",
|
|
329
360
|
);
|
|
@@ -348,6 +379,7 @@ function testContributionCompatibility() {
|
|
|
348
379
|
"getImageViewState",
|
|
349
380
|
"setImageTransform",
|
|
350
381
|
"imageSessionReset",
|
|
382
|
+
"validateImageSession",
|
|
351
383
|
"completeImages",
|
|
352
384
|
"exportUserCroppedImage",
|
|
353
385
|
"focusImage",
|
|
@@ -427,6 +459,7 @@ function testContributionCompatibility() {
|
|
|
427
459
|
"image.frame.dashLength",
|
|
428
460
|
"image.frame.innerBackground",
|
|
429
461
|
"image.frame.outerBackground",
|
|
462
|
+
"image.session.placementPolicy",
|
|
430
463
|
];
|
|
431
464
|
const expectedWhiteInkConfigKeys = [
|
|
432
465
|
"whiteInk.items",
|
|
@@ -472,10 +505,10 @@ function main() {
|
|
|
472
505
|
testBridgeSelection();
|
|
473
506
|
testMaskOps();
|
|
474
507
|
testEdgeScale();
|
|
475
|
-
testFeaturePlacementProjection();
|
|
476
|
-
testVisibilityDsl();
|
|
477
|
-
testImageViewStateHelper();
|
|
478
|
-
testContributionCompatibility();
|
|
508
|
+
testFeaturePlacementProjection();
|
|
509
|
+
testVisibilityDsl();
|
|
510
|
+
testImageViewStateHelper();
|
|
511
|
+
testContributionCompatibility();
|
|
479
512
|
console.log("ok");
|
|
480
513
|
}
|
|
481
514
|
|