@pooder/kit 5.4.0 → 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 (69) hide show
  1. package/.test-dist/src/coordinate.js +74 -0
  2. package/.test-dist/src/extensions/background.js +547 -0
  3. package/.test-dist/src/extensions/bridgeSelection.js +20 -0
  4. package/.test-dist/src/extensions/constraints.js +237 -0
  5. package/.test-dist/src/extensions/dieline.js +931 -0
  6. package/.test-dist/src/extensions/dielineShape.js +66 -0
  7. package/.test-dist/src/extensions/edgeScale.js +12 -0
  8. package/.test-dist/src/extensions/feature.js +910 -0
  9. package/.test-dist/src/extensions/featureComplete.js +32 -0
  10. package/.test-dist/src/extensions/film.js +226 -0
  11. package/.test-dist/src/extensions/geometry.js +609 -0
  12. package/.test-dist/src/extensions/image.js +1613 -0
  13. package/.test-dist/src/extensions/index.js +28 -0
  14. package/.test-dist/src/extensions/maskOps.js +334 -0
  15. package/.test-dist/src/extensions/mirror.js +104 -0
  16. package/.test-dist/src/extensions/ruler.js +442 -0
  17. package/.test-dist/src/extensions/sceneLayout.js +96 -0
  18. package/.test-dist/src/extensions/sceneLayoutModel.js +202 -0
  19. package/.test-dist/src/extensions/sceneVisibility.js +55 -0
  20. package/.test-dist/src/extensions/size.js +331 -0
  21. package/.test-dist/src/extensions/tracer.js +709 -0
  22. package/.test-dist/src/extensions/white-ink.js +1200 -0
  23. package/.test-dist/src/extensions/wrappedOffsets.js +33 -0
  24. package/.test-dist/src/index.js +18 -0
  25. package/.test-dist/src/services/CanvasService.js +1011 -0
  26. package/.test-dist/src/services/ViewportSystem.js +76 -0
  27. package/.test-dist/src/services/index.js +25 -0
  28. package/.test-dist/src/services/renderSpec.js +2 -0
  29. package/.test-dist/src/services/visibility.js +54 -0
  30. package/.test-dist/src/units.js +30 -0
  31. package/.test-dist/tests/run.js +148 -0
  32. package/CHANGELOG.md +6 -0
  33. package/dist/index.d.mts +150 -62
  34. package/dist/index.d.ts +150 -62
  35. package/dist/index.js +2219 -1714
  36. package/dist/index.mjs +2226 -1718
  37. package/package.json +1 -1
  38. package/src/coordinate.ts +106 -106
  39. package/src/extensions/background.ts +716 -323
  40. package/src/extensions/bridgeSelection.ts +17 -17
  41. package/src/extensions/constraints.ts +322 -322
  42. package/src/extensions/dieline.ts +1169 -1149
  43. package/src/extensions/dielineShape.ts +109 -109
  44. package/src/extensions/edgeScale.ts +19 -19
  45. package/src/extensions/feature.ts +1140 -1137
  46. package/src/extensions/featureComplete.ts +46 -46
  47. package/src/extensions/film.ts +270 -266
  48. package/src/extensions/geometry.ts +851 -885
  49. package/src/extensions/image.ts +2007 -2054
  50. package/src/extensions/index.ts +10 -11
  51. package/src/extensions/maskOps.ts +283 -283
  52. package/src/extensions/mirror.ts +128 -128
  53. package/src/extensions/ruler.ts +664 -654
  54. package/src/extensions/sceneLayout.ts +140 -140
  55. package/src/extensions/sceneLayoutModel.ts +364 -364
  56. package/src/extensions/size.ts +389 -389
  57. package/src/extensions/tracer.ts +1019 -1019
  58. package/src/extensions/white-ink.ts +1508 -1575
  59. package/src/extensions/wrappedOffsets.ts +33 -33
  60. package/src/index.ts +2 -2
  61. package/src/services/CanvasService.ts +1286 -832
  62. package/src/services/ViewportSystem.ts +95 -95
  63. package/src/services/index.ts +4 -3
  64. package/src/services/renderSpec.ts +83 -53
  65. package/src/services/visibility.ts +78 -0
  66. package/src/units.ts +27 -27
  67. package/tests/run.ts +253 -118
  68. package/tsconfig.test.json +15 -15
  69. package/src/extensions/sceneVisibility.ts +0 -64
@@ -0,0 +1,1011 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fabric_1 = require("fabric");
4
+ const core_1 = require("@pooder/core");
5
+ const ViewportSystem_1 = require("./ViewportSystem");
6
+ const visibility_1 = require("./visibility");
7
+ class CanvasService {
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
+ };
48
+ if (el instanceof fabric_1.Canvas) {
49
+ this.canvas = el;
50
+ }
51
+ else {
52
+ this.canvas = new fabric_1.Canvas(el, {
53
+ preserveObjectStacking: true,
54
+ ...options,
55
+ });
56
+ }
57
+ this.viewport = new ViewportSystem_1.ViewportSystem();
58
+ if (this.canvas.width !== undefined && this.canvas.height !== undefined) {
59
+ this.viewport.updateContainer(this.canvas.width, this.canvas.height);
60
+ }
61
+ if (options?.eventBus) {
62
+ this.setEventBus(options.eventBus);
63
+ }
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
+ }
87
+ setEventBus(eventBus) {
88
+ this.eventBus = eventBus;
89
+ this.setupEvents();
90
+ }
91
+ setupEvents() {
92
+ if (this.canvasForwardersBound)
93
+ return;
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;
101
+ }
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;
113
+ this.canvas.dispose();
114
+ }
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 });
425
+ }
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
+ });
473
+ }
474
+ requestRenderAll() {
475
+ this.canvas.requestRenderAll();
476
+ }
477
+ resize(width, height) {
478
+ this.canvas.setDimensions({ width, height });
479
+ this.viewport.updateContainer(width, height);
480
+ this.eventBus?.emit("canvas:resized", { width, height });
481
+ this.requestRenderAll();
482
+ }
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
+ };
495
+ }
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
+ };
503
+ }
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,
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
+ }
810
+ const byId = new Map();
811
+ this.getPassCanvasObjects(normalizedPassId).forEach((obj) => {
812
+ const id = obj?.data?.id;
813
+ if (typeof id === "string")
814
+ byId.set(id, obj);
815
+ });
816
+ for (let index = 0; index < normalizedSpecs.length; index += 1) {
817
+ const spec = normalizedSpecs[index];
818
+ let current = byId.get(spec.id);
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)) {
830
+ this.canvas.remove(current);
831
+ byId.delete(spec.id);
832
+ current = undefined;
833
+ }
834
+ if (!current) {
835
+ const created = await this.createFabricObject(spec);
836
+ if (!created)
837
+ continue;
838
+ this.patchFabricObject(created, spec, {
839
+ passId: normalizedPassId,
840
+ layerId: normalizedPassId,
841
+ passOrder: index,
842
+ });
843
+ this.canvas.add(created);
844
+ byId.set(spec.id, created);
845
+ continue;
846
+ }
847
+ this.patchFabricObject(current, spec, {
848
+ passId: normalizedPassId,
849
+ layerId: normalizedPassId,
850
+ passOrder: index,
851
+ });
852
+ }
853
+ if (options.render !== false) {
854
+ this.requestRenderAll();
855
+ }
856
+ }
857
+ patchFabricObject(obj, spec, extraData) {
858
+ const nextData = {
859
+ ...(obj.data || {}),
860
+ ...(spec.data || {}),
861
+ ...(extraData || {}),
862
+ id: spec.id,
863
+ };
864
+ nextData.__renderSourceKey = this.getSpecRenderSourceKey(spec);
865
+ const props = this.resolveFabricProps(spec, spec.props || {});
866
+ obj.set({ ...props, data: nextData });
867
+ obj.setCoords();
868
+ }
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) {
944
+ if (!obj)
945
+ return;
946
+ const moveObjectTo = this.canvas.moveObjectTo;
947
+ if (typeof moveObjectTo === "function") {
948
+ moveObjectTo.call(this.canvas, obj, index);
949
+ return;
950
+ }
951
+ const list = this.canvas._objects;
952
+ if (!Array.isArray(list))
953
+ return;
954
+ const from = list.indexOf(obj);
955
+ if (from < 0 || from === index)
956
+ return;
957
+ list.splice(from, 1);
958
+ const target = Math.max(0, Math.min(index, list.length));
959
+ list.splice(target, 0, obj);
960
+ if (typeof this.canvas._onStackOrderChanged === "function") {
961
+ this.canvas._onStackOrderChanged();
962
+ }
963
+ }
964
+ async createFabricObject(spec) {
965
+ if (spec.type === "rect") {
966
+ const props = this.resolveFabricProps(spec, spec.props || {});
967
+ const rect = new fabric_1.Rect({
968
+ ...props,
969
+ data: { ...(spec.data || {}), id: spec.id },
970
+ });
971
+ rect.setCoords();
972
+ return rect;
973
+ }
974
+ if (spec.type === "path") {
975
+ const pathData = this.readPathDataFromSpec(spec);
976
+ if (!pathData)
977
+ return undefined;
978
+ const props = this.resolveFabricProps(spec, spec.props || {});
979
+ const path = new fabric_1.Path(pathData, {
980
+ ...props,
981
+ data: { ...(spec.data || {}), id: spec.id },
982
+ });
983
+ path.setCoords();
984
+ return path;
985
+ }
986
+ if (spec.type === "image") {
987
+ if (!spec.src)
988
+ return undefined;
989
+ const image = await fabric_1.Image.fromURL(spec.src, { crossOrigin: "anonymous" });
990
+ const props = this.resolveFabricProps(spec, spec.props || {});
991
+ image.set({
992
+ ...props,
993
+ data: { ...(spec.data || {}), id: spec.id },
994
+ });
995
+ image.setCoords();
996
+ return image;
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
+ }
1008
+ return undefined;
1009
+ }
1010
+ }
1011
+ exports.default = CanvasService;