@pooder/kit 5.4.0 → 6.0.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.
- package/.test-dist/src/coordinate.js +74 -0
- package/.test-dist/src/extensions/background.js +547 -0
- package/.test-dist/src/extensions/bridgeSelection.js +20 -0
- package/.test-dist/src/extensions/constraints.js +237 -0
- package/.test-dist/src/extensions/dieline.js +931 -0
- package/.test-dist/src/extensions/dielineShape.js +66 -0
- package/.test-dist/src/extensions/edgeScale.js +12 -0
- package/.test-dist/src/extensions/feature.js +910 -0
- package/.test-dist/src/extensions/featureComplete.js +32 -0
- package/.test-dist/src/extensions/film.js +226 -0
- package/.test-dist/src/extensions/geometry.js +609 -0
- package/.test-dist/src/extensions/image.js +1613 -0
- package/.test-dist/src/extensions/index.js +28 -0
- package/.test-dist/src/extensions/maskOps.js +334 -0
- package/.test-dist/src/extensions/mirror.js +104 -0
- package/.test-dist/src/extensions/ruler.js +442 -0
- package/.test-dist/src/extensions/sceneLayout.js +96 -0
- package/.test-dist/src/extensions/sceneLayoutModel.js +202 -0
- package/.test-dist/src/extensions/sceneVisibility.js +55 -0
- package/.test-dist/src/extensions/size.js +331 -0
- package/.test-dist/src/extensions/tracer.js +709 -0
- package/.test-dist/src/extensions/white-ink.js +1200 -0
- package/.test-dist/src/extensions/wrappedOffsets.js +33 -0
- package/.test-dist/src/index.js +18 -0
- package/.test-dist/src/services/CanvasService.js +1011 -0
- package/.test-dist/src/services/ViewportSystem.js +76 -0
- package/.test-dist/src/services/index.js +25 -0
- package/.test-dist/src/services/renderSpec.js +2 -0
- package/.test-dist/src/services/visibility.js +54 -0
- package/.test-dist/src/units.js +30 -0
- package/.test-dist/tests/run.js +148 -0
- package/CHANGELOG.md +6 -0
- package/dist/index.d.mts +150 -62
- package/dist/index.d.ts +150 -62
- package/dist/index.js +2219 -1714
- package/dist/index.mjs +2226 -1718
- package/package.json +1 -1
- package/src/coordinate.ts +106 -106
- package/src/extensions/background.ts +716 -323
- package/src/extensions/bridgeSelection.ts +17 -17
- package/src/extensions/constraints.ts +322 -322
- package/src/extensions/dieline.ts +1169 -1149
- package/src/extensions/dielineShape.ts +109 -109
- package/src/extensions/edgeScale.ts +19 -19
- package/src/extensions/feature.ts +1140 -1137
- package/src/extensions/featureComplete.ts +46 -46
- package/src/extensions/film.ts +270 -266
- package/src/extensions/geometry.ts +851 -885
- package/src/extensions/image.ts +2007 -2054
- package/src/extensions/index.ts +10 -11
- package/src/extensions/maskOps.ts +283 -283
- package/src/extensions/mirror.ts +128 -128
- package/src/extensions/ruler.ts +664 -654
- package/src/extensions/sceneLayout.ts +140 -140
- package/src/extensions/sceneLayoutModel.ts +364 -364
- package/src/extensions/size.ts +389 -389
- package/src/extensions/tracer.ts +1019 -1019
- package/src/extensions/white-ink.ts +1508 -1575
- package/src/extensions/wrappedOffsets.ts +33 -33
- package/src/index.ts +2 -2
- package/src/services/CanvasService.ts +1286 -832
- package/src/services/ViewportSystem.ts +95 -95
- package/src/services/index.ts +4 -3
- package/src/services/renderSpec.ts +83 -53
- package/src/services/visibility.ts +78 -0
- package/src/units.ts +27 -27
- package/tests/run.ts +253 -118
- package/tsconfig.test.json +15 -15
- package/src/extensions/sceneVisibility.ts +0 -64
|
@@ -1,323 +1,716 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Extension,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
ContributionPointIds,
|
|
5
|
-
CommandContribution,
|
|
6
|
-
ConfigurationContribution,
|
|
7
|
-
ConfigurationService,
|
|
8
|
-
} from "@pooder/core";
|
|
9
|
-
import { FabricImage } from "fabric";
|
|
10
|
-
import { CanvasService, RenderObjectSpec } from "../services";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1
|
+
import {
|
|
2
|
+
Extension,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ContributionPointIds,
|
|
5
|
+
CommandContribution,
|
|
6
|
+
ConfigurationContribution,
|
|
7
|
+
ConfigurationService,
|
|
8
|
+
} from "@pooder/core";
|
|
9
|
+
import { FabricImage } from "fabric";
|
|
10
|
+
import { CanvasService, RenderObjectSpec } from "../services";
|
|
11
|
+
import {
|
|
12
|
+
computeSceneLayout,
|
|
13
|
+
readSizeState,
|
|
14
|
+
type SceneLayoutSnapshot,
|
|
15
|
+
} from "./sceneLayoutModel";
|
|
16
|
+
|
|
17
|
+
interface SourceSize {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Rect {
|
|
23
|
+
left: number;
|
|
24
|
+
top: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type BackgroundLayerKind = "color" | "image";
|
|
30
|
+
export type BackgroundFitMode = "cover" | "contain" | "stretch";
|
|
31
|
+
|
|
32
|
+
export interface BackgroundLayer {
|
|
33
|
+
id: string;
|
|
34
|
+
kind: BackgroundLayerKind;
|
|
35
|
+
anchor: string;
|
|
36
|
+
fit: BackgroundFitMode;
|
|
37
|
+
opacity: number;
|
|
38
|
+
order: number;
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
exportable: boolean;
|
|
41
|
+
color?: string;
|
|
42
|
+
src?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface BackgroundConfig {
|
|
46
|
+
version: number;
|
|
47
|
+
layers: BackgroundLayer[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BACKGROUND_LAYER_ID = "background";
|
|
51
|
+
const BACKGROUND_CONFIG_KEY = "background.config";
|
|
52
|
+
|
|
53
|
+
const DEFAULT_WIDTH = 800;
|
|
54
|
+
const DEFAULT_HEIGHT = 600;
|
|
55
|
+
|
|
56
|
+
const DEFAULT_BACKGROUND_CONFIG: BackgroundConfig = {
|
|
57
|
+
version: 1,
|
|
58
|
+
layers: [
|
|
59
|
+
{
|
|
60
|
+
id: "base-color",
|
|
61
|
+
kind: "color",
|
|
62
|
+
anchor: "viewport",
|
|
63
|
+
fit: "cover",
|
|
64
|
+
opacity: 1,
|
|
65
|
+
order: 0,
|
|
66
|
+
enabled: true,
|
|
67
|
+
exportable: false,
|
|
68
|
+
color: "#fff",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function clampOpacity(value: unknown, fallback: number): number {
|
|
74
|
+
const numeric = Number(value);
|
|
75
|
+
if (!Number.isFinite(numeric)) {
|
|
76
|
+
return Math.max(0, Math.min(1, fallback));
|
|
77
|
+
}
|
|
78
|
+
return Math.max(0, Math.min(1, numeric));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeLayerKind(
|
|
82
|
+
value: unknown,
|
|
83
|
+
fallback: BackgroundLayerKind,
|
|
84
|
+
): BackgroundLayerKind {
|
|
85
|
+
if (value === "color" || value === "image") {
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
return fallback;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeFitMode(
|
|
92
|
+
value: unknown,
|
|
93
|
+
fallback: BackgroundFitMode,
|
|
94
|
+
): BackgroundFitMode {
|
|
95
|
+
if (value === "contain" || value === "cover" || value === "stretch") {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
return fallback;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeAnchor(value: unknown, fallback: string): string {
|
|
102
|
+
if (typeof value !== "string") return fallback;
|
|
103
|
+
const trimmed = value.trim();
|
|
104
|
+
return trimmed || fallback;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeOrder(value: unknown, fallback: number): number {
|
|
108
|
+
const numeric = Number(value);
|
|
109
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
110
|
+
return numeric;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeLayer(
|
|
114
|
+
raw: unknown,
|
|
115
|
+
index: number,
|
|
116
|
+
fallback?: BackgroundLayer,
|
|
117
|
+
): BackgroundLayer {
|
|
118
|
+
const fallbackLayer: BackgroundLayer = fallback || {
|
|
119
|
+
id: `layer-${index + 1}`,
|
|
120
|
+
kind: "image",
|
|
121
|
+
anchor: "viewport",
|
|
122
|
+
fit: "contain",
|
|
123
|
+
opacity: 1,
|
|
124
|
+
order: index,
|
|
125
|
+
enabled: true,
|
|
126
|
+
exportable: false,
|
|
127
|
+
src: "",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (!raw || typeof raw !== "object") {
|
|
131
|
+
return { ...fallbackLayer };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const input = raw as Partial<BackgroundLayer>;
|
|
135
|
+
const kind = normalizeLayerKind(input.kind, fallbackLayer.kind);
|
|
136
|
+
return {
|
|
137
|
+
id:
|
|
138
|
+
typeof input.id === "string" && input.id.trim().length > 0
|
|
139
|
+
? input.id.trim()
|
|
140
|
+
: fallbackLayer.id,
|
|
141
|
+
kind,
|
|
142
|
+
anchor: normalizeAnchor(input.anchor, fallbackLayer.anchor),
|
|
143
|
+
fit: normalizeFitMode(input.fit, fallbackLayer.fit),
|
|
144
|
+
opacity: clampOpacity(input.opacity, fallbackLayer.opacity),
|
|
145
|
+
order: normalizeOrder(input.order, fallbackLayer.order),
|
|
146
|
+
enabled:
|
|
147
|
+
typeof input.enabled === "boolean"
|
|
148
|
+
? input.enabled
|
|
149
|
+
: fallbackLayer.enabled,
|
|
150
|
+
exportable:
|
|
151
|
+
typeof input.exportable === "boolean"
|
|
152
|
+
? input.exportable
|
|
153
|
+
: fallbackLayer.exportable,
|
|
154
|
+
color:
|
|
155
|
+
kind === "color"
|
|
156
|
+
? typeof input.color === "string"
|
|
157
|
+
? input.color
|
|
158
|
+
: typeof fallbackLayer.color === "string"
|
|
159
|
+
? fallbackLayer.color
|
|
160
|
+
: "#ffffff"
|
|
161
|
+
: undefined,
|
|
162
|
+
src:
|
|
163
|
+
kind === "image"
|
|
164
|
+
? typeof input.src === "string"
|
|
165
|
+
? input.src.trim()
|
|
166
|
+
: typeof fallbackLayer.src === "string"
|
|
167
|
+
? fallbackLayer.src
|
|
168
|
+
: ""
|
|
169
|
+
: undefined,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function normalizeConfig(raw: unknown): BackgroundConfig {
|
|
174
|
+
if (!raw || typeof raw !== "object") {
|
|
175
|
+
return cloneConfig(DEFAULT_BACKGROUND_CONFIG);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const input = raw as Partial<BackgroundConfig>;
|
|
179
|
+
const version = Number.isFinite(Number(input.version))
|
|
180
|
+
? Number(input.version)
|
|
181
|
+
: DEFAULT_BACKGROUND_CONFIG.version;
|
|
182
|
+
|
|
183
|
+
const baseLayers = Array.isArray(input.layers)
|
|
184
|
+
? input.layers.map((layer, index) => normalizeLayer(layer, index))
|
|
185
|
+
: cloneConfig(DEFAULT_BACKGROUND_CONFIG).layers;
|
|
186
|
+
|
|
187
|
+
const uniqueLayers: BackgroundLayer[] = [];
|
|
188
|
+
const seen = new Set<string>();
|
|
189
|
+
|
|
190
|
+
baseLayers.forEach((layer, index) => {
|
|
191
|
+
let nextId = layer.id || `layer-${index + 1}`;
|
|
192
|
+
let serial = 1;
|
|
193
|
+
while (seen.has(nextId)) {
|
|
194
|
+
serial += 1;
|
|
195
|
+
nextId = `${layer.id || `layer-${index + 1}`}-${serial}`;
|
|
196
|
+
}
|
|
197
|
+
seen.add(nextId);
|
|
198
|
+
uniqueLayers.push({ ...layer, id: nextId });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
version,
|
|
203
|
+
layers: uniqueLayers,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cloneConfig(config: BackgroundConfig): BackgroundConfig {
|
|
208
|
+
return {
|
|
209
|
+
version: config.version,
|
|
210
|
+
layers: (config.layers || []).map((layer) => ({ ...layer })),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function mergeConfig(base: BackgroundConfig, patch: Partial<BackgroundConfig>) {
|
|
215
|
+
const merged: BackgroundConfig = {
|
|
216
|
+
version:
|
|
217
|
+
patch.version === undefined
|
|
218
|
+
? base.version
|
|
219
|
+
: Number.isFinite(Number(patch.version))
|
|
220
|
+
? Number(patch.version)
|
|
221
|
+
: base.version,
|
|
222
|
+
layers: Array.isArray(patch.layers)
|
|
223
|
+
? patch.layers.map((layer, index) => normalizeLayer(layer, index))
|
|
224
|
+
: base.layers.map((layer) => ({ ...layer })),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return normalizeConfig(merged);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function configSignature(config: BackgroundConfig): string {
|
|
231
|
+
return JSON.stringify(config);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export class BackgroundTool implements Extension {
|
|
235
|
+
id = "pooder.kit.background";
|
|
236
|
+
|
|
237
|
+
public metadata = {
|
|
238
|
+
name: "BackgroundTool",
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
private config: BackgroundConfig = cloneConfig(DEFAULT_BACKGROUND_CONFIG);
|
|
242
|
+
|
|
243
|
+
private canvasService?: CanvasService;
|
|
244
|
+
private configService?: ConfigurationService;
|
|
245
|
+
|
|
246
|
+
private specs: RenderObjectSpec[] = [];
|
|
247
|
+
private renderProducerDisposable?: { dispose: () => void };
|
|
248
|
+
private configChangeDisposable?: { dispose: () => void };
|
|
249
|
+
|
|
250
|
+
private renderSeq = 0;
|
|
251
|
+
private latestSceneLayout: SceneLayoutSnapshot | null = null;
|
|
252
|
+
|
|
253
|
+
private sourceSizeBySrc: Map<string, SourceSize> = new Map();
|
|
254
|
+
private pendingSizeBySrc: Map<string, Promise<SourceSize | null>> = new Map();
|
|
255
|
+
|
|
256
|
+
private onCanvasResized = () => {
|
|
257
|
+
this.latestSceneLayout = null;
|
|
258
|
+
this.updateBackground();
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
private onSceneLayoutChanged = (layout: SceneLayoutSnapshot) => {
|
|
262
|
+
this.latestSceneLayout = layout;
|
|
263
|
+
this.updateBackground();
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
constructor(options?: Partial<BackgroundConfig>) {
|
|
267
|
+
if (options && typeof options === "object") {
|
|
268
|
+
this.config = mergeConfig(this.config, options);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
activate(context: ExtensionContext) {
|
|
273
|
+
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
274
|
+
if (!this.canvasService) {
|
|
275
|
+
console.warn("CanvasService not found for BackgroundTool");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.configService = context.services.get<ConfigurationService>(
|
|
280
|
+
"ConfigurationService",
|
|
281
|
+
);
|
|
282
|
+
if (this.configService) {
|
|
283
|
+
this.config = normalizeConfig(
|
|
284
|
+
this.configService.get(
|
|
285
|
+
BACKGROUND_CONFIG_KEY,
|
|
286
|
+
DEFAULT_BACKGROUND_CONFIG,
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
this.configChangeDisposable?.dispose();
|
|
290
|
+
this.configChangeDisposable = this.configService.onAnyChange(
|
|
291
|
+
(e: { key: string; value: any }) => {
|
|
292
|
+
if (e.key === BACKGROUND_CONFIG_KEY) {
|
|
293
|
+
this.config = normalizeConfig(e.value);
|
|
294
|
+
this.updateBackground();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (e.key.startsWith("size.")) {
|
|
299
|
+
this.latestSceneLayout = null;
|
|
300
|
+
this.updateBackground();
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.renderProducerDisposable?.dispose();
|
|
307
|
+
this.renderProducerDisposable = this.canvasService.registerRenderProducer(
|
|
308
|
+
this.id,
|
|
309
|
+
() => ({
|
|
310
|
+
passes: [
|
|
311
|
+
{
|
|
312
|
+
id: BACKGROUND_LAYER_ID,
|
|
313
|
+
stack: 0,
|
|
314
|
+
order: 0,
|
|
315
|
+
objects: this.specs,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
}),
|
|
319
|
+
{ priority: 0 },
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
context.eventBus.on("canvas:resized", this.onCanvasResized);
|
|
323
|
+
context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
|
|
324
|
+
this.updateBackground();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
deactivate(context: ExtensionContext) {
|
|
328
|
+
context.eventBus.off("canvas:resized", this.onCanvasResized);
|
|
329
|
+
context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
|
|
330
|
+
|
|
331
|
+
this.renderSeq += 1;
|
|
332
|
+
this.specs = [];
|
|
333
|
+
this.latestSceneLayout = null;
|
|
334
|
+
|
|
335
|
+
this.configChangeDisposable?.dispose();
|
|
336
|
+
this.configChangeDisposable = undefined;
|
|
337
|
+
|
|
338
|
+
this.renderProducerDisposable?.dispose();
|
|
339
|
+
this.renderProducerDisposable = undefined;
|
|
340
|
+
|
|
341
|
+
if (!this.canvasService) return;
|
|
342
|
+
|
|
343
|
+
void this.canvasService.flushRenderFromProducers();
|
|
344
|
+
this.canvasService.requestRenderAll();
|
|
345
|
+
|
|
346
|
+
this.canvasService = undefined;
|
|
347
|
+
this.configService = undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
contribute() {
|
|
351
|
+
return {
|
|
352
|
+
[ContributionPointIds.CONFIGURATIONS]: [
|
|
353
|
+
{
|
|
354
|
+
id: BACKGROUND_CONFIG_KEY,
|
|
355
|
+
type: "json",
|
|
356
|
+
label: "Background Config",
|
|
357
|
+
default: cloneConfig(DEFAULT_BACKGROUND_CONFIG),
|
|
358
|
+
},
|
|
359
|
+
] as ConfigurationContribution[],
|
|
360
|
+
[ContributionPointIds.COMMANDS]: [
|
|
361
|
+
{
|
|
362
|
+
command: "background.getConfig",
|
|
363
|
+
title: "Get Background Config",
|
|
364
|
+
handler: () => cloneConfig(this.config),
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
command: "background.resetConfig",
|
|
368
|
+
title: "Reset Background Config",
|
|
369
|
+
handler: () => {
|
|
370
|
+
this.commitConfig(cloneConfig(DEFAULT_BACKGROUND_CONFIG));
|
|
371
|
+
return true;
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
command: "background.replaceConfig",
|
|
376
|
+
title: "Replace Background Config",
|
|
377
|
+
handler: (config: BackgroundConfig) => {
|
|
378
|
+
this.commitConfig(normalizeConfig(config));
|
|
379
|
+
return true;
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
command: "background.patchConfig",
|
|
384
|
+
title: "Patch Background Config",
|
|
385
|
+
handler: (patch: Partial<BackgroundConfig>) => {
|
|
386
|
+
this.commitConfig(mergeConfig(this.config, patch || {}));
|
|
387
|
+
return true;
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
command: "background.upsertLayer",
|
|
392
|
+
title: "Upsert Background Layer",
|
|
393
|
+
handler: (layer: Partial<BackgroundLayer> & { id: string }) => {
|
|
394
|
+
const normalized = normalizeLayer(layer, 0);
|
|
395
|
+
const existingIndex = this.config.layers.findIndex(
|
|
396
|
+
(item) => item.id === normalized.id,
|
|
397
|
+
);
|
|
398
|
+
const nextLayers = [...this.config.layers];
|
|
399
|
+
if (existingIndex >= 0) {
|
|
400
|
+
nextLayers[existingIndex] = normalizeLayer(
|
|
401
|
+
{ ...nextLayers[existingIndex], ...layer },
|
|
402
|
+
existingIndex,
|
|
403
|
+
nextLayers[existingIndex],
|
|
404
|
+
);
|
|
405
|
+
} else {
|
|
406
|
+
nextLayers.push(
|
|
407
|
+
normalizeLayer(
|
|
408
|
+
{
|
|
409
|
+
...normalized,
|
|
410
|
+
order: Number.isFinite(Number(layer.order))
|
|
411
|
+
? Number(layer.order)
|
|
412
|
+
: nextLayers.length,
|
|
413
|
+
},
|
|
414
|
+
nextLayers.length,
|
|
415
|
+
),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
this.commitConfig(
|
|
419
|
+
normalizeConfig({
|
|
420
|
+
...this.config,
|
|
421
|
+
layers: nextLayers,
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
return true;
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
command: "background.removeLayer",
|
|
429
|
+
title: "Remove Background Layer",
|
|
430
|
+
handler: (id: string) => {
|
|
431
|
+
const nextLayers = this.config.layers.filter(
|
|
432
|
+
(layer) => layer.id !== id,
|
|
433
|
+
);
|
|
434
|
+
this.commitConfig(
|
|
435
|
+
normalizeConfig({
|
|
436
|
+
...this.config,
|
|
437
|
+
layers: nextLayers,
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
return true;
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
] as CommandContribution[],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private commitConfig(next: BackgroundConfig) {
|
|
448
|
+
const normalized = normalizeConfig(next);
|
|
449
|
+
if (configSignature(normalized) === configSignature(this.config)) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (this.configService) {
|
|
454
|
+
this.configService.update(BACKGROUND_CONFIG_KEY, cloneConfig(normalized));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
this.config = normalized;
|
|
459
|
+
this.updateBackground();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private getViewportRect(): Rect {
|
|
463
|
+
const width = Number(this.canvasService?.canvas.width || 0);
|
|
464
|
+
const height = Number(this.canvasService?.canvas.height || 0);
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
left: 0,
|
|
468
|
+
top: 0,
|
|
469
|
+
width: width > 0 ? width : DEFAULT_WIDTH,
|
|
470
|
+
height: height > 0 ? height : DEFAULT_HEIGHT,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private resolveSceneLayout(): SceneLayoutSnapshot | null {
|
|
475
|
+
if (this.latestSceneLayout) return this.latestSceneLayout;
|
|
476
|
+
if (!this.canvasService || !this.configService) return null;
|
|
477
|
+
|
|
478
|
+
const layout = computeSceneLayout(
|
|
479
|
+
this.canvasService,
|
|
480
|
+
readSizeState(this.configService),
|
|
481
|
+
);
|
|
482
|
+
this.latestSceneLayout = layout;
|
|
483
|
+
return layout;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private resolveFocusRect(): Rect | null {
|
|
487
|
+
const layout = this.resolveSceneLayout();
|
|
488
|
+
if (!layout) return null;
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
left: layout.trimRect.left,
|
|
492
|
+
top: layout.trimRect.top,
|
|
493
|
+
width: layout.trimRect.width,
|
|
494
|
+
height: layout.trimRect.height,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private resolveAnchorRect(anchor: string): Rect {
|
|
499
|
+
if (anchor === "focus") {
|
|
500
|
+
return this.resolveFocusRect() || this.getViewportRect();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (anchor !== "viewport") {
|
|
504
|
+
return this.getViewportRect();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return this.getViewportRect();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private resolveImagePlacement(
|
|
511
|
+
target: Rect,
|
|
512
|
+
sourceSize: SourceSize,
|
|
513
|
+
fit: BackgroundFitMode,
|
|
514
|
+
): { left: number; top: number; scaleX: number; scaleY: number } {
|
|
515
|
+
const targetWidth = Math.max(1, Number(target.width || 0));
|
|
516
|
+
const targetHeight = Math.max(1, Number(target.height || 0));
|
|
517
|
+
const sourceWidth = Math.max(1, Number(sourceSize.width || 0));
|
|
518
|
+
const sourceHeight = Math.max(1, Number(sourceSize.height || 0));
|
|
519
|
+
|
|
520
|
+
if (fit === "stretch") {
|
|
521
|
+
return {
|
|
522
|
+
left: target.left,
|
|
523
|
+
top: target.top,
|
|
524
|
+
scaleX: targetWidth / sourceWidth,
|
|
525
|
+
scaleY: targetHeight / sourceHeight,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const scale =
|
|
530
|
+
fit === "contain"
|
|
531
|
+
? Math.min(targetWidth / sourceWidth, targetHeight / sourceHeight)
|
|
532
|
+
: Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
|
|
533
|
+
|
|
534
|
+
const renderWidth = sourceWidth * scale;
|
|
535
|
+
const renderHeight = sourceHeight * scale;
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
left: target.left + (targetWidth - renderWidth) / 2,
|
|
539
|
+
top: target.top + (targetHeight - renderHeight) / 2,
|
|
540
|
+
scaleX: scale,
|
|
541
|
+
scaleY: scale,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private buildColorLayerSpec(layer: BackgroundLayer): RenderObjectSpec {
|
|
546
|
+
const rect = this.resolveAnchorRect(layer.anchor);
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
id: `background.layer.${layer.id}.color`,
|
|
550
|
+
type: "rect",
|
|
551
|
+
space: "screen",
|
|
552
|
+
data: {
|
|
553
|
+
id: `background.layer.${layer.id}.color`,
|
|
554
|
+
layerId: BACKGROUND_LAYER_ID,
|
|
555
|
+
type: "background-layer",
|
|
556
|
+
layerRef: layer.id,
|
|
557
|
+
layerKind: layer.kind,
|
|
558
|
+
},
|
|
559
|
+
props: {
|
|
560
|
+
left: rect.left,
|
|
561
|
+
top: rect.top,
|
|
562
|
+
width: rect.width,
|
|
563
|
+
height: rect.height,
|
|
564
|
+
originX: "left",
|
|
565
|
+
originY: "top",
|
|
566
|
+
fill: layer.color || "transparent",
|
|
567
|
+
opacity: layer.opacity,
|
|
568
|
+
selectable: false,
|
|
569
|
+
evented: false,
|
|
570
|
+
excludeFromExport: !layer.exportable,
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private buildImageLayerSpec(layer: BackgroundLayer): RenderObjectSpec[] {
|
|
576
|
+
const src = String(layer.src || "").trim();
|
|
577
|
+
if (!src) return [];
|
|
578
|
+
|
|
579
|
+
const sourceSize = this.sourceSizeBySrc.get(src);
|
|
580
|
+
if (!sourceSize) return [];
|
|
581
|
+
|
|
582
|
+
const rect = this.resolveAnchorRect(layer.anchor);
|
|
583
|
+
const placement = this.resolveImagePlacement(rect, sourceSize, layer.fit);
|
|
584
|
+
|
|
585
|
+
return [
|
|
586
|
+
{
|
|
587
|
+
id: `background.layer.${layer.id}.image`,
|
|
588
|
+
type: "image",
|
|
589
|
+
src,
|
|
590
|
+
space: "screen",
|
|
591
|
+
data: {
|
|
592
|
+
id: `background.layer.${layer.id}.image`,
|
|
593
|
+
layerId: BACKGROUND_LAYER_ID,
|
|
594
|
+
type: "background-layer",
|
|
595
|
+
layerRef: layer.id,
|
|
596
|
+
layerKind: layer.kind,
|
|
597
|
+
},
|
|
598
|
+
props: {
|
|
599
|
+
left: placement.left,
|
|
600
|
+
top: placement.top,
|
|
601
|
+
originX: "left",
|
|
602
|
+
originY: "top",
|
|
603
|
+
scaleX: placement.scaleX,
|
|
604
|
+
scaleY: placement.scaleY,
|
|
605
|
+
opacity: layer.opacity,
|
|
606
|
+
selectable: false,
|
|
607
|
+
evented: false,
|
|
608
|
+
excludeFromExport: !layer.exportable,
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private buildBackgroundSpecs(config: BackgroundConfig): RenderObjectSpec[] {
|
|
615
|
+
const activeLayers = (config.layers || [])
|
|
616
|
+
.filter((layer) => layer.enabled)
|
|
617
|
+
.map((layer, index) => ({ layer, index }))
|
|
618
|
+
.sort((a, b) => {
|
|
619
|
+
if (a.layer.order !== b.layer.order) {
|
|
620
|
+
return a.layer.order - b.layer.order;
|
|
621
|
+
}
|
|
622
|
+
return a.index - b.index;
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const specs: RenderObjectSpec[] = [];
|
|
626
|
+
|
|
627
|
+
activeLayers.forEach(({ layer }) => {
|
|
628
|
+
if (layer.kind === "color") {
|
|
629
|
+
specs.push(this.buildColorLayerSpec(layer));
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
specs.push(...this.buildImageLayerSpec(layer));
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
return specs;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private collectActiveImageUrls(config: BackgroundConfig): string[] {
|
|
639
|
+
const urls = new Set<string>();
|
|
640
|
+
|
|
641
|
+
(config.layers || []).forEach((layer) => {
|
|
642
|
+
if (!layer.enabled || layer.kind !== "image") return;
|
|
643
|
+
const src = String(layer.src || "").trim();
|
|
644
|
+
if (!src) return;
|
|
645
|
+
urls.add(src);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
return Array.from(urls);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private async ensureImageSize(src: string): Promise<SourceSize | null> {
|
|
652
|
+
if (!src) return null;
|
|
653
|
+
|
|
654
|
+
const cached = this.sourceSizeBySrc.get(src);
|
|
655
|
+
if (cached) return cached;
|
|
656
|
+
|
|
657
|
+
const pending = this.pendingSizeBySrc.get(src);
|
|
658
|
+
if (pending) {
|
|
659
|
+
return pending;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const task = this.loadImageSize(src);
|
|
663
|
+
this.pendingSizeBySrc.set(src, task);
|
|
664
|
+
|
|
665
|
+
try {
|
|
666
|
+
return await task;
|
|
667
|
+
} finally {
|
|
668
|
+
if (this.pendingSizeBySrc.get(src) === task) {
|
|
669
|
+
this.pendingSizeBySrc.delete(src);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private async loadImageSize(src: string): Promise<SourceSize | null> {
|
|
675
|
+
try {
|
|
676
|
+
const image = await FabricImage.fromURL(src, {
|
|
677
|
+
crossOrigin: "anonymous",
|
|
678
|
+
});
|
|
679
|
+
const width = Number(image?.width || 0);
|
|
680
|
+
const height = Number(image?.height || 0);
|
|
681
|
+
if (width > 0 && height > 0) {
|
|
682
|
+
const size = { width, height };
|
|
683
|
+
this.sourceSizeBySrc.set(src, size);
|
|
684
|
+
return size;
|
|
685
|
+
}
|
|
686
|
+
} catch (error) {
|
|
687
|
+
console.error("[BackgroundTool] Failed to load image", src, error);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private updateBackground() {
|
|
694
|
+
void this.updateBackgroundAsync();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private async updateBackgroundAsync() {
|
|
698
|
+
if (!this.canvasService) return;
|
|
699
|
+
|
|
700
|
+
const seq = ++this.renderSeq;
|
|
701
|
+
const currentConfig = cloneConfig(this.config);
|
|
702
|
+
const activeUrls = this.collectActiveImageUrls(currentConfig);
|
|
703
|
+
|
|
704
|
+
if (activeUrls.length > 0) {
|
|
705
|
+
await Promise.all(activeUrls.map((url) => this.ensureImageSize(url)));
|
|
706
|
+
if (seq !== this.renderSeq) return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
this.specs = this.buildBackgroundSpecs(currentConfig);
|
|
710
|
+
|
|
711
|
+
await this.canvasService.flushRenderFromProducers();
|
|
712
|
+
if (seq !== this.renderSeq) return;
|
|
713
|
+
|
|
714
|
+
this.canvasService.requestRenderAll();
|
|
715
|
+
}
|
|
716
|
+
}
|