@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
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FeatureTool = void 0;
|
|
4
4
|
const core_1 = require("@pooder/core");
|
|
5
|
-
const fabric_1 = require("fabric");
|
|
6
5
|
const geometry_1 = require("./geometry");
|
|
7
6
|
const constraints_1 = require("./constraints");
|
|
8
7
|
const featureComplete_1 = require("./featureComplete");
|
|
9
8
|
const sceneLayoutModel_1 = require("./sceneLayoutModel");
|
|
9
|
+
const FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
|
|
10
|
+
const FEATURE_STROKE_WIDTH = 2;
|
|
11
|
+
const DEFAULT_RECT_SIZE = 10;
|
|
12
|
+
const DEFAULT_CIRCLE_RADIUS = 5;
|
|
10
13
|
class FeatureTool {
|
|
11
14
|
constructor(options) {
|
|
12
15
|
this.id = "pooder.kit.feature";
|
|
@@ -19,6 +22,8 @@ class FeatureTool {
|
|
|
19
22
|
this.isFeatureSessionActive = false;
|
|
20
23
|
this.sessionOriginalFeatures = null;
|
|
21
24
|
this.hasWorkingChanges = false;
|
|
25
|
+
this.specs = [];
|
|
26
|
+
this.renderSeq = 0;
|
|
22
27
|
this.handleMoving = null;
|
|
23
28
|
this.handleModified = null;
|
|
24
29
|
this.handleSceneGeometryChange = null;
|
|
@@ -41,6 +46,17 @@ class FeatureTool {
|
|
|
41
46
|
console.warn("CanvasService not found for FeatureTool");
|
|
42
47
|
return;
|
|
43
48
|
}
|
|
49
|
+
this.renderProducerDisposable?.dispose();
|
|
50
|
+
this.renderProducerDisposable = this.canvasService.registerRenderProducer(this.id, () => ({
|
|
51
|
+
passes: [
|
|
52
|
+
{
|
|
53
|
+
id: FEATURE_OVERLAY_LAYER_ID,
|
|
54
|
+
stack: 880,
|
|
55
|
+
order: 0,
|
|
56
|
+
objects: this.specs,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}), { priority: 350 });
|
|
44
60
|
const configService = context.services.get("ConfigurationService");
|
|
45
61
|
if (configService) {
|
|
46
62
|
const features = (configService.get("dieline.features", []) ||
|
|
@@ -63,7 +79,6 @@ class FeatureTool {
|
|
|
63
79
|
}
|
|
64
80
|
const toolSessionService = context.services.get("ToolSessionService");
|
|
65
81
|
this.dirtyTrackerDisposable = toolSessionService?.registerDirtyTracker(this.id, () => this.hasWorkingChanges);
|
|
66
|
-
// Listen to tool activation
|
|
67
82
|
context.eventBus.on("tool:activated", this.onToolActivated);
|
|
68
83
|
this.setup();
|
|
69
84
|
}
|
|
@@ -77,23 +92,7 @@ class FeatureTool {
|
|
|
77
92
|
this.context = undefined;
|
|
78
93
|
}
|
|
79
94
|
updateVisibility() {
|
|
80
|
-
|
|
81
|
-
return;
|
|
82
|
-
const canvas = this.canvasService.canvas;
|
|
83
|
-
const markers = canvas
|
|
84
|
-
.getObjects()
|
|
85
|
-
.filter((obj) => obj.data?.type === "feature-marker");
|
|
86
|
-
markers.forEach((marker) => {
|
|
87
|
-
// If tool active, allow selection. If not, disable selection.
|
|
88
|
-
// Also might want to hide them entirely or just disable interaction.
|
|
89
|
-
// Assuming we only want to see/edit holes when tool is active.
|
|
90
|
-
marker.set({
|
|
91
|
-
visible: this.isToolActive, // Or just selectable: false if we want them visible but locked
|
|
92
|
-
selectable: this.isToolActive,
|
|
93
|
-
evented: this.isToolActive,
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
canvas.requestRenderAll();
|
|
95
|
+
this.redraw();
|
|
97
96
|
}
|
|
98
97
|
contribute() {
|
|
99
98
|
return {
|
|
@@ -192,10 +191,10 @@ class FeatureTool {
|
|
|
192
191
|
await this.refreshGeometry();
|
|
193
192
|
this.setWorkingFeatures(original);
|
|
194
193
|
this.hasWorkingChanges = false;
|
|
194
|
+
this.clearFeatureSessionState();
|
|
195
195
|
this.redraw();
|
|
196
196
|
this.emitWorkingChange();
|
|
197
197
|
this.updateCommittedFeatures(original);
|
|
198
|
-
this.clearFeatureSessionState();
|
|
199
198
|
return { ok: true };
|
|
200
199
|
},
|
|
201
200
|
},
|
|
@@ -323,8 +322,7 @@ class FeatureTool {
|
|
|
323
322
|
return { ok: true };
|
|
324
323
|
this.setWorkingFeatures(next);
|
|
325
324
|
this.hasWorkingChanges = true;
|
|
326
|
-
this.redraw();
|
|
327
|
-
this.enforceConstraints();
|
|
325
|
+
this.redraw({ enforceConstraints: true });
|
|
328
326
|
this.emitWorkingChange();
|
|
329
327
|
return { ok: true };
|
|
330
328
|
}
|
|
@@ -354,25 +352,22 @@ class FeatureTool {
|
|
|
354
352
|
}
|
|
355
353
|
this.hasWorkingChanges = false;
|
|
356
354
|
this.clearFeatureSessionState();
|
|
357
|
-
// Keep feature markers above dieline overlay after config-driven redraw.
|
|
358
355
|
this.redraw();
|
|
359
356
|
return { ok: true };
|
|
360
357
|
}
|
|
361
358
|
addFeature(type) {
|
|
362
359
|
if (!this.canvasService)
|
|
363
360
|
return false;
|
|
364
|
-
// Default to top edge center
|
|
365
361
|
const newFeature = {
|
|
366
362
|
id: Date.now().toString(),
|
|
367
363
|
operation: type,
|
|
368
364
|
shape: "rect",
|
|
369
365
|
x: 0.5,
|
|
370
|
-
y: 0,
|
|
366
|
+
y: 0,
|
|
371
367
|
width: 10,
|
|
372
368
|
height: 10,
|
|
373
369
|
rotation: 0,
|
|
374
370
|
renderBehavior: "edge",
|
|
375
|
-
// Default constraint: path (snap to edge)
|
|
376
371
|
constraints: [{ type: "path" }],
|
|
377
372
|
};
|
|
378
373
|
this.setWorkingFeatures([...(this.workingFeatures || []), newFeature]);
|
|
@@ -386,7 +381,6 @@ class FeatureTool {
|
|
|
386
381
|
return false;
|
|
387
382
|
const groupId = Date.now().toString();
|
|
388
383
|
const timestamp = Date.now();
|
|
389
|
-
// 1. Lug (Outer) - Add
|
|
390
384
|
const lug = {
|
|
391
385
|
id: `${timestamp}-lug`,
|
|
392
386
|
groupId,
|
|
@@ -399,7 +393,6 @@ class FeatureTool {
|
|
|
399
393
|
renderBehavior: "edge",
|
|
400
394
|
constraints: [{ type: "path" }],
|
|
401
395
|
};
|
|
402
|
-
// 2. Hole (Inner) - Subtract
|
|
403
396
|
const hole = {
|
|
404
397
|
id: `${timestamp}-hole`,
|
|
405
398
|
groupId,
|
|
@@ -418,25 +411,20 @@ class FeatureTool {
|
|
|
418
411
|
this.emitWorkingChange();
|
|
419
412
|
return true;
|
|
420
413
|
}
|
|
421
|
-
getGeometryForFeature(geometry,
|
|
422
|
-
// Legacy support or specialized scaling can go here if needed
|
|
423
|
-
// Currently all features operate on the base geometry (or scaled version of it)
|
|
414
|
+
getGeometryForFeature(geometry, _feature) {
|
|
424
415
|
return geometry;
|
|
425
416
|
}
|
|
426
417
|
setup() {
|
|
427
418
|
if (!this.canvasService || !this.context)
|
|
428
419
|
return;
|
|
429
420
|
const canvas = this.canvasService.canvas;
|
|
430
|
-
// 1. Listen for Scene Geometry Changes
|
|
431
421
|
if (!this.handleSceneGeometryChange) {
|
|
432
422
|
this.handleSceneGeometryChange = (geometry) => {
|
|
433
423
|
this.currentGeometry = geometry;
|
|
434
|
-
this.redraw();
|
|
435
|
-
this.enforceConstraints();
|
|
424
|
+
this.redraw({ enforceConstraints: true });
|
|
436
425
|
};
|
|
437
426
|
this.context.eventBus.on("scene:geometry:change", this.handleSceneGeometryChange);
|
|
438
427
|
}
|
|
439
|
-
// 2. Initial Fetch of Geometry
|
|
440
428
|
const commandService = this.context.services.get("CommandService");
|
|
441
429
|
if (commandService) {
|
|
442
430
|
try {
|
|
@@ -449,98 +437,35 @@ class FeatureTool {
|
|
|
449
437
|
}
|
|
450
438
|
catch (e) { }
|
|
451
439
|
}
|
|
452
|
-
// 3. Setup Canvas Interaction
|
|
453
440
|
if (!this.handleMoving) {
|
|
454
441
|
this.handleMoving = (e) => {
|
|
455
|
-
const target = e
|
|
456
|
-
if (!target ||
|
|
442
|
+
const target = this.getDraggableMarkerTarget(e?.target);
|
|
443
|
+
if (!target || !this.currentGeometry)
|
|
457
444
|
return;
|
|
458
|
-
|
|
459
|
-
return;
|
|
460
|
-
// Determine feature to use for snapping context
|
|
461
|
-
let feature;
|
|
462
|
-
if (target.data?.isGroup) {
|
|
463
|
-
const indices = target.data?.indices;
|
|
464
|
-
if (indices && indices.length > 0) {
|
|
465
|
-
feature = this.workingFeatures[indices[0]];
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
const index = target.data?.index;
|
|
470
|
-
if (index !== undefined) {
|
|
471
|
-
feature = this.workingFeatures[index];
|
|
472
|
-
}
|
|
473
|
-
}
|
|
445
|
+
const feature = this.getFeatureForMarker(target);
|
|
474
446
|
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
// Calculate limit based on target size (min dimension / 2 ensures overlap)
|
|
480
|
-
// Also subtract stroke width to ensure visual overlap (not just tangent)
|
|
481
|
-
// target.strokeWidth for group is usually 0, need a safe default (e.g. 2 for markers)
|
|
482
|
-
const markerStrokeWidth = (target.strokeWidth || 2) * (target.scaleX || 1);
|
|
483
|
-
const minDim = Math.min(target.getScaledWidth(), target.getScaledHeight());
|
|
484
|
-
const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
|
|
485
|
-
const snapped = this.constrainPosition(p, geometry, limit, feature);
|
|
447
|
+
const snapped = this.constrainPosition({
|
|
448
|
+
x: Number(target.left || 0),
|
|
449
|
+
y: Number(target.top || 0),
|
|
450
|
+
}, geometry, feature);
|
|
486
451
|
target.set({
|
|
487
452
|
left: snapped.x,
|
|
488
453
|
top: snapped.y,
|
|
489
454
|
});
|
|
455
|
+
target.setCoords();
|
|
456
|
+
this.syncMarkerVisualsByTarget(target, snapped);
|
|
490
457
|
};
|
|
491
458
|
canvas.on("object:moving", this.handleMoving);
|
|
492
459
|
}
|
|
493
460
|
if (!this.handleModified) {
|
|
494
461
|
this.handleModified = (e) => {
|
|
495
|
-
const target = e
|
|
496
|
-
if (!target
|
|
462
|
+
const target = this.getDraggableMarkerTarget(e?.target);
|
|
463
|
+
if (!target)
|
|
497
464
|
return;
|
|
498
465
|
if (target.data?.isGroup) {
|
|
499
|
-
|
|
500
|
-
const groupObj = target;
|
|
501
|
-
// @ts-ignore
|
|
502
|
-
const indices = groupObj.data?.indices;
|
|
503
|
-
if (!indices)
|
|
504
|
-
return;
|
|
505
|
-
// We need to update all features in the group based on their new absolute positions.
|
|
506
|
-
// Fabric Group children positions are relative to group center.
|
|
507
|
-
// We need to calculate absolute position for each child.
|
|
508
|
-
// Note: groupObj has already been moved to new position (target.left, target.top)
|
|
509
|
-
const groupCenter = new fabric_1.Point(groupObj.left, groupObj.top);
|
|
510
|
-
// Get group matrix to transform children
|
|
511
|
-
// Simplified: just add relative coordinates if no rotation/scaling on group
|
|
512
|
-
// We locked rotation/scaling, so it's safe.
|
|
513
|
-
const newFeatures = [...this.workingFeatures];
|
|
514
|
-
const { x, y } = this.currentGeometry; // Center is same
|
|
515
|
-
// Fabric Group objects have .getObjects() which returns children
|
|
516
|
-
// But children inside group have coordinates relative to group center.
|
|
517
|
-
// center is (0,0) inside the group local coordinate system.
|
|
518
|
-
groupObj.getObjects().forEach((child, i) => {
|
|
519
|
-
const originalIndex = indices[i];
|
|
520
|
-
const feature = this.workingFeatures[originalIndex];
|
|
521
|
-
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
522
|
-
const { width, height } = geometry;
|
|
523
|
-
const layoutLeft = x - width / 2;
|
|
524
|
-
const layoutTop = y - height / 2;
|
|
525
|
-
// Calculate absolute position
|
|
526
|
-
// child.left/top are relative to group center
|
|
527
|
-
const absX = groupCenter.x + (child.left || 0);
|
|
528
|
-
const absY = groupCenter.y + (child.top || 0);
|
|
529
|
-
// Normalize
|
|
530
|
-
const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
|
|
531
|
-
const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
|
|
532
|
-
newFeatures[originalIndex] = {
|
|
533
|
-
...newFeatures[originalIndex],
|
|
534
|
-
x: normalizedX,
|
|
535
|
-
y: normalizedY,
|
|
536
|
-
};
|
|
537
|
-
});
|
|
538
|
-
this.setWorkingFeatures(newFeatures);
|
|
539
|
-
this.hasWorkingChanges = true;
|
|
540
|
-
this.emitWorkingChange();
|
|
466
|
+
this.syncGroupFromCanvas(target);
|
|
541
467
|
}
|
|
542
468
|
else {
|
|
543
|
-
// Single object
|
|
544
469
|
this.syncFeatureFromCanvas(target);
|
|
545
470
|
}
|
|
546
471
|
};
|
|
@@ -563,263 +488,423 @@ class FeatureTool {
|
|
|
563
488
|
this.context.eventBus.off("scene:geometry:change", this.handleSceneGeometryChange);
|
|
564
489
|
this.handleSceneGeometryChange = null;
|
|
565
490
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
this.canvasService.
|
|
491
|
+
this.renderSeq += 1;
|
|
492
|
+
this.specs = [];
|
|
493
|
+
this.renderProducerDisposable?.dispose();
|
|
494
|
+
this.renderProducerDisposable = undefined;
|
|
495
|
+
void this.canvasService.flushRenderFromProducers();
|
|
496
|
+
}
|
|
497
|
+
getDraggableMarkerTarget(target) {
|
|
498
|
+
if (!this.isFeatureSessionActive || !this.isToolActive)
|
|
499
|
+
return null;
|
|
500
|
+
if (!target || target.data?.type !== "feature-marker")
|
|
501
|
+
return null;
|
|
502
|
+
if (target.data?.markerRole !== "handle")
|
|
503
|
+
return null;
|
|
504
|
+
return target;
|
|
571
505
|
}
|
|
572
|
-
|
|
506
|
+
getFeatureForMarker(target) {
|
|
507
|
+
const data = target?.data || {};
|
|
508
|
+
const index = data.isGroup
|
|
509
|
+
? this.toFeatureIndex(data.anchorIndex)
|
|
510
|
+
: this.toFeatureIndex(data.index);
|
|
511
|
+
if (index === null)
|
|
512
|
+
return undefined;
|
|
513
|
+
return this.workingFeatures[index];
|
|
514
|
+
}
|
|
515
|
+
constrainPosition(p, geometry, feature) {
|
|
573
516
|
if (!feature) {
|
|
574
517
|
return { x: p.x, y: p.y };
|
|
575
518
|
}
|
|
576
519
|
const minX = geometry.x - geometry.width / 2;
|
|
577
520
|
const minY = geometry.y - geometry.height / 2;
|
|
578
|
-
// Normalize
|
|
579
521
|
const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
|
|
580
522
|
const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
|
|
581
523
|
const scale = geometry.scale || 1;
|
|
582
524
|
const dielineWidth = geometry.width / scale;
|
|
583
525
|
const dielineHeight = geometry.height / scale;
|
|
584
|
-
// Filter constraints: only apply those that are NOT validateOnly
|
|
585
526
|
const activeConstraints = feature.constraints?.filter((c) => !c.validateOnly);
|
|
586
527
|
const constrained = constraints_1.ConstraintRegistry.apply(nx, ny, feature, {
|
|
587
528
|
dielineWidth,
|
|
588
529
|
dielineHeight,
|
|
589
530
|
geometry,
|
|
590
531
|
}, activeConstraints);
|
|
591
|
-
// Denormalize
|
|
592
532
|
return {
|
|
593
533
|
x: minX + constrained.x * geometry.width,
|
|
594
534
|
y: minY + constrained.y * geometry.height,
|
|
595
535
|
};
|
|
596
536
|
}
|
|
537
|
+
toNormalizedPoint(point, geometry) {
|
|
538
|
+
const left = geometry.x - geometry.width / 2;
|
|
539
|
+
const top = geometry.y - geometry.height / 2;
|
|
540
|
+
return {
|
|
541
|
+
x: geometry.width > 0 ? (point.x - left) / geometry.width : 0.5,
|
|
542
|
+
y: geometry.height > 0 ? (point.y - top) / geometry.height : 0.5,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
597
545
|
syncFeatureFromCanvas(target) {
|
|
598
|
-
if (!this.currentGeometry
|
|
546
|
+
if (!this.currentGeometry)
|
|
599
547
|
return;
|
|
600
|
-
const index = target.data?.index;
|
|
601
|
-
if (index ===
|
|
602
|
-
index < 0 ||
|
|
603
|
-
index >= this.workingFeatures.length)
|
|
548
|
+
const index = this.toFeatureIndex(target.data?.index);
|
|
549
|
+
if (index === null || index >= this.workingFeatures.length)
|
|
604
550
|
return;
|
|
605
551
|
const feature = this.workingFeatures[index];
|
|
606
552
|
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const top = y - height / 2;
|
|
612
|
-
const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
|
|
613
|
-
const normalizedY = height > 0 ? (target.top - top) / height : 0.5;
|
|
614
|
-
// Update feature
|
|
553
|
+
const normalized = this.toNormalizedPoint({
|
|
554
|
+
x: Number(target.left || 0),
|
|
555
|
+
y: Number(target.top || 0),
|
|
556
|
+
}, geometry);
|
|
615
557
|
const updatedFeature = {
|
|
616
558
|
...feature,
|
|
617
|
-
x:
|
|
618
|
-
y:
|
|
619
|
-
// Could also update rotation if we allowed rotating markers
|
|
559
|
+
x: normalized.x,
|
|
560
|
+
y: normalized.y,
|
|
620
561
|
};
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
this.setWorkingFeatures(
|
|
562
|
+
const next = [...this.workingFeatures];
|
|
563
|
+
next[index] = updatedFeature;
|
|
564
|
+
this.setWorkingFeatures(next);
|
|
624
565
|
this.hasWorkingChanges = true;
|
|
625
566
|
this.emitWorkingChange();
|
|
626
567
|
}
|
|
627
|
-
|
|
628
|
-
if (!this.
|
|
568
|
+
syncGroupFromCanvas(target) {
|
|
569
|
+
if (!this.currentGeometry)
|
|
629
570
|
return;
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
// Remove existing markers
|
|
633
|
-
const existing = canvas
|
|
634
|
-
.getObjects()
|
|
635
|
-
.filter((obj) => obj.data?.type === "feature-marker");
|
|
636
|
-
existing.forEach((obj) => canvas.remove(obj));
|
|
637
|
-
if (!this.workingFeatures || this.workingFeatures.length === 0) {
|
|
638
|
-
this.canvasService.requestRenderAll();
|
|
571
|
+
const indices = this.readGroupIndices(target.data?.indices);
|
|
572
|
+
if (indices.length === 0)
|
|
639
573
|
return;
|
|
574
|
+
const offsets = this.readGroupMemberOffsets(target.data?.memberOffsets, indices);
|
|
575
|
+
const anchorCenter = {
|
|
576
|
+
x: Number(target.left || 0),
|
|
577
|
+
y: Number(target.top || 0),
|
|
578
|
+
};
|
|
579
|
+
const next = [...this.workingFeatures];
|
|
580
|
+
let changed = false;
|
|
581
|
+
offsets.forEach((entry) => {
|
|
582
|
+
const index = entry.index;
|
|
583
|
+
if (index < 0 || index >= next.length)
|
|
584
|
+
return;
|
|
585
|
+
const feature = next[index];
|
|
586
|
+
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
587
|
+
const normalized = this.toNormalizedPoint({
|
|
588
|
+
x: anchorCenter.x + entry.dx,
|
|
589
|
+
y: anchorCenter.y + entry.dy,
|
|
590
|
+
}, geometry);
|
|
591
|
+
if (feature.x !== normalized.x || feature.y !== normalized.y) {
|
|
592
|
+
next[index] = {
|
|
593
|
+
...feature,
|
|
594
|
+
x: normalized.x,
|
|
595
|
+
y: normalized.y,
|
|
596
|
+
};
|
|
597
|
+
changed = true;
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
if (!changed)
|
|
601
|
+
return;
|
|
602
|
+
this.setWorkingFeatures(next);
|
|
603
|
+
this.hasWorkingChanges = true;
|
|
604
|
+
this.emitWorkingChange();
|
|
605
|
+
}
|
|
606
|
+
redraw(options = {}) {
|
|
607
|
+
void this.redrawAsync(options);
|
|
608
|
+
}
|
|
609
|
+
async redrawAsync(options = {}) {
|
|
610
|
+
if (!this.canvasService)
|
|
611
|
+
return;
|
|
612
|
+
const seq = ++this.renderSeq;
|
|
613
|
+
this.specs = this.buildFeatureSpecs();
|
|
614
|
+
if (seq !== this.renderSeq)
|
|
615
|
+
return;
|
|
616
|
+
await this.canvasService.flushRenderFromProducers();
|
|
617
|
+
if (seq !== this.renderSeq)
|
|
618
|
+
return;
|
|
619
|
+
if (options.enforceConstraints) {
|
|
620
|
+
this.enforceConstraints();
|
|
640
621
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
622
|
+
}
|
|
623
|
+
buildFeatureSpecs() {
|
|
624
|
+
if (!this.isFeatureSessionActive ||
|
|
625
|
+
!this.currentGeometry ||
|
|
626
|
+
this.workingFeatures.length === 0) {
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
const groups = new Map();
|
|
645
630
|
const singles = [];
|
|
646
|
-
this.workingFeatures.forEach((
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
631
|
+
this.workingFeatures.forEach((feature, index) => {
|
|
632
|
+
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
633
|
+
const position = (0, geometry_1.resolveFeaturePosition)(feature, geometry);
|
|
634
|
+
const scale = geometry.scale || 1;
|
|
635
|
+
const marker = {
|
|
636
|
+
feature,
|
|
637
|
+
index,
|
|
638
|
+
position,
|
|
639
|
+
geometry,
|
|
640
|
+
scale,
|
|
641
|
+
};
|
|
642
|
+
if (feature.groupId) {
|
|
643
|
+
const list = groups.get(feature.groupId) || [];
|
|
644
|
+
list.push(marker);
|
|
645
|
+
groups.set(feature.groupId, list);
|
|
646
|
+
return;
|
|
654
647
|
}
|
|
648
|
+
singles.push(marker);
|
|
655
649
|
});
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
650
|
+
const specs = [];
|
|
651
|
+
singles.forEach((marker) => {
|
|
652
|
+
this.appendMarkerSpecs(specs, marker, {
|
|
653
|
+
markerRole: "handle",
|
|
654
|
+
isGroup: false,
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
groups.forEach((members, groupId) => {
|
|
658
|
+
if (!members.length)
|
|
659
|
+
return;
|
|
660
|
+
const anchor = members[0];
|
|
661
|
+
const memberOffsets = members.map((member) => ({
|
|
662
|
+
index: member.index,
|
|
663
|
+
dx: member.position.x - anchor.position.x,
|
|
664
|
+
dy: member.position.y - anchor.position.y,
|
|
665
|
+
}));
|
|
666
|
+
const indices = members.map((member) => member.index);
|
|
667
|
+
members
|
|
668
|
+
.filter((member) => member.index !== anchor.index)
|
|
669
|
+
.forEach((member) => {
|
|
670
|
+
this.appendMarkerSpecs(specs, member, {
|
|
671
|
+
markerRole: "member",
|
|
672
|
+
isGroup: false,
|
|
673
|
+
groupId,
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
this.appendMarkerSpecs(specs, anchor, {
|
|
677
|
+
markerRole: "handle",
|
|
678
|
+
isGroup: true,
|
|
679
|
+
groupId,
|
|
680
|
+
indices,
|
|
681
|
+
anchorIndex: anchor.index,
|
|
682
|
+
memberOffsets,
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
return specs;
|
|
686
|
+
}
|
|
687
|
+
appendMarkerSpecs(specs, marker, options) {
|
|
688
|
+
const { feature, index, position, scale, geometry } = marker;
|
|
689
|
+
const baseRadius = feature.shape === "circle"
|
|
690
|
+
? (feature.radius ?? DEFAULT_CIRCLE_RADIUS)
|
|
691
|
+
: (feature.radius ?? 0);
|
|
692
|
+
const baseWidth = feature.shape === "circle"
|
|
693
|
+
? baseRadius * 2
|
|
694
|
+
: (feature.width ?? DEFAULT_RECT_SIZE);
|
|
695
|
+
const baseHeight = feature.shape === "circle"
|
|
696
|
+
? baseRadius * 2
|
|
697
|
+
: (feature.height ?? DEFAULT_RECT_SIZE);
|
|
698
|
+
const visualWidth = baseWidth * scale;
|
|
699
|
+
const visualHeight = baseHeight * scale;
|
|
700
|
+
const visualRadius = baseRadius * scale;
|
|
701
|
+
const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
|
|
702
|
+
const strokeDash = feature.strokeDash ||
|
|
703
|
+
(feature.operation === "subtract" ? [4, 4] : undefined);
|
|
704
|
+
const interactive = options.markerRole === "handle";
|
|
705
|
+
const sessionVisible = this.isToolActive && this.isFeatureSessionActive;
|
|
706
|
+
const baseData = this.buildMarkerData(marker, options);
|
|
707
|
+
const commonProps = {
|
|
708
|
+
visible: sessionVisible,
|
|
709
|
+
selectable: interactive && sessionVisible,
|
|
710
|
+
evented: interactive && sessionVisible,
|
|
711
|
+
hasControls: false,
|
|
712
|
+
hasBorders: false,
|
|
713
|
+
hoverCursor: interactive ? "move" : "default",
|
|
714
|
+
lockRotation: true,
|
|
715
|
+
lockScalingX: true,
|
|
716
|
+
lockScalingY: true,
|
|
717
|
+
fill: "transparent",
|
|
718
|
+
stroke: color,
|
|
719
|
+
strokeWidth: FEATURE_STROKE_WIDTH,
|
|
720
|
+
strokeDashArray: strokeDash,
|
|
721
|
+
originX: "center",
|
|
722
|
+
originY: "center",
|
|
723
|
+
left: position.x,
|
|
724
|
+
top: position.y,
|
|
725
|
+
angle: feature.rotation || 0,
|
|
726
|
+
};
|
|
727
|
+
const markerId = this.markerId(index);
|
|
728
|
+
if (feature.shape === "rect") {
|
|
729
|
+
specs.push({
|
|
730
|
+
id: markerId,
|
|
731
|
+
type: "rect",
|
|
732
|
+
space: "screen",
|
|
733
|
+
data: baseData,
|
|
734
|
+
props: {
|
|
735
|
+
...commonProps,
|
|
668
736
|
width: visualWidth,
|
|
669
737
|
height: visualHeight,
|
|
670
738
|
rx: visualRadius,
|
|
671
739
|
ry: visualRadius,
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
shape.rotate(feature.rotation);
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
specs.push({
|
|
745
|
+
id: markerId,
|
|
746
|
+
type: "rect",
|
|
747
|
+
space: "screen",
|
|
748
|
+
data: baseData,
|
|
749
|
+
props: {
|
|
750
|
+
...commonProps,
|
|
751
|
+
width: visualWidth,
|
|
752
|
+
height: visualHeight,
|
|
753
|
+
rx: visualRadius,
|
|
754
|
+
ry: visualRadius,
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
if (feature.bridge?.type === "vertical") {
|
|
759
|
+
const featureTopY = position.y - visualHeight / 2;
|
|
760
|
+
const dielineTopY = geometry.y - geometry.height / 2;
|
|
761
|
+
const bridgeHeight = Math.max(0, featureTopY - dielineTopY);
|
|
762
|
+
if (bridgeHeight <= 0.001) {
|
|
763
|
+
return;
|
|
697
764
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
765
|
+
specs.push({
|
|
766
|
+
id: this.bridgeIndicatorId(index),
|
|
767
|
+
type: "rect",
|
|
768
|
+
space: "screen",
|
|
769
|
+
data: {
|
|
770
|
+
...baseData,
|
|
771
|
+
markerRole: "indicator",
|
|
772
|
+
markerOffsetX: 0,
|
|
773
|
+
markerOffsetY: -visualHeight / 2,
|
|
774
|
+
},
|
|
775
|
+
props: {
|
|
776
|
+
visible: sessionVisible,
|
|
777
|
+
selectable: false,
|
|
778
|
+
evented: false,
|
|
703
779
|
width: visualWidth,
|
|
704
|
-
height:
|
|
780
|
+
height: bridgeHeight,
|
|
705
781
|
fill: "transparent",
|
|
706
782
|
stroke: "#888",
|
|
707
783
|
strokeWidth: 1,
|
|
708
784
|
strokeDashArray: [2, 2],
|
|
709
|
-
originX: "center",
|
|
710
|
-
originY: "bottom", // Anchor at bottom so it extends up
|
|
711
|
-
left: pos.x,
|
|
712
|
-
top: pos.y - visualHeight / 2, // Start from top of feature
|
|
713
785
|
opacity: 0.5,
|
|
714
|
-
selectable: false,
|
|
715
|
-
evented: false,
|
|
716
|
-
});
|
|
717
|
-
// We need to return a group containing both shape and indicator
|
|
718
|
-
// But createMarkerShape is expected to return one object.
|
|
719
|
-
// If we return a Group, Fabric handles it.
|
|
720
|
-
// But the caller might wrap this in another Group if it's part of a feature group.
|
|
721
|
-
// Fabric supports nested groups.
|
|
722
|
-
const group = new fabric_1.Group([bridgeIndicator, shape], {
|
|
723
786
|
originX: "center",
|
|
724
|
-
originY: "
|
|
725
|
-
left:
|
|
726
|
-
top:
|
|
727
|
-
}
|
|
728
|
-
return group;
|
|
729
|
-
}
|
|
730
|
-
return shape;
|
|
731
|
-
};
|
|
732
|
-
// Render Singles
|
|
733
|
-
singles.forEach(({ feature, index }) => {
|
|
734
|
-
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
735
|
-
const pos = (0, geometry_1.resolveFeaturePosition)(feature, geometry);
|
|
736
|
-
const marker = createMarkerShape(feature, pos);
|
|
737
|
-
marker.set({
|
|
738
|
-
visible: this.isToolActive,
|
|
739
|
-
selectable: this.isToolActive,
|
|
740
|
-
evented: this.isToolActive,
|
|
741
|
-
hasControls: false,
|
|
742
|
-
hasBorders: false,
|
|
743
|
-
hoverCursor: "move",
|
|
744
|
-
lockRotation: true,
|
|
745
|
-
lockScalingX: true,
|
|
746
|
-
lockScalingY: true,
|
|
747
|
-
data: { type: "feature-marker", index, isGroup: false },
|
|
787
|
+
originY: "bottom",
|
|
788
|
+
left: position.x,
|
|
789
|
+
top: position.y - visualHeight / 2,
|
|
790
|
+
},
|
|
748
791
|
});
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
buildMarkerData(marker, options) {
|
|
795
|
+
const data = {
|
|
796
|
+
type: "feature-marker",
|
|
797
|
+
index: marker.index,
|
|
798
|
+
featureId: marker.feature.id,
|
|
799
|
+
markerRole: options.markerRole,
|
|
800
|
+
markerOffsetX: 0,
|
|
801
|
+
markerOffsetY: 0,
|
|
802
|
+
isGroup: options.isGroup,
|
|
803
|
+
};
|
|
804
|
+
if (options.groupId)
|
|
805
|
+
data.groupId = options.groupId;
|
|
806
|
+
if (options.indices)
|
|
807
|
+
data.indices = options.indices;
|
|
808
|
+
if (options.anchorIndex !== undefined)
|
|
809
|
+
data.anchorIndex = options.anchorIndex;
|
|
810
|
+
if (options.memberOffsets)
|
|
811
|
+
data.memberOffsets = options.memberOffsets;
|
|
812
|
+
return data;
|
|
813
|
+
}
|
|
814
|
+
markerId(index) {
|
|
815
|
+
return `feature.marker.${index}`;
|
|
816
|
+
}
|
|
817
|
+
bridgeIndicatorId(index) {
|
|
818
|
+
return `feature.marker.${index}.bridge`;
|
|
819
|
+
}
|
|
820
|
+
toFeatureIndex(value) {
|
|
821
|
+
const numeric = Number(value);
|
|
822
|
+
if (!Number.isInteger(numeric) || numeric < 0)
|
|
823
|
+
return null;
|
|
824
|
+
return numeric;
|
|
825
|
+
}
|
|
826
|
+
readGroupIndices(raw) {
|
|
827
|
+
if (!Array.isArray(raw))
|
|
828
|
+
return [];
|
|
829
|
+
return raw
|
|
830
|
+
.map((value) => this.toFeatureIndex(value))
|
|
831
|
+
.filter((value) => value !== null);
|
|
832
|
+
}
|
|
833
|
+
readGroupMemberOffsets(raw, fallbackIndices = []) {
|
|
834
|
+
if (Array.isArray(raw)) {
|
|
835
|
+
const parsed = raw
|
|
836
|
+
.map((entry) => {
|
|
837
|
+
const index = this.toFeatureIndex(entry?.index);
|
|
838
|
+
const dx = Number(entry?.dx);
|
|
839
|
+
const dy = Number(entry?.dy);
|
|
840
|
+
if (index === null || !Number.isFinite(dx) || !Number.isFinite(dy)) {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
return { index, dx, dy };
|
|
844
|
+
})
|
|
845
|
+
.filter((value) => !!value);
|
|
846
|
+
if (parsed.length > 0)
|
|
847
|
+
return parsed;
|
|
848
|
+
}
|
|
849
|
+
return fallbackIndices.map((index) => ({ index, dx: 0, dy: 0 }));
|
|
850
|
+
}
|
|
851
|
+
syncMarkerVisualsByTarget(target, center) {
|
|
852
|
+
if (target.data?.isGroup) {
|
|
853
|
+
const indices = this.readGroupIndices(target.data?.indices);
|
|
854
|
+
const offsets = this.readGroupMemberOffsets(target.data?.memberOffsets, indices);
|
|
855
|
+
offsets.forEach((entry) => {
|
|
856
|
+
this.syncMarkerVisualObjectsToCenter(entry.index, {
|
|
857
|
+
x: center.x + entry.dx,
|
|
858
|
+
y: center.y + entry.dy,
|
|
859
|
+
});
|
|
765
860
|
});
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
861
|
+
this.canvasService?.requestRenderAll();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const index = this.toFeatureIndex(target.data?.index);
|
|
865
|
+
if (index === null)
|
|
866
|
+
return;
|
|
867
|
+
this.syncMarkerVisualObjectsToCenter(index, center);
|
|
868
|
+
this.canvasService?.requestRenderAll();
|
|
869
|
+
}
|
|
870
|
+
syncMarkerVisualObjectsToCenter(index, center) {
|
|
871
|
+
if (!this.canvasService)
|
|
872
|
+
return;
|
|
873
|
+
const markers = this.canvasService.canvas
|
|
874
|
+
.getObjects()
|
|
875
|
+
.filter((obj) => obj?.data?.type === "feature-marker" &&
|
|
876
|
+
this.toFeatureIndex(obj?.data?.index) === index);
|
|
877
|
+
markers.forEach((marker) => {
|
|
878
|
+
const offsetX = Number(marker?.data?.markerOffsetX || 0);
|
|
879
|
+
const offsetY = Number(marker?.data?.markerOffsetY || 0);
|
|
880
|
+
marker.set({
|
|
881
|
+
left: center.x + offsetX,
|
|
882
|
+
top: center.y + offsetY,
|
|
785
883
|
});
|
|
786
|
-
|
|
787
|
-
canvas.bringObjectToFront(groupObj);
|
|
884
|
+
marker.setCoords();
|
|
788
885
|
});
|
|
789
|
-
this.canvasService.requestRenderAll();
|
|
790
886
|
}
|
|
791
887
|
enforceConstraints() {
|
|
792
888
|
if (!this.canvasService || !this.currentGeometry)
|
|
793
889
|
return;
|
|
794
|
-
|
|
795
|
-
const canvas = this.canvasService.canvas;
|
|
796
|
-
const markers = canvas
|
|
890
|
+
const handles = this.canvasService.canvas
|
|
797
891
|
.getObjects()
|
|
798
|
-
.filter((obj) => obj
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (
|
|
803
|
-
|
|
804
|
-
if (indices && indices.length > 0) {
|
|
805
|
-
feature = this.workingFeatures[indices[0]];
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
else {
|
|
809
|
-
const index = marker.data?.index;
|
|
810
|
-
if (index !== undefined) {
|
|
811
|
-
feature = this.workingFeatures[index];
|
|
812
|
-
}
|
|
813
|
-
}
|
|
892
|
+
.filter((obj) => obj?.data?.type === "feature-marker" &&
|
|
893
|
+
obj?.data?.markerRole === "handle");
|
|
894
|
+
handles.forEach((marker) => {
|
|
895
|
+
const feature = this.getFeatureForMarker(marker);
|
|
896
|
+
if (!feature)
|
|
897
|
+
return;
|
|
814
898
|
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
899
|
+
const snapped = this.constrainPosition({
|
|
900
|
+
x: Number(marker.left || 0),
|
|
901
|
+
y: Number(marker.top || 0),
|
|
902
|
+
}, geometry, feature);
|
|
819
903
|
marker.set({ left: snapped.x, top: snapped.y });
|
|
820
904
|
marker.setCoords();
|
|
905
|
+
this.syncMarkerVisualsByTarget(marker, snapped);
|
|
821
906
|
});
|
|
822
|
-
canvas.requestRenderAll();
|
|
907
|
+
this.canvasService.canvas.requestRenderAll();
|
|
823
908
|
}
|
|
824
909
|
}
|
|
825
910
|
exports.FeatureTool = FeatureTool;
|