@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.
Files changed (65) hide show
  1. package/.test-dist/src/extensions/background.js +475 -131
  2. package/.test-dist/src/extensions/dieline.js +283 -180
  3. package/.test-dist/src/extensions/dielineShape.js +66 -0
  4. package/.test-dist/src/extensions/feature.js +388 -303
  5. package/.test-dist/src/extensions/film.js +133 -74
  6. package/.test-dist/src/extensions/geometry.js +120 -56
  7. package/.test-dist/src/extensions/image.js +296 -212
  8. package/.test-dist/src/extensions/index.js +1 -3
  9. package/.test-dist/src/extensions/maskOps.js +75 -20
  10. package/.test-dist/src/extensions/ruler.js +312 -215
  11. package/.test-dist/src/extensions/sceneLayoutModel.js +9 -3
  12. package/.test-dist/src/extensions/sceneVisibility.js +3 -10
  13. package/.test-dist/src/extensions/tracer.js +229 -58
  14. package/.test-dist/src/extensions/white-ink.js +139 -129
  15. package/.test-dist/src/services/CanvasService.js +888 -126
  16. package/.test-dist/src/services/index.js +1 -0
  17. package/.test-dist/src/services/visibility.js +54 -0
  18. package/.test-dist/tests/run.js +58 -4
  19. package/CHANGELOG.md +12 -0
  20. package/dist/index.d.mts +377 -82
  21. package/dist/index.d.ts +377 -82
  22. package/dist/index.js +3920 -2178
  23. package/dist/index.mjs +3992 -2247
  24. package/package.json +1 -1
  25. package/src/extensions/background.ts +631 -145
  26. package/src/extensions/dieline.ts +280 -187
  27. package/src/extensions/dielineShape.ts +109 -0
  28. package/src/extensions/feature.ts +485 -366
  29. package/src/extensions/film.ts +152 -76
  30. package/src/extensions/geometry.ts +203 -104
  31. package/src/extensions/image.ts +319 -238
  32. package/src/extensions/index.ts +0 -1
  33. package/src/extensions/ruler.ts +481 -268
  34. package/src/extensions/sceneLayoutModel.ts +18 -6
  35. package/src/extensions/white-ink.ts +157 -171
  36. package/src/services/CanvasService.ts +1126 -140
  37. package/src/services/index.ts +1 -0
  38. package/src/services/renderSpec.ts +69 -4
  39. package/src/services/visibility.ts +78 -0
  40. package/tests/run.ts +139 -4
  41. package/.test-dist/src/CanvasService.js +0 -249
  42. package/.test-dist/src/ViewportSystem.js +0 -75
  43. package/.test-dist/src/background.js +0 -203
  44. package/.test-dist/src/bridgeSelection.js +0 -20
  45. package/.test-dist/src/constraints.js +0 -237
  46. package/.test-dist/src/dieline.js +0 -818
  47. package/.test-dist/src/edgeScale.js +0 -12
  48. package/.test-dist/src/feature.js +0 -826
  49. package/.test-dist/src/featureComplete.js +0 -32
  50. package/.test-dist/src/film.js +0 -167
  51. package/.test-dist/src/geometry.js +0 -506
  52. package/.test-dist/src/image.js +0 -1250
  53. package/.test-dist/src/maskOps.js +0 -270
  54. package/.test-dist/src/mirror.js +0 -104
  55. package/.test-dist/src/renderSpec.js +0 -2
  56. package/.test-dist/src/ruler.js +0 -343
  57. package/.test-dist/src/sceneLayout.js +0 -99
  58. package/.test-dist/src/sceneLayoutModel.js +0 -196
  59. package/.test-dist/src/sceneView.js +0 -40
  60. package/.test-dist/src/sceneVisibility.js +0 -42
  61. package/.test-dist/src/size.js +0 -332
  62. package/.test-dist/src/tracer.js +0 -544
  63. package/.test-dist/src/white-ink.js +0 -829
  64. package/.test-dist/src/wrappedOffsets.js +0 -33
  65. package/src/extensions/sceneVisibility.ts +0 -71
@@ -1,9 +1,50 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const fabric_1 = require("fabric");
4
+ const core_1 = require("@pooder/core");
4
5
  const ViewportSystem_1 = require("./ViewportSystem");
6
+ const visibility_1 = require("./visibility");
5
7
  class CanvasService {
6
8
  constructor(el, options) {
9
+ this.renderProducers = new Map();
10
+ this.producerOrder = 0;
11
+ this.producerFlushRequested = false;
12
+ this.producerLoopPending = false;
13
+ this.producerLoopPromise = null;
14
+ this.producerApplyInProgress = false;
15
+ this.visibilityRefreshScheduled = false;
16
+ this.managedProducerPassIds = new Set();
17
+ this.managedPassMetas = new Map();
18
+ this.canvasForwardersBound = false;
19
+ this.forwardSelectionCreated = (e) => {
20
+ this.eventBus?.emit("selection:created", e);
21
+ };
22
+ this.forwardSelectionUpdated = (e) => {
23
+ this.eventBus?.emit("selection:updated", e);
24
+ };
25
+ this.forwardSelectionCleared = (e) => {
26
+ this.eventBus?.emit("selection:cleared", e);
27
+ };
28
+ this.forwardObjectModified = (e) => {
29
+ this.eventBus?.emit("object:modified", e);
30
+ };
31
+ this.forwardObjectAdded = (e) => {
32
+ this.eventBus?.emit("object:added", e);
33
+ };
34
+ this.forwardObjectRemoved = (e) => {
35
+ this.eventBus?.emit("object:removed", e);
36
+ };
37
+ this.onToolActivated = () => {
38
+ this.applyManagedPassVisibility();
39
+ };
40
+ this.onToolSessionChanged = () => {
41
+ this.applyManagedPassVisibility();
42
+ };
43
+ this.onCanvasObjectChanged = () => {
44
+ if (this.producerApplyInProgress)
45
+ return;
46
+ this.scheduleManagedPassVisibilityRefresh();
47
+ };
7
48
  if (el instanceof fabric_1.Canvas) {
8
49
  this.canvas = el;
9
50
  }
@@ -21,60 +62,414 @@ class CanvasService {
21
62
  this.setEventBus(options.eventBus);
22
63
  }
23
64
  }
65
+ init(context) {
66
+ if (this.context) {
67
+ this.detachContextEvents(this.context.eventBus);
68
+ }
69
+ this.context = context;
70
+ this.workbenchService = context.get(core_1.WORKBENCH_SERVICE);
71
+ this.toolSessionService = context.get(core_1.TOOL_SESSION_SERVICE);
72
+ this.setEventBus(context.eventBus);
73
+ this.attachContextEvents(context.eventBus);
74
+ }
75
+ attachContextEvents(eventBus) {
76
+ eventBus.on("tool:activated", this.onToolActivated);
77
+ eventBus.on("tool:session:change", this.onToolSessionChanged);
78
+ eventBus.on("object:added", this.onCanvasObjectChanged);
79
+ eventBus.on("object:removed", this.onCanvasObjectChanged);
80
+ }
81
+ detachContextEvents(eventBus) {
82
+ eventBus.off("tool:activated", this.onToolActivated);
83
+ eventBus.off("tool:session:change", this.onToolSessionChanged);
84
+ eventBus.off("object:added", this.onCanvasObjectChanged);
85
+ eventBus.off("object:removed", this.onCanvasObjectChanged);
86
+ }
24
87
  setEventBus(eventBus) {
25
88
  this.eventBus = eventBus;
26
89
  this.setupEvents();
27
90
  }
28
91
  setupEvents() {
29
- if (!this.eventBus)
92
+ if (this.canvasForwardersBound)
30
93
  return;
31
- const bus = this.eventBus;
32
- const forward = (name) => (e) => bus.emit(name, e);
33
- this.canvas.on("selection:created", forward("selection:created"));
34
- this.canvas.on("selection:updated", forward("selection:updated"));
35
- this.canvas.on("selection:cleared", forward("selection:cleared"));
36
- this.canvas.on("object:modified", forward("object:modified"));
37
- this.canvas.on("object:added", forward("object:added"));
38
- this.canvas.on("object:removed", forward("object:removed"));
94
+ this.canvas.on("selection:created", this.forwardSelectionCreated);
95
+ this.canvas.on("selection:updated", this.forwardSelectionUpdated);
96
+ this.canvas.on("selection:cleared", this.forwardSelectionCleared);
97
+ this.canvas.on("object:modified", this.forwardObjectModified);
98
+ this.canvas.on("object:added", this.forwardObjectAdded);
99
+ this.canvas.on("object:removed", this.forwardObjectRemoved);
100
+ this.canvasForwardersBound = true;
39
101
  }
40
102
  dispose() {
103
+ if (this.context) {
104
+ this.detachContextEvents(this.context.eventBus);
105
+ }
106
+ this.renderProducers.clear();
107
+ this.managedProducerPassIds.clear();
108
+ this.managedPassMetas.clear();
109
+ this.context = undefined;
110
+ this.workbenchService = undefined;
111
+ this.toolSessionService = undefined;
112
+ this.producerFlushRequested = false;
41
113
  this.canvas.dispose();
42
114
  }
43
- /**
44
- * Get a layer (Group) by its ID.
45
- * We assume layers are Groups directly on the canvas with a data.id property.
46
- */
47
- getLayer(id) {
48
- return this.canvas.getObjects().find((obj) => obj.data?.id === id);
49
- }
50
- /**
51
- * Create a layer (Group) with the given ID if it doesn't exist.
52
- */
53
- createLayer(id, options = {}) {
54
- let layer = this.getLayer(id);
55
- if (!layer) {
56
- const defaultOptions = {
57
- selectable: false,
58
- evented: false,
59
- ...options,
60
- data: { ...options.data, id },
61
- };
62
- layer = new fabric_1.Group([], defaultOptions);
63
- this.canvas.add(layer);
64
- }
65
- return layer;
66
- }
67
- /**
68
- * Find an object by ID, optionally within a specific layer.
69
- */
70
- getObject(id, layerId) {
71
- if (layerId) {
72
- const layer = this.getLayer(layerId);
73
- if (!layer)
74
- return undefined;
75
- return layer.getObjects().find((obj) => obj.data?.id === id);
115
+ registerRenderProducer(toolId, producer, options = {}) {
116
+ const normalizedToolId = String(toolId || "").trim();
117
+ if (!normalizedToolId) {
118
+ throw new Error("[CanvasService] registerRenderProducer requires a toolId.");
119
+ }
120
+ if (typeof producer !== "function") {
121
+ throw new Error(`[CanvasService] registerRenderProducer("${normalizedToolId}") requires a producer function.`);
122
+ }
123
+ const entry = {
124
+ toolId: normalizedToolId,
125
+ producer,
126
+ priority: Number.isFinite(options.priority)
127
+ ? Number(options.priority)
128
+ : 0,
129
+ order: this.producerOrder++,
130
+ };
131
+ this.renderProducers.set(normalizedToolId, entry);
132
+ this.requestRenderFromProducers();
133
+ return {
134
+ dispose: () => {
135
+ this.unregisterRenderProducer(normalizedToolId);
136
+ },
137
+ };
138
+ }
139
+ unregisterRenderProducer(toolId) {
140
+ const normalizedToolId = String(toolId || "").trim();
141
+ if (!normalizedToolId)
142
+ return false;
143
+ const removed = this.renderProducers.delete(normalizedToolId);
144
+ if (removed) {
145
+ this.requestRenderFromProducers();
146
+ }
147
+ return removed;
148
+ }
149
+ requestRenderFromProducers() {
150
+ this.producerFlushRequested = true;
151
+ this.scheduleProducerLoop();
152
+ }
153
+ async flushRenderFromProducers() {
154
+ this.requestRenderFromProducers();
155
+ if (this.producerLoopPromise) {
156
+ await this.producerLoopPromise;
157
+ }
158
+ }
159
+ scheduleProducerLoop() {
160
+ if (this.producerLoopPending)
161
+ return;
162
+ this.producerLoopPending = true;
163
+ this.producerLoopPromise = Promise.resolve()
164
+ .then(() => this.runProducerLoop())
165
+ .catch((error) => {
166
+ console.error("[CanvasService] render producer loop failed.", error);
167
+ })
168
+ .finally(() => {
169
+ this.producerLoopPending = false;
170
+ if (this.producerFlushRequested) {
171
+ this.scheduleProducerLoop();
172
+ }
173
+ });
174
+ }
175
+ async runProducerLoop() {
176
+ while (this.producerFlushRequested) {
177
+ this.producerFlushRequested = false;
178
+ await this.collectAndApplyProducerSpecs();
179
+ }
180
+ }
181
+ sortedRenderProducerEntries() {
182
+ return Array.from(this.renderProducers.values()).sort((a, b) => {
183
+ if (a.priority !== b.priority) {
184
+ return a.priority - b.priority;
185
+ }
186
+ if (a.order !== b.order) {
187
+ return a.order - b.order;
188
+ }
189
+ return a.toolId.localeCompare(b.toolId);
190
+ });
191
+ }
192
+ normalizePassSpecValue(spec) {
193
+ const id = String(spec.id || "").trim();
194
+ if (!id)
195
+ return null;
196
+ return {
197
+ id,
198
+ stack: Number.isFinite(spec.stack) ? Number(spec.stack) : 0,
199
+ order: Number.isFinite(spec.order) ? Number(spec.order) : 0,
200
+ replace: spec.replace !== false,
201
+ visibility: spec.visibility,
202
+ effects: Array.isArray(spec.effects)
203
+ ? [...spec.effects]
204
+ : [],
205
+ objects: Array.isArray(spec.objects) ? [...spec.objects] : [],
206
+ };
207
+ }
208
+ normalizeClipPathEffectSpec(effect, passId, index) {
209
+ if (!effect || effect.type !== "clipPath")
210
+ return null;
211
+ const source = effect.source;
212
+ if (!source || typeof source !== "object")
213
+ return null;
214
+ const sourceId = String(source.id || "").trim();
215
+ if (!sourceId)
216
+ return null;
217
+ const targetPassIds = Array.isArray(effect.targetPassIds)
218
+ ? effect.targetPassIds
219
+ .map((item) => String(item || "").trim())
220
+ .filter((item) => item.length > 0)
221
+ : [];
222
+ if (!targetPassIds.length)
223
+ return null;
224
+ const customId = String(effect.id || "").trim();
225
+ const key = customId || `${passId}.effect.clipPath.${index}`;
226
+ return {
227
+ type: "clipPath",
228
+ key,
229
+ source: {
230
+ ...source,
231
+ id: sourceId,
232
+ },
233
+ targetPassIds,
234
+ };
235
+ }
236
+ mergePassSpec(map, rawSpec, producerId) {
237
+ const normalized = this.normalizePassSpecValue(rawSpec);
238
+ if (!normalized)
239
+ return;
240
+ const existing = map.get(normalized.id);
241
+ if (!existing) {
242
+ map.set(normalized.id, normalized);
243
+ return;
244
+ }
245
+ existing.objects.push(...normalized.objects);
246
+ existing.replace = existing.replace || normalized.replace;
247
+ existing.stack = normalized.stack;
248
+ existing.order = normalized.order;
249
+ if (normalized.visibility !== undefined) {
250
+ existing.visibility = normalized.visibility;
251
+ }
252
+ existing.effects.push(...normalized.effects);
253
+ if (normalized.objects.length === 0 && normalized.effects.length === 0) {
254
+ console.debug(`[CanvasService] pass "${normalized.id}" from producer "${producerId}" updated ordering/visibility only.`);
255
+ }
256
+ }
257
+ comparePassMeta(a, b) {
258
+ if (a.stack !== b.stack)
259
+ return a.stack - b.stack;
260
+ if (a.order !== b.order)
261
+ return a.order - b.order;
262
+ return a.id.localeCompare(b.id);
263
+ }
264
+ getPassObjectOrder(obj) {
265
+ const raw = Number(obj?.data?.passOrder);
266
+ return Number.isFinite(raw) ? raw : Number.MAX_SAFE_INTEGER;
267
+ }
268
+ getPassCanvasObjects(passId) {
269
+ const all = this.canvas.getObjects();
270
+ return all
271
+ .filter((obj) => obj?.data?.passId === passId)
272
+ .sort((a, b) => {
273
+ const orderA = this.getPassObjectOrder(a);
274
+ const orderB = this.getPassObjectOrder(b);
275
+ if (orderA !== orderB)
276
+ return orderA - orderB;
277
+ return all.indexOf(a) - all.indexOf(b);
278
+ });
279
+ }
280
+ getPassObjects(passId) {
281
+ return this.getPassCanvasObjects(passId);
282
+ }
283
+ getRootLayerObjects(layerId) {
284
+ return this.getPassCanvasObjects(layerId);
285
+ }
286
+ isManagedPassObject(obj) {
287
+ const passId = obj?.data?.passId;
288
+ return typeof passId === "string" && this.managedPassMetas.has(passId);
289
+ }
290
+ syncManagedPassStacking(passes) {
291
+ const orderedPasses = [...passes].sort((a, b) => this.comparePassMeta(a, b));
292
+ if (!orderedPasses.length)
293
+ return;
294
+ const canvasObjects = this.canvas.getObjects();
295
+ const managedObjects = canvasObjects.filter((obj) => this.isManagedPassObject(obj));
296
+ if (!managedObjects.length)
297
+ return;
298
+ const firstManagedIndex = managedObjects
299
+ .map((obj) => canvasObjects.indexOf(obj))
300
+ .filter((index) => index >= 0)
301
+ .reduce((min, value) => Math.min(min, value), Number.MAX_SAFE_INTEGER);
302
+ let targetIndex = Number.isFinite(firstManagedIndex)
303
+ ? firstManagedIndex
304
+ : 0;
305
+ orderedPasses.forEach((meta) => {
306
+ const objects = this.getPassCanvasObjects(meta.id);
307
+ objects.forEach((obj) => {
308
+ this.moveObjectInCanvas(obj, targetIndex);
309
+ targetIndex += 1;
310
+ });
311
+ });
312
+ }
313
+ getPassRuntimeState() {
314
+ const state = new Map();
315
+ const ensure = (passId) => {
316
+ const id = String(passId || "").trim();
317
+ if (!id)
318
+ return { exists: false, objectCount: 0 };
319
+ let item = state.get(id);
320
+ if (!item) {
321
+ item = { exists: false, objectCount: 0 };
322
+ state.set(id, item);
323
+ }
324
+ return item;
325
+ };
326
+ this.canvas.getObjects().forEach((obj) => {
327
+ const passId = obj?.data?.passId;
328
+ if (typeof passId === "string") {
329
+ const item = ensure(passId);
330
+ item.exists = true;
331
+ item.objectCount += 1;
332
+ }
333
+ });
334
+ this.managedPassMetas.forEach((meta) => {
335
+ const item = ensure(meta.id);
336
+ item.exists = true;
337
+ });
338
+ return state;
339
+ }
340
+ applyManagedPassVisibility(options = {}) {
341
+ if (!this.managedPassMetas.size)
342
+ return false;
343
+ const layers = this.getPassRuntimeState();
344
+ const activeToolId = this.workbenchService?.activeToolId ?? null;
345
+ const isSessionActive = (toolId) => {
346
+ if (!this.toolSessionService)
347
+ return false;
348
+ return this.toolSessionService.getState(toolId).status === "active";
349
+ };
350
+ let changed = false;
351
+ this.managedPassMetas.forEach((meta) => {
352
+ const visible = (0, visibility_1.evaluateVisibilityExpr)(meta.visibility, {
353
+ activeToolId,
354
+ isSessionActive,
355
+ layers,
356
+ });
357
+ changed = this.setPassVisibility(meta.id, visible) || changed;
358
+ });
359
+ if (changed && options.render !== false) {
360
+ this.requestRenderAll();
361
+ }
362
+ return changed;
363
+ }
364
+ scheduleManagedPassVisibilityRefresh() {
365
+ if (this.visibilityRefreshScheduled)
366
+ return;
367
+ this.visibilityRefreshScheduled = true;
368
+ void Promise.resolve().then(() => {
369
+ this.visibilityRefreshScheduled = false;
370
+ this.applyManagedPassVisibility();
371
+ });
372
+ }
373
+ async collectAndApplyProducerSpecs() {
374
+ const passes = new Map();
375
+ const entries = this.sortedRenderProducerEntries();
376
+ this.producerApplyInProgress = true;
377
+ try {
378
+ for (const entry of entries) {
379
+ try {
380
+ const result = await entry.producer();
381
+ if (!result)
382
+ continue;
383
+ const specs = Array.isArray(result.passes) ? result.passes : [];
384
+ specs.forEach((spec) => this.mergePassSpec(passes, spec, entry.toolId));
385
+ }
386
+ catch (error) {
387
+ console.error(`[CanvasService] render producer "${entry.toolId}" failed.`, error);
388
+ }
389
+ }
390
+ const nextPassIds = new Set();
391
+ const nextManagedPassMetas = new Map();
392
+ const nextEffects = [];
393
+ for (const pass of passes.values()) {
394
+ nextPassIds.add(pass.id);
395
+ nextManagedPassMetas.set(pass.id, {
396
+ id: pass.id,
397
+ stack: pass.stack,
398
+ order: pass.order,
399
+ visibility: pass.visibility,
400
+ });
401
+ await this.applyObjectSpecsToPass(pass.id, pass.objects, {
402
+ render: false,
403
+ replace: pass.replace,
404
+ });
405
+ pass.effects.forEach((effect, index) => {
406
+ const normalized = this.normalizeClipPathEffectSpec(effect, pass.id, index);
407
+ if (!normalized)
408
+ return;
409
+ nextEffects.push(normalized);
410
+ });
411
+ }
412
+ for (const passId of this.managedProducerPassIds) {
413
+ if (nextPassIds.has(passId))
414
+ continue;
415
+ await this.applyObjectSpecsToPass(passId, [], {
416
+ render: false,
417
+ replace: true,
418
+ });
419
+ }
420
+ this.managedProducerPassIds = nextPassIds;
421
+ this.managedPassMetas = nextManagedPassMetas;
422
+ this.syncManagedPassStacking(Array.from(nextManagedPassMetas.values()));
423
+ await this.applyManagedPassEffects(nextEffects);
424
+ this.applyManagedPassVisibility({ render: false });
76
425
  }
77
- return this.canvas.getObjects().find((obj) => obj.data?.id === id);
426
+ finally {
427
+ this.producerApplyInProgress = false;
428
+ }
429
+ this.requestRenderAll();
430
+ }
431
+ async applyManagedPassEffects(effects) {
432
+ const effectTargetMap = new Map();
433
+ for (const effect of effects) {
434
+ if (effect.type !== "clipPath")
435
+ continue;
436
+ effect.targetPassIds.forEach((targetPassId) => {
437
+ this.getPassCanvasObjects(targetPassId).forEach((obj) => {
438
+ effectTargetMap.set(obj, effect);
439
+ });
440
+ });
441
+ }
442
+ const managedObjects = this.canvas.getObjects().filter((obj) => this.isManagedPassObject(obj));
443
+ const effectTemplateCache = new Map();
444
+ for (const obj of managedObjects) {
445
+ const targetEffect = effectTargetMap.get(obj);
446
+ if (!targetEffect) {
447
+ this.clearClipPathEffectFromObject(obj);
448
+ continue;
449
+ }
450
+ let template = effectTemplateCache.get(targetEffect.key);
451
+ if (template === undefined) {
452
+ template = await this.createClipPathTemplate(targetEffect);
453
+ effectTemplateCache.set(targetEffect.key, template);
454
+ }
455
+ if (!template) {
456
+ this.clearClipPathEffectFromObject(obj);
457
+ continue;
458
+ }
459
+ await this.applyClipPathEffectToObject(obj, template, targetEffect.key);
460
+ }
461
+ }
462
+ getObject(id, passId) {
463
+ const normalizedId = String(id || "").trim();
464
+ if (!normalizedId)
465
+ return undefined;
466
+ return this.canvas.getObjects().find((obj) => {
467
+ if (obj?.data?.id !== normalizedId)
468
+ return false;
469
+ if (!passId)
470
+ return true;
471
+ return obj?.data?.passId === passId;
472
+ });
78
473
  }
79
474
  requestRenderAll() {
80
475
  this.canvas.requestRenderAll();
@@ -85,42 +480,353 @@ class CanvasService {
85
480
  this.eventBus?.emit("canvas:resized", { width, height });
86
481
  this.requestRenderAll();
87
482
  }
88
- async applyLayerSpec(spec) {
89
- const layer = this.createLayer(spec.id, spec.props || {});
90
- await this.applyObjectSpecsToContainer(layer, spec.objects);
483
+ getSceneScale() {
484
+ const scale = Number(this.viewport.scale);
485
+ return Number.isFinite(scale) && scale > 0 ? scale : 1;
486
+ }
487
+ getSceneOffset() {
488
+ const offset = this.viewport.offset;
489
+ const x = Number(offset.x);
490
+ const y = Number(offset.y);
491
+ return {
492
+ x: Number.isFinite(x) ? x : 0,
493
+ y: Number.isFinite(y) ? y : 0,
494
+ };
91
495
  }
92
- async applyObjectSpecsToLayer(layerId, objects) {
93
- const layer = this.createLayer(layerId, {});
94
- await this.applyObjectSpecsToContainer(layer, objects);
496
+ toScreenPoint(point) {
497
+ const scale = this.getSceneScale();
498
+ const offset = this.getSceneOffset();
499
+ return {
500
+ x: point.x * scale + offset.x,
501
+ y: point.y * scale + offset.y,
502
+ };
95
503
  }
96
- getRootLayerObjects(layerId) {
97
- return this.canvas
98
- .getObjects()
99
- .filter((obj) => obj?.data?.layerId === layerId);
100
- }
101
- async applyObjectSpecsToRootLayer(layerId, specs) {
102
- const desiredIds = new Set(specs.map((s) => s.id));
103
- const existing = this.getRootLayerObjects(layerId);
104
- existing.forEach((obj) => {
105
- const id = obj?.data?.id;
106
- if (typeof id === "string" && !desiredIds.has(id)) {
107
- this.canvas.remove(obj);
108
- }
504
+ toScenePoint(point) {
505
+ const scale = this.getSceneScale();
506
+ const offset = this.getSceneOffset();
507
+ return {
508
+ x: (point.x - offset.x) / scale,
509
+ y: (point.y - offset.y) / scale,
510
+ };
511
+ }
512
+ toScreenLength(value) {
513
+ return value * this.getSceneScale();
514
+ }
515
+ toSceneLength(value) {
516
+ return value / this.getSceneScale();
517
+ }
518
+ toScreenRect(rect) {
519
+ const start = this.toScreenPoint({ x: rect.left, y: rect.top });
520
+ return {
521
+ left: start.x,
522
+ top: start.y,
523
+ width: this.toScreenLength(rect.width),
524
+ height: this.toScreenLength(rect.height),
525
+ };
526
+ }
527
+ toSceneRect(rect) {
528
+ const start = this.toScenePoint({ x: rect.left, y: rect.top });
529
+ return {
530
+ left: start.x,
531
+ top: start.y,
532
+ width: this.toSceneLength(rect.width),
533
+ height: this.toSceneLength(rect.height),
534
+ };
535
+ }
536
+ getSceneViewportRect() {
537
+ const width = Number(this.canvas.width || 0);
538
+ const height = Number(this.canvas.height || 0);
539
+ return this.toSceneRect({ left: 0, top: 0, width, height });
540
+ }
541
+ getScreenViewportRect() {
542
+ return {
543
+ left: 0,
544
+ top: 0,
545
+ width: Number(this.canvas.width || 0),
546
+ height: Number(this.canvas.height || 0),
547
+ };
548
+ }
549
+ toSpaceRect(rect, from, to) {
550
+ if (from === to)
551
+ return { ...rect };
552
+ if (from === "scene") {
553
+ return this.toScreenRect(rect);
554
+ }
555
+ return this.toSceneRect(rect);
556
+ }
557
+ resolveLayoutLength(value, base) {
558
+ if (typeof value === "number") {
559
+ return Number.isFinite(value) ? value : undefined;
560
+ }
561
+ if (typeof value !== "string") {
562
+ return undefined;
563
+ }
564
+ const raw = value.trim();
565
+ if (!raw)
566
+ return undefined;
567
+ if (raw.endsWith("%")) {
568
+ const percent = parseFloat(raw.slice(0, -1));
569
+ if (!Number.isFinite(percent))
570
+ return undefined;
571
+ return (base * percent) / 100;
572
+ }
573
+ const parsed = parseFloat(raw);
574
+ return Number.isFinite(parsed) ? parsed : undefined;
575
+ }
576
+ resolveLayoutInsets(inset, reference) {
577
+ if (typeof inset === "number" || typeof inset === "string") {
578
+ const all = this.resolveLayoutLength(inset, Math.min(reference.width, reference.height)) ?? 0;
579
+ return { top: all, right: all, bottom: all, left: all };
580
+ }
581
+ const source = inset || {};
582
+ const top = this.resolveLayoutLength(source.top, reference.height) ?? 0;
583
+ const right = this.resolveLayoutLength(source.right, reference.width) ?? 0;
584
+ const bottom = this.resolveLayoutLength(source.bottom, reference.height) ?? 0;
585
+ const left = this.resolveLayoutLength(source.left, reference.width) ?? 0;
586
+ return { top, right, bottom, left };
587
+ }
588
+ resolveLayoutReferenceRect(layout, space) {
589
+ if (layout.referenceRect) {
590
+ const sourceSpace = layout.referenceRect.space || space;
591
+ return this.toSpaceRect(layout.referenceRect, sourceSpace, space);
592
+ }
593
+ const reference = layout.reference || "sceneViewport";
594
+ if (reference === "screenViewport") {
595
+ const screenRect = this.getScreenViewportRect();
596
+ return space === "screen" ? screenRect : this.toSceneRect(screenRect);
597
+ }
598
+ const sceneRect = this.getSceneViewportRect();
599
+ return space === "scene" ? sceneRect : this.toScreenRect(sceneRect);
600
+ }
601
+ alignFactor(value) {
602
+ if (value === "end")
603
+ return 1;
604
+ if (value === "center")
605
+ return 0.5;
606
+ return 0;
607
+ }
608
+ normalizeOriginX(value) {
609
+ if (value === "center")
610
+ return "center";
611
+ if (value === "right")
612
+ return "right";
613
+ return "left";
614
+ }
615
+ normalizeOriginY(value) {
616
+ if (value === "center")
617
+ return "center";
618
+ if (value === "bottom")
619
+ return "bottom";
620
+ return "top";
621
+ }
622
+ originFactor(value) {
623
+ if (value === "center")
624
+ return 0.5;
625
+ if (value === "right" || value === "bottom")
626
+ return 1;
627
+ return 0;
628
+ }
629
+ resolveLayoutProps(spec, props) {
630
+ const layout = spec.layout;
631
+ if (!layout) {
632
+ return { ...props };
633
+ }
634
+ const space = spec.space || "scene";
635
+ const reference = this.resolveLayoutReferenceRect(layout, space);
636
+ const inset = this.resolveLayoutInsets(layout.inset, reference);
637
+ const area = {
638
+ left: reference.left + inset.left,
639
+ top: reference.top + inset.top,
640
+ width: Math.max(0, reference.width - inset.left - inset.right),
641
+ height: Math.max(0, reference.height - inset.top - inset.bottom),
642
+ };
643
+ const next = { ...props };
644
+ const width = this.resolveLayoutLength(layout.width, area.width) ??
645
+ (Number.isFinite(next.width) ? Number(next.width) : undefined);
646
+ const height = this.resolveLayoutLength(layout.height, area.height) ??
647
+ (Number.isFinite(next.height) ? Number(next.height) : undefined);
648
+ if (width !== undefined)
649
+ next.width = width;
650
+ if (height !== undefined)
651
+ next.height = height;
652
+ const alignX = this.alignFactor(layout.alignX);
653
+ const alignY = this.alignFactor(layout.alignY);
654
+ const offsetX = this.resolveLayoutLength(layout.offsetX, area.width) ?? 0;
655
+ const offsetY = this.resolveLayoutLength(layout.offsetY, area.height) ?? 0;
656
+ const objectWidth = Number.isFinite(next.width) ? Number(next.width) : 0;
657
+ const objectHeight = Number.isFinite(next.height) ? Number(next.height) : 0;
658
+ const objectLeft = area.left + (area.width - objectWidth) * alignX + offsetX;
659
+ const objectTop = area.top + (area.height - objectHeight) * alignY + offsetY;
660
+ const originX = this.normalizeOriginX(next.originX);
661
+ const originY = this.normalizeOriginY(next.originY);
662
+ next.left = objectLeft + objectWidth * this.originFactor(originX);
663
+ next.top = objectTop + objectHeight * this.originFactor(originY);
664
+ return next;
665
+ }
666
+ setPassVisibility(passId, visible) {
667
+ const objects = this.getPassCanvasObjects(passId);
668
+ let changed = false;
669
+ objects.forEach((obj) => {
670
+ if (obj.visible === visible)
671
+ return;
672
+ obj.set?.({ visible });
673
+ obj.setCoords?.();
674
+ changed = true;
675
+ });
676
+ return changed;
677
+ }
678
+ setLayerVisibility(layerId, visible) {
679
+ return this.setPassVisibility(layerId, visible);
680
+ }
681
+ bringPassToFront(passId) {
682
+ const objects = this.getPassCanvasObjects(passId);
683
+ objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
684
+ }
685
+ bringLayerToFront(layerId) {
686
+ this.bringPassToFront(layerId);
687
+ }
688
+ async applyPassSpec(spec, options = {}) {
689
+ await this.applyObjectSpecsToPass(spec.id, spec.objects, {
690
+ render: options.render,
691
+ replace: spec.replace !== false,
692
+ });
693
+ }
694
+ async applyObjectSpecsToRootLayer(passId, specs, options = {}) {
695
+ await this.applyObjectSpecsToPass(passId, specs, {
696
+ render: options.render,
697
+ replace: true,
698
+ });
699
+ }
700
+ normalizeObjectSpecs(specs) {
701
+ const seen = new Set();
702
+ const normalized = [];
703
+ (specs || []).forEach((spec) => {
704
+ const id = String(spec?.id || "").trim();
705
+ if (!id || seen.has(id))
706
+ return;
707
+ seen.add(id);
708
+ normalized.push({
709
+ ...spec,
710
+ id,
711
+ });
712
+ });
713
+ return normalized;
714
+ }
715
+ async cloneFabricObject(source) {
716
+ const clone = source.clone;
717
+ if (typeof clone !== "function")
718
+ return undefined;
719
+ const result = clone.call(source);
720
+ if (!result || typeof result.then !== "function") {
721
+ return undefined;
722
+ }
723
+ try {
724
+ const copied = (await result);
725
+ return copied;
726
+ }
727
+ catch {
728
+ return undefined;
729
+ }
730
+ }
731
+ async createClipPathTemplate(effect) {
732
+ const source = effect.source;
733
+ const sourceId = String(source.id || "").trim();
734
+ if (!sourceId)
735
+ return null;
736
+ const template = await this.createFabricObject({
737
+ ...source,
738
+ id: sourceId,
739
+ data: {
740
+ ...(source.data || {}),
741
+ id: sourceId,
742
+ type: "clip-path-effect-template",
743
+ effectKey: effect.key,
744
+ },
745
+ props: {
746
+ ...(source.props || {}),
747
+ selectable: false,
748
+ evented: false,
749
+ excludeFromExport: true,
750
+ },
751
+ });
752
+ if (!template)
753
+ return null;
754
+ template.set?.({
755
+ selectable: false,
756
+ evented: false,
757
+ excludeFromExport: true,
758
+ absolutePositioned: true,
759
+ });
760
+ template.setCoords?.();
761
+ return template;
762
+ }
763
+ isClipPathEffectManaged(target) {
764
+ return typeof target?.__pooderEffectClipKey === "string";
765
+ }
766
+ clearClipPathEffectFromObject(target) {
767
+ if (!target)
768
+ return;
769
+ if (!this.isClipPathEffectManaged(target))
770
+ return;
771
+ target.set?.({ clipPath: undefined });
772
+ target.setCoords?.();
773
+ delete target.__pooderEffectClipKey;
774
+ }
775
+ async applyClipPathEffectToObject(target, clipTemplate, effectKey) {
776
+ if (!target)
777
+ return;
778
+ const clipPath = await this.cloneFabricObject(clipTemplate);
779
+ if (!clipPath) {
780
+ this.clearClipPathEffectFromObject(target);
781
+ return;
782
+ }
783
+ clipPath.set?.({
784
+ selectable: false,
785
+ evented: false,
786
+ excludeFromExport: true,
787
+ absolutePositioned: true,
109
788
  });
789
+ clipPath.setCoords?.();
790
+ target.set?.({ clipPath });
791
+ target.setCoords?.();
792
+ target.__pooderEffectClipKey = effectKey;
793
+ }
794
+ async applyObjectSpecsToPass(passId, specs, options = {}) {
795
+ const normalizedPassId = String(passId || "").trim();
796
+ if (!normalizedPassId)
797
+ return;
798
+ const replace = options.replace !== false;
799
+ const normalizedSpecs = this.normalizeObjectSpecs(specs);
800
+ const desiredIds = new Set(normalizedSpecs.map((s) => s.id));
801
+ const existing = this.getPassCanvasObjects(normalizedPassId);
802
+ if (replace) {
803
+ existing.forEach((obj) => {
804
+ const id = obj?.data?.id;
805
+ if (typeof id === "string" && !desiredIds.has(id)) {
806
+ this.canvas.remove(obj);
807
+ }
808
+ });
809
+ }
110
810
  const byId = new Map();
111
- this.getRootLayerObjects(layerId).forEach((obj) => {
811
+ this.getPassCanvasObjects(normalizedPassId).forEach((obj) => {
112
812
  const id = obj?.data?.id;
113
813
  if (typeof id === "string")
114
814
  byId.set(id, obj);
115
815
  });
116
- for (let index = 0; index < specs.length; index += 1) {
117
- const spec = specs[index];
816
+ for (let index = 0; index < normalizedSpecs.length; index += 1) {
817
+ const spec = normalizedSpecs[index];
118
818
  let current = byId.get(spec.id);
119
- if (current &&
120
- spec.type === "image" &&
121
- spec.src &&
122
- current.getSrc &&
123
- current.getSrc() !== spec.src) {
819
+ if (spec.type === "path") {
820
+ const nextPathData = this.readPathDataFromSpec(spec);
821
+ if (!nextPathData || !nextPathData.trim()) {
822
+ if (current) {
823
+ this.canvas.remove(current);
824
+ byId.delete(spec.id);
825
+ }
826
+ continue;
827
+ }
828
+ }
829
+ if (current && this.shouldRecreateObject(current, spec)) {
124
830
  this.canvas.remove(current);
125
831
  byId.delete(spec.id);
126
832
  current = undefined;
@@ -129,57 +835,24 @@ class CanvasService {
129
835
  const created = await this.createFabricObject(spec);
130
836
  if (!created)
131
837
  continue;
132
- this.patchFabricObject(created, spec, { layerId });
838
+ this.patchFabricObject(created, spec, {
839
+ passId: normalizedPassId,
840
+ layerId: normalizedPassId,
841
+ passOrder: index,
842
+ });
133
843
  this.canvas.add(created);
134
844
  byId.set(spec.id, created);
135
845
  continue;
136
846
  }
137
- this.patchFabricObject(current, spec, { layerId });
847
+ this.patchFabricObject(current, spec, {
848
+ passId: normalizedPassId,
849
+ layerId: normalizedPassId,
850
+ passOrder: index,
851
+ });
138
852
  }
139
- this.requestRenderAll();
140
- }
141
- async applyObjectSpecsToContainer(container, specs) {
142
- const desiredIds = new Set(specs.map((s) => s.id));
143
- const existing = container.getObjects();
144
- existing.forEach((obj) => {
145
- const id = obj?.data?.id;
146
- if (typeof id === "string" && !desiredIds.has(id)) {
147
- container.remove(obj);
148
- }
149
- });
150
- const byId = new Map();
151
- container.getObjects().forEach((obj) => {
152
- const id = obj?.data?.id;
153
- if (typeof id === "string")
154
- byId.set(id, obj);
155
- });
156
- for (let index = 0; index < specs.length; index += 1) {
157
- const spec = specs[index];
158
- let current = byId.get(spec.id);
159
- if (current &&
160
- spec.type === "image" &&
161
- spec.src &&
162
- current.getSrc &&
163
- current.getSrc() !== spec.src) {
164
- container.remove(current);
165
- byId.delete(spec.id);
166
- current = undefined;
167
- }
168
- if (!current) {
169
- const created = await this.createFabricObject(spec);
170
- if (!created)
171
- continue;
172
- container.add(created);
173
- current = created;
174
- byId.set(spec.id, current);
175
- }
176
- else {
177
- this.patchFabricObject(current, spec);
178
- }
179
- this.moveObjectInContainer(container, current, index);
853
+ if (options.render !== false) {
854
+ this.requestRenderAll();
180
855
  }
181
- container.dirty = true;
182
- this.requestRenderAll();
183
856
  }
184
857
  patchFabricObject(obj, spec, extraData) {
185
858
  const nextData = {
@@ -188,18 +861,94 @@ class CanvasService {
188
861
  ...(extraData || {}),
189
862
  id: spec.id,
190
863
  };
191
- obj.set({ ...(spec.props || {}), data: nextData });
864
+ nextData.__renderSourceKey = this.getSpecRenderSourceKey(spec);
865
+ const props = this.resolveFabricProps(spec, spec.props || {});
866
+ obj.set({ ...props, data: nextData });
192
867
  obj.setCoords();
193
868
  }
194
- moveObjectInContainer(container, obj, index) {
869
+ readPathDataFromSpec(spec) {
870
+ if (spec.type !== "path")
871
+ return undefined;
872
+ const raw = spec.props?.path ||
873
+ spec.props?.pathData;
874
+ if (typeof raw !== "string")
875
+ return undefined;
876
+ return raw;
877
+ }
878
+ hashText(value) {
879
+ let hash = 2166136261;
880
+ for (let i = 0; i < value.length; i += 1) {
881
+ hash ^= value.charCodeAt(i);
882
+ hash +=
883
+ (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
884
+ }
885
+ return (hash >>> 0).toString(16);
886
+ }
887
+ getSpecRenderSourceKey(spec) {
888
+ switch (spec.type) {
889
+ case "path": {
890
+ const pathData = this.readPathDataFromSpec(spec) || "";
891
+ return `path:${this.hashText(pathData)}`;
892
+ }
893
+ case "image":
894
+ return `image:${String(spec.src || "")}`;
895
+ case "text":
896
+ return `text:${String(spec.props?.text ?? "")}`;
897
+ case "rect":
898
+ return "rect";
899
+ default:
900
+ return String(spec.type || "");
901
+ }
902
+ }
903
+ shouldRecreateObject(current, spec) {
904
+ if (!current)
905
+ return true;
906
+ const currentType = String(current?.type || "").toLowerCase();
907
+ if (currentType !== spec.type)
908
+ return true;
909
+ const expectedKey = this.getSpecRenderSourceKey(spec);
910
+ const currentKey = String(current?.data?.__renderSourceKey || "");
911
+ if (currentKey && expectedKey && currentKey !== expectedKey)
912
+ return true;
913
+ if (spec.type === "image" && spec.src && current.getSrc) {
914
+ return current.getSrc() !== spec.src;
915
+ }
916
+ return false;
917
+ }
918
+ resolveFabricProps(spec, props) {
919
+ const space = spec.space || "scene";
920
+ const next = this.resolveLayoutProps(spec, props);
921
+ if (space === "screen") {
922
+ return next;
923
+ }
924
+ const hasLeft = Number.isFinite(next.left);
925
+ const hasTop = Number.isFinite(next.top);
926
+ if (hasLeft || hasTop) {
927
+ const mapped = this.toScreenPoint({
928
+ x: hasLeft ? Number(next.left) : 0,
929
+ y: hasTop ? Number(next.top) : 0,
930
+ });
931
+ if (hasLeft)
932
+ next.left = mapped.x;
933
+ if (hasTop)
934
+ next.top = mapped.y;
935
+ }
936
+ const rawScaleX = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
937
+ const rawScaleY = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
938
+ const sceneScale = this.getSceneScale();
939
+ next.scaleX = rawScaleX * sceneScale;
940
+ next.scaleY = rawScaleY * sceneScale;
941
+ return next;
942
+ }
943
+ moveObjectInCanvas(obj, index) {
195
944
  if (!obj)
196
945
  return;
197
- const moveObjectTo = container.moveObjectTo;
946
+ const moveObjectTo = this.canvas.moveObjectTo;
198
947
  if (typeof moveObjectTo === "function") {
199
- moveObjectTo.call(container, obj, index);
948
+ moveObjectTo.call(this.canvas, obj, index);
200
949
  return;
201
950
  }
202
- const list = container._objects;
951
+ const list = this.canvas._objects;
203
952
  if (!Array.isArray(list))
204
953
  return;
205
954
  const from = list.indexOf(obj);
@@ -208,25 +957,27 @@ class CanvasService {
208
957
  list.splice(from, 1);
209
958
  const target = Math.max(0, Math.min(index, list.length));
210
959
  list.splice(target, 0, obj);
211
- if (typeof container._onStackOrderChanged === "function") {
212
- container._onStackOrderChanged();
960
+ if (typeof this.canvas._onStackOrderChanged === "function") {
961
+ this.canvas._onStackOrderChanged();
213
962
  }
214
963
  }
215
964
  async createFabricObject(spec) {
216
965
  if (spec.type === "rect") {
966
+ const props = this.resolveFabricProps(spec, spec.props || {});
217
967
  const rect = new fabric_1.Rect({
218
- ...(spec.props || {}),
968
+ ...props,
219
969
  data: { ...(spec.data || {}), id: spec.id },
220
970
  });
221
971
  rect.setCoords();
222
972
  return rect;
223
973
  }
224
974
  if (spec.type === "path") {
225
- const pathData = spec.props?.path || spec.props?.pathData;
975
+ const pathData = this.readPathDataFromSpec(spec);
226
976
  if (!pathData)
227
977
  return undefined;
978
+ const props = this.resolveFabricProps(spec, spec.props || {});
228
979
  const path = new fabric_1.Path(pathData, {
229
- ...(spec.props || {}),
980
+ ...props,
230
981
  data: { ...(spec.data || {}), id: spec.id },
231
982
  });
232
983
  path.setCoords();
@@ -236,13 +987,24 @@ class CanvasService {
236
987
  if (!spec.src)
237
988
  return undefined;
238
989
  const image = await fabric_1.Image.fromURL(spec.src, { crossOrigin: "anonymous" });
990
+ const props = this.resolveFabricProps(spec, spec.props || {});
239
991
  image.set({
240
- ...(spec.props || {}),
992
+ ...props,
241
993
  data: { ...(spec.data || {}), id: spec.id },
242
994
  });
243
995
  image.setCoords();
244
996
  return image;
245
997
  }
998
+ if (spec.type === "text") {
999
+ const content = String(spec.props?.text ?? "");
1000
+ const props = this.resolveFabricProps(spec, spec.props || {});
1001
+ const text = new fabric_1.Text(content, {
1002
+ ...props,
1003
+ data: { ...(spec.data || {}), id: spec.id },
1004
+ });
1005
+ text.setCoords();
1006
+ return text;
1007
+ }
246
1008
  return undefined;
247
1009
  }
248
1010
  }