@pooder/kit 4.1.0 → 4.2.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.
@@ -0,0 +1,687 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeatureTool = void 0;
4
+ const core_1 = require("@pooder/core");
5
+ const fabric_1 = require("fabric");
6
+ const geometry_1 = require("./geometry");
7
+ const constraints_1 = require("./constraints");
8
+ const featureComplete_1 = require("./featureComplete");
9
+ const units_1 = require("./units");
10
+ class FeatureTool {
11
+ constructor(options) {
12
+ this.id = "pooder.kit.feature";
13
+ this.metadata = {
14
+ name: "FeatureTool",
15
+ };
16
+ this.workingFeatures = [];
17
+ this.isUpdatingConfig = false;
18
+ this.isToolActive = false;
19
+ this.handleMoving = null;
20
+ this.handleModified = null;
21
+ this.handleDielineChange = null;
22
+ this.currentGeometry = null;
23
+ this.onToolActivated = (event) => {
24
+ this.isToolActive = event.id === this.id;
25
+ this.updateVisibility();
26
+ };
27
+ if (options) {
28
+ Object.assign(this, options);
29
+ }
30
+ }
31
+ activate(context) {
32
+ this.context = context;
33
+ this.canvasService = context.services.get("CanvasService");
34
+ if (!this.canvasService) {
35
+ console.warn("CanvasService not found for FeatureTool");
36
+ return;
37
+ }
38
+ const configService = context.services.get("ConfigurationService");
39
+ if (configService) {
40
+ const features = (configService.get("dieline.features", []) ||
41
+ []);
42
+ this.workingFeatures = this.cloneFeatures(features);
43
+ configService.onAnyChange((e) => {
44
+ if (this.isUpdatingConfig)
45
+ return;
46
+ if (e.key === "dieline.features") {
47
+ const next = (e.value || []);
48
+ this.workingFeatures = this.cloneFeatures(next);
49
+ this.redraw();
50
+ this.emitWorkingChange();
51
+ }
52
+ });
53
+ }
54
+ // Listen to tool activation
55
+ context.eventBus.on("tool:activated", this.onToolActivated);
56
+ this.setup();
57
+ }
58
+ deactivate(context) {
59
+ context.eventBus.off("tool:activated", this.onToolActivated);
60
+ this.teardown();
61
+ this.canvasService = undefined;
62
+ this.context = undefined;
63
+ }
64
+ updateVisibility() {
65
+ if (!this.canvasService)
66
+ return;
67
+ const canvas = this.canvasService.canvas;
68
+ const markers = canvas
69
+ .getObjects()
70
+ .filter((obj) => obj.data?.type === "feature-marker");
71
+ markers.forEach((marker) => {
72
+ // If tool active, allow selection. If not, disable selection.
73
+ // Also might want to hide them entirely or just disable interaction.
74
+ // Assuming we only want to see/edit holes when tool is active.
75
+ marker.set({
76
+ visible: this.isToolActive, // Or just selectable: false if we want them visible but locked
77
+ selectable: this.isToolActive,
78
+ evented: this.isToolActive
79
+ });
80
+ });
81
+ canvas.requestRenderAll();
82
+ }
83
+ contribute() {
84
+ return {
85
+ [core_1.ContributionPointIds.COMMANDS]: [
86
+ {
87
+ command: "addFeature",
88
+ title: "Add Edge Feature",
89
+ handler: (type = "subtract") => {
90
+ return this.addFeature(type);
91
+ },
92
+ },
93
+ {
94
+ command: "addHole",
95
+ title: "Add Hole",
96
+ handler: () => {
97
+ return this.addFeature("subtract");
98
+ },
99
+ },
100
+ {
101
+ command: "addDoubleLayerHole",
102
+ title: "Add Double Layer Hole",
103
+ handler: () => {
104
+ return this.addDoubleLayerHole();
105
+ },
106
+ },
107
+ {
108
+ command: "clearFeatures",
109
+ title: "Clear Features",
110
+ handler: () => {
111
+ this.setWorkingFeatures([]);
112
+ this.redraw();
113
+ this.emitWorkingChange();
114
+ return true;
115
+ },
116
+ },
117
+ {
118
+ command: "getWorkingFeatures",
119
+ title: "Get Working Features",
120
+ handler: () => {
121
+ return this.cloneFeatures(this.workingFeatures);
122
+ },
123
+ },
124
+ {
125
+ command: "setWorkingFeatures",
126
+ title: "Set Working Features",
127
+ handler: async (features) => {
128
+ await this.refreshGeometry();
129
+ this.setWorkingFeatures(this.cloneFeatures(features || []));
130
+ this.redraw();
131
+ this.emitWorkingChange();
132
+ return { ok: true };
133
+ },
134
+ },
135
+ {
136
+ command: "updateWorkingGroupPosition",
137
+ title: "Update Working Group Position",
138
+ handler: (groupId, x, y) => {
139
+ return this.updateWorkingGroupPosition(groupId, x, y);
140
+ },
141
+ },
142
+ {
143
+ command: "completeFeatures",
144
+ title: "Complete Features",
145
+ handler: () => {
146
+ return this.completeFeatures();
147
+ },
148
+ },
149
+ ],
150
+ };
151
+ }
152
+ cloneFeatures(features) {
153
+ return JSON.parse(JSON.stringify(features || []));
154
+ }
155
+ emitWorkingChange() {
156
+ this.context?.eventBus.emit("feature:working:change", {
157
+ features: this.cloneFeatures(this.workingFeatures),
158
+ });
159
+ }
160
+ async refreshGeometry() {
161
+ if (!this.context)
162
+ return;
163
+ const commandService = this.context.services.get("CommandService");
164
+ if (!commandService)
165
+ return;
166
+ try {
167
+ const g = await Promise.resolve(commandService.executeCommand("getGeometry"));
168
+ if (g)
169
+ this.currentGeometry = g;
170
+ }
171
+ catch (e) { }
172
+ }
173
+ setWorkingFeatures(next) {
174
+ this.workingFeatures = next;
175
+ }
176
+ updateWorkingGroupPosition(groupId, x, y) {
177
+ if (!groupId)
178
+ return { ok: false };
179
+ const configService = this.context?.services.get("ConfigurationService");
180
+ if (!configService)
181
+ return { ok: false };
182
+ const dielineWidth = (0, units_1.parseLengthToMm)(configService.get("dieline.width") ?? 500, "mm");
183
+ const dielineHeight = (0, units_1.parseLengthToMm)(configService.get("dieline.height") ?? 500, "mm");
184
+ let changed = false;
185
+ const next = this.workingFeatures.map((f) => {
186
+ if (f.groupId !== groupId)
187
+ return f;
188
+ let nx = x;
189
+ let ny = y;
190
+ if (f.constraints && dielineWidth > 0 && dielineHeight > 0) {
191
+ const constrained = constraints_1.ConstraintRegistry.apply(nx, ny, f, {
192
+ dielineWidth,
193
+ dielineHeight,
194
+ });
195
+ nx = constrained.x;
196
+ ny = constrained.y;
197
+ }
198
+ if (f.x !== nx || f.y !== ny) {
199
+ changed = true;
200
+ return { ...f, x: nx, y: ny };
201
+ }
202
+ return f;
203
+ });
204
+ if (!changed)
205
+ return { ok: true };
206
+ this.setWorkingFeatures(next);
207
+ this.redraw();
208
+ this.enforceConstraints();
209
+ this.emitWorkingChange();
210
+ return { ok: true };
211
+ }
212
+ completeFeatures() {
213
+ const configService = this.context?.services.get("ConfigurationService");
214
+ if (!configService) {
215
+ return {
216
+ ok: false,
217
+ issues: [
218
+ { featureId: "unknown", reason: "ConfigurationService not found" },
219
+ ],
220
+ };
221
+ }
222
+ const dielineWidth = (0, units_1.parseLengthToMm)(configService.get("dieline.width") ?? 500, "mm");
223
+ const dielineHeight = (0, units_1.parseLengthToMm)(configService.get("dieline.height") ?? 500, "mm");
224
+ const result = (0, featureComplete_1.completeFeaturesStrict)(this.workingFeatures, { dielineWidth, dielineHeight }, (next) => {
225
+ this.isUpdatingConfig = true;
226
+ try {
227
+ configService.update("dieline.features", next);
228
+ }
229
+ finally {
230
+ this.isUpdatingConfig = false;
231
+ }
232
+ this.workingFeatures = this.cloneFeatures(next);
233
+ this.emitWorkingChange();
234
+ });
235
+ if (!result.ok) {
236
+ return {
237
+ ok: false,
238
+ issues: result.issues,
239
+ };
240
+ }
241
+ return { ok: true };
242
+ }
243
+ addFeature(type) {
244
+ if (!this.canvasService)
245
+ return false;
246
+ // Default to top edge center
247
+ const newFeature = {
248
+ id: Date.now().toString(),
249
+ operation: type,
250
+ placement: "edge",
251
+ shape: "rect",
252
+ x: 0.5,
253
+ y: 0, // Top edge
254
+ width: 10,
255
+ height: 10,
256
+ rotation: 0,
257
+ };
258
+ this.setWorkingFeatures([...(this.workingFeatures || []), newFeature]);
259
+ this.redraw();
260
+ this.emitWorkingChange();
261
+ return true;
262
+ }
263
+ addDoubleLayerHole() {
264
+ if (!this.canvasService)
265
+ return false;
266
+ const groupId = Date.now().toString();
267
+ const timestamp = Date.now();
268
+ // 1. Lug (Outer) - Add
269
+ const lug = {
270
+ id: `${timestamp}-lug`,
271
+ groupId,
272
+ operation: "add",
273
+ shape: "circle",
274
+ placement: "edge",
275
+ x: 0.5,
276
+ y: 0,
277
+ radius: 20,
278
+ rotation: 0,
279
+ };
280
+ // 2. Hole (Inner) - Subtract
281
+ const hole = {
282
+ id: `${timestamp}-hole`,
283
+ groupId,
284
+ operation: "subtract",
285
+ shape: "circle",
286
+ placement: "edge",
287
+ x: 0.5,
288
+ y: 0,
289
+ radius: 15,
290
+ rotation: 0,
291
+ };
292
+ this.setWorkingFeatures([...(this.workingFeatures || []), lug, hole]);
293
+ this.redraw();
294
+ this.emitWorkingChange();
295
+ return true;
296
+ }
297
+ getGeometryForFeature(geometry, feature) {
298
+ // Legacy support or specialized scaling can go here if needed
299
+ // Currently all features operate on the base geometry (or scaled version of it)
300
+ return geometry;
301
+ }
302
+ setup() {
303
+ if (!this.canvasService || !this.context)
304
+ return;
305
+ const canvas = this.canvasService.canvas;
306
+ // 1. Listen for Dieline Geometry Changes
307
+ if (!this.handleDielineChange) {
308
+ this.handleDielineChange = (geometry) => {
309
+ this.currentGeometry = geometry;
310
+ this.redraw();
311
+ this.enforceConstraints();
312
+ };
313
+ this.context.eventBus.on("dieline:geometry:change", this.handleDielineChange);
314
+ }
315
+ // 2. Initial Fetch of Geometry
316
+ const commandService = this.context.services.get("CommandService");
317
+ if (commandService) {
318
+ try {
319
+ Promise.resolve(commandService.executeCommand("getGeometry")).then((g) => {
320
+ if (g) {
321
+ this.currentGeometry = g;
322
+ this.redraw();
323
+ }
324
+ });
325
+ }
326
+ catch (e) { }
327
+ }
328
+ // 3. Setup Canvas Interaction
329
+ if (!this.handleMoving) {
330
+ this.handleMoving = (e) => {
331
+ const target = e.target;
332
+ if (!target || target.data?.type !== "feature-marker")
333
+ return;
334
+ if (!this.currentGeometry)
335
+ return;
336
+ // Determine feature to use for snapping context
337
+ let feature;
338
+ if (target.data?.isGroup) {
339
+ const indices = target.data?.indices;
340
+ if (indices && indices.length > 0) {
341
+ feature = this.workingFeatures[indices[0]];
342
+ }
343
+ }
344
+ else {
345
+ const index = target.data?.index;
346
+ if (index !== undefined) {
347
+ feature = this.workingFeatures[index];
348
+ }
349
+ }
350
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
351
+ // Snap to edge during move
352
+ // For Group, target.left/top is group center (or top-left depending on origin)
353
+ // We snap the target position itself.
354
+ const p = new fabric_1.Point(target.left, target.top);
355
+ // Calculate limit based on target size (min dimension / 2 ensures overlap)
356
+ // Also subtract stroke width to ensure visual overlap (not just tangent)
357
+ // target.strokeWidth for group is usually 0, need a safe default (e.g. 2 for markers)
358
+ const markerStrokeWidth = (target.strokeWidth || 2) * (target.scaleX || 1);
359
+ const minDim = Math.min(target.getScaledWidth(), target.getScaledHeight());
360
+ const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
361
+ const snapped = this.constrainPosition(p, geometry, limit, feature);
362
+ target.set({
363
+ left: snapped.x,
364
+ top: snapped.y,
365
+ });
366
+ };
367
+ canvas.on("object:moving", this.handleMoving);
368
+ }
369
+ if (!this.handleModified) {
370
+ this.handleModified = (e) => {
371
+ const target = e.target;
372
+ if (!target || target.data?.type !== "feature-marker")
373
+ return;
374
+ if (target.data?.isGroup) {
375
+ // It's a Group object
376
+ const groupObj = target;
377
+ // @ts-ignore
378
+ const indices = groupObj.data?.indices;
379
+ if (!indices)
380
+ return;
381
+ // We need to update all features in the group based on their new absolute positions.
382
+ // Fabric Group children positions are relative to group center.
383
+ // We need to calculate absolute position for each child.
384
+ // Note: groupObj has already been moved to new position (target.left, target.top)
385
+ const groupCenter = new fabric_1.Point(groupObj.left, groupObj.top);
386
+ // Get group matrix to transform children
387
+ // Simplified: just add relative coordinates if no rotation/scaling on group
388
+ // We locked rotation/scaling, so it's safe.
389
+ const newFeatures = [...this.workingFeatures];
390
+ const { x, y } = this.currentGeometry; // Center is same
391
+ // Fabric Group objects have .getObjects() which returns children
392
+ // But children inside group have coordinates relative to group center.
393
+ // center is (0,0) inside the group local coordinate system.
394
+ groupObj.getObjects().forEach((child, i) => {
395
+ const originalIndex = indices[i];
396
+ const feature = this.workingFeatures[originalIndex];
397
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
398
+ const { width, height } = geometry;
399
+ const layoutLeft = x - width / 2;
400
+ const layoutTop = y - height / 2;
401
+ // Calculate absolute position
402
+ // child.left/top are relative to group center
403
+ const absX = groupCenter.x + (child.left || 0);
404
+ const absY = groupCenter.y + (child.top || 0);
405
+ // Normalize
406
+ const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
407
+ const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
408
+ newFeatures[originalIndex] = {
409
+ ...newFeatures[originalIndex],
410
+ x: normalizedX,
411
+ y: normalizedY,
412
+ };
413
+ });
414
+ this.setWorkingFeatures(newFeatures);
415
+ this.emitWorkingChange();
416
+ }
417
+ else {
418
+ // Single object
419
+ this.syncFeatureFromCanvas(target);
420
+ }
421
+ };
422
+ canvas.on("object:modified", this.handleModified);
423
+ }
424
+ }
425
+ teardown() {
426
+ if (!this.canvasService)
427
+ return;
428
+ const canvas = this.canvasService.canvas;
429
+ if (this.handleMoving) {
430
+ canvas.off("object:moving", this.handleMoving);
431
+ this.handleMoving = null;
432
+ }
433
+ if (this.handleModified) {
434
+ canvas.off("object:modified", this.handleModified);
435
+ this.handleModified = null;
436
+ }
437
+ if (this.handleDielineChange && this.context) {
438
+ this.context.eventBus.off("dieline:geometry:change", this.handleDielineChange);
439
+ this.handleDielineChange = null;
440
+ }
441
+ const objects = canvas
442
+ .getObjects()
443
+ .filter((obj) => obj.data?.type === "feature-marker");
444
+ objects.forEach((obj) => canvas.remove(obj));
445
+ this.canvasService.requestRenderAll();
446
+ }
447
+ constrainPosition(p, geometry, limit, feature) {
448
+ if (feature && feature.constraints) {
449
+ const minX = geometry.x - geometry.width / 2;
450
+ const minY = geometry.y - geometry.height / 2;
451
+ const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
452
+ const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
453
+ const scale = geometry.scale || 1;
454
+ const dielineWidth = geometry.width / scale;
455
+ const dielineHeight = geometry.height / scale;
456
+ const constrained = constraints_1.ConstraintRegistry.apply(nx, ny, feature, {
457
+ dielineWidth,
458
+ dielineHeight,
459
+ });
460
+ return {
461
+ x: minX + constrained.x * geometry.width,
462
+ y: minY + constrained.y * geometry.height,
463
+ };
464
+ }
465
+ if (feature && feature.placement === "internal") {
466
+ const minX = geometry.x - geometry.width / 2;
467
+ const maxX = geometry.x + geometry.width / 2;
468
+ const minY = geometry.y - geometry.height / 2;
469
+ const maxY = geometry.y + geometry.height / 2;
470
+ return {
471
+ x: Math.max(minX, Math.min(maxX, p.x)),
472
+ y: Math.max(minY, Math.min(maxY, p.y)),
473
+ };
474
+ }
475
+ const nearest = (0, geometry_1.getNearestPointOnDieline)({ x: p.x, y: p.y }, {
476
+ ...geometry,
477
+ features: [],
478
+ });
479
+ const dx = p.x - nearest.x;
480
+ const dy = p.y - nearest.y;
481
+ const dist = Math.sqrt(dx * dx + dy * dy);
482
+ if (dist <= limit) {
483
+ return { x: p.x, y: p.y };
484
+ }
485
+ const scale = limit / dist;
486
+ return {
487
+ x: nearest.x + dx * scale,
488
+ y: nearest.y + dy * scale,
489
+ };
490
+ }
491
+ syncFeatureFromCanvas(target) {
492
+ if (!this.currentGeometry || !this.context)
493
+ return;
494
+ const index = target.data?.index;
495
+ if (index === undefined ||
496
+ index < 0 ||
497
+ index >= this.workingFeatures.length)
498
+ return;
499
+ const feature = this.workingFeatures[index];
500
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
501
+ const { width, height, x, y } = geometry;
502
+ // Calculate Normalized Position
503
+ // The geometry x/y is the CENTER.
504
+ const left = x - width / 2;
505
+ const top = y - height / 2;
506
+ const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
507
+ const normalizedY = height > 0 ? (target.top - top) / height : 0.5;
508
+ // Update feature
509
+ const updatedFeature = {
510
+ ...feature,
511
+ x: normalizedX,
512
+ y: normalizedY,
513
+ // Could also update rotation if we allowed rotating markers
514
+ };
515
+ const newFeatures = [...this.workingFeatures];
516
+ newFeatures[index] = updatedFeature;
517
+ this.setWorkingFeatures(newFeatures);
518
+ this.emitWorkingChange();
519
+ }
520
+ redraw() {
521
+ if (!this.canvasService || !this.currentGeometry)
522
+ return;
523
+ const canvas = this.canvasService.canvas;
524
+ const geometry = this.currentGeometry;
525
+ // Remove existing markers
526
+ const existing = canvas
527
+ .getObjects()
528
+ .filter((obj) => obj.data?.type === "feature-marker");
529
+ existing.forEach((obj) => canvas.remove(obj));
530
+ if (!this.workingFeatures || this.workingFeatures.length === 0) {
531
+ this.canvasService.requestRenderAll();
532
+ return;
533
+ }
534
+ const scale = geometry.scale || 1;
535
+ const finalScale = scale;
536
+ // Group features by groupId
537
+ const groups = {};
538
+ const singles = [];
539
+ this.workingFeatures.forEach((f, i) => {
540
+ if (f.groupId) {
541
+ if (!groups[f.groupId])
542
+ groups[f.groupId] = [];
543
+ groups[f.groupId].push({ feature: f, index: i });
544
+ }
545
+ else {
546
+ singles.push({ feature: f, index: i });
547
+ }
548
+ });
549
+ // Helper to create marker shape
550
+ const createMarkerShape = (feature, pos) => {
551
+ const featureScale = scale;
552
+ const visualWidth = (feature.width || 10) * featureScale;
553
+ const visualHeight = (feature.height || 10) * featureScale;
554
+ const visualRadius = (feature.radius || 0) * featureScale;
555
+ const color = feature.color ||
556
+ (feature.operation === "add" ? "#00FF00" : "#FF0000");
557
+ const strokeDash = feature.strokeDash ||
558
+ (feature.operation === "subtract" ? [4, 4] : undefined);
559
+ let shape;
560
+ if (feature.shape === "rect") {
561
+ shape = new fabric_1.Rect({
562
+ width: visualWidth,
563
+ height: visualHeight,
564
+ rx: visualRadius,
565
+ ry: visualRadius,
566
+ fill: "transparent",
567
+ stroke: color,
568
+ strokeWidth: 2,
569
+ strokeDashArray: strokeDash,
570
+ originX: "center",
571
+ originY: "center",
572
+ left: pos.x,
573
+ top: pos.y,
574
+ });
575
+ }
576
+ else {
577
+ shape = new fabric_1.Circle({
578
+ radius: visualRadius || 5 * finalScale,
579
+ fill: "transparent",
580
+ stroke: color,
581
+ strokeWidth: 2,
582
+ strokeDashArray: strokeDash,
583
+ originX: "center",
584
+ originY: "center",
585
+ left: pos.x,
586
+ top: pos.y,
587
+ });
588
+ }
589
+ if (feature.rotation) {
590
+ shape.rotate(feature.rotation);
591
+ }
592
+ return shape;
593
+ };
594
+ // Render Singles
595
+ singles.forEach(({ feature, index }) => {
596
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
597
+ const pos = (0, geometry_1.resolveFeaturePosition)(feature, geometry);
598
+ const marker = createMarkerShape(feature, pos);
599
+ marker.set({
600
+ visible: this.isToolActive,
601
+ selectable: this.isToolActive,
602
+ evented: this.isToolActive,
603
+ hasControls: false,
604
+ hasBorders: false,
605
+ hoverCursor: "move",
606
+ lockRotation: true,
607
+ lockScalingX: true,
608
+ lockScalingY: true,
609
+ data: { type: "feature-marker", index, isGroup: false },
610
+ });
611
+ canvas.add(marker);
612
+ canvas.bringObjectToFront(marker);
613
+ });
614
+ // Render Groups
615
+ Object.keys(groups).forEach((groupId) => {
616
+ const members = groups[groupId];
617
+ if (members.length === 0)
618
+ return;
619
+ // Calculate group center (average position) to position the group correctly
620
+ // But Fabric Group uses relative coordinates.
621
+ // Easiest way: Create shapes at absolute positions, then Group them.
622
+ // Fabric will auto-calculate group center and adjust children.
623
+ const shapes = members.map(({ feature }) => {
624
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
625
+ const pos = (0, geometry_1.resolveFeaturePosition)(feature, geometry);
626
+ return createMarkerShape(feature, pos);
627
+ });
628
+ const groupObj = new fabric_1.Group(shapes, {
629
+ visible: this.isToolActive,
630
+ selectable: this.isToolActive,
631
+ evented: this.isToolActive,
632
+ hasControls: false,
633
+ hasBorders: false,
634
+ hoverCursor: "move",
635
+ lockRotation: true,
636
+ lockScalingX: true,
637
+ lockScalingY: true,
638
+ subTargetCheck: true, // Allow events to pass through if needed, but we treat as one
639
+ interactive: false, // Children not interactive
640
+ // @ts-ignore
641
+ data: {
642
+ type: "feature-marker",
643
+ isGroup: true,
644
+ groupId,
645
+ indices: members.map((m) => m.index),
646
+ },
647
+ });
648
+ canvas.add(groupObj);
649
+ canvas.bringObjectToFront(groupObj);
650
+ });
651
+ this.canvasService.requestRenderAll();
652
+ }
653
+ enforceConstraints() {
654
+ if (!this.canvasService || !this.currentGeometry)
655
+ return;
656
+ // Iterate markers and snap them if geometry changed
657
+ const canvas = this.canvasService.canvas;
658
+ const markers = canvas
659
+ .getObjects()
660
+ .filter((obj) => obj.data?.type === "feature-marker");
661
+ markers.forEach((marker) => {
662
+ // Find associated feature
663
+ let feature;
664
+ if (marker.data?.isGroup) {
665
+ const indices = marker.data?.indices;
666
+ if (indices && indices.length > 0) {
667
+ feature = this.workingFeatures[indices[0]];
668
+ }
669
+ }
670
+ else {
671
+ const index = marker.data?.index;
672
+ if (index !== undefined) {
673
+ feature = this.workingFeatures[index];
674
+ }
675
+ }
676
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
677
+ const markerStrokeWidth = (marker.strokeWidth || 2) * (marker.scaleX || 1);
678
+ const minDim = Math.min(marker.getScaledWidth(), marker.getScaledHeight());
679
+ const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
680
+ const snapped = this.constrainPosition(new fabric_1.Point(marker.left, marker.top), geometry, limit, feature);
681
+ marker.set({ left: snapped.x, top: snapped.y });
682
+ marker.setCoords();
683
+ });
684
+ canvas.requestRenderAll();
685
+ }
686
+ }
687
+ exports.FeatureTool = FeatureTool;