@pooder/kit 6.0.0 → 6.1.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 (99) hide show
  1. package/.test-dist/src/extensions/background/BackgroundTool.js +524 -0
  2. package/.test-dist/src/extensions/background/index.js +17 -0
  3. package/.test-dist/src/extensions/background.js +1 -1
  4. package/.test-dist/src/extensions/dieline/DielineTool.js +748 -0
  5. package/.test-dist/src/extensions/dieline/commands.js +127 -0
  6. package/.test-dist/src/extensions/dieline/config.js +107 -0
  7. package/.test-dist/src/extensions/dieline/index.js +21 -0
  8. package/.test-dist/src/extensions/dieline/model.js +2 -0
  9. package/.test-dist/src/extensions/dieline/renderer.js +2 -0
  10. package/.test-dist/src/extensions/dieline.js +4 -0
  11. package/.test-dist/src/extensions/feature/FeatureTool.js +914 -0
  12. package/.test-dist/src/extensions/feature/index.js +17 -0
  13. package/.test-dist/src/extensions/film/FilmTool.js +207 -0
  14. package/.test-dist/src/extensions/film/index.js +17 -0
  15. package/.test-dist/src/extensions/image/ImageTool.js +1499 -0
  16. package/.test-dist/src/extensions/image/commands.js +162 -0
  17. package/.test-dist/src/extensions/image/config.js +129 -0
  18. package/.test-dist/src/extensions/image/index.js +21 -0
  19. package/.test-dist/src/extensions/image/model.js +2 -0
  20. package/.test-dist/src/extensions/image/renderer.js +5 -0
  21. package/.test-dist/src/extensions/image.js +182 -7
  22. package/.test-dist/src/extensions/mirror/MirrorTool.js +104 -0
  23. package/.test-dist/src/extensions/mirror/index.js +17 -0
  24. package/.test-dist/src/extensions/ruler/RulerTool.js +442 -0
  25. package/.test-dist/src/extensions/ruler/index.js +17 -0
  26. package/.test-dist/src/extensions/sceneLayout.js +2 -93
  27. package/.test-dist/src/extensions/sceneLayoutModel.js +15 -200
  28. package/.test-dist/src/extensions/size/SizeTool.js +332 -0
  29. package/.test-dist/src/extensions/size/index.js +17 -0
  30. package/.test-dist/src/extensions/white-ink/WhiteInkTool.js +1003 -0
  31. package/.test-dist/src/extensions/white-ink/commands.js +148 -0
  32. package/.test-dist/src/extensions/white-ink/config.js +31 -0
  33. package/.test-dist/src/extensions/white-ink/index.js +21 -0
  34. package/.test-dist/src/extensions/white-ink/model.js +2 -0
  35. package/.test-dist/src/extensions/white-ink/renderer.js +5 -0
  36. package/.test-dist/src/services/CanvasService.js +34 -13
  37. package/.test-dist/src/services/SceneLayoutService.js +96 -0
  38. package/.test-dist/src/services/index.js +1 -0
  39. package/.test-dist/src/services/visibility.js +3 -0
  40. package/.test-dist/src/shared/constants/layers.js +25 -0
  41. package/.test-dist/src/shared/imaging/sourceSizeCache.js +82 -0
  42. package/.test-dist/src/shared/index.js +22 -0
  43. package/.test-dist/src/shared/runtime/sessionState.js +74 -0
  44. package/.test-dist/src/shared/runtime/subscriptions.js +30 -0
  45. package/.test-dist/src/shared/scene/frame.js +34 -0
  46. package/.test-dist/src/shared/scene/sceneLayoutModel.js +202 -0
  47. package/.test-dist/tests/run.js +118 -0
  48. package/CHANGELOG.md +12 -0
  49. package/dist/index.d.mts +403 -366
  50. package/dist/index.d.ts +403 -366
  51. package/dist/index.js +5172 -4752
  52. package/dist/index.mjs +1410 -2027
  53. package/dist/tracer-PO7CRBYY.mjs +1016 -0
  54. package/package.json +1 -1
  55. package/src/extensions/{background.ts → background/BackgroundTool.ts} +33 -50
  56. package/src/extensions/background/index.ts +1 -0
  57. package/src/extensions/{dieline.ts → dieline/DielineTool.ts} +18 -218
  58. package/src/extensions/dieline/commands.ts +109 -0
  59. package/src/extensions/dieline/config.ts +106 -0
  60. package/src/extensions/dieline/index.ts +5 -0
  61. package/src/extensions/dieline/model.ts +1 -0
  62. package/src/extensions/dieline/renderer.ts +1 -0
  63. package/src/extensions/{feature.ts → feature/FeatureTool.ts} +27 -21
  64. package/src/extensions/feature/index.ts +1 -0
  65. package/src/extensions/{film.ts → film/FilmTool.ts} +36 -48
  66. package/src/extensions/film/index.ts +1 -0
  67. package/src/extensions/{image.ts → image/ImageTool.ts} +289 -335
  68. package/src/extensions/image/commands.ts +176 -0
  69. package/src/extensions/image/config.ts +128 -0
  70. package/src/extensions/image/index.ts +5 -0
  71. package/src/extensions/image/model.ts +1 -0
  72. package/src/extensions/image/renderer.ts +1 -0
  73. package/src/extensions/{mirror.ts → mirror/MirrorTool.ts} +1 -1
  74. package/src/extensions/mirror/index.ts +1 -0
  75. package/src/extensions/{ruler.ts → ruler/RulerTool.ts} +4 -5
  76. package/src/extensions/ruler/index.ts +1 -0
  77. package/src/extensions/sceneLayout.ts +1 -140
  78. package/src/extensions/sceneLayoutModel.ts +1 -364
  79. package/src/extensions/{size.ts → size/SizeTool.ts} +7 -6
  80. package/src/extensions/size/index.ts +1 -0
  81. package/src/extensions/{white-ink.ts → white-ink/WhiteInkTool.ts} +130 -317
  82. package/src/extensions/white-ink/commands.ts +157 -0
  83. package/src/extensions/white-ink/config.ts +30 -0
  84. package/src/extensions/white-ink/index.ts +5 -0
  85. package/src/extensions/white-ink/model.ts +1 -0
  86. package/src/extensions/white-ink/renderer.ts +1 -0
  87. package/src/services/CanvasService.ts +43 -12
  88. package/src/services/SceneLayoutService.ts +139 -0
  89. package/src/services/index.ts +1 -0
  90. package/src/services/renderSpec.ts +2 -0
  91. package/src/services/visibility.ts +5 -0
  92. package/src/shared/constants/layers.ts +23 -0
  93. package/src/shared/imaging/sourceSizeCache.ts +103 -0
  94. package/src/shared/index.ts +6 -0
  95. package/src/shared/runtime/sessionState.ts +105 -0
  96. package/src/shared/runtime/subscriptions.ts +45 -0
  97. package/src/shared/scene/frame.ts +46 -0
  98. package/src/shared/scene/sceneLayoutModel.ts +367 -0
  99. package/tests/run.ts +151 -0
@@ -0,0 +1,157 @@
1
+ import type { CommandContribution } from "@pooder/core";
2
+
3
+ const WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
4
+ const WHITE_INK_DEFAULT_OPACITY = 0.85;
5
+
6
+ export function createWhiteInkCommands(tool: any): CommandContribution[] {
7
+ return [
8
+ {
9
+ command: "addWhiteInk",
10
+ id: "addWhiteInk",
11
+ title: "Add White Ink",
12
+ handler: async (url: string, options?: Record<string, any>) => {
13
+ return await tool.addWhiteInkEntry(url, options);
14
+ },
15
+ },
16
+ {
17
+ command: "upsertWhiteInk",
18
+ id: "upsertWhiteInk",
19
+ title: "Upsert White Ink",
20
+ handler: async (url: string, options: Record<string, any> = {}) => {
21
+ return await tool.upsertWhiteInkEntry(url, options);
22
+ },
23
+ },
24
+ {
25
+ command: "getWhiteInks",
26
+ id: "getWhiteInks",
27
+ title: "Get White Inks",
28
+ handler: () => tool.cloneItems(tool.items),
29
+ },
30
+ {
31
+ command: "getWhiteInkSettings",
32
+ id: "getWhiteInkSettings",
33
+ title: "Get White Ink Settings",
34
+ handler: () => {
35
+ const first = tool.getEffectiveWhiteInkItem(tool.items);
36
+ const primarySource = tool.getPrimaryImageSource();
37
+ const sourceUrl = tool.resolveSourceUrl(first) || primarySource;
38
+
39
+ return {
40
+ id: first?.id || null,
41
+ url: sourceUrl,
42
+ sourceUrl,
43
+ opacity: WHITE_INK_DEFAULT_OPACITY,
44
+ printWithWhiteInk: tool.printWithWhiteInk,
45
+ previewImageVisible: tool.previewImageVisible,
46
+ };
47
+ },
48
+ },
49
+ {
50
+ command: "setWhiteInkPrintEnabled",
51
+ id: "setWhiteInkPrintEnabled",
52
+ title: "Set White Ink Preview Enabled",
53
+ handler: (enabled: boolean) => {
54
+ tool.printWithWhiteInk = !!enabled;
55
+ const configService = tool.context?.services.get("ConfigurationService");
56
+ configService?.update("whiteInk.printWithWhiteInk", tool.printWithWhiteInk);
57
+ tool.updateWhiteInks();
58
+ return { ok: true };
59
+ },
60
+ },
61
+ {
62
+ command: "setWhiteInkPreviewImageVisible",
63
+ id: "setWhiteInkPreviewImageVisible",
64
+ title: "Set White Ink Cover Visible",
65
+ handler: (visible: boolean) => {
66
+ tool.previewImageVisible = !!visible;
67
+ const configService = tool.context?.services.get("ConfigurationService");
68
+ configService?.update(
69
+ WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY,
70
+ tool.previewImageVisible,
71
+ );
72
+ tool.updateWhiteInks();
73
+ return { ok: true };
74
+ },
75
+ },
76
+ {
77
+ command: "getWorkingWhiteInks",
78
+ id: "getWorkingWhiteInks",
79
+ title: "Get Working White Inks",
80
+ handler: () => tool.cloneItems(tool.workingItems),
81
+ },
82
+ {
83
+ command: "setWorkingWhiteInk",
84
+ id: "setWorkingWhiteInk",
85
+ title: "Set Working White Ink",
86
+ handler: (id: string, updates: Record<string, any>) => {
87
+ tool.updateWhiteInkInWorking(id, updates);
88
+ },
89
+ },
90
+ {
91
+ command: "updateWhiteInk",
92
+ id: "updateWhiteInk",
93
+ title: "Update White Ink",
94
+ handler: async (
95
+ id: string,
96
+ updates: Record<string, any>,
97
+ options: Record<string, any> = {},
98
+ ) => {
99
+ await tool.updateWhiteInkItem(id, updates, options);
100
+ },
101
+ },
102
+ {
103
+ command: "removeWhiteInk",
104
+ id: "removeWhiteInk",
105
+ title: "Remove White Ink",
106
+ handler: (id: string) => {
107
+ tool.removeWhiteInk(id);
108
+ },
109
+ },
110
+ {
111
+ command: "clearWhiteInks",
112
+ id: "clearWhiteInks",
113
+ title: "Clear White Inks",
114
+ handler: () => {
115
+ tool.clearWhiteInks();
116
+ },
117
+ },
118
+ {
119
+ command: "resetWorkingWhiteInks",
120
+ id: "resetWorkingWhiteInks",
121
+ title: "Reset Working White Inks",
122
+ handler: () => {
123
+ tool.workingItems = tool.cloneItems(tool.items);
124
+ tool.hasWorkingChanges = false;
125
+ tool.updateWhiteInks();
126
+ },
127
+ },
128
+ {
129
+ command: "completeWhiteInks",
130
+ id: "completeWhiteInks",
131
+ title: "Complete White Inks",
132
+ handler: async () => {
133
+ return await tool.completeWhiteInks();
134
+ },
135
+ },
136
+ {
137
+ command: "setWhiteInkImage",
138
+ id: "setWhiteInkImage",
139
+ title: "Set White Ink Image",
140
+ handler: async (url: string) => {
141
+ if (!url) {
142
+ tool.clearWhiteInks();
143
+ return { ok: true };
144
+ }
145
+
146
+ const targetId = tool.resolveReplaceTargetId(null);
147
+ const upsertResult = await tool.upsertWhiteInkEntry(url, {
148
+ id: targetId || undefined,
149
+ mode: targetId ? "replace" : "add",
150
+ createIfMissing: true,
151
+ addOptions: {},
152
+ });
153
+ return { ok: true, id: upsertResult.id };
154
+ },
155
+ },
156
+ ];
157
+ }
@@ -0,0 +1,30 @@
1
+ import type { ConfigurationContribution } from "@pooder/core";
2
+
3
+ export function createWhiteInkConfigurations(): ConfigurationContribution[] {
4
+ return [
5
+ {
6
+ id: "whiteInk.items",
7
+ type: "array",
8
+ label: "White Ink Images",
9
+ default: [],
10
+ },
11
+ {
12
+ id: "whiteInk.printWithWhiteInk",
13
+ type: "boolean",
14
+ label: "Preview White Ink",
15
+ default: true,
16
+ },
17
+ {
18
+ id: "whiteInk.previewImageVisible",
19
+ type: "boolean",
20
+ label: "Show Cover During White Ink Preview",
21
+ default: true,
22
+ },
23
+ {
24
+ id: "whiteInk.debug",
25
+ type: "boolean",
26
+ label: "White Ink Debug Log",
27
+ default: false,
28
+ },
29
+ ];
30
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./WhiteInkTool";
2
+ export * from "./commands";
3
+ export * from "./config";
4
+ export * from "./model";
5
+ export * from "./renderer";
@@ -0,0 +1 @@
1
+ export type { WhiteInkItem } from "./WhiteInkTool";
@@ -0,0 +1 @@
1
+ export { getCoverScale as computeWhiteInkCoverScale } from "../../shared/imaging/sourceSizeCache";
@@ -60,6 +60,7 @@ interface ResolvedRenderPassSpec {
60
60
  interface ResolvedClipPathEffectSpec {
61
61
  type: "clipPath";
62
62
  key: string;
63
+ visibility?: RenderPassSpec["visibility"];
63
64
  source: RenderObjectSpec;
64
65
  targetPassIds: string[];
65
66
  }
@@ -89,6 +90,7 @@ export default class CanvasService implements Service {
89
90
 
90
91
  private managedProducerPassIds: Set<string> = new Set();
91
92
  private managedPassMetas: Map<string, ManagedPassMeta> = new Map();
93
+ private managedPassEffects: ResolvedClipPathEffectSpec[] = [];
92
94
 
93
95
  private canvasForwardersBound = false;
94
96
  private readonly forwardSelectionCreated = (e: any) => {
@@ -112,9 +114,11 @@ export default class CanvasService implements Service {
112
114
 
113
115
  private readonly onToolActivated = () => {
114
116
  this.applyManagedPassVisibility();
117
+ void this.applyManagedPassEffects(undefined, { render: true });
115
118
  };
116
119
  private readonly onToolSessionChanged = () => {
117
120
  this.applyManagedPassVisibility();
121
+ void this.applyManagedPassEffects(undefined, { render: true });
118
122
  };
119
123
  private readonly onCanvasObjectChanged = () => {
120
124
  if (this.producerApplyInProgress) return;
@@ -190,6 +194,7 @@ export default class CanvasService implements Service {
190
194
  this.renderProducers.clear();
191
195
  this.managedProducerPassIds.clear();
192
196
  this.managedPassMetas.clear();
197
+ this.managedPassEffects = [];
193
198
  this.context = undefined;
194
199
  this.workbenchService = undefined;
195
200
  this.toolSessionService = undefined;
@@ -332,6 +337,7 @@ export default class CanvasService implements Service {
332
337
  return {
333
338
  type: "clipPath",
334
339
  key,
340
+ visibility: effect.visibility,
335
341
  source: {
336
342
  ...source,
337
343
  id: sourceId,
@@ -466,23 +472,35 @@ export default class CanvasService implements Service {
466
472
  return state;
467
473
  }
468
474
 
475
+ private isSessionActive(toolId: string): boolean {
476
+ if (!this.toolSessionService) return false;
477
+ return this.toolSessionService.getState(toolId).status === "active";
478
+ }
479
+
480
+ private hasAnyActiveSession(): boolean {
481
+ return this.toolSessionService?.hasAnyActiveSession() ?? false;
482
+ }
483
+
484
+ private buildVisibilityEvalContext(
485
+ layers: Map<string, VisibilityLayerState>,
486
+ ) {
487
+ return {
488
+ activeToolId: this.workbenchService?.activeToolId ?? null,
489
+ isSessionActive: (toolId: string) => this.isSessionActive(toolId),
490
+ hasAnyActiveSession: () => this.hasAnyActiveSession(),
491
+ layers,
492
+ };
493
+ }
494
+
469
495
  private applyManagedPassVisibility(options: { render?: boolean } = {}): boolean {
470
496
  if (!this.managedPassMetas.size) return false;
471
497
  const layers = this.getPassRuntimeState();
472
- const activeToolId = this.workbenchService?.activeToolId ?? null;
473
- const isSessionActive = (toolId: string) => {
474
- if (!this.toolSessionService) return false;
475
- return this.toolSessionService.getState(toolId).status === "active";
476
- };
498
+ const context = this.buildVisibilityEvalContext(layers);
477
499
 
478
500
  let changed = false;
479
501
 
480
502
  this.managedPassMetas.forEach((meta) => {
481
- const visible = evaluateVisibilityExpr(meta.visibility, {
482
- activeToolId,
483
- isSessionActive,
484
- layers,
485
- });
503
+ const visible = evaluateVisibilityExpr(meta.visibility, context);
486
504
  changed = this.setPassVisibility(meta.id, visible) || changed;
487
505
  });
488
506
 
@@ -560,9 +578,10 @@ export default class CanvasService implements Service {
560
578
 
561
579
  this.managedProducerPassIds = nextPassIds;
562
580
  this.managedPassMetas = nextManagedPassMetas;
581
+ this.managedPassEffects = nextEffects;
563
582
 
564
583
  this.syncManagedPassStacking(Array.from(nextManagedPassMetas.values()));
565
- await this.applyManagedPassEffects(nextEffects);
584
+ await this.applyManagedPassEffects(nextEffects, { render: false });
566
585
  this.applyManagedPassVisibility({ render: false });
567
586
  } finally {
568
587
  this.producerApplyInProgress = false;
@@ -571,11 +590,19 @@ export default class CanvasService implements Service {
571
590
  this.requestRenderAll();
572
591
  }
573
592
 
574
- private async applyManagedPassEffects(effects: ResolvedClipPathEffectSpec[]) {
593
+ private async applyManagedPassEffects(
594
+ effects: ResolvedClipPathEffectSpec[] = this.managedPassEffects,
595
+ options: { render?: boolean } = {},
596
+ ) {
575
597
  const effectTargetMap = new Map<FabricObject, ResolvedClipPathEffectSpec>();
598
+ const layers = this.getPassRuntimeState();
599
+ const visibilityContext = this.buildVisibilityEvalContext(layers);
576
600
 
577
601
  for (const effect of effects) {
578
602
  if (effect.type !== "clipPath") continue;
603
+ if (!evaluateVisibilityExpr(effect.visibility, visibilityContext)) {
604
+ continue;
605
+ }
579
606
  effect.targetPassIds.forEach((targetPassId) => {
580
607
  this.getPassCanvasObjects(targetPassId).forEach((obj) => {
581
608
  effectTargetMap.set(obj, effect);
@@ -613,6 +640,10 @@ export default class CanvasService implements Service {
613
640
  targetEffect.key,
614
641
  );
615
642
  }
643
+
644
+ if (options.render !== false) {
645
+ this.requestRenderAll();
646
+ }
616
647
  }
617
648
 
618
649
  getObject(id: string, passId?: string): FabricObject | undefined {
@@ -0,0 +1,139 @@
1
+ import {
2
+ COMMAND_SERVICE,
3
+ CONFIGURATION_SERVICE,
4
+ CommandService,
5
+ ConfigurationService,
6
+ Service,
7
+ ServiceContext,
8
+ } from "@pooder/core";
9
+ import CanvasService from "./CanvasService";
10
+ import {
11
+ buildSceneGeometry,
12
+ computeSceneLayout,
13
+ readSizeState,
14
+ type SceneGeometrySnapshot,
15
+ type SceneLayoutSnapshot,
16
+ } from "../shared/scene/sceneLayoutModel";
17
+ import { SubscriptionBag } from "../shared/runtime/subscriptions";
18
+
19
+ interface ConfigChangeEvent {
20
+ key: string;
21
+ value: unknown;
22
+ oldValue: unknown;
23
+ }
24
+
25
+ const CONFIG_WATCH_PREFIXES = ["size.", "dieline."] as const;
26
+ const CANVAS_SERVICE_ID = "CanvasService";
27
+ const GET_SCENE_LAYOUT_COMMAND = "getSceneLayout";
28
+ const GET_SCENE_GEOMETRY_COMMAND = "getSceneGeometry";
29
+
30
+ export class SceneLayoutService implements Service {
31
+ private context?: ServiceContext;
32
+ private canvasService?: CanvasService;
33
+ private configService?: ConfigurationService;
34
+ private lastLayout: SceneLayoutSnapshot | null = null;
35
+ private lastGeometry: SceneGeometrySnapshot | null = null;
36
+ private readonly subscriptions = new SubscriptionBag();
37
+ private commandDisposables: Array<{ dispose(): void }> = [];
38
+
39
+ init(context: ServiceContext) {
40
+ if (this.context) {
41
+ this.dispose(this.context);
42
+ }
43
+
44
+ const canvasService =
45
+ context.get<CanvasService>(CANVAS_SERVICE_ID);
46
+ const configService =
47
+ context.get<ConfigurationService>(CONFIGURATION_SERVICE);
48
+ const commandService = context.get<CommandService>(COMMAND_SERVICE);
49
+
50
+ if (!canvasService || !configService || !commandService) {
51
+ throw new Error(
52
+ "[SceneLayoutService] CanvasService, ConfigurationService and CommandService are required.",
53
+ );
54
+ }
55
+
56
+ this.context = context;
57
+ this.canvasService = canvasService;
58
+ this.configService = configService;
59
+
60
+ this.commandDisposables.push(
61
+ commandService.registerCommand(GET_SCENE_LAYOUT_COMMAND, () =>
62
+ this.getLayout(),
63
+ ),
64
+ commandService.registerCommand(GET_SCENE_GEOMETRY_COMMAND, () =>
65
+ this.getGeometry(),
66
+ ),
67
+ );
68
+
69
+ this.subscriptions.disposeAll();
70
+ this.subscriptions.onConfigChange(configService, this.onConfigChanged);
71
+ this.subscriptions.on(context.eventBus, "canvas:resized", this.onCanvasResized);
72
+ this.refresh();
73
+ }
74
+
75
+ dispose(context: ServiceContext) {
76
+ this.subscriptions.disposeAll();
77
+ this.commandDisposables.forEach((item) => item.dispose());
78
+ this.commandDisposables = [];
79
+ this.context = undefined;
80
+ this.canvasService = undefined;
81
+ this.configService = undefined;
82
+ this.lastLayout = null;
83
+ this.lastGeometry = null;
84
+ }
85
+
86
+ private onCanvasResized = () => {
87
+ this.refresh();
88
+ };
89
+
90
+ private onConfigChanged = (e: ConfigChangeEvent) => {
91
+ if (CONFIG_WATCH_PREFIXES.some((prefix) => e.key.startsWith(prefix))) {
92
+ this.refresh();
93
+ }
94
+ };
95
+
96
+ private refresh() {
97
+ const layout = this.getLayout(true);
98
+ if (!layout) {
99
+ this.lastGeometry = null;
100
+ return;
101
+ }
102
+
103
+ this.context?.eventBus.emit("scene:layout:change", layout);
104
+
105
+ const geometry = this.getGeometry(true);
106
+ if (geometry) {
107
+ this.context?.eventBus.emit("scene:geometry:change", geometry);
108
+ }
109
+ }
110
+
111
+ getLayout(forceRefresh = false): SceneLayoutSnapshot | null {
112
+ if (!this.canvasService || !this.configService) return null;
113
+ if (!forceRefresh && this.lastLayout) return this.lastLayout;
114
+
115
+ const state = readSizeState(this.configService);
116
+ const layout = computeSceneLayout(this.canvasService, state);
117
+ if (!layout) {
118
+ this.lastLayout = null;
119
+ return null;
120
+ }
121
+
122
+ this.lastLayout = layout;
123
+ return layout;
124
+ }
125
+
126
+ getGeometry(forceRefresh = false): SceneGeometrySnapshot | null {
127
+ if (!this.configService) return null;
128
+ const layout = this.getLayout(forceRefresh);
129
+ if (!layout) {
130
+ this.lastGeometry = null;
131
+ return null;
132
+ }
133
+ if (!forceRefresh && this.lastGeometry) return this.lastGeometry;
134
+
135
+ const geometry = buildSceneGeometry(this.configService, layout);
136
+ this.lastGeometry = geometry;
137
+ return geometry;
138
+ }
139
+ }
@@ -1,4 +1,5 @@
1
1
  export { default as CanvasService } from "./CanvasService";
2
+ export * from "./SceneLayoutService";
2
3
  export * from "./renderSpec";
3
4
  export * from "./visibility";
4
5
  export * from "./ViewportSystem";
@@ -52,6 +52,7 @@ export type VisibilityExpr =
52
52
  | { op: "const"; value: boolean }
53
53
  | { op: "activeToolIn"; ids: string[] }
54
54
  | { op: "sessionActive"; toolId: string }
55
+ | { op: "anySessionActive" }
55
56
  | { op: "layerExists"; layerId: string }
56
57
  | {
57
58
  op: "layerObjectCount";
@@ -66,6 +67,7 @@ export type VisibilityExpr =
66
67
  export interface RenderClipPathEffectSpec {
67
68
  type: "clipPath";
68
69
  id?: string;
70
+ visibility?: VisibilityExpr;
69
71
  source: RenderObjectSpec;
70
72
  targetPassIds: string[];
71
73
  }
@@ -8,6 +8,7 @@ export interface VisibilityLayerState {
8
8
  export interface VisibilityEvalContext {
9
9
  activeToolId?: string | null;
10
10
  isSessionActive?: (toolId: string) => boolean;
11
+ hasAnyActiveSession?: () => boolean;
11
12
  layers: Map<string, VisibilityLayerState>;
12
13
  }
13
14
 
@@ -51,6 +52,10 @@ export function evaluateVisibilityExpr(
51
52
  return context.isSessionActive ? context.isSessionActive(toolId) : false;
52
53
  }
53
54
 
55
+ if (expr.op === "anySessionActive") {
56
+ return context.hasAnyActiveSession ? context.hasAnyActiveSession() : false;
57
+ }
58
+
54
59
  if (expr.op === "layerExists") {
55
60
  return layerState(context, expr.layerId).exists === true;
56
61
  }
@@ -0,0 +1,23 @@
1
+ export const BACKGROUND_LAYER_ID = "background";
2
+ export const IMAGE_OBJECT_LAYER_ID = "image.user";
3
+ export const IMAGE_OVERLAY_LAYER_ID = "image-overlay";
4
+ export const WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
5
+ export const WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
6
+ export const WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
7
+ export const DIELINE_LAYER_ID = "dieline-overlay";
8
+ export const FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
9
+ export const RULER_LAYER_ID = "ruler-overlay";
10
+ export const FILM_LAYER_ID = "overlay";
11
+
12
+ export const LAYER_IDS = {
13
+ background: BACKGROUND_LAYER_ID,
14
+ imageObject: IMAGE_OBJECT_LAYER_ID,
15
+ imageOverlay: IMAGE_OVERLAY_LAYER_ID,
16
+ whiteInkObject: WHITE_INK_OBJECT_LAYER_ID,
17
+ whiteInkCover: WHITE_INK_COVER_LAYER_ID,
18
+ whiteInkOverlay: WHITE_INK_OVERLAY_LAYER_ID,
19
+ dieline: DIELINE_LAYER_ID,
20
+ featureOverlay: FEATURE_OVERLAY_LAYER_ID,
21
+ rulerOverlay: RULER_LAYER_ID,
22
+ filmOverlay: FILM_LAYER_ID,
23
+ } as const;
@@ -0,0 +1,103 @@
1
+ export interface SourceSize {
2
+ width: number;
3
+ height: number;
4
+ }
5
+
6
+ export interface RectLike {
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ export interface SourceSizeCache {
12
+ ensureImageSize: (src: string) => Promise<SourceSize | null>;
13
+ rememberSourceSize: (src: string, size: Partial<SourceSize>) => SourceSize | null;
14
+ getSourceSize: (src: string) => SourceSize | null;
15
+ deleteSourceSize: (src: string) => void;
16
+ clear: () => void;
17
+ }
18
+
19
+ export function normalizeSourceSize(size: Partial<SourceSize>): SourceSize | null {
20
+ const width = Number(size.width || 0);
21
+ const height = Number(size.height || 0);
22
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
23
+ if (width <= 0 || height <= 0) return null;
24
+ return { width, height };
25
+ }
26
+
27
+ export function getCoverScale(frame: RectLike, source: RectLike): number {
28
+ const frameWidth = Math.max(1, Number(frame.width || 0));
29
+ const frameHeight = Math.max(1, Number(frame.height || 0));
30
+ const sourceWidth = Math.max(1, Number(source.width || 0));
31
+ const sourceHeight = Math.max(1, Number(source.height || 0));
32
+ return Math.max(frameWidth / sourceWidth, frameHeight / sourceHeight);
33
+ }
34
+
35
+ export function createSourceSizeCache(
36
+ loadSize: (src: string) => Promise<SourceSize | null>,
37
+ ): SourceSizeCache {
38
+ const sizesBySrc = new Map<string, SourceSize>();
39
+ const pendingBySrc = new Map<string, Promise<SourceSize | null>>();
40
+
41
+ const rememberSourceSize = (
42
+ src: string,
43
+ size: Partial<SourceSize>,
44
+ ): SourceSize | null => {
45
+ const normalized = normalizeSourceSize(size);
46
+ if (!src || !normalized) return null;
47
+ sizesBySrc.set(src, normalized);
48
+ return normalized;
49
+ };
50
+
51
+ const getSourceSize = (src: string): SourceSize | null => {
52
+ if (!src) return null;
53
+ const cached = sizesBySrc.get(src);
54
+ if (!cached) return null;
55
+ return { width: cached.width, height: cached.height };
56
+ };
57
+
58
+ const ensureImageSize = async (src: string): Promise<SourceSize | null> => {
59
+ if (!src) return null;
60
+
61
+ const cached = sizesBySrc.get(src);
62
+ if (cached) return { width: cached.width, height: cached.height };
63
+
64
+ const pending = pendingBySrc.get(src);
65
+ if (pending) {
66
+ return pending;
67
+ }
68
+
69
+ const task = loadSize(src);
70
+ pendingBySrc.set(src, task);
71
+
72
+ try {
73
+ const size = await task;
74
+ if (size) {
75
+ rememberSourceSize(src, size);
76
+ }
77
+ return size;
78
+ } finally {
79
+ if (pendingBySrc.get(src) === task) {
80
+ pendingBySrc.delete(src);
81
+ }
82
+ }
83
+ };
84
+
85
+ const deleteSourceSize = (src: string) => {
86
+ if (!src) return;
87
+ sizesBySrc.delete(src);
88
+ pendingBySrc.delete(src);
89
+ };
90
+
91
+ const clear = () => {
92
+ sizesBySrc.clear();
93
+ pendingBySrc.clear();
94
+ };
95
+
96
+ return {
97
+ ensureImageSize,
98
+ rememberSourceSize,
99
+ getSourceSize,
100
+ deleteSourceSize,
101
+ clear,
102
+ };
103
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./constants/layers";
2
+ export * from "./imaging/sourceSizeCache";
3
+ export * from "./runtime/sessionState";
4
+ export * from "./runtime/subscriptions";
5
+ export * from "./scene/frame";
6
+ export * from "./scene/sceneLayoutModel";