@pooder/kit 5.3.1 → 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/extensions/background.js +475 -131
- package/.test-dist/src/extensions/dieline.js +283 -180
- package/.test-dist/src/extensions/dielineShape.js +66 -0
- package/.test-dist/src/extensions/feature.js +388 -303
- package/.test-dist/src/extensions/film.js +133 -74
- package/.test-dist/src/extensions/geometry.js +120 -56
- package/.test-dist/src/extensions/image.js +296 -212
- package/.test-dist/src/extensions/index.js +1 -3
- package/.test-dist/src/extensions/maskOps.js +75 -20
- package/.test-dist/src/extensions/ruler.js +312 -215
- package/.test-dist/src/extensions/sceneLayoutModel.js +9 -3
- package/.test-dist/src/extensions/sceneVisibility.js +3 -10
- package/.test-dist/src/extensions/tracer.js +229 -58
- package/.test-dist/src/extensions/white-ink.js +139 -129
- package/.test-dist/src/services/CanvasService.js +888 -126
- package/.test-dist/src/services/index.js +1 -0
- package/.test-dist/src/services/visibility.js +54 -0
- package/.test-dist/tests/run.js +58 -4
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +377 -82
- package/dist/index.d.ts +377 -82
- package/dist/index.js +3920 -2178
- package/dist/index.mjs +3992 -2247
- package/package.json +1 -1
- package/src/extensions/background.ts +631 -145
- package/src/extensions/dieline.ts +280 -187
- package/src/extensions/dielineShape.ts +109 -0
- package/src/extensions/feature.ts +485 -366
- package/src/extensions/film.ts +152 -76
- package/src/extensions/geometry.ts +203 -104
- package/src/extensions/image.ts +319 -238
- package/src/extensions/index.ts +0 -1
- package/src/extensions/ruler.ts +481 -268
- package/src/extensions/sceneLayoutModel.ts +18 -6
- package/src/extensions/white-ink.ts +157 -171
- package/src/services/CanvasService.ts +1126 -140
- package/src/services/index.ts +1 -0
- package/src/services/renderSpec.ts +69 -4
- package/src/services/visibility.ts +78 -0
- package/tests/run.ts +139 -4
- package/.test-dist/src/CanvasService.js +0 -249
- package/.test-dist/src/ViewportSystem.js +0 -75
- package/.test-dist/src/background.js +0 -203
- package/.test-dist/src/bridgeSelection.js +0 -20
- package/.test-dist/src/constraints.js +0 -237
- package/.test-dist/src/dieline.js +0 -818
- package/.test-dist/src/edgeScale.js +0 -12
- package/.test-dist/src/feature.js +0 -826
- package/.test-dist/src/featureComplete.js +0 -32
- package/.test-dist/src/film.js +0 -167
- package/.test-dist/src/geometry.js +0 -506
- package/.test-dist/src/image.js +0 -1250
- package/.test-dist/src/maskOps.js +0 -270
- package/.test-dist/src/mirror.js +0 -104
- package/.test-dist/src/renderSpec.js +0 -2
- package/.test-dist/src/ruler.js +0 -343
- package/.test-dist/src/sceneLayout.js +0 -99
- package/.test-dist/src/sceneLayoutModel.js +0 -196
- package/.test-dist/src/sceneView.js +0 -40
- package/.test-dist/src/sceneVisibility.js +0 -42
- package/.test-dist/src/size.js +0 -332
- package/.test-dist/src/tracer.js +0 -544
- package/.test-dist/src/white-ink.js +0 -829
- package/.test-dist/src/wrappedOffsets.js +0 -33
- package/src/extensions/sceneVisibility.ts +0 -71
|
@@ -4,29 +4,268 @@ import {
|
|
|
4
4
|
ContributionPointIds,
|
|
5
5
|
CommandContribution,
|
|
6
6
|
ConfigurationContribution,
|
|
7
|
+
ConfigurationService,
|
|
7
8
|
} from "@pooder/core";
|
|
8
|
-
import {
|
|
9
|
-
import { CanvasService } from "../services";
|
|
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
|
+
}
|
|
10
233
|
|
|
11
234
|
export class BackgroundTool implements Extension {
|
|
12
235
|
id = "pooder.kit.background";
|
|
236
|
+
|
|
13
237
|
public metadata = {
|
|
14
238
|
name: "BackgroundTool",
|
|
15
239
|
};
|
|
16
240
|
|
|
17
|
-
private
|
|
18
|
-
private url: string = "";
|
|
241
|
+
private config: BackgroundConfig = cloneConfig(DEFAULT_BACKGROUND_CONFIG);
|
|
19
242
|
|
|
20
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;
|
|
21
252
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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);
|
|
30
269
|
}
|
|
31
270
|
}
|
|
32
271
|
|
|
@@ -37,100 +276,167 @@ export class BackgroundTool implements Extension {
|
|
|
37
276
|
return;
|
|
38
277
|
}
|
|
39
278
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
`[BackgroundTool] Updating option ${prop} to ${e.value}`,
|
|
56
|
-
);
|
|
57
|
-
(this as any)[prop] = e.value;
|
|
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);
|
|
58
294
|
this.updateBackground();
|
|
59
|
-
|
|
60
|
-
console.warn(
|
|
61
|
-
`[BackgroundTool] Property ${prop} not found in options`,
|
|
62
|
-
);
|
|
295
|
+
return;
|
|
63
296
|
}
|
|
64
|
-
|
|
65
|
-
|
|
297
|
+
|
|
298
|
+
if (e.key.startsWith("size.")) {
|
|
299
|
+
this.latestSceneLayout = null;
|
|
300
|
+
this.updateBackground();
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
);
|
|
66
304
|
}
|
|
67
305
|
|
|
68
|
-
this.
|
|
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);
|
|
69
324
|
this.updateBackground();
|
|
70
325
|
}
|
|
71
326
|
|
|
72
327
|
deactivate(context: ExtensionContext) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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;
|
|
80
348
|
}
|
|
81
349
|
|
|
82
350
|
contribute() {
|
|
83
351
|
return {
|
|
84
352
|
[ContributionPointIds.CONFIGURATIONS]: [
|
|
85
353
|
{
|
|
86
|
-
id:
|
|
87
|
-
type: "
|
|
88
|
-
label: "Background
|
|
89
|
-
default:
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
id: "background.url",
|
|
93
|
-
type: "string",
|
|
94
|
-
label: "Image URL",
|
|
95
|
-
default: "",
|
|
354
|
+
id: BACKGROUND_CONFIG_KEY,
|
|
355
|
+
type: "json",
|
|
356
|
+
label: "Background Config",
|
|
357
|
+
default: cloneConfig(DEFAULT_BACKGROUND_CONFIG),
|
|
96
358
|
},
|
|
97
359
|
] as ConfigurationContribution[],
|
|
98
360
|
[ContributionPointIds.COMMANDS]: [
|
|
99
361
|
{
|
|
100
|
-
command: "
|
|
101
|
-
title: "
|
|
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",
|
|
102
369
|
handler: () => {
|
|
103
|
-
this.
|
|
370
|
+
this.commitConfig(cloneConfig(DEFAULT_BACKGROUND_CONFIG));
|
|
104
371
|
return true;
|
|
105
372
|
},
|
|
106
373
|
},
|
|
107
374
|
{
|
|
108
|
-
command: "
|
|
109
|
-
title: "
|
|
110
|
-
handler: () => {
|
|
111
|
-
this.
|
|
112
|
-
this.url = "";
|
|
113
|
-
this.updateBackground();
|
|
375
|
+
command: "background.replaceConfig",
|
|
376
|
+
title: "Replace Background Config",
|
|
377
|
+
handler: (config: BackgroundConfig) => {
|
|
378
|
+
this.commitConfig(normalizeConfig(config));
|
|
114
379
|
return true;
|
|
115
380
|
},
|
|
116
381
|
},
|
|
117
382
|
{
|
|
118
|
-
command: "
|
|
119
|
-
title: "
|
|
120
|
-
handler: (
|
|
121
|
-
|
|
122
|
-
this.color = color;
|
|
123
|
-
this.updateBackground();
|
|
383
|
+
command: "background.patchConfig",
|
|
384
|
+
title: "Patch Background Config",
|
|
385
|
+
handler: (patch: Partial<BackgroundConfig>) => {
|
|
386
|
+
this.commitConfig(mergeConfig(this.config, patch || {}));
|
|
124
387
|
return true;
|
|
125
388
|
},
|
|
126
389
|
},
|
|
127
390
|
{
|
|
128
|
-
command: "
|
|
129
|
-
title: "
|
|
130
|
-
handler: (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
);
|
|
134
440
|
return true;
|
|
135
441
|
},
|
|
136
442
|
},
|
|
@@ -138,93 +444,273 @@ export class BackgroundTool implements Extension {
|
|
|
138
444
|
};
|
|
139
445
|
}
|
|
140
446
|
|
|
141
|
-
private
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
backgroundLayer = this.canvasService.createLayer("background", {
|
|
146
|
-
width: this.canvasService.canvas.width,
|
|
147
|
-
height: this.canvasService.canvas.height,
|
|
148
|
-
selectable: false,
|
|
149
|
-
evented: false,
|
|
150
|
-
});
|
|
151
|
-
this.canvasService.canvas.sendObjectToBack(backgroundLayer);
|
|
447
|
+
private commitConfig(next: BackgroundConfig) {
|
|
448
|
+
const normalized = normalizeConfig(next);
|
|
449
|
+
if (configSignature(normalized) === configSignature(this.config)) {
|
|
450
|
+
return;
|
|
152
451
|
}
|
|
153
|
-
}
|
|
154
452
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const layer = this.canvasService.getLayer("background");
|
|
158
|
-
if (!layer) {
|
|
159
|
-
console.warn("[BackgroundTool] Background layer not found");
|
|
453
|
+
if (this.configService) {
|
|
454
|
+
this.configService.update(BACKGROUND_CONFIG_KEY, cloneConfig(normalized));
|
|
160
455
|
return;
|
|
161
456
|
}
|
|
162
457
|
|
|
163
|
-
|
|
458
|
+
this.config = normalized;
|
|
459
|
+
this.updateBackground();
|
|
460
|
+
}
|
|
164
461
|
|
|
165
|
-
|
|
166
|
-
const
|
|
462
|
+
private getViewportRect(): Rect {
|
|
463
|
+
const width = Number(this.canvasService?.canvas.width || 0);
|
|
464
|
+
const height = Number(this.canvasService?.canvas.height || 0);
|
|
167
465
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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,
|
|
181
568
|
selectable: false,
|
|
182
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",
|
|
183
591
|
data: {
|
|
184
|
-
id:
|
|
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,
|
|
185
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;
|
|
186
623
|
});
|
|
187
|
-
|
|
188
|
-
|
|
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;
|
|
189
660
|
}
|
|
190
661
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
) as Image;
|
|
662
|
+
const task = this.loadImageSize(src);
|
|
663
|
+
this.pendingSizeBySrc.set(src, task);
|
|
664
|
+
|
|
195
665
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
} else {
|
|
201
|
-
layer.remove(img);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
} else {
|
|
205
|
-
if (url) {
|
|
206
|
-
img = await Image.fromURL(url, { crossOrigin: "anonymous" });
|
|
207
|
-
img.set({
|
|
208
|
-
originX: "left",
|
|
209
|
-
originY: "top",
|
|
210
|
-
left: 0,
|
|
211
|
-
top: 0,
|
|
212
|
-
selectable: false,
|
|
213
|
-
evented: false,
|
|
214
|
-
data: {
|
|
215
|
-
id: "background-image",
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
img.scaleToWidth(width);
|
|
219
|
-
if (img.getScaledHeight() < height) img.scaleToHeight(height);
|
|
220
|
-
layer.add(img);
|
|
221
|
-
}
|
|
666
|
+
return await task;
|
|
667
|
+
} finally {
|
|
668
|
+
if (this.pendingSizeBySrc.get(src) === task) {
|
|
669
|
+
this.pendingSizeBySrc.delete(src);
|
|
222
670
|
}
|
|
223
|
-
this.canvasService.requestRenderAll();
|
|
224
|
-
} catch (e) {
|
|
225
|
-
console.error("[BackgroundTool] Failed to load image", e);
|
|
226
671
|
}
|
|
227
|
-
|
|
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
|
+
|
|
228
714
|
this.canvasService.requestRenderAll();
|
|
229
715
|
}
|
|
230
716
|
}
|