@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.
Files changed (65) hide show
  1. package/.test-dist/src/extensions/background.js +475 -131
  2. package/.test-dist/src/extensions/dieline.js +283 -180
  3. package/.test-dist/src/extensions/dielineShape.js +66 -0
  4. package/.test-dist/src/extensions/feature.js +388 -303
  5. package/.test-dist/src/extensions/film.js +133 -74
  6. package/.test-dist/src/extensions/geometry.js +120 -56
  7. package/.test-dist/src/extensions/image.js +296 -212
  8. package/.test-dist/src/extensions/index.js +1 -3
  9. package/.test-dist/src/extensions/maskOps.js +75 -20
  10. package/.test-dist/src/extensions/ruler.js +312 -215
  11. package/.test-dist/src/extensions/sceneLayoutModel.js +9 -3
  12. package/.test-dist/src/extensions/sceneVisibility.js +3 -10
  13. package/.test-dist/src/extensions/tracer.js +229 -58
  14. package/.test-dist/src/extensions/white-ink.js +139 -129
  15. package/.test-dist/src/services/CanvasService.js +888 -126
  16. package/.test-dist/src/services/index.js +1 -0
  17. package/.test-dist/src/services/visibility.js +54 -0
  18. package/.test-dist/tests/run.js +58 -4
  19. package/CHANGELOG.md +12 -0
  20. package/dist/index.d.mts +377 -82
  21. package/dist/index.d.ts +377 -82
  22. package/dist/index.js +3920 -2178
  23. package/dist/index.mjs +3992 -2247
  24. package/package.json +1 -1
  25. package/src/extensions/background.ts +631 -145
  26. package/src/extensions/dieline.ts +280 -187
  27. package/src/extensions/dielineShape.ts +109 -0
  28. package/src/extensions/feature.ts +485 -366
  29. package/src/extensions/film.ts +152 -76
  30. package/src/extensions/geometry.ts +203 -104
  31. package/src/extensions/image.ts +319 -238
  32. package/src/extensions/index.ts +0 -1
  33. package/src/extensions/ruler.ts +481 -268
  34. package/src/extensions/sceneLayoutModel.ts +18 -6
  35. package/src/extensions/white-ink.ts +157 -171
  36. package/src/services/CanvasService.ts +1126 -140
  37. package/src/services/index.ts +1 -0
  38. package/src/services/renderSpec.ts +69 -4
  39. package/src/services/visibility.ts +78 -0
  40. package/tests/run.ts +139 -4
  41. package/.test-dist/src/CanvasService.js +0 -249
  42. package/.test-dist/src/ViewportSystem.js +0 -75
  43. package/.test-dist/src/background.js +0 -203
  44. package/.test-dist/src/bridgeSelection.js +0 -20
  45. package/.test-dist/src/constraints.js +0 -237
  46. package/.test-dist/src/dieline.js +0 -818
  47. package/.test-dist/src/edgeScale.js +0 -12
  48. package/.test-dist/src/feature.js +0 -826
  49. package/.test-dist/src/featureComplete.js +0 -32
  50. package/.test-dist/src/film.js +0 -167
  51. package/.test-dist/src/geometry.js +0 -506
  52. package/.test-dist/src/image.js +0 -1250
  53. package/.test-dist/src/maskOps.js +0 -270
  54. package/.test-dist/src/mirror.js +0 -104
  55. package/.test-dist/src/renderSpec.js +0 -2
  56. package/.test-dist/src/ruler.js +0 -343
  57. package/.test-dist/src/sceneLayout.js +0 -99
  58. package/.test-dist/src/sceneLayoutModel.js +0 -196
  59. package/.test-dist/src/sceneView.js +0 -40
  60. package/.test-dist/src/sceneVisibility.js +0 -42
  61. package/.test-dist/src/size.js +0 -332
  62. package/.test-dist/src/tracer.js +0 -544
  63. package/.test-dist/src/white-ink.js +0 -829
  64. package/.test-dist/src/wrappedOffsets.js +0 -33
  65. package/src/extensions/sceneVisibility.ts +0 -71
@@ -1,826 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FeatureTool = void 0;
4
- const core_1 = require("@pooder/core");
5
- const fabric_1 = require("fabric");
6
- const geometry_1 = require("./geometry");
7
- const constraints_1 = require("./constraints");
8
- const featureComplete_1 = require("./featureComplete");
9
- const sceneLayoutModel_1 = require("./sceneLayoutModel");
10
- class FeatureTool {
11
- constructor(options) {
12
- this.id = "pooder.kit.feature";
13
- this.metadata = {
14
- name: "FeatureTool",
15
- };
16
- this.workingFeatures = [];
17
- this.isUpdatingConfig = false;
18
- this.isToolActive = false;
19
- this.isFeatureSessionActive = false;
20
- this.sessionOriginalFeatures = null;
21
- this.hasWorkingChanges = false;
22
- this.handleMoving = null;
23
- this.handleModified = null;
24
- this.handleSceneGeometryChange = null;
25
- this.currentGeometry = null;
26
- this.onToolActivated = (event) => {
27
- this.isToolActive = event.id === this.id;
28
- if (!this.isToolActive) {
29
- this.restoreSessionFeaturesToConfig();
30
- }
31
- this.updateVisibility();
32
- };
33
- if (options) {
34
- Object.assign(this, options);
35
- }
36
- }
37
- activate(context) {
38
- this.context = context;
39
- this.canvasService = context.services.get("CanvasService");
40
- if (!this.canvasService) {
41
- console.warn("CanvasService not found for FeatureTool");
42
- return;
43
- }
44
- const configService = context.services.get("ConfigurationService");
45
- if (configService) {
46
- const features = (configService.get("dieline.features", []) ||
47
- []);
48
- this.workingFeatures = this.cloneFeatures(features);
49
- this.hasWorkingChanges = false;
50
- configService.onAnyChange((e) => {
51
- if (this.isUpdatingConfig)
52
- return;
53
- if (e.key === "dieline.features") {
54
- if (this.isFeatureSessionActive)
55
- return;
56
- const next = (e.value || []);
57
- this.workingFeatures = this.cloneFeatures(next);
58
- this.hasWorkingChanges = false;
59
- this.redraw();
60
- this.emitWorkingChange();
61
- }
62
- });
63
- }
64
- const toolSessionService = context.services.get("ToolSessionService");
65
- this.dirtyTrackerDisposable = toolSessionService?.registerDirtyTracker(this.id, () => this.hasWorkingChanges);
66
- // Listen to tool activation
67
- context.eventBus.on("tool:activated", this.onToolActivated);
68
- this.setup();
69
- }
70
- deactivate(context) {
71
- context.eventBus.off("tool:activated", this.onToolActivated);
72
- this.restoreSessionFeaturesToConfig();
73
- this.dirtyTrackerDisposable?.dispose();
74
- this.dirtyTrackerDisposable = undefined;
75
- this.teardown();
76
- this.canvasService = undefined;
77
- this.context = undefined;
78
- }
79
- updateVisibility() {
80
- if (!this.canvasService)
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();
97
- }
98
- contribute() {
99
- return {
100
- [core_1.ContributionPointIds.TOOLS]: [
101
- {
102
- id: this.id,
103
- name: "Feature",
104
- interaction: "session",
105
- commands: {
106
- begin: "beginFeatureSession",
107
- commit: "completeFeatures",
108
- rollback: "rollbackFeatureSession",
109
- },
110
- session: {
111
- autoBegin: false,
112
- leavePolicy: "block",
113
- },
114
- },
115
- ],
116
- [core_1.ContributionPointIds.COMMANDS]: [
117
- {
118
- command: "beginFeatureSession",
119
- title: "Begin Feature Session",
120
- handler: async () => {
121
- if (this.isFeatureSessionActive) {
122
- return { ok: true };
123
- }
124
- const original = this.getCommittedFeatures();
125
- this.sessionOriginalFeatures = this.cloneFeatures(original);
126
- this.isFeatureSessionActive = true;
127
- await this.refreshGeometry();
128
- this.setWorkingFeatures(this.cloneFeatures(original));
129
- this.hasWorkingChanges = false;
130
- this.redraw();
131
- this.emitWorkingChange();
132
- this.updateCommittedFeatures([]);
133
- return { ok: true };
134
- },
135
- },
136
- {
137
- command: "addFeature",
138
- title: "Add Edge Feature",
139
- handler: (type = "subtract") => {
140
- return this.addFeature(type);
141
- },
142
- },
143
- {
144
- command: "addHole",
145
- title: "Add Hole",
146
- handler: () => {
147
- return this.addFeature("subtract");
148
- },
149
- },
150
- {
151
- command: "addDoubleLayerHole",
152
- title: "Add Double Layer Hole",
153
- handler: () => {
154
- return this.addDoubleLayerHole();
155
- },
156
- },
157
- {
158
- command: "clearFeatures",
159
- title: "Clear Features",
160
- handler: () => {
161
- this.setWorkingFeatures([]);
162
- this.hasWorkingChanges = true;
163
- this.redraw();
164
- this.emitWorkingChange();
165
- return true;
166
- },
167
- },
168
- {
169
- command: "getWorkingFeatures",
170
- title: "Get Working Features",
171
- handler: () => {
172
- return this.cloneFeatures(this.workingFeatures);
173
- },
174
- },
175
- {
176
- command: "setWorkingFeatures",
177
- title: "Set Working Features",
178
- handler: async (features) => {
179
- await this.refreshGeometry();
180
- this.setWorkingFeatures(this.cloneFeatures(features || []));
181
- this.hasWorkingChanges = true;
182
- this.redraw();
183
- this.emitWorkingChange();
184
- return { ok: true };
185
- },
186
- },
187
- {
188
- command: "rollbackFeatureSession",
189
- title: "Rollback Feature Session",
190
- handler: async () => {
191
- const original = this.cloneFeatures(this.sessionOriginalFeatures || this.getCommittedFeatures());
192
- await this.refreshGeometry();
193
- this.setWorkingFeatures(original);
194
- this.hasWorkingChanges = false;
195
- this.redraw();
196
- this.emitWorkingChange();
197
- this.updateCommittedFeatures(original);
198
- this.clearFeatureSessionState();
199
- return { ok: true };
200
- },
201
- },
202
- {
203
- command: "resetWorkingFeatures",
204
- title: "Reset Working Features",
205
- handler: async () => {
206
- await this.resetWorkingFeaturesFromSource();
207
- return { ok: true };
208
- },
209
- },
210
- {
211
- command: "updateWorkingGroupPosition",
212
- title: "Update Working Group Position",
213
- handler: (groupId, x, y) => {
214
- return this.updateWorkingGroupPosition(groupId, x, y);
215
- },
216
- },
217
- {
218
- command: "completeFeatures",
219
- title: "Complete Features",
220
- handler: () => {
221
- return this.completeFeatures();
222
- },
223
- },
224
- ],
225
- };
226
- }
227
- cloneFeatures(features) {
228
- return JSON.parse(JSON.stringify(features || []));
229
- }
230
- getConfigService() {
231
- return this.context?.services.get("ConfigurationService");
232
- }
233
- getCommittedFeatures() {
234
- const configService = this.getConfigService();
235
- const committed = (configService?.get("dieline.features", []) ||
236
- []);
237
- return this.cloneFeatures(committed);
238
- }
239
- updateCommittedFeatures(next) {
240
- const configService = this.getConfigService();
241
- if (!configService)
242
- return;
243
- this.isUpdatingConfig = true;
244
- try {
245
- configService.update("dieline.features", next);
246
- }
247
- finally {
248
- this.isUpdatingConfig = false;
249
- }
250
- }
251
- clearFeatureSessionState() {
252
- this.isFeatureSessionActive = false;
253
- this.sessionOriginalFeatures = null;
254
- }
255
- restoreSessionFeaturesToConfig() {
256
- if (!this.isFeatureSessionActive)
257
- return;
258
- const original = this.cloneFeatures(this.sessionOriginalFeatures || this.getCommittedFeatures());
259
- this.updateCommittedFeatures(original);
260
- this.clearFeatureSessionState();
261
- }
262
- emitWorkingChange() {
263
- this.context?.eventBus.emit("feature:working:change", {
264
- features: this.cloneFeatures(this.workingFeatures),
265
- });
266
- }
267
- async refreshGeometry() {
268
- if (!this.context)
269
- return;
270
- const commandService = this.context.services.get("CommandService");
271
- if (!commandService)
272
- return;
273
- try {
274
- const g = await Promise.resolve(commandService.executeCommand("getSceneGeometry"));
275
- if (g)
276
- this.currentGeometry = g;
277
- }
278
- catch (e) { }
279
- }
280
- async resetWorkingFeaturesFromSource() {
281
- const next = this.cloneFeatures(this.isFeatureSessionActive && this.sessionOriginalFeatures
282
- ? this.sessionOriginalFeatures
283
- : this.getCommittedFeatures());
284
- await this.refreshGeometry();
285
- this.setWorkingFeatures(next);
286
- this.hasWorkingChanges = false;
287
- this.redraw();
288
- this.emitWorkingChange();
289
- }
290
- setWorkingFeatures(next) {
291
- this.workingFeatures = next;
292
- }
293
- updateWorkingGroupPosition(groupId, x, y) {
294
- if (!groupId)
295
- return { ok: false };
296
- const configService = this.context?.services.get("ConfigurationService");
297
- if (!configService)
298
- return { ok: false };
299
- const sizeState = (0, sceneLayoutModel_1.readSizeState)(configService);
300
- const dielineWidth = sizeState.actualWidthMm;
301
- const dielineHeight = sizeState.actualHeightMm;
302
- let changed = false;
303
- const next = this.workingFeatures.map((f) => {
304
- if (f.groupId !== groupId)
305
- return f;
306
- let nx = x;
307
- let ny = y;
308
- if (f.constraints && dielineWidth > 0 && dielineHeight > 0) {
309
- const constrained = constraints_1.ConstraintRegistry.apply(nx, ny, f, {
310
- dielineWidth,
311
- dielineHeight,
312
- });
313
- nx = constrained.x;
314
- ny = constrained.y;
315
- }
316
- if (f.x !== nx || f.y !== ny) {
317
- changed = true;
318
- return { ...f, x: nx, y: ny };
319
- }
320
- return f;
321
- });
322
- if (!changed)
323
- return { ok: true };
324
- this.setWorkingFeatures(next);
325
- this.hasWorkingChanges = true;
326
- this.redraw();
327
- this.enforceConstraints();
328
- this.emitWorkingChange();
329
- return { ok: true };
330
- }
331
- completeFeatures() {
332
- const configService = this.context?.services.get("ConfigurationService");
333
- if (!configService) {
334
- return {
335
- ok: false,
336
- issues: [
337
- { featureId: "unknown", reason: "ConfigurationService not found" },
338
- ],
339
- };
340
- }
341
- const sizeState = (0, sceneLayoutModel_1.readSizeState)(configService);
342
- const dielineWidth = sizeState.actualWidthMm;
343
- const dielineHeight = sizeState.actualHeightMm;
344
- const result = (0, featureComplete_1.completeFeaturesStrict)(this.workingFeatures, { dielineWidth, dielineHeight }, (next) => {
345
- this.updateCommittedFeatures(next);
346
- this.workingFeatures = this.cloneFeatures(next);
347
- this.emitWorkingChange();
348
- });
349
- if (!result.ok) {
350
- return {
351
- ok: false,
352
- issues: result.issues,
353
- };
354
- }
355
- this.hasWorkingChanges = false;
356
- this.clearFeatureSessionState();
357
- // Keep feature markers above dieline overlay after config-driven redraw.
358
- this.redraw();
359
- return { ok: true };
360
- }
361
- addFeature(type) {
362
- if (!this.canvasService)
363
- return false;
364
- // Default to top edge center
365
- const newFeature = {
366
- id: Date.now().toString(),
367
- operation: type,
368
- shape: "rect",
369
- x: 0.5,
370
- y: 0, // Top edge
371
- width: 10,
372
- height: 10,
373
- rotation: 0,
374
- renderBehavior: "edge",
375
- // Default constraint: path (snap to edge)
376
- constraints: [{ type: "path" }],
377
- };
378
- this.setWorkingFeatures([...(this.workingFeatures || []), newFeature]);
379
- this.hasWorkingChanges = true;
380
- this.redraw();
381
- this.emitWorkingChange();
382
- return true;
383
- }
384
- addDoubleLayerHole() {
385
- if (!this.canvasService)
386
- return false;
387
- const groupId = Date.now().toString();
388
- const timestamp = Date.now();
389
- // 1. Lug (Outer) - Add
390
- const lug = {
391
- id: `${timestamp}-lug`,
392
- groupId,
393
- operation: "add",
394
- shape: "circle",
395
- x: 0.5,
396
- y: 0,
397
- radius: 20,
398
- rotation: 0,
399
- renderBehavior: "edge",
400
- constraints: [{ type: "path" }],
401
- };
402
- // 2. Hole (Inner) - Subtract
403
- const hole = {
404
- id: `${timestamp}-hole`,
405
- groupId,
406
- operation: "subtract",
407
- shape: "circle",
408
- x: 0.5,
409
- y: 0,
410
- radius: 15,
411
- rotation: 0,
412
- renderBehavior: "edge",
413
- constraints: [{ type: "path" }],
414
- };
415
- this.setWorkingFeatures([...(this.workingFeatures || []), lug, hole]);
416
- this.hasWorkingChanges = true;
417
- this.redraw();
418
- this.emitWorkingChange();
419
- return true;
420
- }
421
- getGeometryForFeature(geometry, feature) {
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)
424
- return geometry;
425
- }
426
- setup() {
427
- if (!this.canvasService || !this.context)
428
- return;
429
- const canvas = this.canvasService.canvas;
430
- // 1. Listen for Scene Geometry Changes
431
- if (!this.handleSceneGeometryChange) {
432
- this.handleSceneGeometryChange = (geometry) => {
433
- this.currentGeometry = geometry;
434
- this.redraw();
435
- this.enforceConstraints();
436
- };
437
- this.context.eventBus.on("scene:geometry:change", this.handleSceneGeometryChange);
438
- }
439
- // 2. Initial Fetch of Geometry
440
- const commandService = this.context.services.get("CommandService");
441
- if (commandService) {
442
- try {
443
- Promise.resolve(commandService.executeCommand("getSceneGeometry")).then((g) => {
444
- if (g) {
445
- this.currentGeometry = g;
446
- this.redraw();
447
- }
448
- });
449
- }
450
- catch (e) { }
451
- }
452
- // 3. Setup Canvas Interaction
453
- if (!this.handleMoving) {
454
- this.handleMoving = (e) => {
455
- const target = e.target;
456
- if (!target || target.data?.type !== "feature-marker")
457
- return;
458
- if (!this.currentGeometry)
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
- }
474
- const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
475
- // Snap to edge during move
476
- // For Group, target.left/top is group center (or top-left depending on origin)
477
- // We snap the target position itself.
478
- const p = new fabric_1.Point(target.left, target.top);
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);
486
- target.set({
487
- left: snapped.x,
488
- top: snapped.y,
489
- });
490
- };
491
- canvas.on("object:moving", this.handleMoving);
492
- }
493
- if (!this.handleModified) {
494
- this.handleModified = (e) => {
495
- const target = e.target;
496
- if (!target || target.data?.type !== "feature-marker")
497
- return;
498
- if (target.data?.isGroup) {
499
- // It's a Group object
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();
541
- }
542
- else {
543
- // Single object
544
- this.syncFeatureFromCanvas(target);
545
- }
546
- };
547
- canvas.on("object:modified", this.handleModified);
548
- }
549
- }
550
- teardown() {
551
- if (!this.canvasService)
552
- return;
553
- const canvas = this.canvasService.canvas;
554
- if (this.handleMoving) {
555
- canvas.off("object:moving", this.handleMoving);
556
- this.handleMoving = null;
557
- }
558
- if (this.handleModified) {
559
- canvas.off("object:modified", this.handleModified);
560
- this.handleModified = null;
561
- }
562
- if (this.handleSceneGeometryChange && this.context) {
563
- this.context.eventBus.off("scene:geometry:change", this.handleSceneGeometryChange);
564
- this.handleSceneGeometryChange = null;
565
- }
566
- const objects = canvas
567
- .getObjects()
568
- .filter((obj) => obj.data?.type === "feature-marker");
569
- objects.forEach((obj) => canvas.remove(obj));
570
- this.canvasService.requestRenderAll();
571
- }
572
- constrainPosition(p, geometry, limit, feature) {
573
- if (!feature) {
574
- return { x: p.x, y: p.y };
575
- }
576
- const minX = geometry.x - geometry.width / 2;
577
- const minY = geometry.y - geometry.height / 2;
578
- // Normalize
579
- const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
580
- const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
581
- const scale = geometry.scale || 1;
582
- const dielineWidth = geometry.width / scale;
583
- const dielineHeight = geometry.height / scale;
584
- // Filter constraints: only apply those that are NOT validateOnly
585
- const activeConstraints = feature.constraints?.filter((c) => !c.validateOnly);
586
- const constrained = constraints_1.ConstraintRegistry.apply(nx, ny, feature, {
587
- dielineWidth,
588
- dielineHeight,
589
- geometry,
590
- }, activeConstraints);
591
- // Denormalize
592
- return {
593
- x: minX + constrained.x * geometry.width,
594
- y: minY + constrained.y * geometry.height,
595
- };
596
- }
597
- syncFeatureFromCanvas(target) {
598
- if (!this.currentGeometry || !this.context)
599
- return;
600
- const index = target.data?.index;
601
- if (index === undefined ||
602
- index < 0 ||
603
- index >= this.workingFeatures.length)
604
- return;
605
- const feature = this.workingFeatures[index];
606
- const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
607
- const { width, height, x, y } = geometry;
608
- // Calculate Normalized Position
609
- // The geometry x/y is the CENTER.
610
- const left = x - width / 2;
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
615
- const updatedFeature = {
616
- ...feature,
617
- x: normalizedX,
618
- y: normalizedY,
619
- // Could also update rotation if we allowed rotating markers
620
- };
621
- const newFeatures = [...this.workingFeatures];
622
- newFeatures[index] = updatedFeature;
623
- this.setWorkingFeatures(newFeatures);
624
- this.hasWorkingChanges = true;
625
- this.emitWorkingChange();
626
- }
627
- redraw() {
628
- if (!this.canvasService || !this.currentGeometry)
629
- return;
630
- const canvas = this.canvasService.canvas;
631
- const geometry = this.currentGeometry;
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();
639
- return;
640
- }
641
- const scale = geometry.scale || 1;
642
- const finalScale = scale;
643
- // Group features by groupId
644
- const groups = {};
645
- const singles = [];
646
- this.workingFeatures.forEach((f, i) => {
647
- if (f.groupId) {
648
- if (!groups[f.groupId])
649
- groups[f.groupId] = [];
650
- groups[f.groupId].push({ feature: f, index: i });
651
- }
652
- else {
653
- singles.push({ feature: f, index: i });
654
- }
655
- });
656
- // Helper to create marker shape
657
- const createMarkerShape = (feature, pos) => {
658
- const featureScale = scale;
659
- const visualWidth = (feature.width || 10) * featureScale;
660
- const visualHeight = (feature.height || 10) * featureScale;
661
- const visualRadius = (feature.radius || 0) * featureScale;
662
- const color = feature.color ||
663
- (feature.operation === "add" ? "#00FF00" : "#FF0000");
664
- const strokeDash = feature.strokeDash ||
665
- (feature.operation === "subtract" ? [4, 4] : undefined);
666
- let shape;
667
- if (feature.shape === "rect") {
668
- shape = new fabric_1.Rect({
669
- width: visualWidth,
670
- height: visualHeight,
671
- rx: visualRadius,
672
- ry: visualRadius,
673
- fill: "transparent",
674
- stroke: color,
675
- strokeWidth: 2,
676
- strokeDashArray: strokeDash,
677
- originX: "center",
678
- originY: "center",
679
- left: pos.x,
680
- top: pos.y,
681
- });
682
- }
683
- else {
684
- shape = new fabric_1.Circle({
685
- radius: visualRadius || 5 * finalScale,
686
- fill: "transparent",
687
- stroke: color,
688
- strokeWidth: 2,
689
- strokeDashArray: strokeDash,
690
- originX: "center",
691
- originY: "center",
692
- left: pos.x,
693
- top: pos.y,
694
- });
695
- }
696
- if (feature.rotation) {
697
- shape.rotate(feature.rotation);
698
- }
699
- // Handle Indicator for Bridge
700
- if (feature.bridge && feature.bridge.type === "vertical") {
701
- // Create a visual indicator for the bridge
702
- // A dashed rectangle extending upwards
703
- const bridgeIndicator = new fabric_1.Rect({
704
- width: visualWidth,
705
- height: 100 * featureScale, // Arbitrary long length to show direction
706
- fill: "transparent",
707
- stroke: "#888",
708
- strokeWidth: 1,
709
- strokeDashArray: [2, 2],
710
- originX: "center",
711
- originY: "bottom", // Anchor at bottom so it extends up
712
- left: pos.x,
713
- top: pos.y - visualHeight / 2, // Start from top of feature
714
- opacity: 0.5,
715
- selectable: false,
716
- evented: false
717
- });
718
- // We need to return a group containing both shape and indicator
719
- // But createMarkerShape is expected to return one object.
720
- // If we return a Group, Fabric handles it.
721
- // But the caller might wrap this in another Group if it's part of a feature group.
722
- // Fabric supports nested groups.
723
- const group = new fabric_1.Group([bridgeIndicator, shape], {
724
- originX: "center",
725
- originY: "center",
726
- left: pos.x,
727
- top: pos.y
728
- });
729
- return group;
730
- }
731
- return shape;
732
- };
733
- // Render Singles
734
- singles.forEach(({ feature, index }) => {
735
- const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
736
- const pos = (0, geometry_1.resolveFeaturePosition)(feature, geometry);
737
- const marker = createMarkerShape(feature, pos);
738
- marker.set({
739
- visible: this.isToolActive,
740
- selectable: this.isToolActive,
741
- evented: this.isToolActive,
742
- hasControls: false,
743
- hasBorders: false,
744
- hoverCursor: "move",
745
- lockRotation: true,
746
- lockScalingX: true,
747
- lockScalingY: true,
748
- data: { type: "feature-marker", index, isGroup: false },
749
- });
750
- canvas.add(marker);
751
- canvas.bringObjectToFront(marker);
752
- });
753
- // Render Groups
754
- Object.keys(groups).forEach((groupId) => {
755
- const members = groups[groupId];
756
- if (members.length === 0)
757
- return;
758
- // Calculate group center (average position) to position the group correctly
759
- // But Fabric Group uses relative coordinates.
760
- // Easiest way: Create shapes at absolute positions, then Group them.
761
- // Fabric will auto-calculate group center and adjust children.
762
- const shapes = members.map(({ feature }) => {
763
- const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
764
- const pos = (0, geometry_1.resolveFeaturePosition)(feature, geometry);
765
- return createMarkerShape(feature, pos);
766
- });
767
- const groupObj = new fabric_1.Group(shapes, {
768
- visible: this.isToolActive,
769
- selectable: this.isToolActive,
770
- evented: this.isToolActive,
771
- hasControls: false,
772
- hasBorders: false,
773
- hoverCursor: "move",
774
- lockRotation: true,
775
- lockScalingX: true,
776
- lockScalingY: true,
777
- subTargetCheck: true, // Allow events to pass through if needed, but we treat as one
778
- interactive: false, // Children not interactive
779
- // @ts-ignore
780
- data: {
781
- type: "feature-marker",
782
- isGroup: true,
783
- groupId,
784
- indices: members.map((m) => m.index),
785
- },
786
- });
787
- canvas.add(groupObj);
788
- canvas.bringObjectToFront(groupObj);
789
- });
790
- this.canvasService.requestRenderAll();
791
- }
792
- enforceConstraints() {
793
- if (!this.canvasService || !this.currentGeometry)
794
- return;
795
- // Iterate markers and snap them if geometry changed
796
- const canvas = this.canvasService.canvas;
797
- const markers = canvas
798
- .getObjects()
799
- .filter((obj) => obj.data?.type === "feature-marker");
800
- markers.forEach((marker) => {
801
- // Find associated feature
802
- let feature;
803
- if (marker.data?.isGroup) {
804
- const indices = marker.data?.indices;
805
- if (indices && indices.length > 0) {
806
- feature = this.workingFeatures[indices[0]];
807
- }
808
- }
809
- else {
810
- const index = marker.data?.index;
811
- if (index !== undefined) {
812
- feature = this.workingFeatures[index];
813
- }
814
- }
815
- const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
816
- const markerStrokeWidth = (marker.strokeWidth || 2) * (marker.scaleX || 1);
817
- const minDim = Math.min(marker.getScaledWidth(), marker.getScaledHeight());
818
- const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
819
- const snapped = this.constrainPosition(new fabric_1.Point(marker.left, marker.top), geometry, limit, feature);
820
- marker.set({ left: snapped.x, top: snapped.y });
821
- marker.setCoords();
822
- });
823
- canvas.requestRenderAll();
824
- }
825
- }
826
- exports.FeatureTool = FeatureTool;