@pooder/kit 6.0.1 → 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 (91) 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/dieline/DielineTool.js +748 -0
  4. package/.test-dist/src/extensions/dieline/commands.js +127 -0
  5. package/.test-dist/src/extensions/dieline/config.js +107 -0
  6. package/.test-dist/src/extensions/dieline/index.js +21 -0
  7. package/.test-dist/src/extensions/dieline/model.js +2 -0
  8. package/.test-dist/src/extensions/dieline/renderer.js +2 -0
  9. package/.test-dist/src/extensions/feature/FeatureTool.js +914 -0
  10. package/.test-dist/src/extensions/feature/index.js +17 -0
  11. package/.test-dist/src/extensions/film/FilmTool.js +207 -0
  12. package/.test-dist/src/extensions/film/index.js +17 -0
  13. package/.test-dist/src/extensions/image/ImageTool.js +1499 -0
  14. package/.test-dist/src/extensions/image/commands.js +162 -0
  15. package/.test-dist/src/extensions/image/config.js +129 -0
  16. package/.test-dist/src/extensions/image/index.js +21 -0
  17. package/.test-dist/src/extensions/image/model.js +2 -0
  18. package/.test-dist/src/extensions/image/renderer.js +5 -0
  19. package/.test-dist/src/extensions/mirror/MirrorTool.js +104 -0
  20. package/.test-dist/src/extensions/mirror/index.js +17 -0
  21. package/.test-dist/src/extensions/ruler/RulerTool.js +442 -0
  22. package/.test-dist/src/extensions/ruler/index.js +17 -0
  23. package/.test-dist/src/extensions/sceneLayout.js +2 -93
  24. package/.test-dist/src/extensions/sceneLayoutModel.js +15 -200
  25. package/.test-dist/src/extensions/size/SizeTool.js +332 -0
  26. package/.test-dist/src/extensions/size/index.js +17 -0
  27. package/.test-dist/src/extensions/white-ink/WhiteInkTool.js +1003 -0
  28. package/.test-dist/src/extensions/white-ink/commands.js +148 -0
  29. package/.test-dist/src/extensions/white-ink/config.js +31 -0
  30. package/.test-dist/src/extensions/white-ink/index.js +21 -0
  31. package/.test-dist/src/extensions/white-ink/model.js +2 -0
  32. package/.test-dist/src/extensions/white-ink/renderer.js +5 -0
  33. package/.test-dist/src/services/SceneLayoutService.js +96 -0
  34. package/.test-dist/src/services/index.js +1 -0
  35. package/.test-dist/src/shared/constants/layers.js +25 -0
  36. package/.test-dist/src/shared/imaging/sourceSizeCache.js +82 -0
  37. package/.test-dist/src/shared/index.js +22 -0
  38. package/.test-dist/src/shared/runtime/sessionState.js +74 -0
  39. package/.test-dist/src/shared/runtime/subscriptions.js +30 -0
  40. package/.test-dist/src/shared/scene/frame.js +34 -0
  41. package/.test-dist/src/shared/scene/sceneLayoutModel.js +202 -0
  42. package/.test-dist/tests/run.js +116 -0
  43. package/CHANGELOG.md +6 -0
  44. package/dist/index.d.mts +390 -367
  45. package/dist/index.d.ts +390 -367
  46. package/dist/index.js +5138 -4927
  47. package/dist/index.mjs +1149 -1977
  48. package/dist/tracer-PO7CRBYY.mjs +1016 -0
  49. package/package.json +1 -1
  50. package/src/extensions/{background.ts → background/BackgroundTool.ts} +33 -50
  51. package/src/extensions/background/index.ts +1 -0
  52. package/src/extensions/{dieline.ts → dieline/DielineTool.ts} +14 -218
  53. package/src/extensions/dieline/commands.ts +109 -0
  54. package/src/extensions/dieline/config.ts +106 -0
  55. package/src/extensions/dieline/index.ts +5 -0
  56. package/src/extensions/dieline/model.ts +1 -0
  57. package/src/extensions/dieline/renderer.ts +1 -0
  58. package/src/extensions/{feature.ts → feature/FeatureTool.ts} +27 -21
  59. package/src/extensions/feature/index.ts +1 -0
  60. package/src/extensions/{film.ts → film/FilmTool.ts} +36 -48
  61. package/src/extensions/film/index.ts +1 -0
  62. package/src/extensions/{image.ts → image/ImageTool.ts} +123 -402
  63. package/src/extensions/image/commands.ts +176 -0
  64. package/src/extensions/image/config.ts +128 -0
  65. package/src/extensions/image/index.ts +5 -0
  66. package/src/extensions/image/model.ts +1 -0
  67. package/src/extensions/image/renderer.ts +1 -0
  68. package/src/extensions/{mirror.ts → mirror/MirrorTool.ts} +1 -1
  69. package/src/extensions/mirror/index.ts +1 -0
  70. package/src/extensions/{ruler.ts → ruler/RulerTool.ts} +4 -5
  71. package/src/extensions/ruler/index.ts +1 -0
  72. package/src/extensions/sceneLayout.ts +1 -140
  73. package/src/extensions/sceneLayoutModel.ts +1 -364
  74. package/src/extensions/{size.ts → size/SizeTool.ts} +7 -6
  75. package/src/extensions/size/index.ts +1 -0
  76. package/src/extensions/{white-ink.ts → white-ink/WhiteInkTool.ts} +130 -317
  77. package/src/extensions/white-ink/commands.ts +157 -0
  78. package/src/extensions/white-ink/config.ts +30 -0
  79. package/src/extensions/white-ink/index.ts +5 -0
  80. package/src/extensions/white-ink/model.ts +1 -0
  81. package/src/extensions/white-ink/renderer.ts +1 -0
  82. package/src/services/SceneLayoutService.ts +139 -0
  83. package/src/services/index.ts +1 -0
  84. package/src/shared/constants/layers.ts +23 -0
  85. package/src/shared/imaging/sourceSizeCache.ts +103 -0
  86. package/src/shared/index.ts +6 -0
  87. package/src/shared/runtime/sessionState.ts +105 -0
  88. package/src/shared/runtime/subscriptions.ts +45 -0
  89. package/src/shared/scene/frame.ts +46 -0
  90. package/src/shared/scene/sceneLayoutModel.ts +367 -0
  91. package/tests/run.ts +146 -0
@@ -0,0 +1,914 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeatureTool = void 0;
4
+ const core_1 = require("@pooder/core");
5
+ const geometry_1 = require("../geometry");
6
+ const constraints_1 = require("../constraints");
7
+ const featureComplete_1 = require("../featureComplete");
8
+ const sceneLayoutModel_1 = require("../../shared/scene/sceneLayoutModel");
9
+ const layers_1 = require("../../shared/constants/layers");
10
+ const subscriptions_1 = require("../../shared/runtime/subscriptions");
11
+ const sessionState_1 = require("../../shared/runtime/sessionState");
12
+ const FEATURE_STROKE_WIDTH = 2;
13
+ const DEFAULT_RECT_SIZE = 10;
14
+ const DEFAULT_CIRCLE_RADIUS = 5;
15
+ class FeatureTool {
16
+ constructor(options) {
17
+ this.id = "pooder.kit.feature";
18
+ this.metadata = {
19
+ name: "FeatureTool",
20
+ };
21
+ this.workingFeatures = [];
22
+ this.isUpdatingConfig = false;
23
+ this.isToolActive = false;
24
+ this.isFeatureSessionActive = false;
25
+ this.sessionOriginalFeatures = null;
26
+ this.hasWorkingChanges = false;
27
+ this.specs = [];
28
+ this.renderSeq = 0;
29
+ this.subscriptions = new subscriptions_1.SubscriptionBag();
30
+ this.handleMoving = null;
31
+ this.handleModified = null;
32
+ this.handleSceneGeometryChange = null;
33
+ this.currentGeometry = null;
34
+ this.onToolActivated = (event) => {
35
+ this.isToolActive = event.id === this.id;
36
+ if (!this.isToolActive) {
37
+ this.restoreSessionFeaturesToConfig();
38
+ }
39
+ this.updateVisibility();
40
+ };
41
+ if (options) {
42
+ Object.assign(this, options);
43
+ }
44
+ }
45
+ activate(context) {
46
+ this.subscriptions.disposeAll();
47
+ this.context = context;
48
+ this.canvasService = context.services.get("CanvasService");
49
+ if (!this.canvasService) {
50
+ console.warn("CanvasService not found for FeatureTool");
51
+ return;
52
+ }
53
+ this.renderProducerDisposable?.dispose();
54
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(this.id, () => ({
55
+ passes: [
56
+ {
57
+ id: layers_1.FEATURE_OVERLAY_LAYER_ID,
58
+ stack: 880,
59
+ order: 0,
60
+ objects: this.specs,
61
+ },
62
+ ],
63
+ }), { priority: 350 });
64
+ const configService = context.services.get("ConfigurationService");
65
+ if (configService) {
66
+ const features = (configService.get("dieline.features", []) ||
67
+ []);
68
+ this.workingFeatures = this.cloneFeatures(features);
69
+ this.hasWorkingChanges = false;
70
+ this.subscriptions.onConfigChange(configService, (e) => {
71
+ if (this.isUpdatingConfig)
72
+ return;
73
+ if (e.key === "dieline.features") {
74
+ if (this.isFeatureSessionActive)
75
+ return;
76
+ const next = (e.value || []);
77
+ this.workingFeatures = this.cloneFeatures(next);
78
+ this.hasWorkingChanges = false;
79
+ this.redraw();
80
+ this.emitWorkingChange();
81
+ }
82
+ });
83
+ }
84
+ const toolSessionService = context.services.get("ToolSessionService");
85
+ this.dirtyTrackerDisposable = toolSessionService?.registerDirtyTracker(this.id, () => this.hasWorkingChanges);
86
+ this.subscriptions.on(context.eventBus, "tool:activated", this.onToolActivated);
87
+ this.setup();
88
+ }
89
+ deactivate(context) {
90
+ this.subscriptions.disposeAll();
91
+ this.restoreSessionFeaturesToConfig();
92
+ this.dirtyTrackerDisposable?.dispose();
93
+ this.dirtyTrackerDisposable = undefined;
94
+ this.teardown();
95
+ this.canvasService = undefined;
96
+ this.context = undefined;
97
+ }
98
+ updateVisibility() {
99
+ this.redraw();
100
+ }
101
+ contribute() {
102
+ return {
103
+ [core_1.ContributionPointIds.TOOLS]: [
104
+ {
105
+ id: this.id,
106
+ name: "Feature",
107
+ interaction: "session",
108
+ commands: {
109
+ begin: "beginFeatureSession",
110
+ commit: "completeFeatures",
111
+ rollback: "rollbackFeatureSession",
112
+ },
113
+ session: {
114
+ autoBegin: false,
115
+ leavePolicy: "block",
116
+ },
117
+ },
118
+ ],
119
+ [core_1.ContributionPointIds.COMMANDS]: [
120
+ {
121
+ command: "beginFeatureSession",
122
+ title: "Begin Feature Session",
123
+ handler: async () => {
124
+ if (this.isFeatureSessionActive) {
125
+ return { ok: true };
126
+ }
127
+ const original = this.getCommittedFeatures();
128
+ this.sessionOriginalFeatures = this.cloneFeatures(original);
129
+ this.isFeatureSessionActive = true;
130
+ await this.refreshGeometry();
131
+ this.setWorkingFeatures(this.cloneFeatures(original));
132
+ this.hasWorkingChanges = false;
133
+ this.redraw();
134
+ this.emitWorkingChange();
135
+ this.updateCommittedFeatures([]);
136
+ return { ok: true };
137
+ },
138
+ },
139
+ {
140
+ command: "addFeature",
141
+ title: "Add Edge Feature",
142
+ handler: (type = "subtract") => {
143
+ return this.addFeature(type);
144
+ },
145
+ },
146
+ {
147
+ command: "addHole",
148
+ title: "Add Hole",
149
+ handler: () => {
150
+ return this.addFeature("subtract");
151
+ },
152
+ },
153
+ {
154
+ command: "addDoubleLayerHole",
155
+ title: "Add Double Layer Hole",
156
+ handler: () => {
157
+ return this.addDoubleLayerHole();
158
+ },
159
+ },
160
+ {
161
+ command: "clearFeatures",
162
+ title: "Clear Features",
163
+ handler: () => {
164
+ this.setWorkingFeatures([]);
165
+ this.hasWorkingChanges = true;
166
+ this.redraw();
167
+ this.emitWorkingChange();
168
+ return true;
169
+ },
170
+ },
171
+ {
172
+ command: "getWorkingFeatures",
173
+ title: "Get Working Features",
174
+ handler: () => {
175
+ return this.cloneFeatures(this.workingFeatures);
176
+ },
177
+ },
178
+ {
179
+ command: "setWorkingFeatures",
180
+ title: "Set Working Features",
181
+ handler: async (features) => {
182
+ await this.refreshGeometry();
183
+ this.setWorkingFeatures(this.cloneFeatures(features || []));
184
+ this.hasWorkingChanges = true;
185
+ this.redraw();
186
+ this.emitWorkingChange();
187
+ return { ok: true };
188
+ },
189
+ },
190
+ {
191
+ command: "rollbackFeatureSession",
192
+ title: "Rollback Feature Session",
193
+ handler: async () => {
194
+ const original = this.cloneFeatures(this.sessionOriginalFeatures || this.getCommittedFeatures());
195
+ await this.refreshGeometry();
196
+ this.setWorkingFeatures(original);
197
+ this.hasWorkingChanges = false;
198
+ this.clearFeatureSessionState();
199
+ this.redraw();
200
+ this.emitWorkingChange();
201
+ this.updateCommittedFeatures(original);
202
+ return { ok: true };
203
+ },
204
+ },
205
+ {
206
+ command: "resetWorkingFeatures",
207
+ title: "Reset Working Features",
208
+ handler: async () => {
209
+ await this.resetWorkingFeaturesFromSource();
210
+ return { ok: true };
211
+ },
212
+ },
213
+ {
214
+ command: "updateWorkingGroupPosition",
215
+ title: "Update Working Group Position",
216
+ handler: (groupId, x, y) => {
217
+ return this.updateWorkingGroupPosition(groupId, x, y);
218
+ },
219
+ },
220
+ {
221
+ command: "completeFeatures",
222
+ title: "Complete Features",
223
+ handler: () => {
224
+ return this.completeFeatures();
225
+ },
226
+ },
227
+ ],
228
+ };
229
+ }
230
+ cloneFeatures(features) {
231
+ return (0, sessionState_1.cloneWithJson)(features || []);
232
+ }
233
+ getConfigService() {
234
+ return this.context?.services.get("ConfigurationService");
235
+ }
236
+ getCommittedFeatures() {
237
+ const configService = this.getConfigService();
238
+ const committed = (configService?.get("dieline.features", []) ||
239
+ []);
240
+ return this.cloneFeatures(committed);
241
+ }
242
+ updateCommittedFeatures(next) {
243
+ const configService = this.getConfigService();
244
+ if (!configService)
245
+ return;
246
+ this.isUpdatingConfig = true;
247
+ try {
248
+ configService.update("dieline.features", next);
249
+ }
250
+ finally {
251
+ this.isUpdatingConfig = false;
252
+ }
253
+ }
254
+ clearFeatureSessionState() {
255
+ this.isFeatureSessionActive = false;
256
+ this.sessionOriginalFeatures = null;
257
+ }
258
+ restoreSessionFeaturesToConfig() {
259
+ if (!this.isFeatureSessionActive)
260
+ return;
261
+ const original = this.cloneFeatures(this.sessionOriginalFeatures || this.getCommittedFeatures());
262
+ this.updateCommittedFeatures(original);
263
+ this.clearFeatureSessionState();
264
+ }
265
+ emitWorkingChange() {
266
+ this.context?.eventBus.emit("feature:working:change", {
267
+ features: this.cloneFeatures(this.workingFeatures),
268
+ });
269
+ }
270
+ async refreshGeometry() {
271
+ if (!this.context)
272
+ return;
273
+ const commandService = this.context.services.get("CommandService");
274
+ if (!commandService)
275
+ return;
276
+ try {
277
+ const g = await Promise.resolve(commandService.executeCommand("getSceneGeometry"));
278
+ if (g)
279
+ this.currentGeometry = g;
280
+ }
281
+ catch (e) { }
282
+ }
283
+ async resetWorkingFeaturesFromSource() {
284
+ const next = this.cloneFeatures(this.isFeatureSessionActive && this.sessionOriginalFeatures
285
+ ? this.sessionOriginalFeatures
286
+ : this.getCommittedFeatures());
287
+ await this.refreshGeometry();
288
+ this.setWorkingFeatures(next);
289
+ this.hasWorkingChanges = false;
290
+ this.redraw();
291
+ this.emitWorkingChange();
292
+ }
293
+ setWorkingFeatures(next) {
294
+ this.workingFeatures = next;
295
+ }
296
+ updateWorkingGroupPosition(groupId, x, y) {
297
+ if (!groupId)
298
+ return { ok: false };
299
+ const configService = this.context?.services.get("ConfigurationService");
300
+ if (!configService)
301
+ return { ok: false };
302
+ const sizeState = (0, sceneLayoutModel_1.readSizeState)(configService);
303
+ const dielineWidth = sizeState.actualWidthMm;
304
+ const dielineHeight = sizeState.actualHeightMm;
305
+ let changed = false;
306
+ const next = this.workingFeatures.map((f) => {
307
+ if (f.groupId !== groupId)
308
+ return f;
309
+ let nx = x;
310
+ let ny = y;
311
+ if (f.constraints && dielineWidth > 0 && dielineHeight > 0) {
312
+ const constrained = constraints_1.ConstraintRegistry.apply(nx, ny, f, {
313
+ dielineWidth,
314
+ dielineHeight,
315
+ });
316
+ nx = constrained.x;
317
+ ny = constrained.y;
318
+ }
319
+ if (f.x !== nx || f.y !== ny) {
320
+ changed = true;
321
+ return { ...f, x: nx, y: ny };
322
+ }
323
+ return f;
324
+ });
325
+ if (!changed)
326
+ return { ok: true };
327
+ this.setWorkingFeatures(next);
328
+ this.hasWorkingChanges = true;
329
+ this.redraw({ enforceConstraints: true });
330
+ this.emitWorkingChange();
331
+ return { ok: true };
332
+ }
333
+ completeFeatures() {
334
+ const configService = this.context?.services.get("ConfigurationService");
335
+ if (!configService) {
336
+ return {
337
+ ok: false,
338
+ issues: [
339
+ { featureId: "unknown", reason: "ConfigurationService not found" },
340
+ ],
341
+ };
342
+ }
343
+ const sizeState = (0, sceneLayoutModel_1.readSizeState)(configService);
344
+ const dielineWidth = sizeState.actualWidthMm;
345
+ const dielineHeight = sizeState.actualHeightMm;
346
+ const result = (0, featureComplete_1.completeFeaturesStrict)(this.workingFeatures, { dielineWidth, dielineHeight }, (next) => {
347
+ this.updateCommittedFeatures(next);
348
+ this.workingFeatures = this.cloneFeatures(next);
349
+ this.emitWorkingChange();
350
+ });
351
+ if (!result.ok) {
352
+ return {
353
+ ok: false,
354
+ issues: result.issues,
355
+ };
356
+ }
357
+ this.hasWorkingChanges = false;
358
+ this.clearFeatureSessionState();
359
+ this.redraw();
360
+ return { ok: true };
361
+ }
362
+ addFeature(type) {
363
+ if (!this.canvasService)
364
+ return false;
365
+ const newFeature = {
366
+ id: Date.now().toString(),
367
+ operation: type,
368
+ shape: "rect",
369
+ x: 0.5,
370
+ y: 0,
371
+ width: 10,
372
+ height: 10,
373
+ rotation: 0,
374
+ renderBehavior: "edge",
375
+ constraints: [{ type: "path" }],
376
+ };
377
+ this.setWorkingFeatures([...(this.workingFeatures || []), newFeature]);
378
+ this.hasWorkingChanges = true;
379
+ this.redraw();
380
+ this.emitWorkingChange();
381
+ return true;
382
+ }
383
+ addDoubleLayerHole() {
384
+ if (!this.canvasService)
385
+ return false;
386
+ const groupId = Date.now().toString();
387
+ const timestamp = Date.now();
388
+ const lug = {
389
+ id: `${timestamp}-lug`,
390
+ groupId,
391
+ operation: "add",
392
+ shape: "circle",
393
+ x: 0.5,
394
+ y: 0,
395
+ radius: 20,
396
+ rotation: 0,
397
+ renderBehavior: "edge",
398
+ constraints: [{ type: "path" }],
399
+ };
400
+ const hole = {
401
+ id: `${timestamp}-hole`,
402
+ groupId,
403
+ operation: "subtract",
404
+ shape: "circle",
405
+ x: 0.5,
406
+ y: 0,
407
+ radius: 15,
408
+ rotation: 0,
409
+ renderBehavior: "edge",
410
+ constraints: [{ type: "path" }],
411
+ };
412
+ this.setWorkingFeatures([...(this.workingFeatures || []), lug, hole]);
413
+ this.hasWorkingChanges = true;
414
+ this.redraw();
415
+ this.emitWorkingChange();
416
+ return true;
417
+ }
418
+ getGeometryForFeature(geometry, _feature) {
419
+ return geometry;
420
+ }
421
+ setup() {
422
+ if (!this.canvasService || !this.context)
423
+ return;
424
+ const canvas = this.canvasService.canvas;
425
+ if (!this.handleSceneGeometryChange) {
426
+ this.handleSceneGeometryChange = (geometry) => {
427
+ this.currentGeometry = geometry;
428
+ this.redraw({ enforceConstraints: true });
429
+ };
430
+ this.context.eventBus.on("scene:geometry:change", this.handleSceneGeometryChange);
431
+ }
432
+ const commandService = this.context.services.get("CommandService");
433
+ if (commandService) {
434
+ try {
435
+ Promise.resolve(commandService.executeCommand("getSceneGeometry")).then((g) => {
436
+ if (g) {
437
+ this.currentGeometry = g;
438
+ this.redraw();
439
+ }
440
+ });
441
+ }
442
+ catch (e) { }
443
+ }
444
+ if (!this.handleMoving) {
445
+ this.handleMoving = (e) => {
446
+ const target = this.getDraggableMarkerTarget(e?.target);
447
+ if (!target || !this.currentGeometry)
448
+ return;
449
+ const feature = this.getFeatureForMarker(target);
450
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
451
+ const snapped = this.constrainPosition({
452
+ x: Number(target.left || 0),
453
+ y: Number(target.top || 0),
454
+ }, geometry, feature);
455
+ target.set({
456
+ left: snapped.x,
457
+ top: snapped.y,
458
+ });
459
+ target.setCoords();
460
+ this.syncMarkerVisualsByTarget(target, snapped);
461
+ };
462
+ canvas.on("object:moving", this.handleMoving);
463
+ }
464
+ if (!this.handleModified) {
465
+ this.handleModified = (e) => {
466
+ const target = this.getDraggableMarkerTarget(e?.target);
467
+ if (!target)
468
+ return;
469
+ if (target.data?.isGroup) {
470
+ this.syncGroupFromCanvas(target);
471
+ }
472
+ else {
473
+ this.syncFeatureFromCanvas(target);
474
+ }
475
+ };
476
+ canvas.on("object:modified", this.handleModified);
477
+ }
478
+ }
479
+ teardown() {
480
+ if (!this.canvasService)
481
+ return;
482
+ const canvas = this.canvasService.canvas;
483
+ if (this.handleMoving) {
484
+ canvas.off("object:moving", this.handleMoving);
485
+ this.handleMoving = null;
486
+ }
487
+ if (this.handleModified) {
488
+ canvas.off("object:modified", this.handleModified);
489
+ this.handleModified = null;
490
+ }
491
+ if (this.handleSceneGeometryChange && this.context) {
492
+ this.context.eventBus.off("scene:geometry:change", this.handleSceneGeometryChange);
493
+ this.handleSceneGeometryChange = null;
494
+ }
495
+ this.renderSeq += 1;
496
+ this.specs = [];
497
+ this.renderProducerDisposable?.dispose();
498
+ this.renderProducerDisposable = undefined;
499
+ void this.canvasService.flushRenderFromProducers();
500
+ }
501
+ getDraggableMarkerTarget(target) {
502
+ if (!this.isFeatureSessionActive || !this.isToolActive)
503
+ return null;
504
+ if (!target || target.data?.type !== "feature-marker")
505
+ return null;
506
+ if (target.data?.markerRole !== "handle")
507
+ return null;
508
+ return target;
509
+ }
510
+ getFeatureForMarker(target) {
511
+ const data = target?.data || {};
512
+ const index = data.isGroup
513
+ ? this.toFeatureIndex(data.anchorIndex)
514
+ : this.toFeatureIndex(data.index);
515
+ if (index === null)
516
+ return undefined;
517
+ return this.workingFeatures[index];
518
+ }
519
+ constrainPosition(p, geometry, feature) {
520
+ if (!feature) {
521
+ return { x: p.x, y: p.y };
522
+ }
523
+ const minX = geometry.x - geometry.width / 2;
524
+ const minY = geometry.y - geometry.height / 2;
525
+ const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
526
+ const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
527
+ const scale = geometry.scale || 1;
528
+ const dielineWidth = geometry.width / scale;
529
+ const dielineHeight = geometry.height / scale;
530
+ const activeConstraints = feature.constraints?.filter((c) => !c.validateOnly);
531
+ const constrained = constraints_1.ConstraintRegistry.apply(nx, ny, feature, {
532
+ dielineWidth,
533
+ dielineHeight,
534
+ geometry,
535
+ }, activeConstraints);
536
+ return {
537
+ x: minX + constrained.x * geometry.width,
538
+ y: minY + constrained.y * geometry.height,
539
+ };
540
+ }
541
+ toNormalizedPoint(point, geometry) {
542
+ const left = geometry.x - geometry.width / 2;
543
+ const top = geometry.y - geometry.height / 2;
544
+ return {
545
+ x: geometry.width > 0 ? (point.x - left) / geometry.width : 0.5,
546
+ y: geometry.height > 0 ? (point.y - top) / geometry.height : 0.5,
547
+ };
548
+ }
549
+ syncFeatureFromCanvas(target) {
550
+ if (!this.currentGeometry)
551
+ return;
552
+ const index = this.toFeatureIndex(target.data?.index);
553
+ if (index === null || index >= this.workingFeatures.length)
554
+ return;
555
+ const feature = this.workingFeatures[index];
556
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
557
+ const normalized = this.toNormalizedPoint({
558
+ x: Number(target.left || 0),
559
+ y: Number(target.top || 0),
560
+ }, geometry);
561
+ const updatedFeature = {
562
+ ...feature,
563
+ x: normalized.x,
564
+ y: normalized.y,
565
+ };
566
+ const next = [...this.workingFeatures];
567
+ next[index] = updatedFeature;
568
+ this.setWorkingFeatures(next);
569
+ this.hasWorkingChanges = true;
570
+ this.emitWorkingChange();
571
+ }
572
+ syncGroupFromCanvas(target) {
573
+ if (!this.currentGeometry)
574
+ return;
575
+ const indices = this.readGroupIndices(target.data?.indices);
576
+ if (indices.length === 0)
577
+ return;
578
+ const offsets = this.readGroupMemberOffsets(target.data?.memberOffsets, indices);
579
+ const anchorCenter = {
580
+ x: Number(target.left || 0),
581
+ y: Number(target.top || 0),
582
+ };
583
+ const next = [...this.workingFeatures];
584
+ let changed = false;
585
+ offsets.forEach((entry) => {
586
+ const index = entry.index;
587
+ if (index < 0 || index >= next.length)
588
+ return;
589
+ const feature = next[index];
590
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
591
+ const normalized = this.toNormalizedPoint({
592
+ x: anchorCenter.x + entry.dx,
593
+ y: anchorCenter.y + entry.dy,
594
+ }, geometry);
595
+ if (feature.x !== normalized.x || feature.y !== normalized.y) {
596
+ next[index] = {
597
+ ...feature,
598
+ x: normalized.x,
599
+ y: normalized.y,
600
+ };
601
+ changed = true;
602
+ }
603
+ });
604
+ if (!changed)
605
+ return;
606
+ this.setWorkingFeatures(next);
607
+ this.hasWorkingChanges = true;
608
+ this.emitWorkingChange();
609
+ }
610
+ redraw(options = {}) {
611
+ void this.redrawAsync(options);
612
+ }
613
+ async redrawAsync(options = {}) {
614
+ if (!this.canvasService)
615
+ return;
616
+ const seq = ++this.renderSeq;
617
+ this.specs = this.buildFeatureSpecs();
618
+ if (seq !== this.renderSeq)
619
+ return;
620
+ await this.canvasService.flushRenderFromProducers();
621
+ if (seq !== this.renderSeq)
622
+ return;
623
+ if (options.enforceConstraints) {
624
+ this.enforceConstraints();
625
+ }
626
+ }
627
+ buildFeatureSpecs() {
628
+ if (!this.isFeatureSessionActive ||
629
+ !this.currentGeometry ||
630
+ this.workingFeatures.length === 0) {
631
+ return [];
632
+ }
633
+ const groups = new Map();
634
+ const singles = [];
635
+ this.workingFeatures.forEach((feature, index) => {
636
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
637
+ const position = (0, geometry_1.resolveFeaturePosition)(feature, geometry);
638
+ const scale = geometry.scale || 1;
639
+ const marker = {
640
+ feature,
641
+ index,
642
+ position,
643
+ geometry,
644
+ scale,
645
+ };
646
+ if (feature.groupId) {
647
+ const list = groups.get(feature.groupId) || [];
648
+ list.push(marker);
649
+ groups.set(feature.groupId, list);
650
+ return;
651
+ }
652
+ singles.push(marker);
653
+ });
654
+ const specs = [];
655
+ singles.forEach((marker) => {
656
+ this.appendMarkerSpecs(specs, marker, {
657
+ markerRole: "handle",
658
+ isGroup: false,
659
+ });
660
+ });
661
+ groups.forEach((members, groupId) => {
662
+ if (!members.length)
663
+ return;
664
+ const anchor = members[0];
665
+ const memberOffsets = members.map((member) => ({
666
+ index: member.index,
667
+ dx: member.position.x - anchor.position.x,
668
+ dy: member.position.y - anchor.position.y,
669
+ }));
670
+ const indices = members.map((member) => member.index);
671
+ members
672
+ .filter((member) => member.index !== anchor.index)
673
+ .forEach((member) => {
674
+ this.appendMarkerSpecs(specs, member, {
675
+ markerRole: "member",
676
+ isGroup: false,
677
+ groupId,
678
+ });
679
+ });
680
+ this.appendMarkerSpecs(specs, anchor, {
681
+ markerRole: "handle",
682
+ isGroup: true,
683
+ groupId,
684
+ indices,
685
+ anchorIndex: anchor.index,
686
+ memberOffsets,
687
+ });
688
+ });
689
+ return specs;
690
+ }
691
+ appendMarkerSpecs(specs, marker, options) {
692
+ const { feature, index, position, scale, geometry } = marker;
693
+ const baseRadius = feature.shape === "circle"
694
+ ? (feature.radius ?? DEFAULT_CIRCLE_RADIUS)
695
+ : (feature.radius ?? 0);
696
+ const baseWidth = feature.shape === "circle"
697
+ ? baseRadius * 2
698
+ : (feature.width ?? DEFAULT_RECT_SIZE);
699
+ const baseHeight = feature.shape === "circle"
700
+ ? baseRadius * 2
701
+ : (feature.height ?? DEFAULT_RECT_SIZE);
702
+ const visualWidth = baseWidth * scale;
703
+ const visualHeight = baseHeight * scale;
704
+ const visualRadius = baseRadius * scale;
705
+ const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
706
+ const strokeDash = feature.strokeDash ||
707
+ (feature.operation === "subtract" ? [4, 4] : undefined);
708
+ const interactive = options.markerRole === "handle";
709
+ const sessionVisible = this.isToolActive && this.isFeatureSessionActive;
710
+ const baseData = this.buildMarkerData(marker, options);
711
+ const commonProps = {
712
+ visible: sessionVisible,
713
+ selectable: interactive && sessionVisible,
714
+ evented: interactive && sessionVisible,
715
+ hasControls: false,
716
+ hasBorders: false,
717
+ hoverCursor: interactive ? "move" : "default",
718
+ lockRotation: true,
719
+ lockScalingX: true,
720
+ lockScalingY: true,
721
+ fill: "transparent",
722
+ stroke: color,
723
+ strokeWidth: FEATURE_STROKE_WIDTH,
724
+ strokeDashArray: strokeDash,
725
+ originX: "center",
726
+ originY: "center",
727
+ left: position.x,
728
+ top: position.y,
729
+ angle: feature.rotation || 0,
730
+ };
731
+ const markerId = this.markerId(index);
732
+ if (feature.shape === "rect") {
733
+ specs.push({
734
+ id: markerId,
735
+ type: "rect",
736
+ space: "screen",
737
+ data: baseData,
738
+ props: {
739
+ ...commonProps,
740
+ width: visualWidth,
741
+ height: visualHeight,
742
+ rx: visualRadius,
743
+ ry: visualRadius,
744
+ },
745
+ });
746
+ }
747
+ else {
748
+ specs.push({
749
+ id: markerId,
750
+ type: "rect",
751
+ space: "screen",
752
+ data: baseData,
753
+ props: {
754
+ ...commonProps,
755
+ width: visualWidth,
756
+ height: visualHeight,
757
+ rx: visualRadius,
758
+ ry: visualRadius,
759
+ },
760
+ });
761
+ }
762
+ if (feature.bridge?.type === "vertical") {
763
+ const featureTopY = position.y - visualHeight / 2;
764
+ const dielineTopY = geometry.y - geometry.height / 2;
765
+ const bridgeHeight = Math.max(0, featureTopY - dielineTopY);
766
+ if (bridgeHeight <= 0.001) {
767
+ return;
768
+ }
769
+ specs.push({
770
+ id: this.bridgeIndicatorId(index),
771
+ type: "rect",
772
+ space: "screen",
773
+ data: {
774
+ ...baseData,
775
+ markerRole: "indicator",
776
+ markerOffsetX: 0,
777
+ markerOffsetY: -visualHeight / 2,
778
+ },
779
+ props: {
780
+ visible: sessionVisible,
781
+ selectable: false,
782
+ evented: false,
783
+ width: visualWidth,
784
+ height: bridgeHeight,
785
+ fill: "transparent",
786
+ stroke: "#888",
787
+ strokeWidth: 1,
788
+ strokeDashArray: [2, 2],
789
+ opacity: 0.5,
790
+ originX: "center",
791
+ originY: "bottom",
792
+ left: position.x,
793
+ top: position.y - visualHeight / 2,
794
+ },
795
+ });
796
+ }
797
+ }
798
+ buildMarkerData(marker, options) {
799
+ const data = {
800
+ type: "feature-marker",
801
+ index: marker.index,
802
+ featureId: marker.feature.id,
803
+ markerRole: options.markerRole,
804
+ markerOffsetX: 0,
805
+ markerOffsetY: 0,
806
+ isGroup: options.isGroup,
807
+ };
808
+ if (options.groupId)
809
+ data.groupId = options.groupId;
810
+ if (options.indices)
811
+ data.indices = options.indices;
812
+ if (options.anchorIndex !== undefined)
813
+ data.anchorIndex = options.anchorIndex;
814
+ if (options.memberOffsets)
815
+ data.memberOffsets = options.memberOffsets;
816
+ return data;
817
+ }
818
+ markerId(index) {
819
+ return `feature.marker.${index}`;
820
+ }
821
+ bridgeIndicatorId(index) {
822
+ return `feature.marker.${index}.bridge`;
823
+ }
824
+ toFeatureIndex(value) {
825
+ const numeric = Number(value);
826
+ if (!Number.isInteger(numeric) || numeric < 0)
827
+ return null;
828
+ return numeric;
829
+ }
830
+ readGroupIndices(raw) {
831
+ if (!Array.isArray(raw))
832
+ return [];
833
+ return raw
834
+ .map((value) => this.toFeatureIndex(value))
835
+ .filter((value) => value !== null);
836
+ }
837
+ readGroupMemberOffsets(raw, fallbackIndices = []) {
838
+ if (Array.isArray(raw)) {
839
+ const parsed = raw
840
+ .map((entry) => {
841
+ const index = this.toFeatureIndex(entry?.index);
842
+ const dx = Number(entry?.dx);
843
+ const dy = Number(entry?.dy);
844
+ if (index === null || !Number.isFinite(dx) || !Number.isFinite(dy)) {
845
+ return null;
846
+ }
847
+ return { index, dx, dy };
848
+ })
849
+ .filter((value) => !!value);
850
+ if (parsed.length > 0)
851
+ return parsed;
852
+ }
853
+ return fallbackIndices.map((index) => ({ index, dx: 0, dy: 0 }));
854
+ }
855
+ syncMarkerVisualsByTarget(target, center) {
856
+ if (target.data?.isGroup) {
857
+ const indices = this.readGroupIndices(target.data?.indices);
858
+ const offsets = this.readGroupMemberOffsets(target.data?.memberOffsets, indices);
859
+ offsets.forEach((entry) => {
860
+ this.syncMarkerVisualObjectsToCenter(entry.index, {
861
+ x: center.x + entry.dx,
862
+ y: center.y + entry.dy,
863
+ });
864
+ });
865
+ this.canvasService?.requestRenderAll();
866
+ return;
867
+ }
868
+ const index = this.toFeatureIndex(target.data?.index);
869
+ if (index === null)
870
+ return;
871
+ this.syncMarkerVisualObjectsToCenter(index, center);
872
+ this.canvasService?.requestRenderAll();
873
+ }
874
+ syncMarkerVisualObjectsToCenter(index, center) {
875
+ if (!this.canvasService)
876
+ return;
877
+ const markers = this.canvasService.canvas
878
+ .getObjects()
879
+ .filter((obj) => obj?.data?.type === "feature-marker" &&
880
+ this.toFeatureIndex(obj?.data?.index) === index);
881
+ markers.forEach((marker) => {
882
+ const offsetX = Number(marker?.data?.markerOffsetX || 0);
883
+ const offsetY = Number(marker?.data?.markerOffsetY || 0);
884
+ marker.set({
885
+ left: center.x + offsetX,
886
+ top: center.y + offsetY,
887
+ });
888
+ marker.setCoords();
889
+ });
890
+ }
891
+ enforceConstraints() {
892
+ if (!this.canvasService || !this.currentGeometry)
893
+ return;
894
+ const handles = this.canvasService.canvas
895
+ .getObjects()
896
+ .filter((obj) => obj?.data?.type === "feature-marker" &&
897
+ obj?.data?.markerRole === "handle");
898
+ handles.forEach((marker) => {
899
+ const feature = this.getFeatureForMarker(marker);
900
+ if (!feature)
901
+ return;
902
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
903
+ const snapped = this.constrainPosition({
904
+ x: Number(marker.left || 0),
905
+ y: Number(marker.top || 0),
906
+ }, geometry, feature);
907
+ marker.set({ left: snapped.x, top: snapped.y });
908
+ marker.setCoords();
909
+ this.syncMarkerVisualsByTarget(marker, snapped);
910
+ });
911
+ this.canvasService.canvas.requestRenderAll();
912
+ }
913
+ }
914
+ exports.FeatureTool = FeatureTool;