@pooder/kit 5.3.0 → 5.4.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/CHANGELOG.md +12 -0
- package/dist/index.d.mts +249 -36
- package/dist/index.d.ts +249 -36
- package/dist/index.js +2374 -1049
- package/dist/index.mjs +2375 -1050
- package/package.json +1 -1
- package/src/extensions/background.ts +178 -85
- package/src/extensions/dieline.ts +1149 -1030
- package/src/extensions/dielineShape.ts +109 -0
- package/src/extensions/feature.ts +482 -366
- package/src/extensions/film.ts +148 -76
- package/src/extensions/geometry.ts +210 -44
- package/src/extensions/image.ts +244 -114
- package/src/extensions/ruler.ts +471 -268
- package/src/extensions/sceneLayoutModel.ts +28 -6
- package/src/extensions/sceneVisibility.ts +3 -10
- package/src/extensions/tracer.ts +1019 -980
- package/src/extensions/white-ink.ts +284 -231
- package/src/services/CanvasService.ts +543 -11
- package/src/services/renderSpec.ts +37 -2
- 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/coordinate.js +0 -74
- package/.test-dist/src/dieline.js +0 -818
- package/.test-dist/src/edgeScale.js +0 -12
- package/.test-dist/src/extensions/background.js +0 -203
- package/.test-dist/src/extensions/bridgeSelection.js +0 -20
- package/.test-dist/src/extensions/constraints.js +0 -237
- package/.test-dist/src/extensions/dieline.js +0 -828
- package/.test-dist/src/extensions/edgeScale.js +0 -12
- package/.test-dist/src/extensions/feature.js +0 -825
- package/.test-dist/src/extensions/featureComplete.js +0 -32
- package/.test-dist/src/extensions/film.js +0 -167
- package/.test-dist/src/extensions/geometry.js +0 -545
- package/.test-dist/src/extensions/image.js +0 -1529
- package/.test-dist/src/extensions/index.js +0 -30
- package/.test-dist/src/extensions/maskOps.js +0 -279
- package/.test-dist/src/extensions/mirror.js +0 -104
- package/.test-dist/src/extensions/ruler.js +0 -345
- package/.test-dist/src/extensions/sceneLayout.js +0 -96
- package/.test-dist/src/extensions/sceneLayoutModel.js +0 -196
- package/.test-dist/src/extensions/sceneVisibility.js +0 -62
- package/.test-dist/src/extensions/size.js +0 -331
- package/.test-dist/src/extensions/tracer.js +0 -538
- package/.test-dist/src/extensions/white-ink.js +0 -1190
- package/.test-dist/src/extensions/wrappedOffsets.js +0 -33
- 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/index.js +0 -18
- 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/services/CanvasService.js +0 -249
- package/.test-dist/src/services/ViewportSystem.js +0 -76
- package/.test-dist/src/services/index.js +0 -24
- package/.test-dist/src/services/renderSpec.js +0 -2
- package/.test-dist/src/size.js +0 -332
- package/.test-dist/src/tracer.js +0 -544
- package/.test-dist/src/units.js +0 -30
- package/.test-dist/src/white-ink.js +0 -829
- package/.test-dist/src/wrappedOffsets.js +0 -33
- package/.test-dist/tests/run.js +0 -94
|
@@ -1,12 +1,56 @@
|
|
|
1
|
-
import { Canvas, Group, FabricObject, Rect, Path, Image } from "fabric";
|
|
1
|
+
import { Canvas, Group, FabricObject, Rect, Path, Image, Text } from "fabric";
|
|
2
2
|
import { Service, EventBus } from "@pooder/core";
|
|
3
3
|
import { ViewportSystem } from "./ViewportSystem";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
RenderCoordinateSpace,
|
|
6
|
+
RenderLayerSpec,
|
|
7
|
+
RenderLayoutInsets,
|
|
8
|
+
RenderLayoutLength,
|
|
9
|
+
RenderObjectLayoutSpec,
|
|
10
|
+
RenderObjectSpec,
|
|
11
|
+
} from "./renderSpec";
|
|
12
|
+
|
|
13
|
+
export interface RenderProducerResult {
|
|
14
|
+
layerSpecs?: Record<string, RenderObjectSpec[]>;
|
|
15
|
+
rootLayerSpecs?: Record<string, RenderObjectSpec[]>;
|
|
16
|
+
replaceLayerIds?: string[];
|
|
17
|
+
replaceRootLayerIds?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type RenderProducer = () =>
|
|
21
|
+
| RenderProducerResult
|
|
22
|
+
| undefined
|
|
23
|
+
| Promise<RenderProducerResult | undefined>;
|
|
24
|
+
|
|
25
|
+
export interface RegisterRenderProducerOptions {
|
|
26
|
+
priority?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface RenderProducerEntry {
|
|
30
|
+
toolId: string;
|
|
31
|
+
producer: RenderProducer;
|
|
32
|
+
priority: number;
|
|
33
|
+
order: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RectLike {
|
|
37
|
+
left: number;
|
|
38
|
+
top: number;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
}
|
|
5
42
|
|
|
6
43
|
export default class CanvasService implements Service {
|
|
7
44
|
public canvas: Canvas;
|
|
8
45
|
public viewport: ViewportSystem;
|
|
9
46
|
private eventBus?: EventBus;
|
|
47
|
+
private renderProducers: Map<string, RenderProducerEntry> = new Map();
|
|
48
|
+
private producerOrder = 0;
|
|
49
|
+
private producerFlushRequested = false;
|
|
50
|
+
private producerLoopPending = false;
|
|
51
|
+
private producerLoopPromise: Promise<void> | null = null;
|
|
52
|
+
private managedProducerLayerIds: Set<string> = new Set();
|
|
53
|
+
private managedProducerRootLayerIds: Set<string> = new Set();
|
|
10
54
|
|
|
11
55
|
constructor(el: HTMLCanvasElement | string | Canvas, options?: any) {
|
|
12
56
|
if (el instanceof Canvas) {
|
|
@@ -22,7 +66,7 @@ export default class CanvasService implements Service {
|
|
|
22
66
|
if (this.canvas.width !== undefined && this.canvas.height !== undefined) {
|
|
23
67
|
this.viewport.updateContainer(this.canvas.width, this.canvas.height);
|
|
24
68
|
}
|
|
25
|
-
|
|
69
|
+
|
|
26
70
|
if (options?.eventBus) {
|
|
27
71
|
this.setEventBus(options.eventBus);
|
|
28
72
|
}
|
|
@@ -48,9 +92,185 @@ export default class CanvasService implements Service {
|
|
|
48
92
|
}
|
|
49
93
|
|
|
50
94
|
dispose() {
|
|
95
|
+
this.renderProducers.clear();
|
|
96
|
+
this.managedProducerLayerIds.clear();
|
|
97
|
+
this.managedProducerRootLayerIds.clear();
|
|
98
|
+
this.producerFlushRequested = false;
|
|
51
99
|
this.canvas.dispose();
|
|
52
100
|
}
|
|
53
101
|
|
|
102
|
+
registerRenderProducer(
|
|
103
|
+
toolId: string,
|
|
104
|
+
producer: RenderProducer,
|
|
105
|
+
options: RegisterRenderProducerOptions = {},
|
|
106
|
+
): { dispose: () => void } {
|
|
107
|
+
const normalizedToolId = String(toolId || "").trim();
|
|
108
|
+
if (!normalizedToolId) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"[CanvasService] registerRenderProducer requires a toolId.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (typeof producer !== "function") {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`[CanvasService] registerRenderProducer("${normalizedToolId}") requires a producer function.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const entry: RenderProducerEntry = {
|
|
119
|
+
toolId: normalizedToolId,
|
|
120
|
+
producer,
|
|
121
|
+
priority: Number.isFinite(options.priority)
|
|
122
|
+
? Number(options.priority)
|
|
123
|
+
: 0,
|
|
124
|
+
order: this.producerOrder++,
|
|
125
|
+
};
|
|
126
|
+
this.renderProducers.set(normalizedToolId, entry);
|
|
127
|
+
this.requestRenderFromProducers();
|
|
128
|
+
return {
|
|
129
|
+
dispose: () => {
|
|
130
|
+
this.unregisterRenderProducer(normalizedToolId);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
unregisterRenderProducer(toolId: string): boolean {
|
|
136
|
+
const normalizedToolId = String(toolId || "").trim();
|
|
137
|
+
if (!normalizedToolId) return false;
|
|
138
|
+
const removed = this.renderProducers.delete(normalizedToolId);
|
|
139
|
+
if (removed) {
|
|
140
|
+
this.requestRenderFromProducers();
|
|
141
|
+
}
|
|
142
|
+
return removed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
requestRenderFromProducers() {
|
|
146
|
+
this.producerFlushRequested = true;
|
|
147
|
+
this.scheduleProducerLoop();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async flushRenderFromProducers(): Promise<void> {
|
|
151
|
+
this.requestRenderFromProducers();
|
|
152
|
+
if (this.producerLoopPromise) {
|
|
153
|
+
await this.producerLoopPromise;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private scheduleProducerLoop() {
|
|
158
|
+
if (this.producerLoopPending) return;
|
|
159
|
+
this.producerLoopPending = true;
|
|
160
|
+
this.producerLoopPromise = Promise.resolve()
|
|
161
|
+
.then(() => this.runProducerLoop())
|
|
162
|
+
.catch((error) => {
|
|
163
|
+
console.error("[CanvasService] render producer loop failed.", error);
|
|
164
|
+
})
|
|
165
|
+
.finally(() => {
|
|
166
|
+
this.producerLoopPending = false;
|
|
167
|
+
if (this.producerFlushRequested) {
|
|
168
|
+
this.scheduleProducerLoop();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async runProducerLoop(): Promise<void> {
|
|
174
|
+
while (this.producerFlushRequested) {
|
|
175
|
+
this.producerFlushRequested = false;
|
|
176
|
+
await this.collectAndApplyProducerSpecs();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private sortedRenderProducerEntries(): RenderProducerEntry[] {
|
|
181
|
+
return Array.from(this.renderProducers.values()).sort((a, b) => {
|
|
182
|
+
if (a.priority !== b.priority) {
|
|
183
|
+
return a.priority - b.priority;
|
|
184
|
+
}
|
|
185
|
+
if (a.order !== b.order) {
|
|
186
|
+
return a.order - b.order;
|
|
187
|
+
}
|
|
188
|
+
return a.toolId.localeCompare(b.toolId);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private appendLayerSpecMap(
|
|
193
|
+
map: Map<string, RenderObjectSpec[]>,
|
|
194
|
+
source?: Record<string, RenderObjectSpec[]>,
|
|
195
|
+
) {
|
|
196
|
+
if (!source) return;
|
|
197
|
+
Object.entries(source).forEach(([layerId, specs]) => {
|
|
198
|
+
if (!Array.isArray(specs)) return;
|
|
199
|
+
const list = map.get(layerId) || [];
|
|
200
|
+
list.push(...specs);
|
|
201
|
+
map.set(layerId, list);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async collectAndApplyProducerSpecs(): Promise<void> {
|
|
206
|
+
const groupLayerSpecs = new Map<string, RenderObjectSpec[]>();
|
|
207
|
+
const rootLayerSpecs = new Map<string, RenderObjectSpec[]>();
|
|
208
|
+
const replaceLayerIds = new Set<string>();
|
|
209
|
+
const replaceRootLayerIds = new Set<string>();
|
|
210
|
+
const entries = this.sortedRenderProducerEntries();
|
|
211
|
+
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
try {
|
|
214
|
+
const result = await entry.producer();
|
|
215
|
+
if (!result) continue;
|
|
216
|
+
this.appendLayerSpecMap(groupLayerSpecs, result.layerSpecs);
|
|
217
|
+
this.appendLayerSpecMap(rootLayerSpecs, result.rootLayerSpecs);
|
|
218
|
+
if (Array.isArray(result.replaceLayerIds)) {
|
|
219
|
+
result.replaceLayerIds.forEach((layerId) => {
|
|
220
|
+
if (layerId) replaceLayerIds.add(layerId);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(result.replaceRootLayerIds)) {
|
|
224
|
+
result.replaceRootLayerIds.forEach((layerId) => {
|
|
225
|
+
if (layerId) replaceRootLayerIds.add(layerId);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error(
|
|
230
|
+
`[CanvasService] render producer "${entry.toolId}" failed.`,
|
|
231
|
+
error,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const nextLayerIds = new Set(groupLayerSpecs.keys());
|
|
237
|
+
const nextRootLayerIds = new Set(rootLayerSpecs.keys());
|
|
238
|
+
|
|
239
|
+
for (const [layerId, specs] of groupLayerSpecs.entries()) {
|
|
240
|
+
if (replaceLayerIds.has(layerId)) {
|
|
241
|
+
const layer = this.getLayer(layerId);
|
|
242
|
+
if (layer) {
|
|
243
|
+
(layer.getObjects() as any[]).forEach((obj) => layer.remove(obj));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
await this.applyObjectSpecsToLayer(layerId, specs, { render: false });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const layerId of this.managedProducerLayerIds) {
|
|
250
|
+
if (nextLayerIds.has(layerId)) continue;
|
|
251
|
+
const layer = this.getLayer(layerId);
|
|
252
|
+
if (!layer) continue;
|
|
253
|
+
await this.applyObjectSpecsToContainer(layer, [], { render: false });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const [layerId, specs] of rootLayerSpecs.entries()) {
|
|
257
|
+
if (replaceRootLayerIds.has(layerId)) {
|
|
258
|
+
const existing = this.getRootLayerObjects(layerId) as any[];
|
|
259
|
+
existing.forEach((obj) => this.canvas.remove(obj));
|
|
260
|
+
}
|
|
261
|
+
await this.applyObjectSpecsToRootLayer(layerId, specs, { render: false });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const layerId of this.managedProducerRootLayerIds) {
|
|
265
|
+
if (nextRootLayerIds.has(layerId)) continue;
|
|
266
|
+
await this.applyObjectSpecsToRootLayer(layerId, [], { render: false });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.managedProducerLayerIds = nextLayerIds;
|
|
270
|
+
this.managedProducerRootLayerIds = nextRootLayerIds;
|
|
271
|
+
this.requestRenderAll();
|
|
272
|
+
}
|
|
273
|
+
|
|
54
274
|
/**
|
|
55
275
|
* Get a layer (Group) by its ID.
|
|
56
276
|
* We assume layers are Groups directly on the canvas with a data.id property.
|
|
@@ -102,6 +322,268 @@ export default class CanvasService implements Service {
|
|
|
102
322
|
this.requestRenderAll();
|
|
103
323
|
}
|
|
104
324
|
|
|
325
|
+
getSceneScale(): number {
|
|
326
|
+
const scale = Number(this.viewport.scale);
|
|
327
|
+
return Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getSceneOffset(): { x: number; y: number } {
|
|
331
|
+
const offset = this.viewport.offset;
|
|
332
|
+
const x = Number(offset.x);
|
|
333
|
+
const y = Number(offset.y);
|
|
334
|
+
return {
|
|
335
|
+
x: Number.isFinite(x) ? x : 0,
|
|
336
|
+
y: Number.isFinite(y) ? y : 0,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
toScreenPoint(point: { x: number; y: number }): { x: number; y: number } {
|
|
341
|
+
const scale = this.getSceneScale();
|
|
342
|
+
const offset = this.getSceneOffset();
|
|
343
|
+
return {
|
|
344
|
+
x: point.x * scale + offset.x,
|
|
345
|
+
y: point.y * scale + offset.y,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
toScenePoint(point: { x: number; y: number }): { x: number; y: number } {
|
|
350
|
+
const scale = this.getSceneScale();
|
|
351
|
+
const offset = this.getSceneOffset();
|
|
352
|
+
return {
|
|
353
|
+
x: (point.x - offset.x) / scale,
|
|
354
|
+
y: (point.y - offset.y) / scale,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
toScreenLength(value: number): number {
|
|
359
|
+
return value * this.getSceneScale();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
toSceneLength(value: number): number {
|
|
363
|
+
return value / this.getSceneScale();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
toScreenRect(rect: {
|
|
367
|
+
left: number;
|
|
368
|
+
top: number;
|
|
369
|
+
width: number;
|
|
370
|
+
height: number;
|
|
371
|
+
}): { left: number; top: number; width: number; height: number } {
|
|
372
|
+
const start = this.toScreenPoint({ x: rect.left, y: rect.top });
|
|
373
|
+
return {
|
|
374
|
+
left: start.x,
|
|
375
|
+
top: start.y,
|
|
376
|
+
width: this.toScreenLength(rect.width),
|
|
377
|
+
height: this.toScreenLength(rect.height),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
toSceneRect(rect: {
|
|
382
|
+
left: number;
|
|
383
|
+
top: number;
|
|
384
|
+
width: number;
|
|
385
|
+
height: number;
|
|
386
|
+
}): { left: number; top: number; width: number; height: number } {
|
|
387
|
+
const start = this.toScenePoint({ x: rect.left, y: rect.top });
|
|
388
|
+
return {
|
|
389
|
+
left: start.x,
|
|
390
|
+
top: start.y,
|
|
391
|
+
width: this.toSceneLength(rect.width),
|
|
392
|
+
height: this.toSceneLength(rect.height),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getSceneViewportRect(): {
|
|
397
|
+
left: number;
|
|
398
|
+
top: number;
|
|
399
|
+
width: number;
|
|
400
|
+
height: number;
|
|
401
|
+
} {
|
|
402
|
+
const width = Number(this.canvas.width || 0);
|
|
403
|
+
const height = Number(this.canvas.height || 0);
|
|
404
|
+
return this.toSceneRect({ left: 0, top: 0, width, height });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
getScreenViewportRect(): RectLike {
|
|
408
|
+
return {
|
|
409
|
+
left: 0,
|
|
410
|
+
top: 0,
|
|
411
|
+
width: Number(this.canvas.width || 0),
|
|
412
|
+
height: Number(this.canvas.height || 0),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private toSpaceRect(
|
|
417
|
+
rect: RectLike,
|
|
418
|
+
from: RenderCoordinateSpace,
|
|
419
|
+
to: RenderCoordinateSpace,
|
|
420
|
+
): RectLike {
|
|
421
|
+
if (from === to) return { ...rect };
|
|
422
|
+
if (from === "scene") {
|
|
423
|
+
return this.toScreenRect(rect);
|
|
424
|
+
}
|
|
425
|
+
return this.toSceneRect(rect);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private resolveLayoutLength(
|
|
429
|
+
value: RenderLayoutLength | undefined,
|
|
430
|
+
base: number,
|
|
431
|
+
): number | undefined {
|
|
432
|
+
if (typeof value === "number") {
|
|
433
|
+
return Number.isFinite(value) ? value : undefined;
|
|
434
|
+
}
|
|
435
|
+
if (typeof value !== "string") {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
const raw = value.trim();
|
|
439
|
+
if (!raw) return undefined;
|
|
440
|
+
if (raw.endsWith("%")) {
|
|
441
|
+
const percent = parseFloat(raw.slice(0, -1));
|
|
442
|
+
if (!Number.isFinite(percent)) return undefined;
|
|
443
|
+
return (base * percent) / 100;
|
|
444
|
+
}
|
|
445
|
+
const parsed = parseFloat(raw);
|
|
446
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private resolveLayoutInsets(
|
|
450
|
+
inset: RenderLayoutLength | RenderLayoutInsets | undefined,
|
|
451
|
+
reference: RectLike,
|
|
452
|
+
): { top: number; right: number; bottom: number; left: number } {
|
|
453
|
+
if (typeof inset === "number" || typeof inset === "string") {
|
|
454
|
+
const all =
|
|
455
|
+
this.resolveLayoutLength(
|
|
456
|
+
inset,
|
|
457
|
+
Math.min(reference.width, reference.height),
|
|
458
|
+
) ?? 0;
|
|
459
|
+
return { top: all, right: all, bottom: all, left: all };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const source = inset || {};
|
|
463
|
+
const top = this.resolveLayoutLength(source.top, reference.height) ?? 0;
|
|
464
|
+
const right = this.resolveLayoutLength(source.right, reference.width) ?? 0;
|
|
465
|
+
const bottom =
|
|
466
|
+
this.resolveLayoutLength(source.bottom, reference.height) ?? 0;
|
|
467
|
+
const left = this.resolveLayoutLength(source.left, reference.width) ?? 0;
|
|
468
|
+
return { top, right, bottom, left };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private resolveLayoutReferenceRect(
|
|
472
|
+
layout: RenderObjectLayoutSpec,
|
|
473
|
+
space: RenderCoordinateSpace,
|
|
474
|
+
): RectLike {
|
|
475
|
+
if (layout.referenceRect) {
|
|
476
|
+
const sourceSpace: RenderCoordinateSpace =
|
|
477
|
+
layout.referenceRect.space || space;
|
|
478
|
+
return this.toSpaceRect(layout.referenceRect, sourceSpace, space);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const reference = layout.reference || "sceneViewport";
|
|
482
|
+
if (reference === "screenViewport") {
|
|
483
|
+
const screenRect = this.getScreenViewportRect();
|
|
484
|
+
return space === "screen" ? screenRect : this.toSceneRect(screenRect);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const sceneRect = this.getSceneViewportRect();
|
|
488
|
+
return space === "scene" ? sceneRect : this.toScreenRect(sceneRect);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private alignFactor(value: unknown): number {
|
|
492
|
+
if (value === "end") return 1;
|
|
493
|
+
if (value === "center") return 0.5;
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private normalizeOriginX(value: unknown): "left" | "center" | "right" {
|
|
498
|
+
if (value === "center") return "center";
|
|
499
|
+
if (value === "right") return "right";
|
|
500
|
+
return "left";
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private normalizeOriginY(value: unknown): "top" | "center" | "bottom" {
|
|
504
|
+
if (value === "center") return "center";
|
|
505
|
+
if (value === "bottom") return "bottom";
|
|
506
|
+
return "top";
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private originFactor(
|
|
510
|
+
value: "left" | "center" | "right" | "top" | "bottom",
|
|
511
|
+
): number {
|
|
512
|
+
if (value === "center") return 0.5;
|
|
513
|
+
if (value === "right" || value === "bottom") return 1;
|
|
514
|
+
return 0;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private resolveLayoutProps(
|
|
518
|
+
spec: RenderObjectSpec,
|
|
519
|
+
props: Record<string, any>,
|
|
520
|
+
): Record<string, any> {
|
|
521
|
+
const layout = spec.layout;
|
|
522
|
+
if (!layout) {
|
|
523
|
+
return { ...props };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const space: RenderCoordinateSpace = spec.space || "scene";
|
|
527
|
+
const reference = this.resolveLayoutReferenceRect(layout, space);
|
|
528
|
+
const inset = this.resolveLayoutInsets(layout.inset, reference);
|
|
529
|
+
const area: RectLike = {
|
|
530
|
+
left: reference.left + inset.left,
|
|
531
|
+
top: reference.top + inset.top,
|
|
532
|
+
width: Math.max(0, reference.width - inset.left - inset.right),
|
|
533
|
+
height: Math.max(0, reference.height - inset.top - inset.bottom),
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const next = { ...props };
|
|
537
|
+
const width =
|
|
538
|
+
this.resolveLayoutLength(layout.width, area.width) ??
|
|
539
|
+
(Number.isFinite(next.width) ? Number(next.width) : undefined);
|
|
540
|
+
const height =
|
|
541
|
+
this.resolveLayoutLength(layout.height, area.height) ??
|
|
542
|
+
(Number.isFinite(next.height) ? Number(next.height) : undefined);
|
|
543
|
+
|
|
544
|
+
if (width !== undefined) next.width = width;
|
|
545
|
+
if (height !== undefined) next.height = height;
|
|
546
|
+
|
|
547
|
+
const alignX = this.alignFactor(layout.alignX);
|
|
548
|
+
const alignY = this.alignFactor(layout.alignY);
|
|
549
|
+
const offsetX = this.resolveLayoutLength(layout.offsetX, area.width) ?? 0;
|
|
550
|
+
const offsetY = this.resolveLayoutLength(layout.offsetY, area.height) ?? 0;
|
|
551
|
+
const objectWidth = Number.isFinite(next.width) ? Number(next.width) : 0;
|
|
552
|
+
const objectHeight = Number.isFinite(next.height) ? Number(next.height) : 0;
|
|
553
|
+
|
|
554
|
+
const objectLeft =
|
|
555
|
+
area.left + (area.width - objectWidth) * alignX + offsetX;
|
|
556
|
+
const objectTop =
|
|
557
|
+
area.top + (area.height - objectHeight) * alignY + offsetY;
|
|
558
|
+
|
|
559
|
+
const originX = this.normalizeOriginX(next.originX);
|
|
560
|
+
const originY = this.normalizeOriginY(next.originY);
|
|
561
|
+
next.left = objectLeft + objectWidth * this.originFactor(originX);
|
|
562
|
+
next.top = objectTop + objectHeight * this.originFactor(originY);
|
|
563
|
+
return next;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
setLayerVisibility(layerId: string, visible: boolean) {
|
|
567
|
+
const layer = this.getLayer(layerId);
|
|
568
|
+
if (layer) {
|
|
569
|
+
layer.set({ visible });
|
|
570
|
+
}
|
|
571
|
+
const objects = this.getRootLayerObjects(layerId) as any[];
|
|
572
|
+
objects.forEach((obj) => {
|
|
573
|
+
obj.set?.({ visible });
|
|
574
|
+
obj.setCoords?.();
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
bringLayerToFront(layerId: string) {
|
|
579
|
+
const layer = this.getLayer(layerId);
|
|
580
|
+
if (layer) {
|
|
581
|
+
this.canvas.bringObjectToFront(layer);
|
|
582
|
+
}
|
|
583
|
+
const objects = this.getRootLayerObjects(layerId) as any[];
|
|
584
|
+
objects.forEach((obj) => this.canvas.bringObjectToFront(obj as any));
|
|
585
|
+
}
|
|
586
|
+
|
|
105
587
|
async applyLayerSpec(spec: RenderLayerSpec): Promise<void> {
|
|
106
588
|
const layer = this.createLayer(spec.id, spec.props || {});
|
|
107
589
|
await this.applyObjectSpecsToContainer(layer, spec.objects);
|
|
@@ -110,9 +592,10 @@ export default class CanvasService implements Service {
|
|
|
110
592
|
async applyObjectSpecsToLayer(
|
|
111
593
|
layerId: string,
|
|
112
594
|
objects: RenderObjectSpec[],
|
|
595
|
+
options: { render?: boolean } = {},
|
|
113
596
|
): Promise<void> {
|
|
114
597
|
const layer = this.createLayer(layerId, {});
|
|
115
|
-
await this.applyObjectSpecsToContainer(layer, objects);
|
|
598
|
+
await this.applyObjectSpecsToContainer(layer, objects, options);
|
|
116
599
|
}
|
|
117
600
|
|
|
118
601
|
getRootLayerObjects(layerId: string): FabricObject[] {
|
|
@@ -124,6 +607,7 @@ export default class CanvasService implements Service {
|
|
|
124
607
|
async applyObjectSpecsToRootLayer(
|
|
125
608
|
layerId: string,
|
|
126
609
|
specs: RenderObjectSpec[],
|
|
610
|
+
options: { render?: boolean } = {},
|
|
127
611
|
): Promise<void> {
|
|
128
612
|
const desiredIds = new Set(specs.map((s) => s.id));
|
|
129
613
|
const existing = this.getRootLayerObjects(layerId) as any[];
|
|
@@ -167,12 +651,15 @@ export default class CanvasService implements Service {
|
|
|
167
651
|
this.patchFabricObject(current, spec, { layerId });
|
|
168
652
|
}
|
|
169
653
|
|
|
170
|
-
|
|
654
|
+
if (options.render !== false) {
|
|
655
|
+
this.requestRenderAll();
|
|
656
|
+
}
|
|
171
657
|
}
|
|
172
658
|
|
|
173
659
|
private async applyObjectSpecsToContainer(
|
|
174
660
|
container: Group,
|
|
175
661
|
specs: RenderObjectSpec[],
|
|
662
|
+
options: { render?: boolean } = {},
|
|
176
663
|
): Promise<void> {
|
|
177
664
|
const desiredIds = new Set(specs.map((s) => s.id));
|
|
178
665
|
const existing = container.getObjects() as any[];
|
|
@@ -218,7 +705,9 @@ export default class CanvasService implements Service {
|
|
|
218
705
|
}
|
|
219
706
|
|
|
220
707
|
container.dirty = true;
|
|
221
|
-
|
|
708
|
+
if (options.render !== false) {
|
|
709
|
+
this.requestRenderAll();
|
|
710
|
+
}
|
|
222
711
|
}
|
|
223
712
|
|
|
224
713
|
private patchFabricObject(
|
|
@@ -232,10 +721,38 @@ export default class CanvasService implements Service {
|
|
|
232
721
|
...(extraData || {}),
|
|
233
722
|
id: spec.id,
|
|
234
723
|
};
|
|
235
|
-
|
|
724
|
+
const props = this.resolveFabricProps(spec, spec.props || {});
|
|
725
|
+
obj.set({ ...props, data: nextData });
|
|
236
726
|
obj.setCoords();
|
|
237
727
|
}
|
|
238
728
|
|
|
729
|
+
private resolveFabricProps(
|
|
730
|
+
spec: RenderObjectSpec,
|
|
731
|
+
props: Record<string, any>,
|
|
732
|
+
): Record<string, any> {
|
|
733
|
+
const space: RenderCoordinateSpace = spec.space || "scene";
|
|
734
|
+
const next = this.resolveLayoutProps(spec, props);
|
|
735
|
+
if (space === "screen") {
|
|
736
|
+
return next;
|
|
737
|
+
}
|
|
738
|
+
const hasLeft = Number.isFinite(next.left);
|
|
739
|
+
const hasTop = Number.isFinite(next.top);
|
|
740
|
+
if (hasLeft || hasTop) {
|
|
741
|
+
const mapped = this.toScreenPoint({
|
|
742
|
+
x: hasLeft ? Number(next.left) : 0,
|
|
743
|
+
y: hasTop ? Number(next.top) : 0,
|
|
744
|
+
});
|
|
745
|
+
if (hasLeft) next.left = mapped.x;
|
|
746
|
+
if (hasTop) next.top = mapped.y;
|
|
747
|
+
}
|
|
748
|
+
const rawScaleX = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
|
|
749
|
+
const rawScaleY = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
|
|
750
|
+
const sceneScale = this.getSceneScale();
|
|
751
|
+
next.scaleX = rawScaleX * sceneScale;
|
|
752
|
+
next.scaleY = rawScaleY * sceneScale;
|
|
753
|
+
return next;
|
|
754
|
+
}
|
|
755
|
+
|
|
239
756
|
private moveObjectInContainer(
|
|
240
757
|
container: Group | Canvas,
|
|
241
758
|
obj: any,
|
|
@@ -265,8 +782,9 @@ export default class CanvasService implements Service {
|
|
|
265
782
|
spec: RenderObjectSpec,
|
|
266
783
|
): Promise<FabricObject | undefined> {
|
|
267
784
|
if (spec.type === "rect") {
|
|
785
|
+
const props = this.resolveFabricProps(spec, spec.props || {});
|
|
268
786
|
const rect = new Rect({
|
|
269
|
-
...
|
|
787
|
+
...props,
|
|
270
788
|
data: { ...(spec.data || {}), id: spec.id },
|
|
271
789
|
} as any);
|
|
272
790
|
rect.setCoords();
|
|
@@ -274,10 +792,12 @@ export default class CanvasService implements Service {
|
|
|
274
792
|
}
|
|
275
793
|
|
|
276
794
|
if (spec.type === "path") {
|
|
277
|
-
const pathData =
|
|
795
|
+
const pathData =
|
|
796
|
+
(spec.props as any)?.path || (spec.props as any)?.pathData;
|
|
278
797
|
if (!pathData) return undefined;
|
|
798
|
+
const props = this.resolveFabricProps(spec, spec.props || {});
|
|
279
799
|
const path = new Path(pathData, {
|
|
280
|
-
...
|
|
800
|
+
...props,
|
|
281
801
|
data: { ...(spec.data || {}), id: spec.id },
|
|
282
802
|
} as any);
|
|
283
803
|
path.setCoords();
|
|
@@ -287,14 +807,26 @@ export default class CanvasService implements Service {
|
|
|
287
807
|
if (spec.type === "image") {
|
|
288
808
|
if (!spec.src) return undefined;
|
|
289
809
|
const image = await Image.fromURL(spec.src, { crossOrigin: "anonymous" });
|
|
810
|
+
const props = this.resolveFabricProps(spec, spec.props || {});
|
|
290
811
|
image.set({
|
|
291
|
-
...
|
|
812
|
+
...props,
|
|
292
813
|
data: { ...(spec.data || {}), id: spec.id },
|
|
293
814
|
} as any);
|
|
294
815
|
image.setCoords();
|
|
295
816
|
return image as any;
|
|
296
817
|
}
|
|
297
818
|
|
|
819
|
+
if (spec.type === "text") {
|
|
820
|
+
const content = String((spec.props as any)?.text ?? "");
|
|
821
|
+
const props = this.resolveFabricProps(spec, spec.props || {});
|
|
822
|
+
const text = new Text(content, {
|
|
823
|
+
...props,
|
|
824
|
+
data: { ...(spec.data || {}), id: spec.id },
|
|
825
|
+
} as any);
|
|
826
|
+
text.setCoords();
|
|
827
|
+
return text as any;
|
|
828
|
+
}
|
|
829
|
+
|
|
298
830
|
return undefined;
|
|
299
831
|
}
|
|
300
832
|
}
|
|
@@ -1,6 +1,40 @@
|
|
|
1
|
-
export type RenderObjectType = "rect" | "image" | "path";
|
|
1
|
+
export type RenderObjectType = "rect" | "image" | "path" | "text";
|
|
2
2
|
|
|
3
3
|
export type RenderProps = Record<string, any>;
|
|
4
|
+
export type RenderCoordinateSpace = "scene" | "screen";
|
|
5
|
+
export type RenderLayoutLength = number | string;
|
|
6
|
+
export type RenderLayoutAlign = "start" | "center" | "end";
|
|
7
|
+
export type RenderLayoutReference =
|
|
8
|
+
| "sceneViewport"
|
|
9
|
+
| "screenViewport"
|
|
10
|
+
| "custom";
|
|
11
|
+
|
|
12
|
+
export interface RenderLayoutInsets {
|
|
13
|
+
top?: RenderLayoutLength;
|
|
14
|
+
right?: RenderLayoutLength;
|
|
15
|
+
bottom?: RenderLayoutLength;
|
|
16
|
+
left?: RenderLayoutLength;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RenderLayoutRect {
|
|
20
|
+
left: number;
|
|
21
|
+
top: number;
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
space?: RenderCoordinateSpace;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RenderObjectLayoutSpec {
|
|
28
|
+
reference?: RenderLayoutReference;
|
|
29
|
+
referenceRect?: RenderLayoutRect;
|
|
30
|
+
inset?: RenderLayoutLength | RenderLayoutInsets;
|
|
31
|
+
alignX?: RenderLayoutAlign;
|
|
32
|
+
alignY?: RenderLayoutAlign;
|
|
33
|
+
offsetX?: RenderLayoutLength;
|
|
34
|
+
offsetY?: RenderLayoutLength;
|
|
35
|
+
width?: RenderLayoutLength;
|
|
36
|
+
height?: RenderLayoutLength;
|
|
37
|
+
}
|
|
4
38
|
|
|
5
39
|
export interface RenderObjectSpec {
|
|
6
40
|
id: string;
|
|
@@ -8,6 +42,8 @@ export interface RenderObjectSpec {
|
|
|
8
42
|
props: RenderProps;
|
|
9
43
|
data?: Record<string, any>;
|
|
10
44
|
src?: string;
|
|
45
|
+
space?: RenderCoordinateSpace;
|
|
46
|
+
layout?: RenderObjectLayoutSpec;
|
|
11
47
|
}
|
|
12
48
|
|
|
13
49
|
export interface RenderLayerSpec {
|
|
@@ -15,4 +51,3 @@ export interface RenderLayerSpec {
|
|
|
15
51
|
objects: RenderObjectSpec[];
|
|
16
52
|
props?: RenderProps;
|
|
17
53
|
}
|
|
18
|
-
|