@pooder/kit 5.3.0 → 5.4.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.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +249 -36
- package/dist/index.d.ts +249 -36
- package/dist/index.js +2374 -1049
- package/dist/index.mjs +2375 -1050
- package/package.json +1 -1
- package/src/extensions/background.ts +178 -85
- package/src/extensions/dieline.ts +1149 -1030
- package/src/extensions/dielineShape.ts +109 -0
- package/src/extensions/feature.ts +482 -366
- package/src/extensions/film.ts +148 -76
- package/src/extensions/geometry.ts +210 -44
- package/src/extensions/image.ts +244 -114
- package/src/extensions/ruler.ts +471 -268
- package/src/extensions/sceneLayoutModel.ts +28 -6
- package/src/extensions/sceneVisibility.ts +3 -10
- package/src/extensions/tracer.ts +1019 -980
- package/src/extensions/white-ink.ts +284 -231
- package/src/services/CanvasService.ts +543 -11
- package/src/services/renderSpec.ts +37 -2
- package/.test-dist/src/CanvasService.js +0 -249
- package/.test-dist/src/ViewportSystem.js +0 -75
- package/.test-dist/src/background.js +0 -203
- package/.test-dist/src/bridgeSelection.js +0 -20
- package/.test-dist/src/constraints.js +0 -237
- package/.test-dist/src/coordinate.js +0 -74
- package/.test-dist/src/dieline.js +0 -818
- package/.test-dist/src/edgeScale.js +0 -12
- package/.test-dist/src/extensions/background.js +0 -203
- package/.test-dist/src/extensions/bridgeSelection.js +0 -20
- package/.test-dist/src/extensions/constraints.js +0 -237
- package/.test-dist/src/extensions/dieline.js +0 -828
- package/.test-dist/src/extensions/edgeScale.js +0 -12
- package/.test-dist/src/extensions/feature.js +0 -825
- package/.test-dist/src/extensions/featureComplete.js +0 -32
- package/.test-dist/src/extensions/film.js +0 -167
- package/.test-dist/src/extensions/geometry.js +0 -545
- package/.test-dist/src/extensions/image.js +0 -1529
- package/.test-dist/src/extensions/index.js +0 -30
- package/.test-dist/src/extensions/maskOps.js +0 -279
- package/.test-dist/src/extensions/mirror.js +0 -104
- package/.test-dist/src/extensions/ruler.js +0 -345
- package/.test-dist/src/extensions/sceneLayout.js +0 -96
- package/.test-dist/src/extensions/sceneLayoutModel.js +0 -196
- package/.test-dist/src/extensions/sceneVisibility.js +0 -62
- package/.test-dist/src/extensions/size.js +0 -331
- package/.test-dist/src/extensions/tracer.js +0 -538
- package/.test-dist/src/extensions/white-ink.js +0 -1190
- package/.test-dist/src/extensions/wrappedOffsets.js +0 -33
- package/.test-dist/src/feature.js +0 -826
- package/.test-dist/src/featureComplete.js +0 -32
- package/.test-dist/src/film.js +0 -167
- package/.test-dist/src/geometry.js +0 -506
- package/.test-dist/src/image.js +0 -1250
- package/.test-dist/src/index.js +0 -18
- package/.test-dist/src/maskOps.js +0 -270
- package/.test-dist/src/mirror.js +0 -104
- package/.test-dist/src/renderSpec.js +0 -2
- package/.test-dist/src/ruler.js +0 -343
- package/.test-dist/src/sceneLayout.js +0 -99
- package/.test-dist/src/sceneLayoutModel.js +0 -196
- package/.test-dist/src/sceneView.js +0 -40
- package/.test-dist/src/sceneVisibility.js +0 -42
- package/.test-dist/src/services/CanvasService.js +0 -249
- package/.test-dist/src/services/ViewportSystem.js +0 -76
- package/.test-dist/src/services/index.js +0 -24
- package/.test-dist/src/services/renderSpec.js +0 -2
- package/.test-dist/src/size.js +0 -332
- package/.test-dist/src/tracer.js +0 -544
- package/.test-dist/src/units.js +0 -30
- package/.test-dist/src/white-ink.js +0 -829
- package/.test-dist/src/wrappedOffsets.js +0 -33
- package/.test-dist/tests/run.js +0 -94
|
@@ -6,13 +6,8 @@ import {
|
|
|
6
6
|
ConfigurationService,
|
|
7
7
|
ToolSessionService,
|
|
8
8
|
} from "@pooder/core";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
getNearestPointOnDieline,
|
|
13
|
-
DielineFeature,
|
|
14
|
-
resolveFeaturePosition,
|
|
15
|
-
} from "./geometry";
|
|
9
|
+
import { CanvasService, RenderObjectSpec } from "../services";
|
|
10
|
+
import { resolveFeaturePosition } from "./geometry";
|
|
16
11
|
import { ConstraintRegistry, ConstraintFeature } from "./constraints";
|
|
17
12
|
import { completeFeaturesStrict } from "./featureComplete";
|
|
18
13
|
import {
|
|
@@ -20,6 +15,41 @@ import {
|
|
|
20
15
|
type SceneGeometrySnapshot as DielineGeometry,
|
|
21
16
|
} from "./sceneLayoutModel";
|
|
22
17
|
|
|
18
|
+
const FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
|
|
19
|
+
const FEATURE_STROKE_WIDTH = 2;
|
|
20
|
+
const DEFAULT_RECT_SIZE = 10;
|
|
21
|
+
const DEFAULT_CIRCLE_RADIUS = 5;
|
|
22
|
+
|
|
23
|
+
type MarkerPoint = { x: number; y: number };
|
|
24
|
+
|
|
25
|
+
interface GroupMemberOffset {
|
|
26
|
+
index: number;
|
|
27
|
+
dx: number;
|
|
28
|
+
dy: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface MarkerRenderState {
|
|
32
|
+
feature: ConstraintFeature;
|
|
33
|
+
index: number;
|
|
34
|
+
position: MarkerPoint;
|
|
35
|
+
geometry: DielineGeometry;
|
|
36
|
+
scale: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface MarkerData {
|
|
40
|
+
type: "feature-marker";
|
|
41
|
+
index: number;
|
|
42
|
+
featureId: string;
|
|
43
|
+
groupId?: string;
|
|
44
|
+
markerRole: "handle" | "member" | "indicator";
|
|
45
|
+
markerOffsetX: number;
|
|
46
|
+
markerOffsetY: number;
|
|
47
|
+
isGroup: boolean;
|
|
48
|
+
indices?: number[];
|
|
49
|
+
anchorIndex?: number;
|
|
50
|
+
memberOffsets?: GroupMemberOffset[];
|
|
51
|
+
}
|
|
52
|
+
|
|
23
53
|
export class FeatureTool implements Extension {
|
|
24
54
|
id = "pooder.kit.feature";
|
|
25
55
|
|
|
@@ -36,6 +66,9 @@ export class FeatureTool implements Extension {
|
|
|
36
66
|
private sessionOriginalFeatures: ConstraintFeature[] | null = null;
|
|
37
67
|
private hasWorkingChanges = false;
|
|
38
68
|
private dirtyTrackerDisposable?: { dispose(): void };
|
|
69
|
+
private renderProducerDisposable?: { dispose: () => void };
|
|
70
|
+
private specs: RenderObjectSpec[] = [];
|
|
71
|
+
private renderSeq = 0;
|
|
39
72
|
|
|
40
73
|
private handleMoving: ((e: any) => void) | null = null;
|
|
41
74
|
private handleModified: ((e: any) => void) | null = null;
|
|
@@ -64,6 +97,17 @@ export class FeatureTool implements Extension {
|
|
|
64
97
|
return;
|
|
65
98
|
}
|
|
66
99
|
|
|
100
|
+
this.renderProducerDisposable?.dispose();
|
|
101
|
+
this.renderProducerDisposable = this.canvasService.registerRenderProducer(
|
|
102
|
+
this.id,
|
|
103
|
+
() => ({
|
|
104
|
+
rootLayerSpecs: {
|
|
105
|
+
[FEATURE_OVERLAY_LAYER_ID]: this.specs,
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
{ priority: 350 },
|
|
109
|
+
);
|
|
110
|
+
|
|
67
111
|
const configService = context.services.get<ConfigurationService>(
|
|
68
112
|
"ConfigurationService",
|
|
69
113
|
);
|
|
@@ -94,7 +138,6 @@ export class FeatureTool implements Extension {
|
|
|
94
138
|
() => this.hasWorkingChanges,
|
|
95
139
|
);
|
|
96
140
|
|
|
97
|
-
// Listen to tool activation
|
|
98
141
|
context.eventBus.on("tool:activated", this.onToolActivated);
|
|
99
142
|
|
|
100
143
|
this.setup();
|
|
@@ -119,23 +162,7 @@ export class FeatureTool implements Extension {
|
|
|
119
162
|
};
|
|
120
163
|
|
|
121
164
|
private updateVisibility() {
|
|
122
|
-
|
|
123
|
-
const canvas = this.canvasService.canvas;
|
|
124
|
-
const markers = canvas
|
|
125
|
-
.getObjects()
|
|
126
|
-
.filter((obj: any) => obj.data?.type === "feature-marker");
|
|
127
|
-
|
|
128
|
-
markers.forEach((marker: any) => {
|
|
129
|
-
// If tool active, allow selection. If not, disable selection.
|
|
130
|
-
// Also might want to hide them entirely or just disable interaction.
|
|
131
|
-
// Assuming we only want to see/edit holes when tool is active.
|
|
132
|
-
marker.set({
|
|
133
|
-
visible: this.isToolActive, // Or just selectable: false if we want them visible but locked
|
|
134
|
-
selectable: this.isToolActive,
|
|
135
|
-
evented: this.isToolActive,
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
canvas.requestRenderAll();
|
|
165
|
+
this.redraw();
|
|
139
166
|
}
|
|
140
167
|
|
|
141
168
|
contribute() {
|
|
@@ -384,8 +411,7 @@ export class FeatureTool implements Extension {
|
|
|
384
411
|
|
|
385
412
|
this.setWorkingFeatures(next);
|
|
386
413
|
this.hasWorkingChanges = true;
|
|
387
|
-
this.redraw();
|
|
388
|
-
this.enforceConstraints();
|
|
414
|
+
this.redraw({ enforceConstraints: true });
|
|
389
415
|
this.emitWorkingChange();
|
|
390
416
|
|
|
391
417
|
return { ok: true };
|
|
@@ -420,7 +446,7 @@ export class FeatureTool implements Extension {
|
|
|
420
446
|
{ dielineWidth, dielineHeight },
|
|
421
447
|
(next) => {
|
|
422
448
|
this.updateCommittedFeatures(next as ConstraintFeature[]);
|
|
423
|
-
this.workingFeatures = this.cloneFeatures(next as
|
|
449
|
+
this.workingFeatures = this.cloneFeatures(next as ConstraintFeature[]);
|
|
424
450
|
this.emitWorkingChange();
|
|
425
451
|
},
|
|
426
452
|
);
|
|
@@ -434,7 +460,6 @@ export class FeatureTool implements Extension {
|
|
|
434
460
|
|
|
435
461
|
this.hasWorkingChanges = false;
|
|
436
462
|
this.clearFeatureSessionState();
|
|
437
|
-
// Keep feature markers above dieline overlay after config-driven redraw.
|
|
438
463
|
this.redraw();
|
|
439
464
|
return { ok: true };
|
|
440
465
|
}
|
|
@@ -442,18 +467,16 @@ export class FeatureTool implements Extension {
|
|
|
442
467
|
private addFeature(type: "add" | "subtract") {
|
|
443
468
|
if (!this.canvasService) return false;
|
|
444
469
|
|
|
445
|
-
// Default to top edge center
|
|
446
470
|
const newFeature: ConstraintFeature = {
|
|
447
471
|
id: Date.now().toString(),
|
|
448
472
|
operation: type,
|
|
449
473
|
shape: "rect",
|
|
450
474
|
x: 0.5,
|
|
451
|
-
y: 0,
|
|
475
|
+
y: 0,
|
|
452
476
|
width: 10,
|
|
453
477
|
height: 10,
|
|
454
478
|
rotation: 0,
|
|
455
479
|
renderBehavior: "edge",
|
|
456
|
-
// Default constraint: path (snap to edge)
|
|
457
480
|
constraints: [{ type: "path" }],
|
|
458
481
|
};
|
|
459
482
|
|
|
@@ -470,7 +493,6 @@ export class FeatureTool implements Extension {
|
|
|
470
493
|
const groupId = Date.now().toString();
|
|
471
494
|
const timestamp = Date.now();
|
|
472
495
|
|
|
473
|
-
// 1. Lug (Outer) - Add
|
|
474
496
|
const lug: ConstraintFeature = {
|
|
475
497
|
id: `${timestamp}-lug`,
|
|
476
498
|
groupId,
|
|
@@ -484,7 +506,6 @@ export class FeatureTool implements Extension {
|
|
|
484
506
|
constraints: [{ type: "path" }],
|
|
485
507
|
};
|
|
486
508
|
|
|
487
|
-
// 2. Hole (Inner) - Subtract
|
|
488
509
|
const hole: ConstraintFeature = {
|
|
489
510
|
id: `${timestamp}-hole`,
|
|
490
511
|
groupId,
|
|
@@ -507,10 +528,8 @@ export class FeatureTool implements Extension {
|
|
|
507
528
|
|
|
508
529
|
private getGeometryForFeature(
|
|
509
530
|
geometry: DielineGeometry,
|
|
510
|
-
|
|
531
|
+
_feature?: ConstraintFeature,
|
|
511
532
|
): DielineGeometry {
|
|
512
|
-
// Legacy support or specialized scaling can go here if needed
|
|
513
|
-
// Currently all features operate on the base geometry (or scaled version of it)
|
|
514
533
|
return geometry;
|
|
515
534
|
}
|
|
516
535
|
|
|
@@ -518,12 +537,10 @@ export class FeatureTool implements Extension {
|
|
|
518
537
|
if (!this.canvasService || !this.context) return;
|
|
519
538
|
const canvas = this.canvasService.canvas;
|
|
520
539
|
|
|
521
|
-
// 1. Listen for Scene Geometry Changes
|
|
522
540
|
if (!this.handleSceneGeometryChange) {
|
|
523
541
|
this.handleSceneGeometryChange = (geometry: DielineGeometry) => {
|
|
524
542
|
this.currentGeometry = geometry;
|
|
525
|
-
this.redraw();
|
|
526
|
-
this.enforceConstraints();
|
|
543
|
+
this.redraw({ enforceConstraints: true });
|
|
527
544
|
};
|
|
528
545
|
this.context.eventBus.on(
|
|
529
546
|
"scene:geometry:change",
|
|
@@ -531,7 +548,6 @@ export class FeatureTool implements Extension {
|
|
|
531
548
|
);
|
|
532
549
|
}
|
|
533
550
|
|
|
534
|
-
// 2. Initial Fetch of Geometry
|
|
535
551
|
const commandService = this.context.services.get<any>("CommandService");
|
|
536
552
|
if (commandService) {
|
|
537
553
|
try {
|
|
@@ -546,119 +562,41 @@ export class FeatureTool implements Extension {
|
|
|
546
562
|
} catch (e) {}
|
|
547
563
|
}
|
|
548
564
|
|
|
549
|
-
// 3. Setup Canvas Interaction
|
|
550
565
|
if (!this.handleMoving) {
|
|
551
566
|
this.handleMoving = (e: any) => {
|
|
552
|
-
const target = e
|
|
553
|
-
if (!target ||
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
} else {
|
|
564
|
-
const index = target.data?.index;
|
|
565
|
-
if (index !== undefined) {
|
|
566
|
-
feature = this.workingFeatures[index];
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const geometry = this.getGeometryForFeature(
|
|
571
|
-
this.currentGeometry,
|
|
567
|
+
const target = this.getDraggableMarkerTarget(e?.target);
|
|
568
|
+
if (!target || !this.currentGeometry) return;
|
|
569
|
+
|
|
570
|
+
const feature = this.getFeatureForMarker(target);
|
|
571
|
+
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
572
|
+
const snapped = this.constrainPosition(
|
|
573
|
+
{
|
|
574
|
+
x: Number(target.left || 0),
|
|
575
|
+
y: Number(target.top || 0),
|
|
576
|
+
},
|
|
577
|
+
geometry,
|
|
572
578
|
feature,
|
|
573
579
|
);
|
|
574
580
|
|
|
575
|
-
// Snap to edge during move
|
|
576
|
-
// For Group, target.left/top is group center (or top-left depending on origin)
|
|
577
|
-
// We snap the target position itself.
|
|
578
|
-
const p = new Point(target.left, target.top);
|
|
579
|
-
|
|
580
|
-
// Calculate limit based on target size (min dimension / 2 ensures overlap)
|
|
581
|
-
// Also subtract stroke width to ensure visual overlap (not just tangent)
|
|
582
|
-
// target.strokeWidth for group is usually 0, need a safe default (e.g. 2 for markers)
|
|
583
|
-
const markerStrokeWidth =
|
|
584
|
-
(target.strokeWidth || 2) * (target.scaleX || 1);
|
|
585
|
-
const minDim = Math.min(
|
|
586
|
-
target.getScaledWidth(),
|
|
587
|
-
target.getScaledHeight(),
|
|
588
|
-
);
|
|
589
|
-
const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
|
|
590
|
-
|
|
591
|
-
const snapped = this.constrainPosition(p, geometry, limit, feature);
|
|
592
|
-
|
|
593
581
|
target.set({
|
|
594
582
|
left: snapped.x,
|
|
595
583
|
top: snapped.y,
|
|
596
584
|
});
|
|
585
|
+
target.setCoords();
|
|
586
|
+
|
|
587
|
+
this.syncMarkerVisualsByTarget(target, snapped);
|
|
597
588
|
};
|
|
598
589
|
canvas.on("object:moving", this.handleMoving);
|
|
599
590
|
}
|
|
600
591
|
|
|
601
592
|
if (!this.handleModified) {
|
|
602
593
|
this.handleModified = (e: any) => {
|
|
603
|
-
const target = e
|
|
604
|
-
if (!target
|
|
594
|
+
const target = this.getDraggableMarkerTarget(e?.target);
|
|
595
|
+
if (!target) return;
|
|
605
596
|
|
|
606
597
|
if (target.data?.isGroup) {
|
|
607
|
-
|
|
608
|
-
const groupObj = target as Group;
|
|
609
|
-
// @ts-ignore
|
|
610
|
-
const indices = groupObj.data?.indices as number[];
|
|
611
|
-
if (!indices) return;
|
|
612
|
-
|
|
613
|
-
// We need to update all features in the group based on their new absolute positions.
|
|
614
|
-
// Fabric Group children positions are relative to group center.
|
|
615
|
-
// We need to calculate absolute position for each child.
|
|
616
|
-
// Note: groupObj has already been moved to new position (target.left, target.top)
|
|
617
|
-
|
|
618
|
-
const groupCenter = new Point(groupObj.left, groupObj.top);
|
|
619
|
-
// Get group matrix to transform children
|
|
620
|
-
// Simplified: just add relative coordinates if no rotation/scaling on group
|
|
621
|
-
// We locked rotation/scaling, so it's safe.
|
|
622
|
-
|
|
623
|
-
const newFeatures = [...this.workingFeatures];
|
|
624
|
-
const { x, y } = this.currentGeometry!; // Center is same
|
|
625
|
-
|
|
626
|
-
// Fabric Group objects have .getObjects() which returns children
|
|
627
|
-
// But children inside group have coordinates relative to group center.
|
|
628
|
-
// center is (0,0) inside the group local coordinate system.
|
|
629
|
-
|
|
630
|
-
groupObj.getObjects().forEach((child, i) => {
|
|
631
|
-
const originalIndex = indices[i];
|
|
632
|
-
const feature = this.workingFeatures[originalIndex];
|
|
633
|
-
const geometry = this.getGeometryForFeature(
|
|
634
|
-
this.currentGeometry!,
|
|
635
|
-
feature,
|
|
636
|
-
);
|
|
637
|
-
const { width, height } = geometry;
|
|
638
|
-
const layoutLeft = x - width / 2;
|
|
639
|
-
const layoutTop = y - height / 2;
|
|
640
|
-
|
|
641
|
-
// Calculate absolute position
|
|
642
|
-
// child.left/top are relative to group center
|
|
643
|
-
const absX = groupCenter.x + (child.left || 0);
|
|
644
|
-
const absY = groupCenter.y + (child.top || 0);
|
|
645
|
-
|
|
646
|
-
// Normalize
|
|
647
|
-
const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
|
|
648
|
-
const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
|
|
649
|
-
|
|
650
|
-
newFeatures[originalIndex] = {
|
|
651
|
-
...newFeatures[originalIndex],
|
|
652
|
-
x: normalizedX,
|
|
653
|
-
y: normalizedY,
|
|
654
|
-
};
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
this.setWorkingFeatures(newFeatures);
|
|
658
|
-
this.hasWorkingChanges = true;
|
|
659
|
-
this.emitWorkingChange();
|
|
598
|
+
this.syncGroupFromCanvas(target);
|
|
660
599
|
} else {
|
|
661
|
-
// Single object
|
|
662
600
|
this.syncFeatureFromCanvas(target);
|
|
663
601
|
}
|
|
664
602
|
};
|
|
@@ -686,20 +624,33 @@ export class FeatureTool implements Extension {
|
|
|
686
624
|
this.handleSceneGeometryChange = null;
|
|
687
625
|
}
|
|
688
626
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
627
|
+
this.renderSeq += 1;
|
|
628
|
+
this.specs = [];
|
|
629
|
+
this.renderProducerDisposable?.dispose();
|
|
630
|
+
this.renderProducerDisposable = undefined;
|
|
631
|
+
void this.canvasService.flushRenderFromProducers();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private getDraggableMarkerTarget(target: any): any | null {
|
|
635
|
+
if (!target || target.data?.type !== "feature-marker") return null;
|
|
636
|
+
if (target.data?.markerRole !== "handle") return null;
|
|
637
|
+
return target;
|
|
638
|
+
}
|
|
693
639
|
|
|
694
|
-
|
|
640
|
+
private getFeatureForMarker(target: any): ConstraintFeature | undefined {
|
|
641
|
+
const data = target?.data || {};
|
|
642
|
+
const index = data.isGroup
|
|
643
|
+
? this.toFeatureIndex(data.anchorIndex)
|
|
644
|
+
: this.toFeatureIndex(data.index);
|
|
645
|
+
if (index === null) return undefined;
|
|
646
|
+
return this.workingFeatures[index];
|
|
695
647
|
}
|
|
696
648
|
|
|
697
649
|
private constrainPosition(
|
|
698
|
-
p:
|
|
650
|
+
p: MarkerPoint,
|
|
699
651
|
geometry: DielineGeometry,
|
|
700
|
-
limit: number,
|
|
701
652
|
feature?: ConstraintFeature,
|
|
702
|
-
):
|
|
653
|
+
): MarkerPoint {
|
|
703
654
|
if (!feature) {
|
|
704
655
|
return { x: p.x, y: p.y };
|
|
705
656
|
}
|
|
@@ -707,7 +658,6 @@ export class FeatureTool implements Extension {
|
|
|
707
658
|
const minX = geometry.x - geometry.width / 2;
|
|
708
659
|
const minY = geometry.y - geometry.height / 2;
|
|
709
660
|
|
|
710
|
-
// Normalize
|
|
711
661
|
const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
|
|
712
662
|
const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
|
|
713
663
|
|
|
@@ -715,7 +665,6 @@ export class FeatureTool implements Extension {
|
|
|
715
665
|
const dielineWidth = geometry.width / scale;
|
|
716
666
|
const dielineHeight = geometry.height / scale;
|
|
717
667
|
|
|
718
|
-
// Filter constraints: only apply those that are NOT validateOnly
|
|
719
668
|
const activeConstraints = feature.constraints?.filter(
|
|
720
669
|
(c) => !c.validateOnly,
|
|
721
670
|
);
|
|
@@ -732,290 +681,457 @@ export class FeatureTool implements Extension {
|
|
|
732
681
|
activeConstraints,
|
|
733
682
|
);
|
|
734
683
|
|
|
735
|
-
// Denormalize
|
|
736
684
|
return {
|
|
737
685
|
x: minX + constrained.x * geometry.width,
|
|
738
686
|
y: minY + constrained.y * geometry.height,
|
|
739
687
|
};
|
|
740
688
|
}
|
|
741
689
|
|
|
690
|
+
private toNormalizedPoint(
|
|
691
|
+
point: MarkerPoint,
|
|
692
|
+
geometry: DielineGeometry,
|
|
693
|
+
): MarkerPoint {
|
|
694
|
+
const left = geometry.x - geometry.width / 2;
|
|
695
|
+
const top = geometry.y - geometry.height / 2;
|
|
696
|
+
return {
|
|
697
|
+
x: geometry.width > 0 ? (point.x - left) / geometry.width : 0.5,
|
|
698
|
+
y: geometry.height > 0 ? (point.y - top) / geometry.height : 0.5,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
742
702
|
private syncFeatureFromCanvas(target: any) {
|
|
743
|
-
if (!this.currentGeometry
|
|
744
|
-
|
|
745
|
-
const index = target.data?.index;
|
|
746
|
-
if (
|
|
747
|
-
index === undefined ||
|
|
748
|
-
index < 0 ||
|
|
749
|
-
index >= this.workingFeatures.length
|
|
750
|
-
)
|
|
751
|
-
return;
|
|
703
|
+
if (!this.currentGeometry) return;
|
|
704
|
+
|
|
705
|
+
const index = this.toFeatureIndex(target.data?.index);
|
|
706
|
+
if (index === null || index >= this.workingFeatures.length) return;
|
|
752
707
|
|
|
753
708
|
const feature = this.workingFeatures[index];
|
|
754
709
|
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
|
|
763
|
-
const normalizedY = height > 0 ? (target.top - top) / height : 0.5;
|
|
710
|
+
const normalized = this.toNormalizedPoint(
|
|
711
|
+
{
|
|
712
|
+
x: Number(target.left || 0),
|
|
713
|
+
y: Number(target.top || 0),
|
|
714
|
+
},
|
|
715
|
+
geometry,
|
|
716
|
+
);
|
|
764
717
|
|
|
765
|
-
// Update feature
|
|
766
718
|
const updatedFeature = {
|
|
767
719
|
...feature,
|
|
768
|
-
x:
|
|
769
|
-
y:
|
|
770
|
-
// Could also update rotation if we allowed rotating markers
|
|
720
|
+
x: normalized.x,
|
|
721
|
+
y: normalized.y,
|
|
771
722
|
};
|
|
772
723
|
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
this.setWorkingFeatures(
|
|
724
|
+
const next = [...this.workingFeatures];
|
|
725
|
+
next[index] = updatedFeature;
|
|
726
|
+
this.setWorkingFeatures(next);
|
|
776
727
|
this.hasWorkingChanges = true;
|
|
777
728
|
this.emitWorkingChange();
|
|
778
729
|
}
|
|
779
730
|
|
|
780
|
-
private
|
|
781
|
-
if (!this.
|
|
782
|
-
const canvas = this.canvasService.canvas;
|
|
783
|
-
const geometry = this.currentGeometry;
|
|
731
|
+
private syncGroupFromCanvas(target: any) {
|
|
732
|
+
if (!this.currentGeometry) return;
|
|
784
733
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
.filter((obj: any) => obj.data?.type === "feature-marker");
|
|
789
|
-
existing.forEach((obj) => canvas.remove(obj));
|
|
734
|
+
const indices = this.readGroupIndices(target.data?.indices);
|
|
735
|
+
if (indices.length === 0) return;
|
|
736
|
+
const offsets = this.readGroupMemberOffsets(target.data?.memberOffsets, indices);
|
|
790
737
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
738
|
+
const anchorCenter = {
|
|
739
|
+
x: Number(target.left || 0),
|
|
740
|
+
y: Number(target.top || 0),
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const next = [...this.workingFeatures];
|
|
744
|
+
let changed = false;
|
|
745
|
+
offsets.forEach((entry) => {
|
|
746
|
+
const index = entry.index;
|
|
747
|
+
if (index < 0 || index >= next.length) return;
|
|
748
|
+
const feature = next[index];
|
|
749
|
+
const geometry = this.getGeometryForFeature(this.currentGeometry!, feature);
|
|
750
|
+
const normalized = this.toNormalizedPoint(
|
|
751
|
+
{
|
|
752
|
+
x: anchorCenter.x + entry.dx,
|
|
753
|
+
y: anchorCenter.y + entry.dy,
|
|
754
|
+
},
|
|
755
|
+
geometry,
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
if (feature.x !== normalized.x || feature.y !== normalized.y) {
|
|
759
|
+
next[index] = {
|
|
760
|
+
...feature,
|
|
761
|
+
x: normalized.x,
|
|
762
|
+
y: normalized.y,
|
|
763
|
+
};
|
|
764
|
+
changed = true;
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
if (!changed) return;
|
|
769
|
+
this.setWorkingFeatures(next);
|
|
770
|
+
this.hasWorkingChanges = true;
|
|
771
|
+
this.emitWorkingChange();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
private redraw(options: { enforceConstraints?: boolean } = {}) {
|
|
775
|
+
void this.redrawAsync(options);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private async redrawAsync(options: { enforceConstraints?: boolean } = {}) {
|
|
779
|
+
if (!this.canvasService) return;
|
|
780
|
+
|
|
781
|
+
const seq = ++this.renderSeq;
|
|
782
|
+
this.specs = this.buildFeatureSpecs();
|
|
783
|
+
if (seq !== this.renderSeq) return;
|
|
784
|
+
|
|
785
|
+
await this.canvasService.flushRenderFromProducers();
|
|
786
|
+
if (seq !== this.renderSeq) return;
|
|
787
|
+
|
|
788
|
+
this.syncOverlayOrder();
|
|
789
|
+
if (options.enforceConstraints) {
|
|
790
|
+
this.enforceConstraints();
|
|
794
791
|
}
|
|
792
|
+
}
|
|
795
793
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
794
|
+
private syncOverlayOrder() {
|
|
795
|
+
if (!this.canvasService) return;
|
|
796
|
+
this.canvasService.bringLayerToFront(FEATURE_OVERLAY_LAYER_ID);
|
|
797
|
+
this.canvasService.bringLayerToFront("ruler-overlay");
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private buildFeatureSpecs(): RenderObjectSpec[] {
|
|
801
|
+
if (!this.currentGeometry || this.workingFeatures.length === 0) {
|
|
802
|
+
return [];
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const groups = new Map<string, MarkerRenderState[]>();
|
|
806
|
+
const singles: MarkerRenderState[] = [];
|
|
807
|
+
|
|
808
|
+
this.workingFeatures.forEach((feature, index) => {
|
|
809
|
+
const geometry = this.getGeometryForFeature(this.currentGeometry!, feature);
|
|
810
|
+
const position = resolveFeaturePosition(feature, geometry);
|
|
811
|
+
const scale = geometry.scale || 1;
|
|
812
|
+
const marker: MarkerRenderState = {
|
|
813
|
+
feature,
|
|
814
|
+
index,
|
|
815
|
+
position,
|
|
816
|
+
geometry,
|
|
817
|
+
scale,
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
if (feature.groupId) {
|
|
821
|
+
const list = groups.get(feature.groupId) || [];
|
|
822
|
+
list.push(marker);
|
|
823
|
+
groups.set(feature.groupId, list);
|
|
824
|
+
return;
|
|
811
825
|
}
|
|
826
|
+
singles.push(marker);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const specs: RenderObjectSpec[] = [];
|
|
830
|
+
|
|
831
|
+
singles.forEach((marker) => {
|
|
832
|
+
this.appendMarkerSpecs(specs, marker, {
|
|
833
|
+
markerRole: "handle",
|
|
834
|
+
isGroup: false,
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
groups.forEach((members, groupId) => {
|
|
839
|
+
if (!members.length) return;
|
|
840
|
+
const anchor = members[0];
|
|
841
|
+
const memberOffsets: GroupMemberOffset[] = members.map((member) => ({
|
|
842
|
+
index: member.index,
|
|
843
|
+
dx: member.position.x - anchor.position.x,
|
|
844
|
+
dy: member.position.y - anchor.position.y,
|
|
845
|
+
}));
|
|
846
|
+
const indices = members.map((member) => member.index);
|
|
847
|
+
|
|
848
|
+
members
|
|
849
|
+
.filter((member) => member.index !== anchor.index)
|
|
850
|
+
.forEach((member) => {
|
|
851
|
+
this.appendMarkerSpecs(specs, member, {
|
|
852
|
+
markerRole: "member",
|
|
853
|
+
isGroup: false,
|
|
854
|
+
groupId,
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
this.appendMarkerSpecs(specs, anchor, {
|
|
859
|
+
markerRole: "handle",
|
|
860
|
+
isGroup: true,
|
|
861
|
+
groupId,
|
|
862
|
+
indices,
|
|
863
|
+
anchorIndex: anchor.index,
|
|
864
|
+
memberOffsets,
|
|
865
|
+
});
|
|
812
866
|
});
|
|
813
867
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
868
|
+
return specs;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private appendMarkerSpecs(
|
|
872
|
+
specs: RenderObjectSpec[],
|
|
873
|
+
marker: MarkerRenderState,
|
|
874
|
+
options: {
|
|
875
|
+
markerRole: "handle" | "member";
|
|
876
|
+
isGroup: boolean;
|
|
877
|
+
groupId?: string;
|
|
878
|
+
indices?: number[];
|
|
879
|
+
anchorIndex?: number;
|
|
880
|
+
memberOffsets?: GroupMemberOffset[];
|
|
881
|
+
},
|
|
882
|
+
) {
|
|
883
|
+
const { feature, index, position, scale, geometry } = marker;
|
|
884
|
+
const baseRadius =
|
|
885
|
+
feature.shape === "circle"
|
|
886
|
+
? (feature.radius ?? DEFAULT_CIRCLE_RADIUS)
|
|
887
|
+
: (feature.radius ?? 0);
|
|
888
|
+
const baseWidth =
|
|
889
|
+
feature.shape === "circle"
|
|
890
|
+
? baseRadius * 2
|
|
891
|
+
: (feature.width ?? DEFAULT_RECT_SIZE);
|
|
892
|
+
const baseHeight =
|
|
893
|
+
feature.shape === "circle"
|
|
894
|
+
? baseRadius * 2
|
|
895
|
+
: (feature.height ?? DEFAULT_RECT_SIZE);
|
|
896
|
+
const visualWidth = baseWidth * scale;
|
|
897
|
+
const visualHeight = baseHeight * scale;
|
|
898
|
+
const visualRadius = baseRadius * scale;
|
|
899
|
+
const color =
|
|
900
|
+
feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
|
|
901
|
+
const strokeDash =
|
|
902
|
+
feature.strokeDash ||
|
|
903
|
+
(feature.operation === "subtract" ? [4, 4] : undefined);
|
|
904
|
+
|
|
905
|
+
const interactive = options.markerRole === "handle";
|
|
906
|
+
const baseData = this.buildMarkerData(marker, options);
|
|
907
|
+
const commonProps = {
|
|
908
|
+
visible: this.isToolActive,
|
|
909
|
+
selectable: interactive && this.isToolActive,
|
|
910
|
+
evented: interactive && this.isToolActive,
|
|
911
|
+
hasControls: false,
|
|
912
|
+
hasBorders: false,
|
|
913
|
+
hoverCursor: interactive ? "move" : "default",
|
|
914
|
+
lockRotation: true,
|
|
915
|
+
lockScalingX: true,
|
|
916
|
+
lockScalingY: true,
|
|
917
|
+
fill: "transparent",
|
|
918
|
+
stroke: color,
|
|
919
|
+
strokeWidth: FEATURE_STROKE_WIDTH,
|
|
920
|
+
strokeDashArray: strokeDash,
|
|
921
|
+
originX: "center" as const,
|
|
922
|
+
originY: "center" as const,
|
|
923
|
+
left: position.x,
|
|
924
|
+
top: position.y,
|
|
925
|
+
angle: feature.rotation || 0,
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
const markerId = this.markerId(index);
|
|
929
|
+
if (feature.shape === "rect") {
|
|
930
|
+
specs.push({
|
|
931
|
+
id: markerId,
|
|
932
|
+
type: "rect",
|
|
933
|
+
space: "screen",
|
|
934
|
+
data: baseData,
|
|
935
|
+
props: {
|
|
936
|
+
...commonProps,
|
|
833
937
|
width: visualWidth,
|
|
834
938
|
height: visualHeight,
|
|
835
939
|
rx: visualRadius,
|
|
836
940
|
ry: visualRadius,
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
originY: "center",
|
|
855
|
-
left: pos.x,
|
|
856
|
-
top: pos.y,
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
if (feature.rotation) {
|
|
860
|
-
shape.rotate(feature.rotation);
|
|
861
|
-
}
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
} else {
|
|
944
|
+
specs.push({
|
|
945
|
+
id: markerId,
|
|
946
|
+
type: "rect",
|
|
947
|
+
space: "screen",
|
|
948
|
+
data: baseData,
|
|
949
|
+
props: {
|
|
950
|
+
...commonProps,
|
|
951
|
+
width: visualWidth,
|
|
952
|
+
height: visualHeight,
|
|
953
|
+
rx: visualRadius,
|
|
954
|
+
ry: visualRadius,
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
}
|
|
862
958
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
959
|
+
if (feature.bridge?.type === "vertical") {
|
|
960
|
+
const featureTopY = position.y - visualHeight / 2;
|
|
961
|
+
const dielineTopY = geometry.y - geometry.height / 2;
|
|
962
|
+
const bridgeHeight = Math.max(0, featureTopY - dielineTopY);
|
|
963
|
+
if (bridgeHeight <= 0.001) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
specs.push({
|
|
967
|
+
id: this.bridgeIndicatorId(index),
|
|
968
|
+
type: "rect",
|
|
969
|
+
space: "screen",
|
|
970
|
+
data: {
|
|
971
|
+
...baseData,
|
|
972
|
+
markerRole: "indicator",
|
|
973
|
+
markerOffsetX: 0,
|
|
974
|
+
markerOffsetY: -visualHeight / 2,
|
|
975
|
+
} as MarkerData,
|
|
976
|
+
props: {
|
|
977
|
+
visible: this.isToolActive,
|
|
978
|
+
selectable: false,
|
|
979
|
+
evented: false,
|
|
868
980
|
width: visualWidth,
|
|
869
|
-
height:
|
|
981
|
+
height: bridgeHeight,
|
|
870
982
|
fill: "transparent",
|
|
871
983
|
stroke: "#888",
|
|
872
984
|
strokeWidth: 1,
|
|
873
985
|
strokeDashArray: [2, 2],
|
|
874
|
-
originX: "center",
|
|
875
|
-
originY: "bottom", // Anchor at bottom so it extends up
|
|
876
|
-
left: pos.x,
|
|
877
|
-
top: pos.y - visualHeight / 2, // Start from top of feature
|
|
878
986
|
opacity: 0.5,
|
|
879
|
-
selectable: false,
|
|
880
|
-
evented: false,
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
// We need to return a group containing both shape and indicator
|
|
884
|
-
// But createMarkerShape is expected to return one object.
|
|
885
|
-
// If we return a Group, Fabric handles it.
|
|
886
|
-
// But the caller might wrap this in another Group if it's part of a feature group.
|
|
887
|
-
// Fabric supports nested groups.
|
|
888
|
-
|
|
889
|
-
const group = new Group([bridgeIndicator, shape], {
|
|
890
987
|
originX: "center",
|
|
891
|
-
originY: "
|
|
892
|
-
left:
|
|
893
|
-
top:
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
|
|
988
|
+
originY: "bottom",
|
|
989
|
+
left: position.x,
|
|
990
|
+
top: position.y - visualHeight / 2,
|
|
991
|
+
},
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
897
995
|
|
|
898
|
-
|
|
996
|
+
private buildMarkerData(
|
|
997
|
+
marker: MarkerRenderState,
|
|
998
|
+
options: {
|
|
999
|
+
markerRole: "handle" | "member";
|
|
1000
|
+
isGroup: boolean;
|
|
1001
|
+
groupId?: string;
|
|
1002
|
+
indices?: number[];
|
|
1003
|
+
anchorIndex?: number;
|
|
1004
|
+
memberOffsets?: GroupMemberOffset[];
|
|
1005
|
+
},
|
|
1006
|
+
): MarkerData {
|
|
1007
|
+
const data: MarkerData = {
|
|
1008
|
+
type: "feature-marker",
|
|
1009
|
+
index: marker.index,
|
|
1010
|
+
featureId: marker.feature.id,
|
|
1011
|
+
markerRole: options.markerRole,
|
|
1012
|
+
markerOffsetX: 0,
|
|
1013
|
+
markerOffsetY: 0,
|
|
1014
|
+
isGroup: options.isGroup,
|
|
899
1015
|
};
|
|
1016
|
+
if (options.groupId) data.groupId = options.groupId;
|
|
1017
|
+
if (options.indices) data.indices = options.indices;
|
|
1018
|
+
if (options.anchorIndex !== undefined) data.anchorIndex = options.anchorIndex;
|
|
1019
|
+
if (options.memberOffsets) data.memberOffsets = options.memberOffsets;
|
|
1020
|
+
return data;
|
|
1021
|
+
}
|
|
900
1022
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
this.currentGeometry!,
|
|
905
|
-
feature,
|
|
906
|
-
);
|
|
907
|
-
const pos = resolveFeaturePosition(feature, geometry);
|
|
908
|
-
const marker = createMarkerShape(feature, pos);
|
|
1023
|
+
private markerId(index: number): string {
|
|
1024
|
+
return `feature.marker.${index}`;
|
|
1025
|
+
}
|
|
909
1026
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
evented: this.isToolActive,
|
|
914
|
-
hasControls: false,
|
|
915
|
-
hasBorders: false,
|
|
916
|
-
hoverCursor: "move",
|
|
917
|
-
lockRotation: true,
|
|
918
|
-
lockScalingX: true,
|
|
919
|
-
lockScalingY: true,
|
|
920
|
-
data: { type: "feature-marker", index, isGroup: false },
|
|
921
|
-
});
|
|
1027
|
+
private bridgeIndicatorId(index: number): string {
|
|
1028
|
+
return `feature.marker.${index}.bridge`;
|
|
1029
|
+
}
|
|
922
1030
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1031
|
+
private toFeatureIndex(value: unknown): number | null {
|
|
1032
|
+
const numeric = Number(value);
|
|
1033
|
+
if (!Number.isInteger(numeric) || numeric < 0) return null;
|
|
1034
|
+
return numeric;
|
|
1035
|
+
}
|
|
926
1036
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1037
|
+
private readGroupIndices(raw: unknown): number[] {
|
|
1038
|
+
if (!Array.isArray(raw)) return [];
|
|
1039
|
+
return raw
|
|
1040
|
+
.map((value) => this.toFeatureIndex(value))
|
|
1041
|
+
.filter((value): value is number => value !== null);
|
|
1042
|
+
}
|
|
931
1043
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1044
|
+
private readGroupMemberOffsets(
|
|
1045
|
+
raw: unknown,
|
|
1046
|
+
fallbackIndices: number[] = [],
|
|
1047
|
+
): GroupMemberOffset[] {
|
|
1048
|
+
if (Array.isArray(raw)) {
|
|
1049
|
+
const parsed = raw
|
|
1050
|
+
.map((entry) => {
|
|
1051
|
+
const index = this.toFeatureIndex((entry as any)?.index);
|
|
1052
|
+
const dx = Number((entry as any)?.dx);
|
|
1053
|
+
const dy = Number((entry as any)?.dy);
|
|
1054
|
+
if (index === null || !Number.isFinite(dx) || !Number.isFinite(dy)) {
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
return { index, dx, dy };
|
|
1058
|
+
})
|
|
1059
|
+
.filter((value): value is GroupMemberOffset => !!value);
|
|
1060
|
+
if (parsed.length > 0) return parsed;
|
|
1061
|
+
}
|
|
936
1062
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
this.currentGeometry!,
|
|
940
|
-
feature,
|
|
941
|
-
);
|
|
942
|
-
const pos = resolveFeaturePosition(feature, geometry);
|
|
943
|
-
return createMarkerShape(feature, pos);
|
|
944
|
-
});
|
|
1063
|
+
return fallbackIndices.map((index) => ({ index, dx: 0, dy: 0 }));
|
|
1064
|
+
}
|
|
945
1065
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
lockScalingY: true,
|
|
956
|
-
subTargetCheck: true, // Allow events to pass through if needed, but we treat as one
|
|
957
|
-
interactive: false, // Children not interactive
|
|
958
|
-
// @ts-ignore
|
|
959
|
-
data: {
|
|
960
|
-
type: "feature-marker",
|
|
961
|
-
isGroup: true,
|
|
962
|
-
groupId,
|
|
963
|
-
indices: members.map((m) => m.index),
|
|
964
|
-
},
|
|
1066
|
+
private syncMarkerVisualsByTarget(target: any, center: MarkerPoint) {
|
|
1067
|
+
if (target.data?.isGroup) {
|
|
1068
|
+
const indices = this.readGroupIndices(target.data?.indices);
|
|
1069
|
+
const offsets = this.readGroupMemberOffsets(target.data?.memberOffsets, indices);
|
|
1070
|
+
offsets.forEach((entry) => {
|
|
1071
|
+
this.syncMarkerVisualObjectsToCenter(entry.index, {
|
|
1072
|
+
x: center.x + entry.dx,
|
|
1073
|
+
y: center.y + entry.dy,
|
|
1074
|
+
});
|
|
965
1075
|
});
|
|
1076
|
+
this.canvasService?.requestRenderAll();
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
966
1079
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
this.canvasService.requestRenderAll();
|
|
1080
|
+
const index = this.toFeatureIndex(target.data?.index);
|
|
1081
|
+
if (index === null) return;
|
|
1082
|
+
this.syncMarkerVisualObjectsToCenter(index, center);
|
|
1083
|
+
this.canvasService?.requestRenderAll();
|
|
972
1084
|
}
|
|
973
1085
|
|
|
974
|
-
private
|
|
975
|
-
if (!this.canvasService
|
|
976
|
-
|
|
977
|
-
const canvas = this.canvasService.canvas;
|
|
978
|
-
const markers = canvas
|
|
1086
|
+
private syncMarkerVisualObjectsToCenter(index: number, center: MarkerPoint) {
|
|
1087
|
+
if (!this.canvasService) return;
|
|
1088
|
+
const markers = this.canvasService.canvas
|
|
979
1089
|
.getObjects()
|
|
980
|
-
.filter(
|
|
1090
|
+
.filter(
|
|
1091
|
+
(obj: any) =>
|
|
1092
|
+
obj?.data?.type === "feature-marker" &&
|
|
1093
|
+
this.toFeatureIndex(obj?.data?.index) === index,
|
|
1094
|
+
);
|
|
981
1095
|
|
|
982
1096
|
markers.forEach((marker: any) => {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
if (index !== undefined) {
|
|
993
|
-
feature = this.workingFeatures[index];
|
|
994
|
-
}
|
|
995
|
-
}
|
|
1097
|
+
const offsetX = Number(marker?.data?.markerOffsetX || 0);
|
|
1098
|
+
const offsetY = Number(marker?.data?.markerOffsetY || 0);
|
|
1099
|
+
marker.set({
|
|
1100
|
+
left: center.x + offsetX,
|
|
1101
|
+
top: center.y + offsetY,
|
|
1102
|
+
});
|
|
1103
|
+
marker.setCoords();
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
996
1106
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
feature,
|
|
1000
|
-
);
|
|
1107
|
+
private enforceConstraints() {
|
|
1108
|
+
if (!this.canvasService || !this.currentGeometry) return;
|
|
1001
1109
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1110
|
+
const handles = this.canvasService.canvas
|
|
1111
|
+
.getObjects()
|
|
1112
|
+
.filter(
|
|
1113
|
+
(obj: any) =>
|
|
1114
|
+
obj?.data?.type === "feature-marker" &&
|
|
1115
|
+
obj?.data?.markerRole === "handle",
|
|
1007
1116
|
);
|
|
1008
|
-
const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
|
|
1009
1117
|
|
|
1118
|
+
handles.forEach((marker: any) => {
|
|
1119
|
+
const feature = this.getFeatureForMarker(marker);
|
|
1120
|
+
if (!feature) return;
|
|
1121
|
+
const geometry = this.getGeometryForFeature(this.currentGeometry!, feature);
|
|
1010
1122
|
const snapped = this.constrainPosition(
|
|
1011
|
-
|
|
1123
|
+
{
|
|
1124
|
+
x: Number(marker.left || 0),
|
|
1125
|
+
y: Number(marker.top || 0),
|
|
1126
|
+
},
|
|
1012
1127
|
geometry,
|
|
1013
|
-
limit,
|
|
1014
1128
|
feature,
|
|
1015
1129
|
);
|
|
1016
1130
|
marker.set({ left: snapped.x, top: snapped.y });
|
|
1017
1131
|
marker.setCoords();
|
|
1132
|
+
this.syncMarkerVisualsByTarget(marker, snapped);
|
|
1018
1133
|
});
|
|
1019
|
-
|
|
1134
|
+
|
|
1135
|
+
this.canvasService.canvas.requestRenderAll();
|
|
1020
1136
|
}
|
|
1021
1137
|
}
|