@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +249 -36
  3. package/dist/index.d.ts +249 -36
  4. package/dist/index.js +2374 -1049
  5. package/dist/index.mjs +2375 -1050
  6. package/package.json +1 -1
  7. package/src/extensions/background.ts +178 -85
  8. package/src/extensions/dieline.ts +1149 -1030
  9. package/src/extensions/dielineShape.ts +109 -0
  10. package/src/extensions/feature.ts +482 -366
  11. package/src/extensions/film.ts +148 -76
  12. package/src/extensions/geometry.ts +210 -44
  13. package/src/extensions/image.ts +244 -114
  14. package/src/extensions/ruler.ts +471 -268
  15. package/src/extensions/sceneLayoutModel.ts +28 -6
  16. package/src/extensions/sceneVisibility.ts +3 -10
  17. package/src/extensions/tracer.ts +1019 -980
  18. package/src/extensions/white-ink.ts +284 -231
  19. package/src/services/CanvasService.ts +543 -11
  20. package/src/services/renderSpec.ts +37 -2
  21. package/.test-dist/src/CanvasService.js +0 -249
  22. package/.test-dist/src/ViewportSystem.js +0 -75
  23. package/.test-dist/src/background.js +0 -203
  24. package/.test-dist/src/bridgeSelection.js +0 -20
  25. package/.test-dist/src/constraints.js +0 -237
  26. package/.test-dist/src/coordinate.js +0 -74
  27. package/.test-dist/src/dieline.js +0 -818
  28. package/.test-dist/src/edgeScale.js +0 -12
  29. package/.test-dist/src/extensions/background.js +0 -203
  30. package/.test-dist/src/extensions/bridgeSelection.js +0 -20
  31. package/.test-dist/src/extensions/constraints.js +0 -237
  32. package/.test-dist/src/extensions/dieline.js +0 -828
  33. package/.test-dist/src/extensions/edgeScale.js +0 -12
  34. package/.test-dist/src/extensions/feature.js +0 -825
  35. package/.test-dist/src/extensions/featureComplete.js +0 -32
  36. package/.test-dist/src/extensions/film.js +0 -167
  37. package/.test-dist/src/extensions/geometry.js +0 -545
  38. package/.test-dist/src/extensions/image.js +0 -1529
  39. package/.test-dist/src/extensions/index.js +0 -30
  40. package/.test-dist/src/extensions/maskOps.js +0 -279
  41. package/.test-dist/src/extensions/mirror.js +0 -104
  42. package/.test-dist/src/extensions/ruler.js +0 -345
  43. package/.test-dist/src/extensions/sceneLayout.js +0 -96
  44. package/.test-dist/src/extensions/sceneLayoutModel.js +0 -196
  45. package/.test-dist/src/extensions/sceneVisibility.js +0 -62
  46. package/.test-dist/src/extensions/size.js +0 -331
  47. package/.test-dist/src/extensions/tracer.js +0 -538
  48. package/.test-dist/src/extensions/white-ink.js +0 -1190
  49. package/.test-dist/src/extensions/wrappedOffsets.js +0 -33
  50. package/.test-dist/src/feature.js +0 -826
  51. package/.test-dist/src/featureComplete.js +0 -32
  52. package/.test-dist/src/film.js +0 -167
  53. package/.test-dist/src/geometry.js +0 -506
  54. package/.test-dist/src/image.js +0 -1250
  55. package/.test-dist/src/index.js +0 -18
  56. package/.test-dist/src/maskOps.js +0 -270
  57. package/.test-dist/src/mirror.js +0 -104
  58. package/.test-dist/src/renderSpec.js +0 -2
  59. package/.test-dist/src/ruler.js +0 -343
  60. package/.test-dist/src/sceneLayout.js +0 -99
  61. package/.test-dist/src/sceneLayoutModel.js +0 -196
  62. package/.test-dist/src/sceneView.js +0 -40
  63. package/.test-dist/src/sceneVisibility.js +0 -42
  64. package/.test-dist/src/services/CanvasService.js +0 -249
  65. package/.test-dist/src/services/ViewportSystem.js +0 -76
  66. package/.test-dist/src/services/index.js +0 -24
  67. package/.test-dist/src/services/renderSpec.js +0 -2
  68. package/.test-dist/src/size.js +0 -332
  69. package/.test-dist/src/tracer.js +0 -544
  70. package/.test-dist/src/units.js +0 -30
  71. package/.test-dist/src/white-ink.js +0 -829
  72. package/.test-dist/src/wrappedOffsets.js +0 -33
  73. 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 { RenderLayerSpec, RenderObjectSpec } from "./renderSpec";
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
- this.requestRenderAll();
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
- this.requestRenderAll();
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
- obj.set({ ...(spec.props || {}), data: nextData });
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
- ...(spec.props || {}),
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 = (spec.props as any)?.path || (spec.props as any)?.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
- ...(spec.props || {}),
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
- ...(spec.props || {}),
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
-