@pooder/kit 5.3.1 → 6.0.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/.test-dist/src/extensions/background.js +475 -131
- package/.test-dist/src/extensions/dieline.js +283 -180
- package/.test-dist/src/extensions/dielineShape.js +66 -0
- package/.test-dist/src/extensions/feature.js +388 -303
- package/.test-dist/src/extensions/film.js +133 -74
- package/.test-dist/src/extensions/geometry.js +120 -56
- package/.test-dist/src/extensions/image.js +296 -212
- package/.test-dist/src/extensions/index.js +1 -3
- package/.test-dist/src/extensions/maskOps.js +75 -20
- package/.test-dist/src/extensions/ruler.js +312 -215
- package/.test-dist/src/extensions/sceneLayoutModel.js +9 -3
- package/.test-dist/src/extensions/sceneVisibility.js +3 -10
- package/.test-dist/src/extensions/tracer.js +229 -58
- package/.test-dist/src/extensions/white-ink.js +139 -129
- package/.test-dist/src/services/CanvasService.js +888 -126
- package/.test-dist/src/services/index.js +1 -0
- package/.test-dist/src/services/visibility.js +54 -0
- package/.test-dist/tests/run.js +58 -4
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +377 -82
- package/dist/index.d.ts +377 -82
- package/dist/index.js +3920 -2178
- package/dist/index.mjs +3992 -2247
- package/package.json +1 -1
- package/src/extensions/background.ts +631 -145
- package/src/extensions/dieline.ts +280 -187
- package/src/extensions/dielineShape.ts +109 -0
- package/src/extensions/feature.ts +485 -366
- package/src/extensions/film.ts +152 -76
- package/src/extensions/geometry.ts +203 -104
- package/src/extensions/image.ts +319 -238
- package/src/extensions/index.ts +0 -1
- package/src/extensions/ruler.ts +481 -268
- package/src/extensions/sceneLayoutModel.ts +18 -6
- package/src/extensions/white-ink.ts +157 -171
- package/src/services/CanvasService.ts +1126 -140
- package/src/services/index.ts +1 -0
- package/src/services/renderSpec.ts +69 -4
- package/src/services/visibility.ts +78 -0
- package/tests/run.ts +139 -4
- 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/dieline.js +0 -818
- package/.test-dist/src/edgeScale.js +0 -12
- 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/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/size.js +0 -332
- package/.test-dist/src/tracer.js +0 -544
- package/.test-dist/src/white-ink.js +0 -829
- package/.test-dist/src/wrappedOffsets.js +0 -33
- package/src/extensions/sceneVisibility.ts +0 -71
|
@@ -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,22 @@ 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
|
+
passes: [
|
|
105
|
+
{
|
|
106
|
+
id: FEATURE_OVERLAY_LAYER_ID,
|
|
107
|
+
stack: 880,
|
|
108
|
+
order: 0,
|
|
109
|
+
objects: this.specs,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
}),
|
|
113
|
+
{ priority: 350 },
|
|
114
|
+
);
|
|
115
|
+
|
|
67
116
|
const configService = context.services.get<ConfigurationService>(
|
|
68
117
|
"ConfigurationService",
|
|
69
118
|
);
|
|
@@ -94,7 +143,6 @@ export class FeatureTool implements Extension {
|
|
|
94
143
|
() => this.hasWorkingChanges,
|
|
95
144
|
);
|
|
96
145
|
|
|
97
|
-
// Listen to tool activation
|
|
98
146
|
context.eventBus.on("tool:activated", this.onToolActivated);
|
|
99
147
|
|
|
100
148
|
this.setup();
|
|
@@ -119,23 +167,7 @@ export class FeatureTool implements Extension {
|
|
|
119
167
|
};
|
|
120
168
|
|
|
121
169
|
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();
|
|
170
|
+
this.redraw();
|
|
139
171
|
}
|
|
140
172
|
|
|
141
173
|
contribute() {
|
|
@@ -237,10 +269,10 @@ export class FeatureTool implements Extension {
|
|
|
237
269
|
await this.refreshGeometry();
|
|
238
270
|
this.setWorkingFeatures(original);
|
|
239
271
|
this.hasWorkingChanges = false;
|
|
272
|
+
this.clearFeatureSessionState();
|
|
240
273
|
this.redraw();
|
|
241
274
|
this.emitWorkingChange();
|
|
242
275
|
this.updateCommittedFeatures(original);
|
|
243
|
-
this.clearFeatureSessionState();
|
|
244
276
|
return { ok: true };
|
|
245
277
|
},
|
|
246
278
|
},
|
|
@@ -384,8 +416,7 @@ export class FeatureTool implements Extension {
|
|
|
384
416
|
|
|
385
417
|
this.setWorkingFeatures(next);
|
|
386
418
|
this.hasWorkingChanges = true;
|
|
387
|
-
this.redraw();
|
|
388
|
-
this.enforceConstraints();
|
|
419
|
+
this.redraw({ enforceConstraints: true });
|
|
389
420
|
this.emitWorkingChange();
|
|
390
421
|
|
|
391
422
|
return { ok: true };
|
|
@@ -420,7 +451,7 @@ export class FeatureTool implements Extension {
|
|
|
420
451
|
{ dielineWidth, dielineHeight },
|
|
421
452
|
(next) => {
|
|
422
453
|
this.updateCommittedFeatures(next as ConstraintFeature[]);
|
|
423
|
-
this.workingFeatures = this.cloneFeatures(next as
|
|
454
|
+
this.workingFeatures = this.cloneFeatures(next as ConstraintFeature[]);
|
|
424
455
|
this.emitWorkingChange();
|
|
425
456
|
},
|
|
426
457
|
);
|
|
@@ -434,7 +465,6 @@ export class FeatureTool implements Extension {
|
|
|
434
465
|
|
|
435
466
|
this.hasWorkingChanges = false;
|
|
436
467
|
this.clearFeatureSessionState();
|
|
437
|
-
// Keep feature markers above dieline overlay after config-driven redraw.
|
|
438
468
|
this.redraw();
|
|
439
469
|
return { ok: true };
|
|
440
470
|
}
|
|
@@ -442,18 +472,16 @@ export class FeatureTool implements Extension {
|
|
|
442
472
|
private addFeature(type: "add" | "subtract") {
|
|
443
473
|
if (!this.canvasService) return false;
|
|
444
474
|
|
|
445
|
-
// Default to top edge center
|
|
446
475
|
const newFeature: ConstraintFeature = {
|
|
447
476
|
id: Date.now().toString(),
|
|
448
477
|
operation: type,
|
|
449
478
|
shape: "rect",
|
|
450
479
|
x: 0.5,
|
|
451
|
-
y: 0,
|
|
480
|
+
y: 0,
|
|
452
481
|
width: 10,
|
|
453
482
|
height: 10,
|
|
454
483
|
rotation: 0,
|
|
455
484
|
renderBehavior: "edge",
|
|
456
|
-
// Default constraint: path (snap to edge)
|
|
457
485
|
constraints: [{ type: "path" }],
|
|
458
486
|
};
|
|
459
487
|
|
|
@@ -470,7 +498,6 @@ export class FeatureTool implements Extension {
|
|
|
470
498
|
const groupId = Date.now().toString();
|
|
471
499
|
const timestamp = Date.now();
|
|
472
500
|
|
|
473
|
-
// 1. Lug (Outer) - Add
|
|
474
501
|
const lug: ConstraintFeature = {
|
|
475
502
|
id: `${timestamp}-lug`,
|
|
476
503
|
groupId,
|
|
@@ -484,7 +511,6 @@ export class FeatureTool implements Extension {
|
|
|
484
511
|
constraints: [{ type: "path" }],
|
|
485
512
|
};
|
|
486
513
|
|
|
487
|
-
// 2. Hole (Inner) - Subtract
|
|
488
514
|
const hole: ConstraintFeature = {
|
|
489
515
|
id: `${timestamp}-hole`,
|
|
490
516
|
groupId,
|
|
@@ -507,10 +533,8 @@ export class FeatureTool implements Extension {
|
|
|
507
533
|
|
|
508
534
|
private getGeometryForFeature(
|
|
509
535
|
geometry: DielineGeometry,
|
|
510
|
-
|
|
536
|
+
_feature?: ConstraintFeature,
|
|
511
537
|
): 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
538
|
return geometry;
|
|
515
539
|
}
|
|
516
540
|
|
|
@@ -518,12 +542,10 @@ export class FeatureTool implements Extension {
|
|
|
518
542
|
if (!this.canvasService || !this.context) return;
|
|
519
543
|
const canvas = this.canvasService.canvas;
|
|
520
544
|
|
|
521
|
-
// 1. Listen for Scene Geometry Changes
|
|
522
545
|
if (!this.handleSceneGeometryChange) {
|
|
523
546
|
this.handleSceneGeometryChange = (geometry: DielineGeometry) => {
|
|
524
547
|
this.currentGeometry = geometry;
|
|
525
|
-
this.redraw();
|
|
526
|
-
this.enforceConstraints();
|
|
548
|
+
this.redraw({ enforceConstraints: true });
|
|
527
549
|
};
|
|
528
550
|
this.context.eventBus.on(
|
|
529
551
|
"scene:geometry:change",
|
|
@@ -531,7 +553,6 @@ export class FeatureTool implements Extension {
|
|
|
531
553
|
);
|
|
532
554
|
}
|
|
533
555
|
|
|
534
|
-
// 2. Initial Fetch of Geometry
|
|
535
556
|
const commandService = this.context.services.get<any>("CommandService");
|
|
536
557
|
if (commandService) {
|
|
537
558
|
try {
|
|
@@ -546,119 +567,41 @@ export class FeatureTool implements Extension {
|
|
|
546
567
|
} catch (e) {}
|
|
547
568
|
}
|
|
548
569
|
|
|
549
|
-
// 3. Setup Canvas Interaction
|
|
550
570
|
if (!this.handleMoving) {
|
|
551
571
|
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,
|
|
572
|
+
const target = this.getDraggableMarkerTarget(e?.target);
|
|
573
|
+
if (!target || !this.currentGeometry) return;
|
|
574
|
+
|
|
575
|
+
const feature = this.getFeatureForMarker(target);
|
|
576
|
+
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
577
|
+
const snapped = this.constrainPosition(
|
|
578
|
+
{
|
|
579
|
+
x: Number(target.left || 0),
|
|
580
|
+
y: Number(target.top || 0),
|
|
581
|
+
},
|
|
582
|
+
geometry,
|
|
572
583
|
feature,
|
|
573
584
|
);
|
|
574
585
|
|
|
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
586
|
target.set({
|
|
594
587
|
left: snapped.x,
|
|
595
588
|
top: snapped.y,
|
|
596
589
|
});
|
|
590
|
+
target.setCoords();
|
|
591
|
+
|
|
592
|
+
this.syncMarkerVisualsByTarget(target, snapped);
|
|
597
593
|
};
|
|
598
594
|
canvas.on("object:moving", this.handleMoving);
|
|
599
595
|
}
|
|
600
596
|
|
|
601
597
|
if (!this.handleModified) {
|
|
602
598
|
this.handleModified = (e: any) => {
|
|
603
|
-
const target = e
|
|
604
|
-
if (!target
|
|
599
|
+
const target = this.getDraggableMarkerTarget(e?.target);
|
|
600
|
+
if (!target) return;
|
|
605
601
|
|
|
606
602
|
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();
|
|
603
|
+
this.syncGroupFromCanvas(target);
|
|
660
604
|
} else {
|
|
661
|
-
// Single object
|
|
662
605
|
this.syncFeatureFromCanvas(target);
|
|
663
606
|
}
|
|
664
607
|
};
|
|
@@ -686,20 +629,34 @@ export class FeatureTool implements Extension {
|
|
|
686
629
|
this.handleSceneGeometryChange = null;
|
|
687
630
|
}
|
|
688
631
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
632
|
+
this.renderSeq += 1;
|
|
633
|
+
this.specs = [];
|
|
634
|
+
this.renderProducerDisposable?.dispose();
|
|
635
|
+
this.renderProducerDisposable = undefined;
|
|
636
|
+
void this.canvasService.flushRenderFromProducers();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private getDraggableMarkerTarget(target: any): any | null {
|
|
640
|
+
if (!this.isFeatureSessionActive || !this.isToolActive) return null;
|
|
641
|
+
if (!target || target.data?.type !== "feature-marker") return null;
|
|
642
|
+
if (target.data?.markerRole !== "handle") return null;
|
|
643
|
+
return target;
|
|
644
|
+
}
|
|
693
645
|
|
|
694
|
-
|
|
646
|
+
private getFeatureForMarker(target: any): ConstraintFeature | undefined {
|
|
647
|
+
const data = target?.data || {};
|
|
648
|
+
const index = data.isGroup
|
|
649
|
+
? this.toFeatureIndex(data.anchorIndex)
|
|
650
|
+
: this.toFeatureIndex(data.index);
|
|
651
|
+
if (index === null) return undefined;
|
|
652
|
+
return this.workingFeatures[index];
|
|
695
653
|
}
|
|
696
654
|
|
|
697
655
|
private constrainPosition(
|
|
698
|
-
p:
|
|
656
|
+
p: MarkerPoint,
|
|
699
657
|
geometry: DielineGeometry,
|
|
700
|
-
limit: number,
|
|
701
658
|
feature?: ConstraintFeature,
|
|
702
|
-
):
|
|
659
|
+
): MarkerPoint {
|
|
703
660
|
if (!feature) {
|
|
704
661
|
return { x: p.x, y: p.y };
|
|
705
662
|
}
|
|
@@ -707,7 +664,6 @@ export class FeatureTool implements Extension {
|
|
|
707
664
|
const minX = geometry.x - geometry.width / 2;
|
|
708
665
|
const minY = geometry.y - geometry.height / 2;
|
|
709
666
|
|
|
710
|
-
// Normalize
|
|
711
667
|
const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
|
|
712
668
|
const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
|
|
713
669
|
|
|
@@ -715,7 +671,6 @@ export class FeatureTool implements Extension {
|
|
|
715
671
|
const dielineWidth = geometry.width / scale;
|
|
716
672
|
const dielineHeight = geometry.height / scale;
|
|
717
673
|
|
|
718
|
-
// Filter constraints: only apply those that are NOT validateOnly
|
|
719
674
|
const activeConstraints = feature.constraints?.filter(
|
|
720
675
|
(c) => !c.validateOnly,
|
|
721
676
|
);
|
|
@@ -732,290 +687,454 @@ export class FeatureTool implements Extension {
|
|
|
732
687
|
activeConstraints,
|
|
733
688
|
);
|
|
734
689
|
|
|
735
|
-
// Denormalize
|
|
736
690
|
return {
|
|
737
691
|
x: minX + constrained.x * geometry.width,
|
|
738
692
|
y: minY + constrained.y * geometry.height,
|
|
739
693
|
};
|
|
740
694
|
}
|
|
741
695
|
|
|
696
|
+
private toNormalizedPoint(
|
|
697
|
+
point: MarkerPoint,
|
|
698
|
+
geometry: DielineGeometry,
|
|
699
|
+
): MarkerPoint {
|
|
700
|
+
const left = geometry.x - geometry.width / 2;
|
|
701
|
+
const top = geometry.y - geometry.height / 2;
|
|
702
|
+
return {
|
|
703
|
+
x: geometry.width > 0 ? (point.x - left) / geometry.width : 0.5,
|
|
704
|
+
y: geometry.height > 0 ? (point.y - top) / geometry.height : 0.5,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
742
708
|
private syncFeatureFromCanvas(target: any) {
|
|
743
|
-
if (!this.currentGeometry
|
|
709
|
+
if (!this.currentGeometry) return;
|
|
744
710
|
|
|
745
|
-
const index = target.data?.index;
|
|
746
|
-
if (
|
|
747
|
-
index === undefined ||
|
|
748
|
-
index < 0 ||
|
|
749
|
-
index >= this.workingFeatures.length
|
|
750
|
-
)
|
|
751
|
-
return;
|
|
711
|
+
const index = this.toFeatureIndex(target.data?.index);
|
|
712
|
+
if (index === null || index >= this.workingFeatures.length) return;
|
|
752
713
|
|
|
753
714
|
const feature = this.workingFeatures[index];
|
|
754
715
|
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;
|
|
716
|
+
const normalized = this.toNormalizedPoint(
|
|
717
|
+
{
|
|
718
|
+
x: Number(target.left || 0),
|
|
719
|
+
y: Number(target.top || 0),
|
|
720
|
+
},
|
|
721
|
+
geometry,
|
|
722
|
+
);
|
|
764
723
|
|
|
765
|
-
// Update feature
|
|
766
724
|
const updatedFeature = {
|
|
767
725
|
...feature,
|
|
768
|
-
x:
|
|
769
|
-
y:
|
|
770
|
-
// Could also update rotation if we allowed rotating markers
|
|
726
|
+
x: normalized.x,
|
|
727
|
+
y: normalized.y,
|
|
771
728
|
};
|
|
772
729
|
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
this.setWorkingFeatures(
|
|
730
|
+
const next = [...this.workingFeatures];
|
|
731
|
+
next[index] = updatedFeature;
|
|
732
|
+
this.setWorkingFeatures(next);
|
|
776
733
|
this.hasWorkingChanges = true;
|
|
777
734
|
this.emitWorkingChange();
|
|
778
735
|
}
|
|
779
736
|
|
|
780
|
-
private
|
|
781
|
-
if (!this.
|
|
782
|
-
const canvas = this.canvasService.canvas;
|
|
783
|
-
const geometry = this.currentGeometry;
|
|
737
|
+
private syncGroupFromCanvas(target: any) {
|
|
738
|
+
if (!this.currentGeometry) return;
|
|
784
739
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
.filter((obj: any) => obj.data?.type === "feature-marker");
|
|
789
|
-
existing.forEach((obj) => canvas.remove(obj));
|
|
740
|
+
const indices = this.readGroupIndices(target.data?.indices);
|
|
741
|
+
if (indices.length === 0) return;
|
|
742
|
+
const offsets = this.readGroupMemberOffsets(target.data?.memberOffsets, indices);
|
|
790
743
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
744
|
+
const anchorCenter = {
|
|
745
|
+
x: Number(target.left || 0),
|
|
746
|
+
y: Number(target.top || 0),
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const next = [...this.workingFeatures];
|
|
750
|
+
let changed = false;
|
|
751
|
+
offsets.forEach((entry) => {
|
|
752
|
+
const index = entry.index;
|
|
753
|
+
if (index < 0 || index >= next.length) return;
|
|
754
|
+
const feature = next[index];
|
|
755
|
+
const geometry = this.getGeometryForFeature(this.currentGeometry!, feature);
|
|
756
|
+
const normalized = this.toNormalizedPoint(
|
|
757
|
+
{
|
|
758
|
+
x: anchorCenter.x + entry.dx,
|
|
759
|
+
y: anchorCenter.y + entry.dy,
|
|
760
|
+
},
|
|
761
|
+
geometry,
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
if (feature.x !== normalized.x || feature.y !== normalized.y) {
|
|
765
|
+
next[index] = {
|
|
766
|
+
...feature,
|
|
767
|
+
x: normalized.x,
|
|
768
|
+
y: normalized.y,
|
|
769
|
+
};
|
|
770
|
+
changed = true;
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
if (!changed) return;
|
|
775
|
+
this.setWorkingFeatures(next);
|
|
776
|
+
this.hasWorkingChanges = true;
|
|
777
|
+
this.emitWorkingChange();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
private redraw(options: { enforceConstraints?: boolean } = {}) {
|
|
781
|
+
void this.redrawAsync(options);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private async redrawAsync(options: { enforceConstraints?: boolean } = {}) {
|
|
785
|
+
if (!this.canvasService) return;
|
|
786
|
+
|
|
787
|
+
const seq = ++this.renderSeq;
|
|
788
|
+
this.specs = this.buildFeatureSpecs();
|
|
789
|
+
if (seq !== this.renderSeq) return;
|
|
790
|
+
|
|
791
|
+
await this.canvasService.flushRenderFromProducers();
|
|
792
|
+
if (seq !== this.renderSeq) return;
|
|
793
|
+
if (options.enforceConstraints) {
|
|
794
|
+
this.enforceConstraints();
|
|
794
795
|
}
|
|
796
|
+
}
|
|
795
797
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
798
|
+
private buildFeatureSpecs(): RenderObjectSpec[] {
|
|
799
|
+
if (
|
|
800
|
+
!this.isFeatureSessionActive ||
|
|
801
|
+
!this.currentGeometry ||
|
|
802
|
+
this.workingFeatures.length === 0
|
|
803
|
+
) {
|
|
804
|
+
return [];
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const groups = new Map<string, MarkerRenderState[]>();
|
|
808
|
+
const singles: MarkerRenderState[] = [];
|
|
809
|
+
|
|
810
|
+
this.workingFeatures.forEach((feature, index) => {
|
|
811
|
+
const geometry = this.getGeometryForFeature(this.currentGeometry!, feature);
|
|
812
|
+
const position = resolveFeaturePosition(feature, geometry);
|
|
813
|
+
const scale = geometry.scale || 1;
|
|
814
|
+
const marker: MarkerRenderState = {
|
|
815
|
+
feature,
|
|
816
|
+
index,
|
|
817
|
+
position,
|
|
818
|
+
geometry,
|
|
819
|
+
scale,
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
if (feature.groupId) {
|
|
823
|
+
const list = groups.get(feature.groupId) || [];
|
|
824
|
+
list.push(marker);
|
|
825
|
+
groups.set(feature.groupId, list);
|
|
826
|
+
return;
|
|
811
827
|
}
|
|
828
|
+
singles.push(marker);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
const specs: RenderObjectSpec[] = [];
|
|
832
|
+
|
|
833
|
+
singles.forEach((marker) => {
|
|
834
|
+
this.appendMarkerSpecs(specs, marker, {
|
|
835
|
+
markerRole: "handle",
|
|
836
|
+
isGroup: false,
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
groups.forEach((members, groupId) => {
|
|
841
|
+
if (!members.length) return;
|
|
842
|
+
const anchor = members[0];
|
|
843
|
+
const memberOffsets: GroupMemberOffset[] = members.map((member) => ({
|
|
844
|
+
index: member.index,
|
|
845
|
+
dx: member.position.x - anchor.position.x,
|
|
846
|
+
dy: member.position.y - anchor.position.y,
|
|
847
|
+
}));
|
|
848
|
+
const indices = members.map((member) => member.index);
|
|
849
|
+
|
|
850
|
+
members
|
|
851
|
+
.filter((member) => member.index !== anchor.index)
|
|
852
|
+
.forEach((member) => {
|
|
853
|
+
this.appendMarkerSpecs(specs, member, {
|
|
854
|
+
markerRole: "member",
|
|
855
|
+
isGroup: false,
|
|
856
|
+
groupId,
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
this.appendMarkerSpecs(specs, anchor, {
|
|
861
|
+
markerRole: "handle",
|
|
862
|
+
isGroup: true,
|
|
863
|
+
groupId,
|
|
864
|
+
indices,
|
|
865
|
+
anchorIndex: anchor.index,
|
|
866
|
+
memberOffsets,
|
|
867
|
+
});
|
|
812
868
|
});
|
|
813
869
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
870
|
+
return specs;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
private appendMarkerSpecs(
|
|
874
|
+
specs: RenderObjectSpec[],
|
|
875
|
+
marker: MarkerRenderState,
|
|
876
|
+
options: {
|
|
877
|
+
markerRole: "handle" | "member";
|
|
878
|
+
isGroup: boolean;
|
|
879
|
+
groupId?: string;
|
|
880
|
+
indices?: number[];
|
|
881
|
+
anchorIndex?: number;
|
|
882
|
+
memberOffsets?: GroupMemberOffset[];
|
|
883
|
+
},
|
|
884
|
+
) {
|
|
885
|
+
const { feature, index, position, scale, geometry } = marker;
|
|
886
|
+
const baseRadius =
|
|
887
|
+
feature.shape === "circle"
|
|
888
|
+
? (feature.radius ?? DEFAULT_CIRCLE_RADIUS)
|
|
889
|
+
: (feature.radius ?? 0);
|
|
890
|
+
const baseWidth =
|
|
891
|
+
feature.shape === "circle"
|
|
892
|
+
? baseRadius * 2
|
|
893
|
+
: (feature.width ?? DEFAULT_RECT_SIZE);
|
|
894
|
+
const baseHeight =
|
|
895
|
+
feature.shape === "circle"
|
|
896
|
+
? baseRadius * 2
|
|
897
|
+
: (feature.height ?? DEFAULT_RECT_SIZE);
|
|
898
|
+
const visualWidth = baseWidth * scale;
|
|
899
|
+
const visualHeight = baseHeight * scale;
|
|
900
|
+
const visualRadius = baseRadius * scale;
|
|
901
|
+
const color =
|
|
902
|
+
feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
|
|
903
|
+
const strokeDash =
|
|
904
|
+
feature.strokeDash ||
|
|
905
|
+
(feature.operation === "subtract" ? [4, 4] : undefined);
|
|
906
|
+
|
|
907
|
+
const interactive = options.markerRole === "handle";
|
|
908
|
+
const sessionVisible = this.isToolActive && this.isFeatureSessionActive;
|
|
909
|
+
const baseData = this.buildMarkerData(marker, options);
|
|
910
|
+
const commonProps = {
|
|
911
|
+
visible: sessionVisible,
|
|
912
|
+
selectable: interactive && sessionVisible,
|
|
913
|
+
evented: interactive && sessionVisible,
|
|
914
|
+
hasControls: false,
|
|
915
|
+
hasBorders: false,
|
|
916
|
+
hoverCursor: interactive ? "move" : "default",
|
|
917
|
+
lockRotation: true,
|
|
918
|
+
lockScalingX: true,
|
|
919
|
+
lockScalingY: true,
|
|
920
|
+
fill: "transparent",
|
|
921
|
+
stroke: color,
|
|
922
|
+
strokeWidth: FEATURE_STROKE_WIDTH,
|
|
923
|
+
strokeDashArray: strokeDash,
|
|
924
|
+
originX: "center" as const,
|
|
925
|
+
originY: "center" as const,
|
|
926
|
+
left: position.x,
|
|
927
|
+
top: position.y,
|
|
928
|
+
angle: feature.rotation || 0,
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
const markerId = this.markerId(index);
|
|
932
|
+
if (feature.shape === "rect") {
|
|
933
|
+
specs.push({
|
|
934
|
+
id: markerId,
|
|
935
|
+
type: "rect",
|
|
936
|
+
space: "screen",
|
|
937
|
+
data: baseData,
|
|
938
|
+
props: {
|
|
939
|
+
...commonProps,
|
|
833
940
|
width: visualWidth,
|
|
834
941
|
height: visualHeight,
|
|
835
942
|
rx: visualRadius,
|
|
836
943
|
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
|
-
}
|
|
944
|
+
},
|
|
945
|
+
});
|
|
946
|
+
} else {
|
|
947
|
+
specs.push({
|
|
948
|
+
id: markerId,
|
|
949
|
+
type: "rect",
|
|
950
|
+
space: "screen",
|
|
951
|
+
data: baseData,
|
|
952
|
+
props: {
|
|
953
|
+
...commonProps,
|
|
954
|
+
width: visualWidth,
|
|
955
|
+
height: visualHeight,
|
|
956
|
+
rx: visualRadius,
|
|
957
|
+
ry: visualRadius,
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
}
|
|
862
961
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
962
|
+
if (feature.bridge?.type === "vertical") {
|
|
963
|
+
const featureTopY = position.y - visualHeight / 2;
|
|
964
|
+
const dielineTopY = geometry.y - geometry.height / 2;
|
|
965
|
+
const bridgeHeight = Math.max(0, featureTopY - dielineTopY);
|
|
966
|
+
if (bridgeHeight <= 0.001) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
specs.push({
|
|
970
|
+
id: this.bridgeIndicatorId(index),
|
|
971
|
+
type: "rect",
|
|
972
|
+
space: "screen",
|
|
973
|
+
data: {
|
|
974
|
+
...baseData,
|
|
975
|
+
markerRole: "indicator",
|
|
976
|
+
markerOffsetX: 0,
|
|
977
|
+
markerOffsetY: -visualHeight / 2,
|
|
978
|
+
} as MarkerData,
|
|
979
|
+
props: {
|
|
980
|
+
visible: sessionVisible,
|
|
981
|
+
selectable: false,
|
|
982
|
+
evented: false,
|
|
868
983
|
width: visualWidth,
|
|
869
|
-
height:
|
|
984
|
+
height: bridgeHeight,
|
|
870
985
|
fill: "transparent",
|
|
871
986
|
stroke: "#888",
|
|
872
987
|
strokeWidth: 1,
|
|
873
988
|
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
989
|
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
990
|
originX: "center",
|
|
891
|
-
originY: "
|
|
892
|
-
left:
|
|
893
|
-
top:
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
|
|
991
|
+
originY: "bottom",
|
|
992
|
+
left: position.x,
|
|
993
|
+
top: position.y - visualHeight / 2,
|
|
994
|
+
},
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
897
998
|
|
|
898
|
-
|
|
999
|
+
private buildMarkerData(
|
|
1000
|
+
marker: MarkerRenderState,
|
|
1001
|
+
options: {
|
|
1002
|
+
markerRole: "handle" | "member";
|
|
1003
|
+
isGroup: boolean;
|
|
1004
|
+
groupId?: string;
|
|
1005
|
+
indices?: number[];
|
|
1006
|
+
anchorIndex?: number;
|
|
1007
|
+
memberOffsets?: GroupMemberOffset[];
|
|
1008
|
+
},
|
|
1009
|
+
): MarkerData {
|
|
1010
|
+
const data: MarkerData = {
|
|
1011
|
+
type: "feature-marker",
|
|
1012
|
+
index: marker.index,
|
|
1013
|
+
featureId: marker.feature.id,
|
|
1014
|
+
markerRole: options.markerRole,
|
|
1015
|
+
markerOffsetX: 0,
|
|
1016
|
+
markerOffsetY: 0,
|
|
1017
|
+
isGroup: options.isGroup,
|
|
899
1018
|
};
|
|
1019
|
+
if (options.groupId) data.groupId = options.groupId;
|
|
1020
|
+
if (options.indices) data.indices = options.indices;
|
|
1021
|
+
if (options.anchorIndex !== undefined) data.anchorIndex = options.anchorIndex;
|
|
1022
|
+
if (options.memberOffsets) data.memberOffsets = options.memberOffsets;
|
|
1023
|
+
return data;
|
|
1024
|
+
}
|
|
900
1025
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
this.currentGeometry!,
|
|
905
|
-
feature,
|
|
906
|
-
);
|
|
907
|
-
const pos = resolveFeaturePosition(feature, geometry);
|
|
908
|
-
const marker = createMarkerShape(feature, pos);
|
|
1026
|
+
private markerId(index: number): string {
|
|
1027
|
+
return `feature.marker.${index}`;
|
|
1028
|
+
}
|
|
909
1029
|
|
|
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
|
-
});
|
|
1030
|
+
private bridgeIndicatorId(index: number): string {
|
|
1031
|
+
return `feature.marker.${index}.bridge`;
|
|
1032
|
+
}
|
|
922
1033
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1034
|
+
private toFeatureIndex(value: unknown): number | null {
|
|
1035
|
+
const numeric = Number(value);
|
|
1036
|
+
if (!Number.isInteger(numeric) || numeric < 0) return null;
|
|
1037
|
+
return numeric;
|
|
1038
|
+
}
|
|
926
1039
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1040
|
+
private readGroupIndices(raw: unknown): number[] {
|
|
1041
|
+
if (!Array.isArray(raw)) return [];
|
|
1042
|
+
return raw
|
|
1043
|
+
.map((value) => this.toFeatureIndex(value))
|
|
1044
|
+
.filter((value): value is number => value !== null);
|
|
1045
|
+
}
|
|
931
1046
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1047
|
+
private readGroupMemberOffsets(
|
|
1048
|
+
raw: unknown,
|
|
1049
|
+
fallbackIndices: number[] = [],
|
|
1050
|
+
): GroupMemberOffset[] {
|
|
1051
|
+
if (Array.isArray(raw)) {
|
|
1052
|
+
const parsed = raw
|
|
1053
|
+
.map((entry) => {
|
|
1054
|
+
const index = this.toFeatureIndex((entry as any)?.index);
|
|
1055
|
+
const dx = Number((entry as any)?.dx);
|
|
1056
|
+
const dy = Number((entry as any)?.dy);
|
|
1057
|
+
if (index === null || !Number.isFinite(dx) || !Number.isFinite(dy)) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
return { index, dx, dy };
|
|
1061
|
+
})
|
|
1062
|
+
.filter((value): value is GroupMemberOffset => !!value);
|
|
1063
|
+
if (parsed.length > 0) return parsed;
|
|
1064
|
+
}
|
|
936
1065
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
this.currentGeometry!,
|
|
940
|
-
feature,
|
|
941
|
-
);
|
|
942
|
-
const pos = resolveFeaturePosition(feature, geometry);
|
|
943
|
-
return createMarkerShape(feature, pos);
|
|
944
|
-
});
|
|
1066
|
+
return fallbackIndices.map((index) => ({ index, dx: 0, dy: 0 }));
|
|
1067
|
+
}
|
|
945
1068
|
|
|
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
|
-
},
|
|
1069
|
+
private syncMarkerVisualsByTarget(target: any, center: MarkerPoint) {
|
|
1070
|
+
if (target.data?.isGroup) {
|
|
1071
|
+
const indices = this.readGroupIndices(target.data?.indices);
|
|
1072
|
+
const offsets = this.readGroupMemberOffsets(target.data?.memberOffsets, indices);
|
|
1073
|
+
offsets.forEach((entry) => {
|
|
1074
|
+
this.syncMarkerVisualObjectsToCenter(entry.index, {
|
|
1075
|
+
x: center.x + entry.dx,
|
|
1076
|
+
y: center.y + entry.dy,
|
|
1077
|
+
});
|
|
965
1078
|
});
|
|
1079
|
+
this.canvasService?.requestRenderAll();
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
966
1082
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
this.canvasService.requestRenderAll();
|
|
1083
|
+
const index = this.toFeatureIndex(target.data?.index);
|
|
1084
|
+
if (index === null) return;
|
|
1085
|
+
this.syncMarkerVisualObjectsToCenter(index, center);
|
|
1086
|
+
this.canvasService?.requestRenderAll();
|
|
972
1087
|
}
|
|
973
1088
|
|
|
974
|
-
private
|
|
975
|
-
if (!this.canvasService
|
|
976
|
-
|
|
977
|
-
const canvas = this.canvasService.canvas;
|
|
978
|
-
const markers = canvas
|
|
1089
|
+
private syncMarkerVisualObjectsToCenter(index: number, center: MarkerPoint) {
|
|
1090
|
+
if (!this.canvasService) return;
|
|
1091
|
+
const markers = this.canvasService.canvas
|
|
979
1092
|
.getObjects()
|
|
980
|
-
.filter(
|
|
1093
|
+
.filter(
|
|
1094
|
+
(obj: any) =>
|
|
1095
|
+
obj?.data?.type === "feature-marker" &&
|
|
1096
|
+
this.toFeatureIndex(obj?.data?.index) === index,
|
|
1097
|
+
);
|
|
981
1098
|
|
|
982
1099
|
markers.forEach((marker: any) => {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
if (index !== undefined) {
|
|
993
|
-
feature = this.workingFeatures[index];
|
|
994
|
-
}
|
|
995
|
-
}
|
|
1100
|
+
const offsetX = Number(marker?.data?.markerOffsetX || 0);
|
|
1101
|
+
const offsetY = Number(marker?.data?.markerOffsetY || 0);
|
|
1102
|
+
marker.set({
|
|
1103
|
+
left: center.x + offsetX,
|
|
1104
|
+
top: center.y + offsetY,
|
|
1105
|
+
});
|
|
1106
|
+
marker.setCoords();
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
996
1109
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
feature,
|
|
1000
|
-
);
|
|
1110
|
+
private enforceConstraints() {
|
|
1111
|
+
if (!this.canvasService || !this.currentGeometry) return;
|
|
1001
1112
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1113
|
+
const handles = this.canvasService.canvas
|
|
1114
|
+
.getObjects()
|
|
1115
|
+
.filter(
|
|
1116
|
+
(obj: any) =>
|
|
1117
|
+
obj?.data?.type === "feature-marker" &&
|
|
1118
|
+
obj?.data?.markerRole === "handle",
|
|
1007
1119
|
);
|
|
1008
|
-
const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
|
|
1009
1120
|
|
|
1121
|
+
handles.forEach((marker: any) => {
|
|
1122
|
+
const feature = this.getFeatureForMarker(marker);
|
|
1123
|
+
if (!feature) return;
|
|
1124
|
+
const geometry = this.getGeometryForFeature(this.currentGeometry!, feature);
|
|
1010
1125
|
const snapped = this.constrainPosition(
|
|
1011
|
-
|
|
1126
|
+
{
|
|
1127
|
+
x: Number(marker.left || 0),
|
|
1128
|
+
y: Number(marker.top || 0),
|
|
1129
|
+
},
|
|
1012
1130
|
geometry,
|
|
1013
|
-
limit,
|
|
1014
1131
|
feature,
|
|
1015
1132
|
);
|
|
1016
1133
|
marker.set({ left: snapped.x, top: snapped.y });
|
|
1017
1134
|
marker.setCoords();
|
|
1135
|
+
this.syncMarkerVisualsByTarget(marker, snapped);
|
|
1018
1136
|
});
|
|
1019
|
-
|
|
1137
|
+
|
|
1138
|
+
this.canvasService.canvas.requestRenderAll();
|
|
1020
1139
|
}
|
|
1021
1140
|
}
|