@pooder/kit 5.4.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.test-dist/src/coordinate.js +74 -0
  2. package/.test-dist/src/extensions/background.js +547 -0
  3. package/.test-dist/src/extensions/bridgeSelection.js +20 -0
  4. package/.test-dist/src/extensions/constraints.js +237 -0
  5. package/.test-dist/src/extensions/dieline.js +935 -0
  6. package/.test-dist/src/extensions/dielineShape.js +66 -0
  7. package/.test-dist/src/extensions/edgeScale.js +12 -0
  8. package/.test-dist/src/extensions/feature.js +910 -0
  9. package/.test-dist/src/extensions/featureComplete.js +32 -0
  10. package/.test-dist/src/extensions/film.js +226 -0
  11. package/.test-dist/src/extensions/geometry.js +609 -0
  12. package/.test-dist/src/extensions/image.js +1788 -0
  13. package/.test-dist/src/extensions/index.js +28 -0
  14. package/.test-dist/src/extensions/maskOps.js +334 -0
  15. package/.test-dist/src/extensions/mirror.js +104 -0
  16. package/.test-dist/src/extensions/ruler.js +442 -0
  17. package/.test-dist/src/extensions/sceneLayout.js +96 -0
  18. package/.test-dist/src/extensions/sceneLayoutModel.js +202 -0
  19. package/.test-dist/src/extensions/sceneVisibility.js +55 -0
  20. package/.test-dist/src/extensions/size.js +331 -0
  21. package/.test-dist/src/extensions/tracer.js +709 -0
  22. package/.test-dist/src/extensions/white-ink.js +1200 -0
  23. package/.test-dist/src/extensions/wrappedOffsets.js +33 -0
  24. package/.test-dist/src/index.js +18 -0
  25. package/.test-dist/src/services/CanvasService.js +1032 -0
  26. package/.test-dist/src/services/ViewportSystem.js +76 -0
  27. package/.test-dist/src/services/index.js +25 -0
  28. package/.test-dist/src/services/renderSpec.js +2 -0
  29. package/.test-dist/src/services/visibility.js +57 -0
  30. package/.test-dist/src/units.js +30 -0
  31. package/.test-dist/tests/run.js +150 -0
  32. package/CHANGELOG.md +12 -0
  33. package/dist/index.d.mts +164 -62
  34. package/dist/index.d.ts +164 -62
  35. package/dist/index.js +2433 -1719
  36. package/dist/index.mjs +2442 -1723
  37. package/package.json +1 -1
  38. package/src/coordinate.ts +106 -106
  39. package/src/extensions/background.ts +716 -323
  40. package/src/extensions/bridgeSelection.ts +17 -17
  41. package/src/extensions/constraints.ts +322 -322
  42. package/src/extensions/dieline.ts +1173 -1149
  43. package/src/extensions/dielineShape.ts +109 -109
  44. package/src/extensions/edgeScale.ts +19 -19
  45. package/src/extensions/feature.ts +1140 -1137
  46. package/src/extensions/featureComplete.ts +46 -46
  47. package/src/extensions/film.ts +270 -266
  48. package/src/extensions/geometry.ts +851 -885
  49. package/src/extensions/image.ts +2240 -2054
  50. package/src/extensions/index.ts +10 -11
  51. package/src/extensions/maskOps.ts +283 -283
  52. package/src/extensions/mirror.ts +128 -128
  53. package/src/extensions/ruler.ts +664 -654
  54. package/src/extensions/sceneLayout.ts +140 -140
  55. package/src/extensions/sceneLayoutModel.ts +364 -364
  56. package/src/extensions/size.ts +389 -389
  57. package/src/extensions/tracer.ts +1019 -1019
  58. package/src/extensions/white-ink.ts +1508 -1575
  59. package/src/extensions/wrappedOffsets.ts +33 -33
  60. package/src/index.ts +2 -2
  61. package/src/services/CanvasService.ts +1317 -832
  62. package/src/services/ViewportSystem.ts +95 -95
  63. package/src/services/index.ts +4 -3
  64. package/src/services/renderSpec.ts +85 -53
  65. package/src/services/visibility.ts +83 -0
  66. package/src/units.ts +27 -27
  67. package/tests/run.ts +258 -118
  68. package/tsconfig.test.json +15 -15
  69. package/src/extensions/sceneVisibility.ts +0 -64
@@ -0,0 +1,910 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeatureTool = void 0;
4
+ const core_1 = require("@pooder/core");
5
+ const geometry_1 = require("./geometry");
6
+ const constraints_1 = require("./constraints");
7
+ const featureComplete_1 = require("./featureComplete");
8
+ const sceneLayoutModel_1 = require("./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;
13
+ class FeatureTool {
14
+ constructor(options) {
15
+ this.id = "pooder.kit.feature";
16
+ this.metadata = {
17
+ name: "FeatureTool",
18
+ };
19
+ this.workingFeatures = [];
20
+ this.isUpdatingConfig = false;
21
+ this.isToolActive = false;
22
+ this.isFeatureSessionActive = false;
23
+ this.sessionOriginalFeatures = null;
24
+ this.hasWorkingChanges = false;
25
+ this.specs = [];
26
+ this.renderSeq = 0;
27
+ this.handleMoving = null;
28
+ this.handleModified = null;
29
+ this.handleSceneGeometryChange = null;
30
+ this.currentGeometry = null;
31
+ this.onToolActivated = (event) => {
32
+ this.isToolActive = event.id === this.id;
33
+ if (!this.isToolActive) {
34
+ this.restoreSessionFeaturesToConfig();
35
+ }
36
+ this.updateVisibility();
37
+ };
38
+ if (options) {
39
+ Object.assign(this, options);
40
+ }
41
+ }
42
+ activate(context) {
43
+ this.context = context;
44
+ this.canvasService = context.services.get("CanvasService");
45
+ if (!this.canvasService) {
46
+ console.warn("CanvasService not found for FeatureTool");
47
+ return;
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 });
60
+ const configService = context.services.get("ConfigurationService");
61
+ if (configService) {
62
+ const features = (configService.get("dieline.features", []) ||
63
+ []);
64
+ this.workingFeatures = this.cloneFeatures(features);
65
+ this.hasWorkingChanges = false;
66
+ configService.onAnyChange((e) => {
67
+ if (this.isUpdatingConfig)
68
+ return;
69
+ if (e.key === "dieline.features") {
70
+ if (this.isFeatureSessionActive)
71
+ return;
72
+ const next = (e.value || []);
73
+ this.workingFeatures = this.cloneFeatures(next);
74
+ this.hasWorkingChanges = false;
75
+ this.redraw();
76
+ this.emitWorkingChange();
77
+ }
78
+ });
79
+ }
80
+ const toolSessionService = context.services.get("ToolSessionService");
81
+ this.dirtyTrackerDisposable = toolSessionService?.registerDirtyTracker(this.id, () => this.hasWorkingChanges);
82
+ context.eventBus.on("tool:activated", this.onToolActivated);
83
+ this.setup();
84
+ }
85
+ deactivate(context) {
86
+ context.eventBus.off("tool:activated", this.onToolActivated);
87
+ this.restoreSessionFeaturesToConfig();
88
+ this.dirtyTrackerDisposable?.dispose();
89
+ this.dirtyTrackerDisposable = undefined;
90
+ this.teardown();
91
+ this.canvasService = undefined;
92
+ this.context = undefined;
93
+ }
94
+ updateVisibility() {
95
+ this.redraw();
96
+ }
97
+ contribute() {
98
+ return {
99
+ [core_1.ContributionPointIds.TOOLS]: [
100
+ {
101
+ id: this.id,
102
+ name: "Feature",
103
+ interaction: "session",
104
+ commands: {
105
+ begin: "beginFeatureSession",
106
+ commit: "completeFeatures",
107
+ rollback: "rollbackFeatureSession",
108
+ },
109
+ session: {
110
+ autoBegin: false,
111
+ leavePolicy: "block",
112
+ },
113
+ },
114
+ ],
115
+ [core_1.ContributionPointIds.COMMANDS]: [
116
+ {
117
+ command: "beginFeatureSession",
118
+ title: "Begin Feature Session",
119
+ handler: async () => {
120
+ if (this.isFeatureSessionActive) {
121
+ return { ok: true };
122
+ }
123
+ const original = this.getCommittedFeatures();
124
+ this.sessionOriginalFeatures = this.cloneFeatures(original);
125
+ this.isFeatureSessionActive = true;
126
+ await this.refreshGeometry();
127
+ this.setWorkingFeatures(this.cloneFeatures(original));
128
+ this.hasWorkingChanges = false;
129
+ this.redraw();
130
+ this.emitWorkingChange();
131
+ this.updateCommittedFeatures([]);
132
+ return { ok: true };
133
+ },
134
+ },
135
+ {
136
+ command: "addFeature",
137
+ title: "Add Edge Feature",
138
+ handler: (type = "subtract") => {
139
+ return this.addFeature(type);
140
+ },
141
+ },
142
+ {
143
+ command: "addHole",
144
+ title: "Add Hole",
145
+ handler: () => {
146
+ return this.addFeature("subtract");
147
+ },
148
+ },
149
+ {
150
+ command: "addDoubleLayerHole",
151
+ title: "Add Double Layer Hole",
152
+ handler: () => {
153
+ return this.addDoubleLayerHole();
154
+ },
155
+ },
156
+ {
157
+ command: "clearFeatures",
158
+ title: "Clear Features",
159
+ handler: () => {
160
+ this.setWorkingFeatures([]);
161
+ this.hasWorkingChanges = true;
162
+ this.redraw();
163
+ this.emitWorkingChange();
164
+ return true;
165
+ },
166
+ },
167
+ {
168
+ command: "getWorkingFeatures",
169
+ title: "Get Working Features",
170
+ handler: () => {
171
+ return this.cloneFeatures(this.workingFeatures);
172
+ },
173
+ },
174
+ {
175
+ command: "setWorkingFeatures",
176
+ title: "Set Working Features",
177
+ handler: async (features) => {
178
+ await this.refreshGeometry();
179
+ this.setWorkingFeatures(this.cloneFeatures(features || []));
180
+ this.hasWorkingChanges = true;
181
+ this.redraw();
182
+ this.emitWorkingChange();
183
+ return { ok: true };
184
+ },
185
+ },
186
+ {
187
+ command: "rollbackFeatureSession",
188
+ title: "Rollback Feature Session",
189
+ handler: async () => {
190
+ const original = this.cloneFeatures(this.sessionOriginalFeatures || this.getCommittedFeatures());
191
+ await this.refreshGeometry();
192
+ this.setWorkingFeatures(original);
193
+ this.hasWorkingChanges = false;
194
+ this.clearFeatureSessionState();
195
+ this.redraw();
196
+ this.emitWorkingChange();
197
+ this.updateCommittedFeatures(original);
198
+ return { ok: true };
199
+ },
200
+ },
201
+ {
202
+ command: "resetWorkingFeatures",
203
+ title: "Reset Working Features",
204
+ handler: async () => {
205
+ await this.resetWorkingFeaturesFromSource();
206
+ return { ok: true };
207
+ },
208
+ },
209
+ {
210
+ command: "updateWorkingGroupPosition",
211
+ title: "Update Working Group Position",
212
+ handler: (groupId, x, y) => {
213
+ return this.updateWorkingGroupPosition(groupId, x, y);
214
+ },
215
+ },
216
+ {
217
+ command: "completeFeatures",
218
+ title: "Complete Features",
219
+ handler: () => {
220
+ return this.completeFeatures();
221
+ },
222
+ },
223
+ ],
224
+ };
225
+ }
226
+ cloneFeatures(features) {
227
+ return JSON.parse(JSON.stringify(features || []));
228
+ }
229
+ getConfigService() {
230
+ return this.context?.services.get("ConfigurationService");
231
+ }
232
+ getCommittedFeatures() {
233
+ const configService = this.getConfigService();
234
+ const committed = (configService?.get("dieline.features", []) ||
235
+ []);
236
+ return this.cloneFeatures(committed);
237
+ }
238
+ updateCommittedFeatures(next) {
239
+ const configService = this.getConfigService();
240
+ if (!configService)
241
+ return;
242
+ this.isUpdatingConfig = true;
243
+ try {
244
+ configService.update("dieline.features", next);
245
+ }
246
+ finally {
247
+ this.isUpdatingConfig = false;
248
+ }
249
+ }
250
+ clearFeatureSessionState() {
251
+ this.isFeatureSessionActive = false;
252
+ this.sessionOriginalFeatures = null;
253
+ }
254
+ restoreSessionFeaturesToConfig() {
255
+ if (!this.isFeatureSessionActive)
256
+ return;
257
+ const original = this.cloneFeatures(this.sessionOriginalFeatures || this.getCommittedFeatures());
258
+ this.updateCommittedFeatures(original);
259
+ this.clearFeatureSessionState();
260
+ }
261
+ emitWorkingChange() {
262
+ this.context?.eventBus.emit("feature:working:change", {
263
+ features: this.cloneFeatures(this.workingFeatures),
264
+ });
265
+ }
266
+ async refreshGeometry() {
267
+ if (!this.context)
268
+ return;
269
+ const commandService = this.context.services.get("CommandService");
270
+ if (!commandService)
271
+ return;
272
+ try {
273
+ const g = await Promise.resolve(commandService.executeCommand("getSceneGeometry"));
274
+ if (g)
275
+ this.currentGeometry = g;
276
+ }
277
+ catch (e) { }
278
+ }
279
+ async resetWorkingFeaturesFromSource() {
280
+ const next = this.cloneFeatures(this.isFeatureSessionActive && this.sessionOriginalFeatures
281
+ ? this.sessionOriginalFeatures
282
+ : this.getCommittedFeatures());
283
+ await this.refreshGeometry();
284
+ this.setWorkingFeatures(next);
285
+ this.hasWorkingChanges = false;
286
+ this.redraw();
287
+ this.emitWorkingChange();
288
+ }
289
+ setWorkingFeatures(next) {
290
+ this.workingFeatures = next;
291
+ }
292
+ updateWorkingGroupPosition(groupId, x, y) {
293
+ if (!groupId)
294
+ return { ok: false };
295
+ const configService = this.context?.services.get("ConfigurationService");
296
+ if (!configService)
297
+ return { ok: false };
298
+ const sizeState = (0, sceneLayoutModel_1.readSizeState)(configService);
299
+ const dielineWidth = sizeState.actualWidthMm;
300
+ const dielineHeight = sizeState.actualHeightMm;
301
+ let changed = false;
302
+ const next = this.workingFeatures.map((f) => {
303
+ if (f.groupId !== groupId)
304
+ return f;
305
+ let nx = x;
306
+ let ny = y;
307
+ if (f.constraints && dielineWidth > 0 && dielineHeight > 0) {
308
+ const constrained = constraints_1.ConstraintRegistry.apply(nx, ny, f, {
309
+ dielineWidth,
310
+ dielineHeight,
311
+ });
312
+ nx = constrained.x;
313
+ ny = constrained.y;
314
+ }
315
+ if (f.x !== nx || f.y !== ny) {
316
+ changed = true;
317
+ return { ...f, x: nx, y: ny };
318
+ }
319
+ return f;
320
+ });
321
+ if (!changed)
322
+ return { ok: true };
323
+ this.setWorkingFeatures(next);
324
+ this.hasWorkingChanges = true;
325
+ this.redraw({ enforceConstraints: true });
326
+ this.emitWorkingChange();
327
+ return { ok: true };
328
+ }
329
+ completeFeatures() {
330
+ const configService = this.context?.services.get("ConfigurationService");
331
+ if (!configService) {
332
+ return {
333
+ ok: false,
334
+ issues: [
335
+ { featureId: "unknown", reason: "ConfigurationService not found" },
336
+ ],
337
+ };
338
+ }
339
+ const sizeState = (0, sceneLayoutModel_1.readSizeState)(configService);
340
+ const dielineWidth = sizeState.actualWidthMm;
341
+ const dielineHeight = sizeState.actualHeightMm;
342
+ const result = (0, featureComplete_1.completeFeaturesStrict)(this.workingFeatures, { dielineWidth, dielineHeight }, (next) => {
343
+ this.updateCommittedFeatures(next);
344
+ this.workingFeatures = this.cloneFeatures(next);
345
+ this.emitWorkingChange();
346
+ });
347
+ if (!result.ok) {
348
+ return {
349
+ ok: false,
350
+ issues: result.issues,
351
+ };
352
+ }
353
+ this.hasWorkingChanges = false;
354
+ this.clearFeatureSessionState();
355
+ this.redraw();
356
+ return { ok: true };
357
+ }
358
+ addFeature(type) {
359
+ if (!this.canvasService)
360
+ return false;
361
+ const newFeature = {
362
+ id: Date.now().toString(),
363
+ operation: type,
364
+ shape: "rect",
365
+ x: 0.5,
366
+ y: 0,
367
+ width: 10,
368
+ height: 10,
369
+ rotation: 0,
370
+ renderBehavior: "edge",
371
+ constraints: [{ type: "path" }],
372
+ };
373
+ this.setWorkingFeatures([...(this.workingFeatures || []), newFeature]);
374
+ this.hasWorkingChanges = true;
375
+ this.redraw();
376
+ this.emitWorkingChange();
377
+ return true;
378
+ }
379
+ addDoubleLayerHole() {
380
+ if (!this.canvasService)
381
+ return false;
382
+ const groupId = Date.now().toString();
383
+ const timestamp = Date.now();
384
+ const lug = {
385
+ id: `${timestamp}-lug`,
386
+ groupId,
387
+ operation: "add",
388
+ shape: "circle",
389
+ x: 0.5,
390
+ y: 0,
391
+ radius: 20,
392
+ rotation: 0,
393
+ renderBehavior: "edge",
394
+ constraints: [{ type: "path" }],
395
+ };
396
+ const hole = {
397
+ id: `${timestamp}-hole`,
398
+ groupId,
399
+ operation: "subtract",
400
+ shape: "circle",
401
+ x: 0.5,
402
+ y: 0,
403
+ radius: 15,
404
+ rotation: 0,
405
+ renderBehavior: "edge",
406
+ constraints: [{ type: "path" }],
407
+ };
408
+ this.setWorkingFeatures([...(this.workingFeatures || []), lug, hole]);
409
+ this.hasWorkingChanges = true;
410
+ this.redraw();
411
+ this.emitWorkingChange();
412
+ return true;
413
+ }
414
+ getGeometryForFeature(geometry, _feature) {
415
+ return geometry;
416
+ }
417
+ setup() {
418
+ if (!this.canvasService || !this.context)
419
+ return;
420
+ const canvas = this.canvasService.canvas;
421
+ if (!this.handleSceneGeometryChange) {
422
+ this.handleSceneGeometryChange = (geometry) => {
423
+ this.currentGeometry = geometry;
424
+ this.redraw({ enforceConstraints: true });
425
+ };
426
+ this.context.eventBus.on("scene:geometry:change", this.handleSceneGeometryChange);
427
+ }
428
+ const commandService = this.context.services.get("CommandService");
429
+ if (commandService) {
430
+ try {
431
+ Promise.resolve(commandService.executeCommand("getSceneGeometry")).then((g) => {
432
+ if (g) {
433
+ this.currentGeometry = g;
434
+ this.redraw();
435
+ }
436
+ });
437
+ }
438
+ catch (e) { }
439
+ }
440
+ if (!this.handleMoving) {
441
+ this.handleMoving = (e) => {
442
+ const target = this.getDraggableMarkerTarget(e?.target);
443
+ if (!target || !this.currentGeometry)
444
+ return;
445
+ const feature = this.getFeatureForMarker(target);
446
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
447
+ const snapped = this.constrainPosition({
448
+ x: Number(target.left || 0),
449
+ y: Number(target.top || 0),
450
+ }, geometry, feature);
451
+ target.set({
452
+ left: snapped.x,
453
+ top: snapped.y,
454
+ });
455
+ target.setCoords();
456
+ this.syncMarkerVisualsByTarget(target, snapped);
457
+ };
458
+ canvas.on("object:moving", this.handleMoving);
459
+ }
460
+ if (!this.handleModified) {
461
+ this.handleModified = (e) => {
462
+ const target = this.getDraggableMarkerTarget(e?.target);
463
+ if (!target)
464
+ return;
465
+ if (target.data?.isGroup) {
466
+ this.syncGroupFromCanvas(target);
467
+ }
468
+ else {
469
+ this.syncFeatureFromCanvas(target);
470
+ }
471
+ };
472
+ canvas.on("object:modified", this.handleModified);
473
+ }
474
+ }
475
+ teardown() {
476
+ if (!this.canvasService)
477
+ return;
478
+ const canvas = this.canvasService.canvas;
479
+ if (this.handleMoving) {
480
+ canvas.off("object:moving", this.handleMoving);
481
+ this.handleMoving = null;
482
+ }
483
+ if (this.handleModified) {
484
+ canvas.off("object:modified", this.handleModified);
485
+ this.handleModified = null;
486
+ }
487
+ if (this.handleSceneGeometryChange && this.context) {
488
+ this.context.eventBus.off("scene:geometry:change", this.handleSceneGeometryChange);
489
+ this.handleSceneGeometryChange = null;
490
+ }
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;
505
+ }
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) {
516
+ if (!feature) {
517
+ return { x: p.x, y: p.y };
518
+ }
519
+ const minX = geometry.x - geometry.width / 2;
520
+ const minY = geometry.y - geometry.height / 2;
521
+ const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
522
+ const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
523
+ const scale = geometry.scale || 1;
524
+ const dielineWidth = geometry.width / scale;
525
+ const dielineHeight = geometry.height / scale;
526
+ const activeConstraints = feature.constraints?.filter((c) => !c.validateOnly);
527
+ const constrained = constraints_1.ConstraintRegistry.apply(nx, ny, feature, {
528
+ dielineWidth,
529
+ dielineHeight,
530
+ geometry,
531
+ }, activeConstraints);
532
+ return {
533
+ x: minX + constrained.x * geometry.width,
534
+ y: minY + constrained.y * geometry.height,
535
+ };
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
+ }
545
+ syncFeatureFromCanvas(target) {
546
+ if (!this.currentGeometry)
547
+ return;
548
+ const index = this.toFeatureIndex(target.data?.index);
549
+ if (index === null || index >= this.workingFeatures.length)
550
+ return;
551
+ const feature = this.workingFeatures[index];
552
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
553
+ const normalized = this.toNormalizedPoint({
554
+ x: Number(target.left || 0),
555
+ y: Number(target.top || 0),
556
+ }, geometry);
557
+ const updatedFeature = {
558
+ ...feature,
559
+ x: normalized.x,
560
+ y: normalized.y,
561
+ };
562
+ const next = [...this.workingFeatures];
563
+ next[index] = updatedFeature;
564
+ this.setWorkingFeatures(next);
565
+ this.hasWorkingChanges = true;
566
+ this.emitWorkingChange();
567
+ }
568
+ syncGroupFromCanvas(target) {
569
+ if (!this.currentGeometry)
570
+ return;
571
+ const indices = this.readGroupIndices(target.data?.indices);
572
+ if (indices.length === 0)
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();
621
+ }
622
+ }
623
+ buildFeatureSpecs() {
624
+ if (!this.isFeatureSessionActive ||
625
+ !this.currentGeometry ||
626
+ this.workingFeatures.length === 0) {
627
+ return [];
628
+ }
629
+ const groups = new Map();
630
+ const singles = [];
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;
647
+ }
648
+ singles.push(marker);
649
+ });
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,
736
+ width: visualWidth,
737
+ height: visualHeight,
738
+ rx: visualRadius,
739
+ ry: visualRadius,
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;
764
+ }
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,
779
+ width: visualWidth,
780
+ height: bridgeHeight,
781
+ fill: "transparent",
782
+ stroke: "#888",
783
+ strokeWidth: 1,
784
+ strokeDashArray: [2, 2],
785
+ opacity: 0.5,
786
+ originX: "center",
787
+ originY: "bottom",
788
+ left: position.x,
789
+ top: position.y - visualHeight / 2,
790
+ },
791
+ });
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
+ });
860
+ });
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,
883
+ });
884
+ marker.setCoords();
885
+ });
886
+ }
887
+ enforceConstraints() {
888
+ if (!this.canvasService || !this.currentGeometry)
889
+ return;
890
+ const handles = this.canvasService.canvas
891
+ .getObjects()
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;
898
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
899
+ const snapped = this.constrainPosition({
900
+ x: Number(marker.left || 0),
901
+ y: Number(marker.top || 0),
902
+ }, geometry, feature);
903
+ marker.set({ left: snapped.x, top: snapped.y });
904
+ marker.setCoords();
905
+ this.syncMarkerVisualsByTarget(marker, snapped);
906
+ });
907
+ this.canvasService.canvas.requestRenderAll();
908
+ }
909
+ }
910
+ exports.FeatureTool = FeatureTool;