@pooder/kit 6.0.0 → 6.0.1

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.
@@ -20,7 +20,7 @@ const DEFAULT_BACKGROUND_CONFIG = {
20
20
  order: 0,
21
21
  enabled: true,
22
22
  exportable: false,
23
- color: "#aaa",
23
+ color: "#fff",
24
24
  },
25
25
  ],
26
26
  };
@@ -702,6 +702,10 @@ class DielineTool {
702
702
  {
703
703
  type: "clipPath",
704
704
  id: "dieline.clip.image",
705
+ visibility: {
706
+ op: "not",
707
+ expr: { op: "anySessionActive" },
708
+ },
705
709
  targetPassIds: [IMAGE_OBJECT_LAYER_ID],
706
710
  source: {
707
711
  id: "dieline.effect.clip-path",
@@ -8,6 +8,34 @@ const geometry_1 = require("./geometry");
8
8
  const sceneLayoutModel_1 = require("./sceneLayoutModel");
9
9
  const IMAGE_OBJECT_LAYER_ID = "image.user";
10
10
  const IMAGE_OVERLAY_LAYER_ID = "image-overlay";
11
+ const IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
12
+ "rotate",
13
+ "scale",
14
+ ];
15
+ const IMAGE_CONTROL_DESCRIPTORS = [
16
+ {
17
+ key: "tl",
18
+ capability: "rotate",
19
+ create: () => new fabric_1.Control({
20
+ x: -0.5,
21
+ y: -0.5,
22
+ actionName: "rotate",
23
+ actionHandler: fabric_1.controlsUtils.rotationWithSnapping,
24
+ cursorStyleHandler: fabric_1.controlsUtils.rotationStyleHandler,
25
+ }),
26
+ },
27
+ {
28
+ key: "br",
29
+ capability: "scale",
30
+ create: () => new fabric_1.Control({
31
+ x: 0.5,
32
+ y: 0.5,
33
+ actionName: "scale",
34
+ actionHandler: fabric_1.controlsUtils.scalingEqually,
35
+ cursorStyleHandler: fabric_1.controlsUtils.scaleCursorStyleHandler,
36
+ }),
37
+ },
38
+ ];
11
39
  class ImageTool {
12
40
  constructor() {
13
41
  this.id = "pooder.kit.image";
@@ -26,6 +54,7 @@ class ImageTool {
26
54
  this.renderSeq = 0;
27
55
  this.imageSpecs = [];
28
56
  this.overlaySpecs = [];
57
+ this.imageControlsByCapabilityKey = new Map();
29
58
  this.onToolActivated = (event) => {
30
59
  const before = this.isToolActive;
31
60
  this.syncToolActiveFromWorkbench(event.id);
@@ -183,7 +212,12 @@ class ImageTool {
183
212
  this.updateImages();
184
213
  return;
185
214
  }
186
- if (e.key.startsWith("size.") || e.key.startsWith("image.frame.")) {
215
+ if (e.key.startsWith("size.") ||
216
+ e.key.startsWith("image.frame.") ||
217
+ e.key.startsWith("image.control.")) {
218
+ if (e.key.startsWith("image.control.")) {
219
+ this.imageControlsByCapabilityKey.clear();
220
+ }
187
221
  this.updateImages();
188
222
  }
189
223
  });
@@ -207,6 +241,7 @@ class ImageTool {
207
241
  this.cropShapeHatchPatternKey = undefined;
208
242
  this.imageSpecs = [];
209
243
  this.overlaySpecs = [];
244
+ this.imageControlsByCapabilityKey.clear();
210
245
  this.clearRenderedImages();
211
246
  this.renderProducerDisposable?.dispose();
212
247
  this.renderProducerDisposable = undefined;
@@ -228,6 +263,83 @@ class ImageTool {
228
263
  isImageEditingVisible() {
229
264
  return (this.isToolActive || this.isImageSelectionActive || !!this.focusedImageId);
230
265
  }
266
+ getEnabledImageControlCapabilities() {
267
+ return IMAGE_DEFAULT_CONTROL_CAPABILITIES;
268
+ }
269
+ getImageControls(capabilities) {
270
+ const normalized = [...new Set(capabilities)].sort();
271
+ const cacheKey = normalized.join("|");
272
+ const cached = this.imageControlsByCapabilityKey.get(cacheKey);
273
+ if (cached) {
274
+ return cached;
275
+ }
276
+ const enabled = new Set(normalized);
277
+ const controls = {};
278
+ IMAGE_CONTROL_DESCRIPTORS.forEach((descriptor) => {
279
+ if (!enabled.has(descriptor.capability))
280
+ return;
281
+ controls[descriptor.key] = descriptor.create();
282
+ });
283
+ this.imageControlsByCapabilityKey.set(cacheKey, controls);
284
+ return controls;
285
+ }
286
+ getImageControlVisualConfig() {
287
+ const cornerSizeRaw = Number(this.getConfig("image.control.cornerSize", 14) ?? 14);
288
+ const touchCornerSizeRaw = Number(this.getConfig("image.control.touchCornerSize", 24) ?? 24);
289
+ const borderScaleFactorRaw = Number(this.getConfig("image.control.borderScaleFactor", 1.5) ?? 1.5);
290
+ const paddingRaw = Number(this.getConfig("image.control.padding", 0) ?? 0);
291
+ const cornerStyleRaw = (this.getConfig("image.control.cornerStyle", "circle") || "circle");
292
+ const cornerStyle = cornerStyleRaw === "rect" ? "rect" : "circle";
293
+ return {
294
+ cornerSize: Number.isFinite(cornerSizeRaw)
295
+ ? Math.max(4, Math.min(64, cornerSizeRaw))
296
+ : 14,
297
+ touchCornerSize: Number.isFinite(touchCornerSizeRaw)
298
+ ? Math.max(8, Math.min(96, touchCornerSizeRaw))
299
+ : 24,
300
+ cornerStyle,
301
+ cornerColor: this.getConfig("image.control.cornerColor", "#ffffff") ||
302
+ "#ffffff",
303
+ cornerStrokeColor: this.getConfig("image.control.cornerStrokeColor", "#1677ff") ||
304
+ "#1677ff",
305
+ transparentCorners: !!this.getConfig("image.control.transparentCorners", false),
306
+ borderColor: this.getConfig("image.control.borderColor", "#1677ff") ||
307
+ "#1677ff",
308
+ borderScaleFactor: Number.isFinite(borderScaleFactorRaw)
309
+ ? Math.max(0.5, Math.min(8, borderScaleFactorRaw))
310
+ : 1.5,
311
+ padding: Number.isFinite(paddingRaw)
312
+ ? Math.max(0, Math.min(64, paddingRaw))
313
+ : 0,
314
+ };
315
+ }
316
+ applyImageObjectInteractionState(obj) {
317
+ if (!obj)
318
+ return;
319
+ const visible = this.isImageEditingVisible();
320
+ const visual = this.getImageControlVisualConfig();
321
+ obj.set({
322
+ selectable: visible,
323
+ evented: visible,
324
+ hasControls: visible,
325
+ hasBorders: visible,
326
+ lockScalingFlip: true,
327
+ cornerSize: visual.cornerSize,
328
+ touchCornerSize: visual.touchCornerSize,
329
+ cornerStyle: visual.cornerStyle,
330
+ cornerColor: visual.cornerColor,
331
+ cornerStrokeColor: visual.cornerStrokeColor,
332
+ transparentCorners: visual.transparentCorners,
333
+ borderColor: visual.borderColor,
334
+ borderScaleFactor: visual.borderScaleFactor,
335
+ padding: visual.padding,
336
+ });
337
+ obj.controls = this.getImageControls(this.getEnabledImageControlCapabilities());
338
+ obj.setCoords?.();
339
+ }
340
+ refreshImageObjectInteractionState() {
341
+ this.getImageObjects().forEach((obj) => this.applyImageObjectInteractionState(obj));
342
+ }
231
343
  isDebugEnabled() {
232
344
  return !!this.getConfig("image.debug", false);
233
345
  }
@@ -271,6 +383,73 @@ class ImageTool {
271
383
  label: "Image Debug Log",
272
384
  default: false,
273
385
  },
386
+ {
387
+ id: "image.control.cornerSize",
388
+ type: "number",
389
+ label: "Image Control Corner Size",
390
+ min: 4,
391
+ max: 64,
392
+ step: 1,
393
+ default: 14,
394
+ },
395
+ {
396
+ id: "image.control.touchCornerSize",
397
+ type: "number",
398
+ label: "Image Control Touch Corner Size",
399
+ min: 8,
400
+ max: 96,
401
+ step: 1,
402
+ default: 24,
403
+ },
404
+ {
405
+ id: "image.control.cornerStyle",
406
+ type: "select",
407
+ label: "Image Control Corner Style",
408
+ options: ["circle", "rect"],
409
+ default: "circle",
410
+ },
411
+ {
412
+ id: "image.control.cornerColor",
413
+ type: "color",
414
+ label: "Image Control Corner Color",
415
+ default: "#ffffff",
416
+ },
417
+ {
418
+ id: "image.control.cornerStrokeColor",
419
+ type: "color",
420
+ label: "Image Control Corner Stroke Color",
421
+ default: "#1677ff",
422
+ },
423
+ {
424
+ id: "image.control.transparentCorners",
425
+ type: "boolean",
426
+ label: "Image Control Transparent Corners",
427
+ default: false,
428
+ },
429
+ {
430
+ id: "image.control.borderColor",
431
+ type: "color",
432
+ label: "Image Control Border Color",
433
+ default: "#1677ff",
434
+ },
435
+ {
436
+ id: "image.control.borderScaleFactor",
437
+ type: "number",
438
+ label: "Image Control Border Width",
439
+ min: 0.5,
440
+ max: 8,
441
+ step: 0.1,
442
+ default: 1.5,
443
+ },
444
+ {
445
+ id: "image.control.padding",
446
+ type: "number",
447
+ label: "Image Control Padding",
448
+ min: 0,
449
+ max: 64,
450
+ step: 1,
451
+ default: 0,
452
+ },
274
453
  {
275
454
  id: "image.frame.strokeColor",
276
455
  type: "color",
@@ -513,12 +692,7 @@ class ImageTool {
513
692
  else {
514
693
  const obj = this.getImageObject(id);
515
694
  if (obj) {
516
- obj.set({
517
- selectable: true,
518
- evented: true,
519
- hasControls: true,
520
- hasBorders: true,
521
- });
695
+ this.applyImageObjectInteractionState(obj);
522
696
  canvas.setActiveObject(obj);
523
697
  }
524
698
  }
@@ -1317,6 +1491,7 @@ class ImageTool {
1317
1491
  await this.canvasService.flushRenderFromProducers();
1318
1492
  if (seq !== this.renderSeq)
1319
1493
  return;
1494
+ this.refreshImageObjectInteractionState();
1320
1495
  renderItems.forEach((item) => {
1321
1496
  if (!this.getImageObject(item.id))
1322
1497
  return;
@@ -15,6 +15,7 @@ class CanvasService {
15
15
  this.visibilityRefreshScheduled = false;
16
16
  this.managedProducerPassIds = new Set();
17
17
  this.managedPassMetas = new Map();
18
+ this.managedPassEffects = [];
18
19
  this.canvasForwardersBound = false;
19
20
  this.forwardSelectionCreated = (e) => {
20
21
  this.eventBus?.emit("selection:created", e);
@@ -36,9 +37,11 @@ class CanvasService {
36
37
  };
37
38
  this.onToolActivated = () => {
38
39
  this.applyManagedPassVisibility();
40
+ void this.applyManagedPassEffects(undefined, { render: true });
39
41
  };
40
42
  this.onToolSessionChanged = () => {
41
43
  this.applyManagedPassVisibility();
44
+ void this.applyManagedPassEffects(undefined, { render: true });
42
45
  };
43
46
  this.onCanvasObjectChanged = () => {
44
47
  if (this.producerApplyInProgress)
@@ -106,6 +109,7 @@ class CanvasService {
106
109
  this.renderProducers.clear();
107
110
  this.managedProducerPassIds.clear();
108
111
  this.managedPassMetas.clear();
112
+ this.managedPassEffects = [];
109
113
  this.context = undefined;
110
114
  this.workbenchService = undefined;
111
115
  this.toolSessionService = undefined;
@@ -226,6 +230,7 @@ class CanvasService {
226
230
  return {
227
231
  type: "clipPath",
228
232
  key,
233
+ visibility: effect.visibility,
229
234
  source: {
230
235
  ...source,
231
236
  id: sourceId,
@@ -337,23 +342,30 @@ class CanvasService {
337
342
  });
338
343
  return state;
339
344
  }
345
+ isSessionActive(toolId) {
346
+ if (!this.toolSessionService)
347
+ return false;
348
+ return this.toolSessionService.getState(toolId).status === "active";
349
+ }
350
+ hasAnyActiveSession() {
351
+ return this.toolSessionService?.hasAnyActiveSession() ?? false;
352
+ }
353
+ buildVisibilityEvalContext(layers) {
354
+ return {
355
+ activeToolId: this.workbenchService?.activeToolId ?? null,
356
+ isSessionActive: (toolId) => this.isSessionActive(toolId),
357
+ hasAnyActiveSession: () => this.hasAnyActiveSession(),
358
+ layers,
359
+ };
360
+ }
340
361
  applyManagedPassVisibility(options = {}) {
341
362
  if (!this.managedPassMetas.size)
342
363
  return false;
343
364
  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
- };
365
+ const context = this.buildVisibilityEvalContext(layers);
350
366
  let changed = false;
351
367
  this.managedPassMetas.forEach((meta) => {
352
- const visible = (0, visibility_1.evaluateVisibilityExpr)(meta.visibility, {
353
- activeToolId,
354
- isSessionActive,
355
- layers,
356
- });
368
+ const visible = (0, visibility_1.evaluateVisibilityExpr)(meta.visibility, context);
357
369
  changed = this.setPassVisibility(meta.id, visible) || changed;
358
370
  });
359
371
  if (changed && options.render !== false) {
@@ -419,8 +431,9 @@ class CanvasService {
419
431
  }
420
432
  this.managedProducerPassIds = nextPassIds;
421
433
  this.managedPassMetas = nextManagedPassMetas;
434
+ this.managedPassEffects = nextEffects;
422
435
  this.syncManagedPassStacking(Array.from(nextManagedPassMetas.values()));
423
- await this.applyManagedPassEffects(nextEffects);
436
+ await this.applyManagedPassEffects(nextEffects, { render: false });
424
437
  this.applyManagedPassVisibility({ render: false });
425
438
  }
426
439
  finally {
@@ -428,11 +441,16 @@ class CanvasService {
428
441
  }
429
442
  this.requestRenderAll();
430
443
  }
431
- async applyManagedPassEffects(effects) {
444
+ async applyManagedPassEffects(effects = this.managedPassEffects, options = {}) {
432
445
  const effectTargetMap = new Map();
446
+ const layers = this.getPassRuntimeState();
447
+ const visibilityContext = this.buildVisibilityEvalContext(layers);
433
448
  for (const effect of effects) {
434
449
  if (effect.type !== "clipPath")
435
450
  continue;
451
+ if (!(0, visibility_1.evaluateVisibilityExpr)(effect.visibility, visibilityContext)) {
452
+ continue;
453
+ }
436
454
  effect.targetPassIds.forEach((targetPassId) => {
437
455
  this.getPassCanvasObjects(targetPassId).forEach((obj) => {
438
456
  effectTargetMap.set(obj, effect);
@@ -458,6 +476,9 @@ class CanvasService {
458
476
  }
459
477
  await this.applyClipPathEffectToObject(obj, template, targetEffect.key);
460
478
  }
479
+ if (options.render !== false) {
480
+ this.requestRenderAll();
481
+ }
461
482
  }
462
483
  getObject(id, passId) {
463
484
  const normalizedId = String(id || "").trim();
@@ -31,6 +31,9 @@ function evaluateVisibilityExpr(expr, context) {
31
31
  return false;
32
32
  return context.isSessionActive ? context.isSessionActive(toolId) : false;
33
33
  }
34
+ if (expr.op === "anySessionActive") {
35
+ return context.hasAnyActiveSession ? context.hasAnyActiveSession() : false;
36
+ }
34
37
  if (expr.op === "layerExists") {
35
38
  return layerState(context, expr.layerId).exists === true;
36
39
  }
@@ -93,6 +93,7 @@ function testVisibilityDsl() {
93
93
  const context = {
94
94
  activeToolId: "pooder.kit.image",
95
95
  isSessionActive: (toolId) => toolId === "pooder.kit.feature",
96
+ hasAnyActiveSession: () => true,
96
97
  layers,
97
98
  };
98
99
  assert((0, visibility_1.evaluateVisibilityExpr)({ op: "const", value: true }, context) === true, "const true failed");
@@ -101,6 +102,7 @@ function testVisibilityDsl() {
101
102
  assert((0, visibility_1.evaluateVisibilityExpr)({ op: "activeToolIn", ids: ["pooder.kit.white-ink"] }, context) === false, "activeToolIn false failed");
102
103
  assert((0, visibility_1.evaluateVisibilityExpr)({ op: "sessionActive", toolId: "pooder.kit.feature" }, context) === true, "sessionActive true failed");
103
104
  assert((0, visibility_1.evaluateVisibilityExpr)({ op: "sessionActive", toolId: "pooder.kit.ruler" }, context) === false, "sessionActive false failed");
105
+ assert((0, visibility_1.evaluateVisibilityExpr)({ op: "anySessionActive" }, context) === true, "anySessionActive true failed");
104
106
  assert((0, visibility_1.evaluateVisibilityExpr)({ op: "layerExists", layerId: "ruler-overlay" }, context) === true, "layerExists true failed");
105
107
  assert((0, visibility_1.evaluateVisibilityExpr)({ op: "layerExists", layerId: "missing-layer" }, context) === false, "layerExists false failed");
106
108
  const comparisons = [
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @pooder/kit
2
2
 
3
+ ## 6.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - image control
8
+
3
9
  ## 6.0.0
4
10
 
5
11
  ### Major Changes
package/dist/index.d.mts CHANGED
@@ -94,6 +94,7 @@ declare class ImageTool implements Extension {
94
94
  private imageSpecs;
95
95
  private overlaySpecs;
96
96
  private renderProducerDisposable?;
97
+ private imageControlsByCapabilityKey;
97
98
  activate(context: ExtensionContext): void;
98
99
  deactivate(context: ExtensionContext): void;
99
100
  private onToolActivated;
@@ -103,6 +104,11 @@ declare class ImageTool implements Extension {
103
104
  private onSceneGeometryChanged;
104
105
  private syncToolActiveFromWorkbench;
105
106
  private isImageEditingVisible;
107
+ private getEnabledImageControlCapabilities;
108
+ private getImageControls;
109
+ private getImageControlVisualConfig;
110
+ private applyImageObjectInteractionState;
111
+ private refreshImageObjectInteractionState;
106
112
  private isDebugEnabled;
107
113
  private debug;
108
114
  contribute(): {
@@ -707,6 +713,8 @@ type VisibilityExpr = {
707
713
  } | {
708
714
  op: "sessionActive";
709
715
  toolId: string;
716
+ } | {
717
+ op: "anySessionActive";
710
718
  } | {
711
719
  op: "layerExists";
712
720
  layerId: string;
@@ -728,6 +736,7 @@ type VisibilityExpr = {
728
736
  interface RenderClipPathEffectSpec {
729
737
  type: "clipPath";
730
738
  id?: string;
739
+ visibility?: VisibilityExpr;
731
740
  source: RenderObjectSpec;
732
741
  targetPassIds: string[];
733
742
  }
@@ -771,6 +780,7 @@ declare class CanvasService implements Service {
771
780
  private visibilityRefreshScheduled;
772
781
  private managedProducerPassIds;
773
782
  private managedPassMetas;
783
+ private managedPassEffects;
774
784
  private canvasForwardersBound;
775
785
  private readonly forwardSelectionCreated;
776
786
  private readonly forwardSelectionUpdated;
@@ -808,6 +818,9 @@ declare class CanvasService implements Service {
808
818
  private isManagedPassObject;
809
819
  private syncManagedPassStacking;
810
820
  private getPassRuntimeState;
821
+ private isSessionActive;
822
+ private hasAnyActiveSession;
823
+ private buildVisibilityEvalContext;
811
824
  private applyManagedPassVisibility;
812
825
  private scheduleManagedPassVisibilityRefresh;
813
826
  private collectAndApplyProducerSpecs;
@@ -911,6 +924,7 @@ interface VisibilityLayerState {
911
924
  interface VisibilityEvalContext {
912
925
  activeToolId?: string | null;
913
926
  isSessionActive?: (toolId: string) => boolean;
927
+ hasAnyActiveSession?: () => boolean;
914
928
  layers: Map<string, VisibilityLayerState>;
915
929
  }
916
930
  declare function evaluateVisibilityExpr(expr: VisibilityExpr | undefined, context: VisibilityEvalContext): boolean;
package/dist/index.d.ts CHANGED
@@ -94,6 +94,7 @@ declare class ImageTool implements Extension {
94
94
  private imageSpecs;
95
95
  private overlaySpecs;
96
96
  private renderProducerDisposable?;
97
+ private imageControlsByCapabilityKey;
97
98
  activate(context: ExtensionContext): void;
98
99
  deactivate(context: ExtensionContext): void;
99
100
  private onToolActivated;
@@ -103,6 +104,11 @@ declare class ImageTool implements Extension {
103
104
  private onSceneGeometryChanged;
104
105
  private syncToolActiveFromWorkbench;
105
106
  private isImageEditingVisible;
107
+ private getEnabledImageControlCapabilities;
108
+ private getImageControls;
109
+ private getImageControlVisualConfig;
110
+ private applyImageObjectInteractionState;
111
+ private refreshImageObjectInteractionState;
106
112
  private isDebugEnabled;
107
113
  private debug;
108
114
  contribute(): {
@@ -707,6 +713,8 @@ type VisibilityExpr = {
707
713
  } | {
708
714
  op: "sessionActive";
709
715
  toolId: string;
716
+ } | {
717
+ op: "anySessionActive";
710
718
  } | {
711
719
  op: "layerExists";
712
720
  layerId: string;
@@ -728,6 +736,7 @@ type VisibilityExpr = {
728
736
  interface RenderClipPathEffectSpec {
729
737
  type: "clipPath";
730
738
  id?: string;
739
+ visibility?: VisibilityExpr;
731
740
  source: RenderObjectSpec;
732
741
  targetPassIds: string[];
733
742
  }
@@ -771,6 +780,7 @@ declare class CanvasService implements Service {
771
780
  private visibilityRefreshScheduled;
772
781
  private managedProducerPassIds;
773
782
  private managedPassMetas;
783
+ private managedPassEffects;
774
784
  private canvasForwardersBound;
775
785
  private readonly forwardSelectionCreated;
776
786
  private readonly forwardSelectionUpdated;
@@ -808,6 +818,9 @@ declare class CanvasService implements Service {
808
818
  private isManagedPassObject;
809
819
  private syncManagedPassStacking;
810
820
  private getPassRuntimeState;
821
+ private isSessionActive;
822
+ private hasAnyActiveSession;
823
+ private buildVisibilityEvalContext;
811
824
  private applyManagedPassVisibility;
812
825
  private scheduleManagedPassVisibilityRefresh;
813
826
  private collectAndApplyProducerSpecs;
@@ -911,6 +924,7 @@ interface VisibilityLayerState {
911
924
  interface VisibilityEvalContext {
912
925
  activeToolId?: string | null;
913
926
  isSessionActive?: (toolId: string) => boolean;
927
+ hasAnyActiveSession?: () => boolean;
914
928
  layers: Map<string, VisibilityLayerState>;
915
929
  }
916
930
  declare function evaluateVisibilityExpr(expr: VisibilityExpr | undefined, context: VisibilityEvalContext): boolean;