@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
|
@@ -1,12 +1,125 @@
|
|
|
1
|
-
import { Canvas,
|
|
2
|
-
import {
|
|
1
|
+
import { Canvas, FabricObject, Rect, Path, Image, Text } from "fabric";
|
|
2
|
+
import {
|
|
3
|
+
Service,
|
|
4
|
+
EventBus,
|
|
5
|
+
ServiceContext,
|
|
6
|
+
TOOL_SESSION_SERVICE,
|
|
7
|
+
ToolSessionService,
|
|
8
|
+
WORKBENCH_SERVICE,
|
|
9
|
+
WorkbenchService,
|
|
10
|
+
} from "@pooder/core";
|
|
3
11
|
import { ViewportSystem } from "./ViewportSystem";
|
|
4
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
RenderCoordinateSpace,
|
|
14
|
+
RenderEffectSpec,
|
|
15
|
+
RenderLayoutInsets,
|
|
16
|
+
RenderLayoutLength,
|
|
17
|
+
RenderObjectLayoutSpec,
|
|
18
|
+
RenderObjectSpec,
|
|
19
|
+
RenderPassSpec,
|
|
20
|
+
} from "./renderSpec";
|
|
21
|
+
import { evaluateVisibilityExpr, type VisibilityLayerState } from "./visibility";
|
|
22
|
+
|
|
23
|
+
export interface RenderProducerResult {
|
|
24
|
+
passes?: RenderPassSpec[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type RenderProducer = () =>
|
|
28
|
+
| RenderProducerResult
|
|
29
|
+
| undefined
|
|
30
|
+
| Promise<RenderProducerResult | undefined>;
|
|
31
|
+
|
|
32
|
+
export interface RegisterRenderProducerOptions {
|
|
33
|
+
priority?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RenderProducerEntry {
|
|
37
|
+
toolId: string;
|
|
38
|
+
producer: RenderProducer;
|
|
39
|
+
priority: number;
|
|
40
|
+
order: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface RectLike {
|
|
44
|
+
left: number;
|
|
45
|
+
top: number;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ResolvedRenderPassSpec {
|
|
51
|
+
id: string;
|
|
52
|
+
stack: number;
|
|
53
|
+
order: number;
|
|
54
|
+
replace: boolean;
|
|
55
|
+
visibility?: RenderPassSpec["visibility"];
|
|
56
|
+
effects: RenderEffectSpec[];
|
|
57
|
+
objects: RenderObjectSpec[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface ResolvedClipPathEffectSpec {
|
|
61
|
+
type: "clipPath";
|
|
62
|
+
key: string;
|
|
63
|
+
source: RenderObjectSpec;
|
|
64
|
+
targetPassIds: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ManagedPassMeta {
|
|
68
|
+
id: string;
|
|
69
|
+
stack: number;
|
|
70
|
+
order: number;
|
|
71
|
+
visibility?: RenderPassSpec["visibility"];
|
|
72
|
+
}
|
|
5
73
|
|
|
6
74
|
export default class CanvasService implements Service {
|
|
7
75
|
public canvas: Canvas;
|
|
8
76
|
public viewport: ViewportSystem;
|
|
77
|
+
private context?: ServiceContext;
|
|
9
78
|
private eventBus?: EventBus;
|
|
79
|
+
private workbenchService?: WorkbenchService;
|
|
80
|
+
private toolSessionService?: ToolSessionService;
|
|
81
|
+
|
|
82
|
+
private renderProducers: Map<string, RenderProducerEntry> = new Map();
|
|
83
|
+
private producerOrder = 0;
|
|
84
|
+
private producerFlushRequested = false;
|
|
85
|
+
private producerLoopPending = false;
|
|
86
|
+
private producerLoopPromise: Promise<void> | null = null;
|
|
87
|
+
private producerApplyInProgress = false;
|
|
88
|
+
private visibilityRefreshScheduled = false;
|
|
89
|
+
|
|
90
|
+
private managedProducerPassIds: Set<string> = new Set();
|
|
91
|
+
private managedPassMetas: Map<string, ManagedPassMeta> = new Map();
|
|
92
|
+
|
|
93
|
+
private canvasForwardersBound = false;
|
|
94
|
+
private readonly forwardSelectionCreated = (e: any) => {
|
|
95
|
+
this.eventBus?.emit("selection:created", e);
|
|
96
|
+
};
|
|
97
|
+
private readonly forwardSelectionUpdated = (e: any) => {
|
|
98
|
+
this.eventBus?.emit("selection:updated", e);
|
|
99
|
+
};
|
|
100
|
+
private readonly forwardSelectionCleared = (e: any) => {
|
|
101
|
+
this.eventBus?.emit("selection:cleared", e);
|
|
102
|
+
};
|
|
103
|
+
private readonly forwardObjectModified = (e: any) => {
|
|
104
|
+
this.eventBus?.emit("object:modified", e);
|
|
105
|
+
};
|
|
106
|
+
private readonly forwardObjectAdded = (e: any) => {
|
|
107
|
+
this.eventBus?.emit("object:added", e);
|
|
108
|
+
};
|
|
109
|
+
private readonly forwardObjectRemoved = (e: any) => {
|
|
110
|
+
this.eventBus?.emit("object:removed", e);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
private readonly onToolActivated = () => {
|
|
114
|
+
this.applyManagedPassVisibility();
|
|
115
|
+
};
|
|
116
|
+
private readonly onToolSessionChanged = () => {
|
|
117
|
+
this.applyManagedPassVisibility();
|
|
118
|
+
};
|
|
119
|
+
private readonly onCanvasObjectChanged = () => {
|
|
120
|
+
if (this.producerApplyInProgress) return;
|
|
121
|
+
this.scheduleManagedPassVisibilityRefresh();
|
|
122
|
+
};
|
|
10
123
|
|
|
11
124
|
constructor(el: HTMLCanvasElement | string | Canvas, options?: any) {
|
|
12
125
|
if (el instanceof Canvas) {
|
|
@@ -22,73 +135,495 @@ export default class CanvasService implements Service {
|
|
|
22
135
|
if (this.canvas.width !== undefined && this.canvas.height !== undefined) {
|
|
23
136
|
this.viewport.updateContainer(this.canvas.width, this.canvas.height);
|
|
24
137
|
}
|
|
25
|
-
|
|
138
|
+
|
|
26
139
|
if (options?.eventBus) {
|
|
27
140
|
this.setEventBus(options.eventBus);
|
|
28
141
|
}
|
|
29
142
|
}
|
|
30
143
|
|
|
144
|
+
init(context: ServiceContext) {
|
|
145
|
+
if (this.context) {
|
|
146
|
+
this.detachContextEvents(this.context.eventBus);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.context = context;
|
|
150
|
+
this.workbenchService = context.get(WORKBENCH_SERVICE);
|
|
151
|
+
this.toolSessionService = context.get(TOOL_SESSION_SERVICE);
|
|
152
|
+
this.setEventBus(context.eventBus);
|
|
153
|
+
this.attachContextEvents(context.eventBus);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private attachContextEvents(eventBus: EventBus) {
|
|
157
|
+
eventBus.on("tool:activated", this.onToolActivated);
|
|
158
|
+
eventBus.on("tool:session:change", this.onToolSessionChanged);
|
|
159
|
+
eventBus.on("object:added", this.onCanvasObjectChanged);
|
|
160
|
+
eventBus.on("object:removed", this.onCanvasObjectChanged);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private detachContextEvents(eventBus: EventBus) {
|
|
164
|
+
eventBus.off("tool:activated", this.onToolActivated);
|
|
165
|
+
eventBus.off("tool:session:change", this.onToolSessionChanged);
|
|
166
|
+
eventBus.off("object:added", this.onCanvasObjectChanged);
|
|
167
|
+
eventBus.off("object:removed", this.onCanvasObjectChanged);
|
|
168
|
+
}
|
|
169
|
+
|
|
31
170
|
setEventBus(eventBus: EventBus) {
|
|
32
171
|
this.eventBus = eventBus;
|
|
33
172
|
this.setupEvents();
|
|
34
173
|
}
|
|
35
174
|
|
|
36
175
|
private setupEvents() {
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.canvas.on("
|
|
43
|
-
this.canvas.on("
|
|
44
|
-
this.
|
|
45
|
-
this.canvas.on("object:modified", forward("object:modified"));
|
|
46
|
-
this.canvas.on("object:added", forward("object:added"));
|
|
47
|
-
this.canvas.on("object:removed", forward("object:removed"));
|
|
176
|
+
if (this.canvasForwardersBound) return;
|
|
177
|
+
this.canvas.on("selection:created", this.forwardSelectionCreated);
|
|
178
|
+
this.canvas.on("selection:updated", this.forwardSelectionUpdated);
|
|
179
|
+
this.canvas.on("selection:cleared", this.forwardSelectionCleared);
|
|
180
|
+
this.canvas.on("object:modified", this.forwardObjectModified);
|
|
181
|
+
this.canvas.on("object:added", this.forwardObjectAdded);
|
|
182
|
+
this.canvas.on("object:removed", this.forwardObjectRemoved);
|
|
183
|
+
this.canvasForwardersBound = true;
|
|
48
184
|
}
|
|
49
185
|
|
|
50
186
|
dispose() {
|
|
187
|
+
if (this.context) {
|
|
188
|
+
this.detachContextEvents(this.context.eventBus);
|
|
189
|
+
}
|
|
190
|
+
this.renderProducers.clear();
|
|
191
|
+
this.managedProducerPassIds.clear();
|
|
192
|
+
this.managedPassMetas.clear();
|
|
193
|
+
this.context = undefined;
|
|
194
|
+
this.workbenchService = undefined;
|
|
195
|
+
this.toolSessionService = undefined;
|
|
196
|
+
this.producerFlushRequested = false;
|
|
51
197
|
this.canvas.dispose();
|
|
52
198
|
}
|
|
53
199
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
200
|
+
registerRenderProducer(
|
|
201
|
+
toolId: string,
|
|
202
|
+
producer: RenderProducer,
|
|
203
|
+
options: RegisterRenderProducerOptions = {},
|
|
204
|
+
): { dispose: () => void } {
|
|
205
|
+
const normalizedToolId = String(toolId || "").trim();
|
|
206
|
+
if (!normalizedToolId) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
"[CanvasService] registerRenderProducer requires a toolId.",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (typeof producer !== "function") {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`[CanvasService] registerRenderProducer("${normalizedToolId}") requires a producer function.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const entry: RenderProducerEntry = {
|
|
217
|
+
toolId: normalizedToolId,
|
|
218
|
+
producer,
|
|
219
|
+
priority: Number.isFinite(options.priority)
|
|
220
|
+
? Number(options.priority)
|
|
221
|
+
: 0,
|
|
222
|
+
order: this.producerOrder++,
|
|
223
|
+
};
|
|
224
|
+
this.renderProducers.set(normalizedToolId, entry);
|
|
225
|
+
this.requestRenderFromProducers();
|
|
226
|
+
return {
|
|
227
|
+
dispose: () => {
|
|
228
|
+
this.unregisterRenderProducer(normalizedToolId);
|
|
229
|
+
},
|
|
230
|
+
};
|
|
62
231
|
}
|
|
63
232
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
233
|
+
unregisterRenderProducer(toolId: string): boolean {
|
|
234
|
+
const normalizedToolId = String(toolId || "").trim();
|
|
235
|
+
if (!normalizedToolId) return false;
|
|
236
|
+
const removed = this.renderProducers.delete(normalizedToolId);
|
|
237
|
+
if (removed) {
|
|
238
|
+
this.requestRenderFromProducers();
|
|
239
|
+
}
|
|
240
|
+
return removed;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
requestRenderFromProducers() {
|
|
244
|
+
this.producerFlushRequested = true;
|
|
245
|
+
this.scheduleProducerLoop();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async flushRenderFromProducers(): Promise<void> {
|
|
249
|
+
this.requestRenderFromProducers();
|
|
250
|
+
if (this.producerLoopPromise) {
|
|
251
|
+
await this.producerLoopPromise;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private scheduleProducerLoop() {
|
|
256
|
+
if (this.producerLoopPending) return;
|
|
257
|
+
this.producerLoopPending = true;
|
|
258
|
+
this.producerLoopPromise = Promise.resolve()
|
|
259
|
+
.then(() => this.runProducerLoop())
|
|
260
|
+
.catch((error) => {
|
|
261
|
+
console.error("[CanvasService] render producer loop failed.", error);
|
|
262
|
+
})
|
|
263
|
+
.finally(() => {
|
|
264
|
+
this.producerLoopPending = false;
|
|
265
|
+
if (this.producerFlushRequested) {
|
|
266
|
+
this.scheduleProducerLoop();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async runProducerLoop(): Promise<void> {
|
|
272
|
+
while (this.producerFlushRequested) {
|
|
273
|
+
this.producerFlushRequested = false;
|
|
274
|
+
await this.collectAndApplyProducerSpecs();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private sortedRenderProducerEntries(): RenderProducerEntry[] {
|
|
279
|
+
return Array.from(this.renderProducers.values()).sort((a, b) => {
|
|
280
|
+
if (a.priority !== b.priority) {
|
|
281
|
+
return a.priority - b.priority;
|
|
282
|
+
}
|
|
283
|
+
if (a.order !== b.order) {
|
|
284
|
+
return a.order - b.order;
|
|
285
|
+
}
|
|
286
|
+
return a.toolId.localeCompare(b.toolId);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private normalizePassSpecValue(
|
|
291
|
+
spec: RenderPassSpec,
|
|
292
|
+
): ResolvedRenderPassSpec | null {
|
|
293
|
+
const id = String(spec.id || "").trim();
|
|
294
|
+
if (!id) return null;
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
id,
|
|
298
|
+
stack: Number.isFinite(spec.stack) ? Number(spec.stack) : 0,
|
|
299
|
+
order: Number.isFinite(spec.order) ? Number(spec.order) : 0,
|
|
300
|
+
replace: spec.replace !== false,
|
|
301
|
+
visibility: spec.visibility,
|
|
302
|
+
effects: Array.isArray(spec.effects)
|
|
303
|
+
? [...spec.effects]
|
|
304
|
+
: [],
|
|
305
|
+
objects: Array.isArray(spec.objects) ? [...spec.objects] : [],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private normalizeClipPathEffectSpec(
|
|
310
|
+
effect: RenderEffectSpec,
|
|
311
|
+
passId: string,
|
|
312
|
+
index: number,
|
|
313
|
+
): ResolvedClipPathEffectSpec | null {
|
|
314
|
+
if (!effect || effect.type !== "clipPath") return null;
|
|
315
|
+
|
|
316
|
+
const source = effect.source;
|
|
317
|
+
if (!source || typeof source !== "object") return null;
|
|
318
|
+
|
|
319
|
+
const sourceId = String(source.id || "").trim();
|
|
320
|
+
if (!sourceId) return null;
|
|
321
|
+
|
|
322
|
+
const targetPassIds = Array.isArray(effect.targetPassIds)
|
|
323
|
+
? effect.targetPassIds
|
|
324
|
+
.map((item) => String(item || "").trim())
|
|
325
|
+
.filter((item) => item.length > 0)
|
|
326
|
+
: [];
|
|
327
|
+
if (!targetPassIds.length) return null;
|
|
328
|
+
|
|
329
|
+
const customId = String((effect as any).id || "").trim();
|
|
330
|
+
const key = customId || `${passId}.effect.clipPath.${index}`;
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
type: "clipPath",
|
|
334
|
+
key,
|
|
335
|
+
source: {
|
|
336
|
+
...source,
|
|
337
|
+
id: sourceId,
|
|
338
|
+
},
|
|
339
|
+
targetPassIds,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private mergePassSpec(
|
|
344
|
+
map: Map<string, ResolvedRenderPassSpec>,
|
|
345
|
+
rawSpec: RenderPassSpec,
|
|
346
|
+
producerId: string,
|
|
347
|
+
) {
|
|
348
|
+
const normalized = this.normalizePassSpecValue(rawSpec);
|
|
349
|
+
if (!normalized) return;
|
|
350
|
+
|
|
351
|
+
const existing = map.get(normalized.id);
|
|
352
|
+
if (!existing) {
|
|
353
|
+
map.set(normalized.id, normalized);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
existing.objects.push(...normalized.objects);
|
|
358
|
+
existing.replace = existing.replace || normalized.replace;
|
|
359
|
+
existing.stack = normalized.stack;
|
|
360
|
+
existing.order = normalized.order;
|
|
361
|
+
if (normalized.visibility !== undefined) {
|
|
362
|
+
existing.visibility = normalized.visibility;
|
|
363
|
+
}
|
|
364
|
+
existing.effects.push(...normalized.effects);
|
|
365
|
+
|
|
366
|
+
if (normalized.objects.length === 0 && normalized.effects.length === 0) {
|
|
367
|
+
console.debug(
|
|
368
|
+
`[CanvasService] pass "${normalized.id}" from producer "${producerId}" updated ordering/visibility only.`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private comparePassMeta(a: ManagedPassMeta, b: ManagedPassMeta): number {
|
|
374
|
+
if (a.stack !== b.stack) return a.stack - b.stack;
|
|
375
|
+
if (a.order !== b.order) return a.order - b.order;
|
|
376
|
+
return a.id.localeCompare(b.id);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private getPassObjectOrder(obj: FabricObject): number {
|
|
380
|
+
const raw = Number((obj as any)?.data?.passOrder);
|
|
381
|
+
return Number.isFinite(raw) ? raw : Number.MAX_SAFE_INTEGER;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private getPassCanvasObjects(passId: string): FabricObject[] {
|
|
385
|
+
const all = this.canvas.getObjects();
|
|
386
|
+
return all
|
|
387
|
+
.filter((obj: any) => obj?.data?.passId === passId)
|
|
388
|
+
.sort((a, b) => {
|
|
389
|
+
const orderA = this.getPassObjectOrder(a as FabricObject);
|
|
390
|
+
const orderB = this.getPassObjectOrder(b as FabricObject);
|
|
391
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
392
|
+
return all.indexOf(a) - all.indexOf(b);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getPassObjects(passId: string): FabricObject[] {
|
|
397
|
+
return this.getPassCanvasObjects(passId);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
getRootLayerObjects(layerId: string): FabricObject[] {
|
|
401
|
+
return this.getPassCanvasObjects(layerId);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private isManagedPassObject(obj: FabricObject): boolean {
|
|
405
|
+
const passId = (obj as any)?.data?.passId;
|
|
406
|
+
return typeof passId === "string" && this.managedPassMetas.has(passId);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private syncManagedPassStacking(passes: ManagedPassMeta[]) {
|
|
410
|
+
const orderedPasses = [...passes].sort((a, b) => this.comparePassMeta(a, b));
|
|
411
|
+
if (!orderedPasses.length) return;
|
|
412
|
+
|
|
413
|
+
const canvasObjects = this.canvas.getObjects();
|
|
414
|
+
const managedObjects = canvasObjects.filter((obj: any) =>
|
|
415
|
+
this.isManagedPassObject(obj as FabricObject),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
if (!managedObjects.length) return;
|
|
419
|
+
|
|
420
|
+
const firstManagedIndex = managedObjects
|
|
421
|
+
.map((obj) => canvasObjects.indexOf(obj as any))
|
|
422
|
+
.filter((index) => index >= 0)
|
|
423
|
+
.reduce((min, value) => Math.min(min, value), Number.MAX_SAFE_INTEGER);
|
|
424
|
+
|
|
425
|
+
let targetIndex = Number.isFinite(firstManagedIndex)
|
|
426
|
+
? firstManagedIndex
|
|
427
|
+
: 0;
|
|
428
|
+
|
|
429
|
+
orderedPasses.forEach((meta) => {
|
|
430
|
+
const objects = this.getPassCanvasObjects(meta.id);
|
|
431
|
+
objects.forEach((obj) => {
|
|
432
|
+
this.moveObjectInCanvas(obj, targetIndex);
|
|
433
|
+
targetIndex += 1;
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private getPassRuntimeState(): Map<string, VisibilityLayerState> {
|
|
439
|
+
const state = new Map<string, VisibilityLayerState>();
|
|
440
|
+
|
|
441
|
+
const ensure = (passId: string): VisibilityLayerState => {
|
|
442
|
+
const id = String(passId || "").trim();
|
|
443
|
+
if (!id) return { exists: false, objectCount: 0 };
|
|
444
|
+
let item = state.get(id);
|
|
445
|
+
if (!item) {
|
|
446
|
+
item = { exists: false, objectCount: 0 };
|
|
447
|
+
state.set(id, item);
|
|
448
|
+
}
|
|
449
|
+
return item;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
this.canvas.getObjects().forEach((obj: any) => {
|
|
453
|
+
const passId = obj?.data?.passId;
|
|
454
|
+
if (typeof passId === "string") {
|
|
455
|
+
const item = ensure(passId);
|
|
456
|
+
item.exists = true;
|
|
457
|
+
item.objectCount += 1;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
this.managedPassMetas.forEach((meta) => {
|
|
462
|
+
const item = ensure(meta.id);
|
|
463
|
+
item.exists = true;
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
return state;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private applyManagedPassVisibility(options: { render?: boolean } = {}): boolean {
|
|
470
|
+
if (!this.managedPassMetas.size) return false;
|
|
471
|
+
const layers = this.getPassRuntimeState();
|
|
472
|
+
const activeToolId = this.workbenchService?.activeToolId ?? null;
|
|
473
|
+
const isSessionActive = (toolId: string) => {
|
|
474
|
+
if (!this.toolSessionService) return false;
|
|
475
|
+
return this.toolSessionService.getState(toolId).status === "active";
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
let changed = false;
|
|
479
|
+
|
|
480
|
+
this.managedPassMetas.forEach((meta) => {
|
|
481
|
+
const visible = evaluateVisibilityExpr(meta.visibility, {
|
|
482
|
+
activeToolId,
|
|
483
|
+
isSessionActive,
|
|
484
|
+
layers,
|
|
485
|
+
});
|
|
486
|
+
changed = this.setPassVisibility(meta.id, visible) || changed;
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
if (changed && options.render !== false) {
|
|
490
|
+
this.requestRenderAll();
|
|
78
491
|
}
|
|
79
|
-
return
|
|
492
|
+
return changed;
|
|
80
493
|
}
|
|
81
494
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
495
|
+
private scheduleManagedPassVisibilityRefresh() {
|
|
496
|
+
if (this.visibilityRefreshScheduled) return;
|
|
497
|
+
this.visibilityRefreshScheduled = true;
|
|
498
|
+
void Promise.resolve().then(() => {
|
|
499
|
+
this.visibilityRefreshScheduled = false;
|
|
500
|
+
this.applyManagedPassVisibility();
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private async collectAndApplyProducerSpecs(): Promise<void> {
|
|
505
|
+
const passes = new Map<string, ResolvedRenderPassSpec>();
|
|
506
|
+
const entries = this.sortedRenderProducerEntries();
|
|
507
|
+
|
|
508
|
+
this.producerApplyInProgress = true;
|
|
509
|
+
try {
|
|
510
|
+
for (const entry of entries) {
|
|
511
|
+
try {
|
|
512
|
+
const result = await entry.producer();
|
|
513
|
+
if (!result) continue;
|
|
514
|
+
const specs = Array.isArray(result.passes) ? result.passes : [];
|
|
515
|
+
specs.forEach((spec) => this.mergePassSpec(passes, spec, entry.toolId));
|
|
516
|
+
} catch (error) {
|
|
517
|
+
console.error(
|
|
518
|
+
`[CanvasService] render producer "${entry.toolId}" failed.`,
|
|
519
|
+
error,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const nextPassIds = new Set<string>();
|
|
525
|
+
const nextManagedPassMetas = new Map<string, ManagedPassMeta>();
|
|
526
|
+
const nextEffects: ResolvedClipPathEffectSpec[] = [];
|
|
527
|
+
|
|
528
|
+
for (const pass of passes.values()) {
|
|
529
|
+
nextPassIds.add(pass.id);
|
|
530
|
+
nextManagedPassMetas.set(pass.id, {
|
|
531
|
+
id: pass.id,
|
|
532
|
+
stack: pass.stack,
|
|
533
|
+
order: pass.order,
|
|
534
|
+
visibility: pass.visibility,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
await this.applyObjectSpecsToPass(pass.id, pass.objects, {
|
|
538
|
+
render: false,
|
|
539
|
+
replace: pass.replace,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
pass.effects.forEach((effect, index) => {
|
|
543
|
+
const normalized = this.normalizeClipPathEffectSpec(
|
|
544
|
+
effect,
|
|
545
|
+
pass.id,
|
|
546
|
+
index,
|
|
547
|
+
);
|
|
548
|
+
if (!normalized) return;
|
|
549
|
+
nextEffects.push(normalized);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
for (const passId of this.managedProducerPassIds) {
|
|
554
|
+
if (nextPassIds.has(passId)) continue;
|
|
555
|
+
await this.applyObjectSpecsToPass(passId, [], {
|
|
556
|
+
render: false,
|
|
557
|
+
replace: true,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
this.managedProducerPassIds = nextPassIds;
|
|
562
|
+
this.managedPassMetas = nextManagedPassMetas;
|
|
563
|
+
|
|
564
|
+
this.syncManagedPassStacking(Array.from(nextManagedPassMetas.values()));
|
|
565
|
+
await this.applyManagedPassEffects(nextEffects);
|
|
566
|
+
this.applyManagedPassVisibility({ render: false });
|
|
567
|
+
} finally {
|
|
568
|
+
this.producerApplyInProgress = false;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
this.requestRenderAll();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private async applyManagedPassEffects(effects: ResolvedClipPathEffectSpec[]) {
|
|
575
|
+
const effectTargetMap = new Map<FabricObject, ResolvedClipPathEffectSpec>();
|
|
576
|
+
|
|
577
|
+
for (const effect of effects) {
|
|
578
|
+
if (effect.type !== "clipPath") continue;
|
|
579
|
+
effect.targetPassIds.forEach((targetPassId) => {
|
|
580
|
+
this.getPassCanvasObjects(targetPassId).forEach((obj) => {
|
|
581
|
+
effectTargetMap.set(obj, effect);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const managedObjects = this.canvas.getObjects().filter((obj: any) =>
|
|
587
|
+
this.isManagedPassObject(obj as FabricObject),
|
|
588
|
+
) as FabricObject[];
|
|
589
|
+
|
|
590
|
+
const effectTemplateCache = new Map<string, FabricObject | null>();
|
|
591
|
+
|
|
592
|
+
for (const obj of managedObjects) {
|
|
593
|
+
const targetEffect = effectTargetMap.get(obj);
|
|
594
|
+
if (!targetEffect) {
|
|
595
|
+
this.clearClipPathEffectFromObject(obj as any);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
let template = effectTemplateCache.get(targetEffect.key);
|
|
600
|
+
if (template === undefined) {
|
|
601
|
+
template = await this.createClipPathTemplate(targetEffect);
|
|
602
|
+
effectTemplateCache.set(targetEffect.key, template);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!template) {
|
|
606
|
+
this.clearClipPathEffectFromObject(obj as any);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
await this.applyClipPathEffectToObject(
|
|
611
|
+
obj as any,
|
|
612
|
+
template,
|
|
613
|
+
targetEffect.key,
|
|
614
|
+
);
|
|
90
615
|
}
|
|
91
|
-
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
getObject(id: string, passId?: string): FabricObject | undefined {
|
|
619
|
+
const normalizedId = String(id || "").trim();
|
|
620
|
+
if (!normalizedId) return undefined;
|
|
621
|
+
|
|
622
|
+
return this.canvas.getObjects().find((obj: any) => {
|
|
623
|
+
if (obj?.data?.id !== normalizedId) return false;
|
|
624
|
+
if (!passId) return true;
|
|
625
|
+
return obj?.data?.passId === passId;
|
|
626
|
+
}) as FabricObject | undefined;
|
|
92
627
|
}
|
|
93
628
|
|
|
94
629
|
requestRenderAll() {
|
|
@@ -102,104 +637,452 @@ export default class CanvasService implements Service {
|
|
|
102
637
|
this.requestRenderAll();
|
|
103
638
|
}
|
|
104
639
|
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
640
|
+
getSceneScale(): number {
|
|
641
|
+
const scale = Number(this.viewport.scale);
|
|
642
|
+
return Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
108
643
|
}
|
|
109
644
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
645
|
+
getSceneOffset(): { x: number; y: number } {
|
|
646
|
+
const offset = this.viewport.offset;
|
|
647
|
+
const x = Number(offset.x);
|
|
648
|
+
const y = Number(offset.y);
|
|
649
|
+
return {
|
|
650
|
+
x: Number.isFinite(x) ? x : 0,
|
|
651
|
+
y: Number.isFinite(y) ? y : 0,
|
|
652
|
+
};
|
|
116
653
|
}
|
|
117
654
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
655
|
+
toScreenPoint(point: { x: number; y: number }): { x: number; y: number } {
|
|
656
|
+
const scale = this.getSceneScale();
|
|
657
|
+
const offset = this.getSceneOffset();
|
|
658
|
+
return {
|
|
659
|
+
x: point.x * scale + offset.x,
|
|
660
|
+
y: point.y * scale + offset.y,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
toScenePoint(point: { x: number; y: number }): { x: number; y: number } {
|
|
665
|
+
const scale = this.getSceneScale();
|
|
666
|
+
const offset = this.getSceneOffset();
|
|
667
|
+
return {
|
|
668
|
+
x: (point.x - offset.x) / scale,
|
|
669
|
+
y: (point.y - offset.y) / scale,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
toScreenLength(value: number): number {
|
|
674
|
+
return value * this.getSceneScale();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
toSceneLength(value: number): number {
|
|
678
|
+
return value / this.getSceneScale();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
toScreenRect(rect: {
|
|
682
|
+
left: number;
|
|
683
|
+
top: number;
|
|
684
|
+
width: number;
|
|
685
|
+
height: number;
|
|
686
|
+
}): { left: number; top: number; width: number; height: number } {
|
|
687
|
+
const start = this.toScreenPoint({ x: rect.left, y: rect.top });
|
|
688
|
+
return {
|
|
689
|
+
left: start.x,
|
|
690
|
+
top: start.y,
|
|
691
|
+
width: this.toScreenLength(rect.width),
|
|
692
|
+
height: this.toScreenLength(rect.height),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
toSceneRect(rect: {
|
|
697
|
+
left: number;
|
|
698
|
+
top: number;
|
|
699
|
+
width: number;
|
|
700
|
+
height: number;
|
|
701
|
+
}): { left: number; top: number; width: number; height: number } {
|
|
702
|
+
const start = this.toScenePoint({ x: rect.left, y: rect.top });
|
|
703
|
+
return {
|
|
704
|
+
left: start.x,
|
|
705
|
+
top: start.y,
|
|
706
|
+
width: this.toSceneLength(rect.width),
|
|
707
|
+
height: this.toSceneLength(rect.height),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
getSceneViewportRect(): {
|
|
712
|
+
left: number;
|
|
713
|
+
top: number;
|
|
714
|
+
width: number;
|
|
715
|
+
height: number;
|
|
716
|
+
} {
|
|
717
|
+
const width = Number(this.canvas.width || 0);
|
|
718
|
+
const height = Number(this.canvas.height || 0);
|
|
719
|
+
return this.toSceneRect({ left: 0, top: 0, width, height });
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
getScreenViewportRect(): RectLike {
|
|
723
|
+
return {
|
|
724
|
+
left: 0,
|
|
725
|
+
top: 0,
|
|
726
|
+
width: Number(this.canvas.width || 0),
|
|
727
|
+
height: Number(this.canvas.height || 0),
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private toSpaceRect(
|
|
732
|
+
rect: RectLike,
|
|
733
|
+
from: RenderCoordinateSpace,
|
|
734
|
+
to: RenderCoordinateSpace,
|
|
735
|
+
): RectLike {
|
|
736
|
+
if (from === to) return { ...rect };
|
|
737
|
+
if (from === "scene") {
|
|
738
|
+
return this.toScreenRect(rect);
|
|
739
|
+
}
|
|
740
|
+
return this.toSceneRect(rect);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private resolveLayoutLength(
|
|
744
|
+
value: RenderLayoutLength | undefined,
|
|
745
|
+
base: number,
|
|
746
|
+
): number | undefined {
|
|
747
|
+
if (typeof value === "number") {
|
|
748
|
+
return Number.isFinite(value) ? value : undefined;
|
|
749
|
+
}
|
|
750
|
+
if (typeof value !== "string") {
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
753
|
+
const raw = value.trim();
|
|
754
|
+
if (!raw) return undefined;
|
|
755
|
+
if (raw.endsWith("%")) {
|
|
756
|
+
const percent = parseFloat(raw.slice(0, -1));
|
|
757
|
+
if (!Number.isFinite(percent)) return undefined;
|
|
758
|
+
return (base * percent) / 100;
|
|
759
|
+
}
|
|
760
|
+
const parsed = parseFloat(raw);
|
|
761
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private resolveLayoutInsets(
|
|
765
|
+
inset: RenderLayoutLength | RenderLayoutInsets | undefined,
|
|
766
|
+
reference: RectLike,
|
|
767
|
+
): { top: number; right: number; bottom: number; left: number } {
|
|
768
|
+
if (typeof inset === "number" || typeof inset === "string") {
|
|
769
|
+
const all =
|
|
770
|
+
this.resolveLayoutLength(
|
|
771
|
+
inset,
|
|
772
|
+
Math.min(reference.width, reference.height),
|
|
773
|
+
) ?? 0;
|
|
774
|
+
return { top: all, right: all, bottom: all, left: all };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const source = inset || {};
|
|
778
|
+
const top = this.resolveLayoutLength(source.top, reference.height) ?? 0;
|
|
779
|
+
const right = this.resolveLayoutLength(source.right, reference.width) ?? 0;
|
|
780
|
+
const bottom =
|
|
781
|
+
this.resolveLayoutLength(source.bottom, reference.height) ?? 0;
|
|
782
|
+
const left = this.resolveLayoutLength(source.left, reference.width) ?? 0;
|
|
783
|
+
return { top, right, bottom, left };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private resolveLayoutReferenceRect(
|
|
787
|
+
layout: RenderObjectLayoutSpec,
|
|
788
|
+
space: RenderCoordinateSpace,
|
|
789
|
+
): RectLike {
|
|
790
|
+
if (layout.referenceRect) {
|
|
791
|
+
const sourceSpace: RenderCoordinateSpace =
|
|
792
|
+
layout.referenceRect.space || space;
|
|
793
|
+
return this.toSpaceRect(layout.referenceRect, sourceSpace, space);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const reference = layout.reference || "sceneViewport";
|
|
797
|
+
if (reference === "screenViewport") {
|
|
798
|
+
const screenRect = this.getScreenViewportRect();
|
|
799
|
+
return space === "screen" ? screenRect : this.toSceneRect(screenRect);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const sceneRect = this.getSceneViewportRect();
|
|
803
|
+
return space === "scene" ? sceneRect : this.toScreenRect(sceneRect);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private alignFactor(value: unknown): number {
|
|
807
|
+
if (value === "end") return 1;
|
|
808
|
+
if (value === "center") return 0.5;
|
|
809
|
+
return 0;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
private normalizeOriginX(value: unknown): "left" | "center" | "right" {
|
|
813
|
+
if (value === "center") return "center";
|
|
814
|
+
if (value === "right") return "right";
|
|
815
|
+
return "left";
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
private normalizeOriginY(value: unknown): "top" | "center" | "bottom" {
|
|
819
|
+
if (value === "center") return "center";
|
|
820
|
+
if (value === "bottom") return "bottom";
|
|
821
|
+
return "top";
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private originFactor(
|
|
825
|
+
value: "left" | "center" | "right" | "top" | "bottom",
|
|
826
|
+
): number {
|
|
827
|
+
if (value === "center") return 0.5;
|
|
828
|
+
if (value === "right" || value === "bottom") return 1;
|
|
829
|
+
return 0;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private resolveLayoutProps(
|
|
833
|
+
spec: RenderObjectSpec,
|
|
834
|
+
props: Record<string, any>,
|
|
835
|
+
): Record<string, any> {
|
|
836
|
+
const layout = spec.layout;
|
|
837
|
+
if (!layout) {
|
|
838
|
+
return { ...props };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const space: RenderCoordinateSpace = spec.space || "scene";
|
|
842
|
+
const reference = this.resolveLayoutReferenceRect(layout, space);
|
|
843
|
+
const inset = this.resolveLayoutInsets(layout.inset, reference);
|
|
844
|
+
const area: RectLike = {
|
|
845
|
+
left: reference.left + inset.left,
|
|
846
|
+
top: reference.top + inset.top,
|
|
847
|
+
width: Math.max(0, reference.width - inset.left - inset.right),
|
|
848
|
+
height: Math.max(0, reference.height - inset.top - inset.bottom),
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const next = { ...props };
|
|
852
|
+
const width =
|
|
853
|
+
this.resolveLayoutLength(layout.width, area.width) ??
|
|
854
|
+
(Number.isFinite(next.width) ? Number(next.width) : undefined);
|
|
855
|
+
const height =
|
|
856
|
+
this.resolveLayoutLength(layout.height, area.height) ??
|
|
857
|
+
(Number.isFinite(next.height) ? Number(next.height) : undefined);
|
|
858
|
+
|
|
859
|
+
if (width !== undefined) next.width = width;
|
|
860
|
+
if (height !== undefined) next.height = height;
|
|
861
|
+
|
|
862
|
+
const alignX = this.alignFactor(layout.alignX);
|
|
863
|
+
const alignY = this.alignFactor(layout.alignY);
|
|
864
|
+
const offsetX = this.resolveLayoutLength(layout.offsetX, area.width) ?? 0;
|
|
865
|
+
const offsetY = this.resolveLayoutLength(layout.offsetY, area.height) ?? 0;
|
|
866
|
+
const objectWidth = Number.isFinite(next.width) ? Number(next.width) : 0;
|
|
867
|
+
const objectHeight = Number.isFinite(next.height) ? Number(next.height) : 0;
|
|
868
|
+
|
|
869
|
+
const objectLeft =
|
|
870
|
+
area.left + (area.width - objectWidth) * alignX + offsetX;
|
|
871
|
+
const objectTop =
|
|
872
|
+
area.top + (area.height - objectHeight) * alignY + offsetY;
|
|
873
|
+
|
|
874
|
+
const originX = this.normalizeOriginX(next.originX);
|
|
875
|
+
const originY = this.normalizeOriginY(next.originY);
|
|
876
|
+
next.left = objectLeft + objectWidth * this.originFactor(originX);
|
|
877
|
+
next.top = objectTop + objectHeight * this.originFactor(originY);
|
|
878
|
+
return next;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
setPassVisibility(passId: string, visible: boolean): boolean {
|
|
882
|
+
const objects = this.getPassCanvasObjects(passId) as any[];
|
|
883
|
+
let changed = false;
|
|
884
|
+
|
|
885
|
+
objects.forEach((obj) => {
|
|
886
|
+
if (obj.visible === visible) return;
|
|
887
|
+
obj.set?.({ visible });
|
|
888
|
+
obj.setCoords?.();
|
|
889
|
+
changed = true;
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
return changed;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
setLayerVisibility(layerId: string, visible: boolean): boolean {
|
|
896
|
+
return this.setPassVisibility(layerId, visible);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
bringPassToFront(passId: string) {
|
|
900
|
+
const objects = this.getPassCanvasObjects(passId) as any[];
|
|
901
|
+
objects.forEach((obj) => this.canvas.bringObjectToFront(obj as any));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
bringLayerToFront(layerId: string) {
|
|
905
|
+
this.bringPassToFront(layerId);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async applyPassSpec(
|
|
909
|
+
spec: RenderPassSpec,
|
|
910
|
+
options: { render?: boolean } = {},
|
|
911
|
+
): Promise<void> {
|
|
912
|
+
await this.applyObjectSpecsToPass(spec.id, spec.objects, {
|
|
913
|
+
render: options.render,
|
|
914
|
+
replace: spec.replace !== false,
|
|
915
|
+
});
|
|
122
916
|
}
|
|
123
917
|
|
|
124
918
|
async applyObjectSpecsToRootLayer(
|
|
125
|
-
|
|
919
|
+
passId: string,
|
|
126
920
|
specs: RenderObjectSpec[],
|
|
921
|
+
options: { render?: boolean } = {},
|
|
127
922
|
): Promise<void> {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const id = obj?.data?.id;
|
|
132
|
-
if (typeof id === "string" && !desiredIds.has(id)) {
|
|
133
|
-
this.canvas.remove(obj);
|
|
134
|
-
}
|
|
923
|
+
await this.applyObjectSpecsToPass(passId, specs, {
|
|
924
|
+
render: options.render,
|
|
925
|
+
replace: true,
|
|
135
926
|
});
|
|
927
|
+
}
|
|
136
928
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
929
|
+
private normalizeObjectSpecs(specs: RenderObjectSpec[]): RenderObjectSpec[] {
|
|
930
|
+
const seen = new Set<string>();
|
|
931
|
+
const normalized: RenderObjectSpec[] = [];
|
|
932
|
+
|
|
933
|
+
(specs || []).forEach((spec) => {
|
|
934
|
+
const id = String(spec?.id || "").trim();
|
|
935
|
+
if (!id || seen.has(id)) return;
|
|
936
|
+
seen.add(id);
|
|
937
|
+
normalized.push({
|
|
938
|
+
...spec,
|
|
939
|
+
id,
|
|
940
|
+
});
|
|
141
941
|
});
|
|
142
942
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
let current = byId.get(spec.id);
|
|
146
|
-
if (
|
|
147
|
-
current &&
|
|
148
|
-
spec.type === "image" &&
|
|
149
|
-
spec.src &&
|
|
150
|
-
current.getSrc &&
|
|
151
|
-
current.getSrc() !== spec.src
|
|
152
|
-
) {
|
|
153
|
-
this.canvas.remove(current);
|
|
154
|
-
byId.delete(spec.id);
|
|
155
|
-
current = undefined;
|
|
156
|
-
}
|
|
943
|
+
return normalized;
|
|
944
|
+
}
|
|
157
945
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
byId.set(spec.id, created);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
946
|
+
private async cloneFabricObject(
|
|
947
|
+
source: FabricObject,
|
|
948
|
+
): Promise<FabricObject | undefined> {
|
|
949
|
+
const clone = (source as any).clone;
|
|
950
|
+
if (typeof clone !== "function") return undefined;
|
|
166
951
|
|
|
167
|
-
|
|
952
|
+
const result = clone.call(source);
|
|
953
|
+
if (!result || typeof result.then !== "function") {
|
|
954
|
+
return undefined;
|
|
168
955
|
}
|
|
169
956
|
|
|
170
|
-
|
|
957
|
+
try {
|
|
958
|
+
const copied = (await result) as FabricObject;
|
|
959
|
+
return copied;
|
|
960
|
+
} catch {
|
|
961
|
+
return undefined;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
private async createClipPathTemplate(
|
|
966
|
+
effect: ResolvedClipPathEffectSpec,
|
|
967
|
+
): Promise<FabricObject | null> {
|
|
968
|
+
const source = effect.source;
|
|
969
|
+
const sourceId = String(source.id || "").trim();
|
|
970
|
+
if (!sourceId) return null;
|
|
971
|
+
|
|
972
|
+
const template = await this.createFabricObject({
|
|
973
|
+
...source,
|
|
974
|
+
id: sourceId,
|
|
975
|
+
data: {
|
|
976
|
+
...(source.data || {}),
|
|
977
|
+
id: sourceId,
|
|
978
|
+
type: "clip-path-effect-template",
|
|
979
|
+
effectKey: effect.key,
|
|
980
|
+
},
|
|
981
|
+
props: {
|
|
982
|
+
...(source.props || {}),
|
|
983
|
+
selectable: false,
|
|
984
|
+
evented: false,
|
|
985
|
+
excludeFromExport: true,
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
if (!template) return null;
|
|
989
|
+
|
|
990
|
+
(template as any).set?.({
|
|
991
|
+
selectable: false,
|
|
992
|
+
evented: false,
|
|
993
|
+
excludeFromExport: true,
|
|
994
|
+
absolutePositioned: true,
|
|
995
|
+
});
|
|
996
|
+
(template as any).setCoords?.();
|
|
997
|
+
return template;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private isClipPathEffectManaged(target: any): boolean {
|
|
1001
|
+
return typeof target?.__pooderEffectClipKey === "string";
|
|
171
1002
|
}
|
|
172
1003
|
|
|
173
|
-
private
|
|
174
|
-
|
|
1004
|
+
private clearClipPathEffectFromObject(target: any) {
|
|
1005
|
+
if (!target) return;
|
|
1006
|
+
if (!this.isClipPathEffectManaged(target)) return;
|
|
1007
|
+
target.set?.({ clipPath: undefined });
|
|
1008
|
+
target.setCoords?.();
|
|
1009
|
+
delete target.__pooderEffectClipKey;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private async applyClipPathEffectToObject(
|
|
1013
|
+
target: any,
|
|
1014
|
+
clipTemplate: FabricObject,
|
|
1015
|
+
effectKey: string,
|
|
1016
|
+
) {
|
|
1017
|
+
if (!target) return;
|
|
1018
|
+
|
|
1019
|
+
const clipPath = await this.cloneFabricObject(clipTemplate);
|
|
1020
|
+
if (!clipPath) {
|
|
1021
|
+
this.clearClipPathEffectFromObject(target);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
(clipPath as any).set?.({
|
|
1026
|
+
selectable: false,
|
|
1027
|
+
evented: false,
|
|
1028
|
+
excludeFromExport: true,
|
|
1029
|
+
absolutePositioned: true,
|
|
1030
|
+
});
|
|
1031
|
+
(clipPath as any).setCoords?.();
|
|
1032
|
+
|
|
1033
|
+
target.set?.({ clipPath });
|
|
1034
|
+
target.setCoords?.();
|
|
1035
|
+
target.__pooderEffectClipKey = effectKey;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async applyObjectSpecsToPass(
|
|
1039
|
+
passId: string,
|
|
175
1040
|
specs: RenderObjectSpec[],
|
|
1041
|
+
options: {
|
|
1042
|
+
render?: boolean;
|
|
1043
|
+
replace?: boolean;
|
|
1044
|
+
} = {},
|
|
176
1045
|
): Promise<void> {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
1046
|
+
const normalizedPassId = String(passId || "").trim();
|
|
1047
|
+
if (!normalizedPassId) return;
|
|
1048
|
+
|
|
1049
|
+
const replace = options.replace !== false;
|
|
1050
|
+
const normalizedSpecs = this.normalizeObjectSpecs(specs);
|
|
1051
|
+
const desiredIds = new Set(normalizedSpecs.map((s) => s.id));
|
|
1052
|
+
|
|
1053
|
+
const existing = this.getPassCanvasObjects(normalizedPassId) as any[];
|
|
1054
|
+
if (replace) {
|
|
1055
|
+
existing.forEach((obj) => {
|
|
1056
|
+
const id = obj?.data?.id;
|
|
1057
|
+
if (typeof id === "string" && !desiredIds.has(id)) {
|
|
1058
|
+
this.canvas.remove(obj);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
185
1062
|
|
|
186
1063
|
const byId = new Map<string, any>();
|
|
187
|
-
|
|
1064
|
+
this.getPassCanvasObjects(normalizedPassId).forEach((obj: any) => {
|
|
188
1065
|
const id = obj?.data?.id;
|
|
189
1066
|
if (typeof id === "string") byId.set(id, obj);
|
|
190
1067
|
});
|
|
191
1068
|
|
|
192
|
-
for (let index = 0; index <
|
|
193
|
-
const spec =
|
|
1069
|
+
for (let index = 0; index < normalizedSpecs.length; index += 1) {
|
|
1070
|
+
const spec = normalizedSpecs[index];
|
|
194
1071
|
let current = byId.get(spec.id);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
1072
|
+
|
|
1073
|
+
if (spec.type === "path") {
|
|
1074
|
+
const nextPathData = this.readPathDataFromSpec(spec);
|
|
1075
|
+
if (!nextPathData || !nextPathData.trim()) {
|
|
1076
|
+
if (current) {
|
|
1077
|
+
this.canvas.remove(current);
|
|
1078
|
+
byId.delete(spec.id);
|
|
1079
|
+
}
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (current && this.shouldRecreateObject(current, spec)) {
|
|
1085
|
+
this.canvas.remove(current);
|
|
203
1086
|
byId.delete(spec.id);
|
|
204
1087
|
current = undefined;
|
|
205
1088
|
}
|
|
@@ -207,18 +1090,26 @@ export default class CanvasService implements Service {
|
|
|
207
1090
|
if (!current) {
|
|
208
1091
|
const created = await this.createFabricObject(spec);
|
|
209
1092
|
if (!created) continue;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
1093
|
+
this.patchFabricObject(created as any, spec, {
|
|
1094
|
+
passId: normalizedPassId,
|
|
1095
|
+
layerId: normalizedPassId,
|
|
1096
|
+
passOrder: index,
|
|
1097
|
+
});
|
|
1098
|
+
this.canvas.add(created as any);
|
|
1099
|
+
byId.set(spec.id, created);
|
|
1100
|
+
continue;
|
|
215
1101
|
}
|
|
216
1102
|
|
|
217
|
-
this.
|
|
1103
|
+
this.patchFabricObject(current, spec, {
|
|
1104
|
+
passId: normalizedPassId,
|
|
1105
|
+
layerId: normalizedPassId,
|
|
1106
|
+
passOrder: index,
|
|
1107
|
+
});
|
|
218
1108
|
}
|
|
219
1109
|
|
|
220
|
-
|
|
221
|
-
|
|
1110
|
+
if (options.render !== false) {
|
|
1111
|
+
this.requestRenderAll();
|
|
1112
|
+
}
|
|
222
1113
|
}
|
|
223
1114
|
|
|
224
1115
|
private patchFabricObject(
|
|
@@ -232,32 +1123,113 @@ export default class CanvasService implements Service {
|
|
|
232
1123
|
...(extraData || {}),
|
|
233
1124
|
id: spec.id,
|
|
234
1125
|
};
|
|
235
|
-
|
|
1126
|
+
nextData.__renderSourceKey = this.getSpecRenderSourceKey(spec);
|
|
1127
|
+
const props = this.resolveFabricProps(spec, spec.props || {});
|
|
1128
|
+
obj.set({ ...props, data: nextData });
|
|
236
1129
|
obj.setCoords();
|
|
237
1130
|
}
|
|
238
1131
|
|
|
239
|
-
private
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
1132
|
+
private readPathDataFromSpec(spec: RenderObjectSpec): string | undefined {
|
|
1133
|
+
if (spec.type !== "path") return undefined;
|
|
1134
|
+
const raw =
|
|
1135
|
+
(spec.props as any)?.path ||
|
|
1136
|
+
(spec.props as any)?.pathData;
|
|
1137
|
+
if (typeof raw !== "string") return undefined;
|
|
1138
|
+
return raw;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
private hashText(value: string): string {
|
|
1142
|
+
let hash = 2166136261;
|
|
1143
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1144
|
+
hash ^= value.charCodeAt(i);
|
|
1145
|
+
hash +=
|
|
1146
|
+
(hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
1147
|
+
}
|
|
1148
|
+
return (hash >>> 0).toString(16);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
private getSpecRenderSourceKey(spec: RenderObjectSpec): string {
|
|
1152
|
+
switch (spec.type) {
|
|
1153
|
+
case "path": {
|
|
1154
|
+
const pathData = this.readPathDataFromSpec(spec) || "";
|
|
1155
|
+
return `path:${this.hashText(pathData)}`;
|
|
1156
|
+
}
|
|
1157
|
+
case "image":
|
|
1158
|
+
return `image:${String(spec.src || "")}`;
|
|
1159
|
+
case "text":
|
|
1160
|
+
return `text:${String((spec.props as any)?.text ?? "")}`;
|
|
1161
|
+
case "rect":
|
|
1162
|
+
return "rect";
|
|
1163
|
+
default:
|
|
1164
|
+
return String(spec.type || "");
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
private shouldRecreateObject(current: any, spec: RenderObjectSpec): boolean {
|
|
1169
|
+
if (!current) return true;
|
|
1170
|
+
|
|
1171
|
+
const currentType = String(current?.type || "").toLowerCase();
|
|
1172
|
+
if (currentType !== spec.type) return true;
|
|
1173
|
+
|
|
1174
|
+
const expectedKey = this.getSpecRenderSourceKey(spec);
|
|
1175
|
+
const currentKey = String(current?.data?.__renderSourceKey || "");
|
|
1176
|
+
if (currentKey && expectedKey && currentKey !== expectedKey) return true;
|
|
1177
|
+
|
|
1178
|
+
if (spec.type === "image" && spec.src && current.getSrc) {
|
|
1179
|
+
return current.getSrc() !== spec.src;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private resolveFabricProps(
|
|
1186
|
+
spec: RenderObjectSpec,
|
|
1187
|
+
props: Record<string, any>,
|
|
1188
|
+
): Record<string, any> {
|
|
1189
|
+
const space: RenderCoordinateSpace = spec.space || "scene";
|
|
1190
|
+
const next = this.resolveLayoutProps(spec, props);
|
|
1191
|
+
if (space === "screen") {
|
|
1192
|
+
return next;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const hasLeft = Number.isFinite(next.left);
|
|
1196
|
+
const hasTop = Number.isFinite(next.top);
|
|
1197
|
+
if (hasLeft || hasTop) {
|
|
1198
|
+
const mapped = this.toScreenPoint({
|
|
1199
|
+
x: hasLeft ? Number(next.left) : 0,
|
|
1200
|
+
y: hasTop ? Number(next.top) : 0,
|
|
1201
|
+
});
|
|
1202
|
+
if (hasLeft) next.left = mapped.x;
|
|
1203
|
+
if (hasTop) next.top = mapped.y;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const rawScaleX = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
|
|
1207
|
+
const rawScaleY = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
|
|
1208
|
+
const sceneScale = this.getSceneScale();
|
|
1209
|
+
next.scaleX = rawScaleX * sceneScale;
|
|
1210
|
+
next.scaleY = rawScaleY * sceneScale;
|
|
1211
|
+
return next;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
private moveObjectInCanvas(obj: any, index: number) {
|
|
244
1215
|
if (!obj) return;
|
|
245
1216
|
|
|
246
|
-
const moveObjectTo = (
|
|
1217
|
+
const moveObjectTo = (this.canvas as any).moveObjectTo;
|
|
247
1218
|
if (typeof moveObjectTo === "function") {
|
|
248
|
-
moveObjectTo.call(
|
|
1219
|
+
moveObjectTo.call(this.canvas, obj, index);
|
|
249
1220
|
return;
|
|
250
1221
|
}
|
|
251
1222
|
|
|
252
|
-
const list = (
|
|
1223
|
+
const list = (this.canvas as any)._objects as any[] | undefined;
|
|
253
1224
|
if (!Array.isArray(list)) return;
|
|
254
1225
|
const from = list.indexOf(obj);
|
|
255
1226
|
if (from < 0 || from === index) return;
|
|
1227
|
+
|
|
256
1228
|
list.splice(from, 1);
|
|
257
1229
|
const target = Math.max(0, Math.min(index, list.length));
|
|
258
1230
|
list.splice(target, 0, obj);
|
|
259
|
-
if (typeof (
|
|
260
|
-
(
|
|
1231
|
+
if (typeof (this.canvas as any)._onStackOrderChanged === "function") {
|
|
1232
|
+
(this.canvas as any)._onStackOrderChanged();
|
|
261
1233
|
}
|
|
262
1234
|
}
|
|
263
1235
|
|
|
@@ -265,8 +1237,9 @@ export default class CanvasService implements Service {
|
|
|
265
1237
|
spec: RenderObjectSpec,
|
|
266
1238
|
): Promise<FabricObject | undefined> {
|
|
267
1239
|
if (spec.type === "rect") {
|
|
1240
|
+
const props = this.resolveFabricProps(spec, spec.props || {});
|
|
268
1241
|
const rect = new Rect({
|
|
269
|
-
...
|
|
1242
|
+
...props,
|
|
270
1243
|
data: { ...(spec.data || {}), id: spec.id },
|
|
271
1244
|
} as any);
|
|
272
1245
|
rect.setCoords();
|
|
@@ -274,10 +1247,11 @@ export default class CanvasService implements Service {
|
|
|
274
1247
|
}
|
|
275
1248
|
|
|
276
1249
|
if (spec.type === "path") {
|
|
277
|
-
const pathData =
|
|
1250
|
+
const pathData = this.readPathDataFromSpec(spec);
|
|
278
1251
|
if (!pathData) return undefined;
|
|
1252
|
+
const props = this.resolveFabricProps(spec, spec.props || {});
|
|
279
1253
|
const path = new Path(pathData, {
|
|
280
|
-
...
|
|
1254
|
+
...props,
|
|
281
1255
|
data: { ...(spec.data || {}), id: spec.id },
|
|
282
1256
|
} as any);
|
|
283
1257
|
path.setCoords();
|
|
@@ -287,14 +1261,26 @@ export default class CanvasService implements Service {
|
|
|
287
1261
|
if (spec.type === "image") {
|
|
288
1262
|
if (!spec.src) return undefined;
|
|
289
1263
|
const image = await Image.fromURL(spec.src, { crossOrigin: "anonymous" });
|
|
1264
|
+
const props = this.resolveFabricProps(spec, spec.props || {});
|
|
290
1265
|
image.set({
|
|
291
|
-
...
|
|
1266
|
+
...props,
|
|
292
1267
|
data: { ...(spec.data || {}), id: spec.id },
|
|
293
1268
|
} as any);
|
|
294
1269
|
image.setCoords();
|
|
295
1270
|
return image as any;
|
|
296
1271
|
}
|
|
297
1272
|
|
|
1273
|
+
if (spec.type === "text") {
|
|
1274
|
+
const content = String((spec.props as any)?.text ?? "");
|
|
1275
|
+
const props = this.resolveFabricProps(spec, spec.props || {});
|
|
1276
|
+
const text = new Text(content, {
|
|
1277
|
+
...props,
|
|
1278
|
+
data: { ...(spec.data || {}), id: spec.id },
|
|
1279
|
+
} as any);
|
|
1280
|
+
text.setCoords();
|
|
1281
|
+
return text as any;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
298
1284
|
return undefined;
|
|
299
1285
|
}
|
|
300
1286
|
}
|