@pooder/kit 4.1.0 → 4.2.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/CanvasService.js +83 -0
- package/.test-dist/src/ViewportSystem.js +75 -0
- package/.test-dist/src/background.js +203 -0
- package/.test-dist/src/constraints.js +153 -0
- package/.test-dist/src/coordinate.js +74 -0
- package/.test-dist/src/dieline.js +758 -0
- package/.test-dist/src/feature.js +687 -0
- package/.test-dist/src/featureComplete.js +31 -0
- package/.test-dist/src/featureDraft.js +31 -0
- package/.test-dist/src/film.js +167 -0
- package/.test-dist/src/geometry.js +292 -0
- package/.test-dist/src/image.js +421 -0
- package/.test-dist/src/index.js +31 -0
- package/.test-dist/src/mirror.js +104 -0
- package/.test-dist/src/ruler.js +383 -0
- package/.test-dist/src/tracer.js +448 -0
- package/.test-dist/src/units.js +30 -0
- package/.test-dist/src/white-ink.js +310 -0
- package/.test-dist/tests/run.js +60 -0
- package/CHANGELOG.md +6 -0
- package/dist/index.d.mts +50 -5
- package/dist/index.d.ts +50 -5
- package/dist/index.js +544 -297
- package/dist/index.mjs +541 -296
- package/package.json +3 -2
- package/src/CanvasService.ts +7 -0
- package/src/ViewportSystem.ts +92 -0
- package/src/constraints.ts +53 -4
- package/src/dieline.ts +169 -85
- package/src/feature.ts +217 -150
- package/src/featureComplete.ts +45 -0
- package/src/index.ts +1 -0
- package/src/ruler.ts +26 -18
- package/src/units.ts +27 -0
- package/tests/run.ts +81 -0
- package/tsconfig.test.json +15 -0
package/src/feature.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
ContributionPointIds,
|
|
5
5
|
CommandContribution,
|
|
6
|
-
ConfigurationContribution,
|
|
7
6
|
ConfigurationService,
|
|
8
7
|
} from "@pooder/core";
|
|
9
8
|
import { Circle, Group, Point, Rect } from "fabric";
|
|
@@ -14,8 +13,11 @@ import {
|
|
|
14
13
|
DielineFeature,
|
|
15
14
|
resolveFeaturePosition,
|
|
16
15
|
} from "./geometry";
|
|
17
|
-
import { Coordinate } from "./coordinate";
|
|
18
16
|
import { ConstraintRegistry } from "./constraints";
|
|
17
|
+
import {
|
|
18
|
+
completeFeaturesStrict,
|
|
19
|
+
} from "./featureComplete";
|
|
20
|
+
import { parseLengthToMm } from "./units";
|
|
19
21
|
|
|
20
22
|
export class FeatureTool implements Extension {
|
|
21
23
|
id = "pooder.kit.feature";
|
|
@@ -24,7 +26,7 @@ export class FeatureTool implements Extension {
|
|
|
24
26
|
name: "FeatureTool",
|
|
25
27
|
};
|
|
26
28
|
|
|
27
|
-
private
|
|
29
|
+
private workingFeatures: DielineFeature[] = [];
|
|
28
30
|
private canvasService?: CanvasService;
|
|
29
31
|
private context?: ExtensionContext;
|
|
30
32
|
private isUpdatingConfig = false;
|
|
@@ -60,14 +62,18 @@ export class FeatureTool implements Extension {
|
|
|
60
62
|
"ConfigurationService",
|
|
61
63
|
);
|
|
62
64
|
if (configService) {
|
|
63
|
-
|
|
65
|
+
const features = (configService.get("dieline.features", []) ||
|
|
66
|
+
[]) as DielineFeature[];
|
|
67
|
+
this.workingFeatures = this.cloneFeatures(features);
|
|
64
68
|
|
|
65
69
|
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
66
70
|
if (this.isUpdatingConfig) return;
|
|
67
71
|
|
|
68
72
|
if (e.key === "dieline.features") {
|
|
69
|
-
|
|
73
|
+
const next = (e.value || []) as DielineFeature[];
|
|
74
|
+
this.workingFeatures = this.cloneFeatures(next);
|
|
70
75
|
this.redraw();
|
|
76
|
+
this.emitWorkingChange();
|
|
71
77
|
}
|
|
72
78
|
});
|
|
73
79
|
}
|
|
@@ -138,28 +144,175 @@ export class FeatureTool implements Extension {
|
|
|
138
144
|
command: "clearFeatures",
|
|
139
145
|
title: "Clear Features",
|
|
140
146
|
handler: () => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
145
|
-
if (configService) {
|
|
146
|
-
configService.update("dieline.features", []);
|
|
147
|
-
}
|
|
147
|
+
this.setWorkingFeatures([]);
|
|
148
|
+
this.redraw();
|
|
149
|
+
this.emitWorkingChange();
|
|
148
150
|
return true;
|
|
149
151
|
},
|
|
150
152
|
},
|
|
153
|
+
{
|
|
154
|
+
command: "getWorkingFeatures",
|
|
155
|
+
title: "Get Working Features",
|
|
156
|
+
handler: () => {
|
|
157
|
+
return this.cloneFeatures(this.workingFeatures);
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
command: "setWorkingFeatures",
|
|
162
|
+
title: "Set Working Features",
|
|
163
|
+
handler: async (features: DielineFeature[]) => {
|
|
164
|
+
await this.refreshGeometry();
|
|
165
|
+
this.setWorkingFeatures(this.cloneFeatures(features || []));
|
|
166
|
+
this.redraw();
|
|
167
|
+
this.emitWorkingChange();
|
|
168
|
+
return { ok: true };
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
command: "updateWorkingGroupPosition",
|
|
173
|
+
title: "Update Working Group Position",
|
|
174
|
+
handler: (groupId: string, x: number, y: number) => {
|
|
175
|
+
return this.updateWorkingGroupPosition(groupId, x, y);
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
command: "completeFeatures",
|
|
180
|
+
title: "Complete Features",
|
|
181
|
+
handler: () => {
|
|
182
|
+
return this.completeFeatures();
|
|
183
|
+
},
|
|
184
|
+
},
|
|
151
185
|
] as CommandContribution[],
|
|
152
186
|
};
|
|
153
187
|
}
|
|
154
188
|
|
|
155
|
-
private
|
|
156
|
-
|
|
189
|
+
private cloneFeatures(features: DielineFeature[]): DielineFeature[] {
|
|
190
|
+
return JSON.parse(JSON.stringify(features || [])) as DielineFeature[];
|
|
191
|
+
}
|
|
157
192
|
|
|
158
|
-
|
|
159
|
-
|
|
193
|
+
private emitWorkingChange() {
|
|
194
|
+
this.context?.eventBus.emit("feature:working:change", {
|
|
195
|
+
features: this.cloneFeatures(this.workingFeatures),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async refreshGeometry() {
|
|
200
|
+
if (!this.context) return;
|
|
201
|
+
const commandService = this.context.services.get<any>("CommandService");
|
|
202
|
+
if (!commandService) return;
|
|
203
|
+
try {
|
|
204
|
+
const g = await Promise.resolve(commandService.executeCommand("getGeometry"));
|
|
205
|
+
if (g) this.currentGeometry = g as DielineGeometry;
|
|
206
|
+
} catch (e) {}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private setWorkingFeatures(next: DielineFeature[]) {
|
|
210
|
+
this.workingFeatures = next;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private updateWorkingGroupPosition(groupId: string, x: number, y: number) {
|
|
214
|
+
if (!groupId) return { ok: false };
|
|
215
|
+
|
|
216
|
+
const configService =
|
|
217
|
+
this.context?.services.get<ConfigurationService>("ConfigurationService");
|
|
218
|
+
if (!configService) return { ok: false };
|
|
219
|
+
|
|
220
|
+
const dielineWidth = parseLengthToMm(
|
|
221
|
+
configService.get("dieline.width") ?? 500,
|
|
222
|
+
"mm",
|
|
223
|
+
);
|
|
224
|
+
const dielineHeight = parseLengthToMm(
|
|
225
|
+
configService.get("dieline.height") ?? 500,
|
|
226
|
+
"mm",
|
|
160
227
|
);
|
|
161
|
-
|
|
162
|
-
|
|
228
|
+
|
|
229
|
+
let changed = false;
|
|
230
|
+
const next = this.workingFeatures.map((f) => {
|
|
231
|
+
if (f.groupId !== groupId) return f;
|
|
232
|
+
let nx = x;
|
|
233
|
+
let ny = y;
|
|
234
|
+
if (f.constraints && dielineWidth > 0 && dielineHeight > 0) {
|
|
235
|
+
const constrained = ConstraintRegistry.apply(nx, ny, f, {
|
|
236
|
+
dielineWidth,
|
|
237
|
+
dielineHeight,
|
|
238
|
+
});
|
|
239
|
+
nx = constrained.x;
|
|
240
|
+
ny = constrained.y;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (f.x !== nx || f.y !== ny) {
|
|
244
|
+
changed = true;
|
|
245
|
+
return { ...f, x: nx, y: ny };
|
|
246
|
+
}
|
|
247
|
+
return f;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (!changed) return { ok: true };
|
|
251
|
+
|
|
252
|
+
this.setWorkingFeatures(next);
|
|
253
|
+
this.redraw();
|
|
254
|
+
this.enforceConstraints();
|
|
255
|
+
this.emitWorkingChange();
|
|
256
|
+
|
|
257
|
+
return { ok: true };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private completeFeatures(): {
|
|
261
|
+
ok: boolean;
|
|
262
|
+
issues?: Array<{
|
|
263
|
+
featureId: string;
|
|
264
|
+
groupId?: string;
|
|
265
|
+
reason: string;
|
|
266
|
+
}>;
|
|
267
|
+
} {
|
|
268
|
+
const configService =
|
|
269
|
+
this.context?.services.get<ConfigurationService>("ConfigurationService");
|
|
270
|
+
if (!configService) {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
issues: [
|
|
274
|
+
{ featureId: "unknown", reason: "ConfigurationService not found" },
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const dielineWidth = parseLengthToMm(
|
|
280
|
+
configService.get("dieline.width") ?? 500,
|
|
281
|
+
"mm",
|
|
282
|
+
);
|
|
283
|
+
const dielineHeight = parseLengthToMm(
|
|
284
|
+
configService.get("dieline.height") ?? 500,
|
|
285
|
+
"mm",
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const result = completeFeaturesStrict(
|
|
289
|
+
this.workingFeatures,
|
|
290
|
+
{ dielineWidth, dielineHeight },
|
|
291
|
+
(next) => {
|
|
292
|
+
this.isUpdatingConfig = true;
|
|
293
|
+
try {
|
|
294
|
+
configService.update("dieline.features", next);
|
|
295
|
+
} finally {
|
|
296
|
+
this.isUpdatingConfig = false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.workingFeatures = this.cloneFeatures(next as any);
|
|
300
|
+
this.emitWorkingChange();
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (!result.ok) {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
issues: result.issues,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { ok: true };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private addFeature(type: "add" | "subtract") {
|
|
315
|
+
if (!this.canvasService) return false;
|
|
163
316
|
|
|
164
317
|
// Default to top edge center
|
|
165
318
|
const newFeature: DielineFeature = {
|
|
@@ -169,31 +322,20 @@ export class FeatureTool implements Extension {
|
|
|
169
322
|
shape: "rect",
|
|
170
323
|
x: 0.5,
|
|
171
324
|
y: 0, // Top edge
|
|
172
|
-
width:
|
|
173
|
-
height:
|
|
325
|
+
width: 10,
|
|
326
|
+
height: 10,
|
|
174
327
|
rotation: 0,
|
|
175
328
|
};
|
|
176
329
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
[],
|
|
181
|
-
) as DielineFeature[];
|
|
182
|
-
configService.update("dieline.features", [...current, newFeature]);
|
|
183
|
-
}
|
|
330
|
+
this.setWorkingFeatures([...(this.workingFeatures || []), newFeature]);
|
|
331
|
+
this.redraw();
|
|
332
|
+
this.emitWorkingChange();
|
|
184
333
|
return true;
|
|
185
334
|
}
|
|
186
335
|
|
|
187
336
|
private addDoubleLayerHole() {
|
|
188
337
|
if (!this.canvasService) return false;
|
|
189
338
|
|
|
190
|
-
const configService = this.context?.services.get<ConfigurationService>(
|
|
191
|
-
"ConfigurationService",
|
|
192
|
-
);
|
|
193
|
-
const unit = configService?.get("dieline.unit", "mm") || "mm";
|
|
194
|
-
const lugRadius = Coordinate.convertUnit(20, "mm", unit);
|
|
195
|
-
const holeRadius = Coordinate.convertUnit(15, "mm", unit);
|
|
196
|
-
|
|
197
339
|
const groupId = Date.now().toString();
|
|
198
340
|
const timestamp = Date.now();
|
|
199
341
|
|
|
@@ -206,7 +348,7 @@ export class FeatureTool implements Extension {
|
|
|
206
348
|
placement: "edge",
|
|
207
349
|
x: 0.5,
|
|
208
350
|
y: 0,
|
|
209
|
-
radius:
|
|
351
|
+
radius: 20,
|
|
210
352
|
rotation: 0,
|
|
211
353
|
};
|
|
212
354
|
|
|
@@ -219,17 +361,13 @@ export class FeatureTool implements Extension {
|
|
|
219
361
|
placement: "edge",
|
|
220
362
|
x: 0.5,
|
|
221
363
|
y: 0,
|
|
222
|
-
radius:
|
|
364
|
+
radius: 15,
|
|
223
365
|
rotation: 0,
|
|
224
366
|
};
|
|
225
367
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
[],
|
|
230
|
-
) as DielineFeature[];
|
|
231
|
-
configService.update("dieline.features", [...current, lug, hole]);
|
|
232
|
-
}
|
|
368
|
+
this.setWorkingFeatures([...(this.workingFeatures || []), lug, hole]);
|
|
369
|
+
this.redraw();
|
|
370
|
+
this.emitWorkingChange();
|
|
233
371
|
return true;
|
|
234
372
|
}
|
|
235
373
|
|
|
@@ -286,12 +424,12 @@ export class FeatureTool implements Extension {
|
|
|
286
424
|
if (target.data?.isGroup) {
|
|
287
425
|
const indices = target.data?.indices as number[];
|
|
288
426
|
if (indices && indices.length > 0) {
|
|
289
|
-
feature = this.
|
|
427
|
+
feature = this.workingFeatures[indices[0]];
|
|
290
428
|
}
|
|
291
429
|
} else {
|
|
292
430
|
const index = target.data?.index;
|
|
293
431
|
if (index !== undefined) {
|
|
294
|
-
feature = this.
|
|
432
|
+
feature = this.workingFeatures[index];
|
|
295
433
|
}
|
|
296
434
|
}
|
|
297
435
|
|
|
@@ -327,7 +465,6 @@ export class FeatureTool implements Extension {
|
|
|
327
465
|
const target = e.target;
|
|
328
466
|
if (!target || target.data?.type !== "feature-marker") return;
|
|
329
467
|
|
|
330
|
-
// Sync changes back to config
|
|
331
468
|
if (target.data?.isGroup) {
|
|
332
469
|
// It's a Group object
|
|
333
470
|
const groupObj = target as Group;
|
|
@@ -345,7 +482,7 @@ export class FeatureTool implements Extension {
|
|
|
345
482
|
// Simplified: just add relative coordinates if no rotation/scaling on group
|
|
346
483
|
// We locked rotation/scaling, so it's safe.
|
|
347
484
|
|
|
348
|
-
const newFeatures = [...this.
|
|
485
|
+
const newFeatures = [...this.workingFeatures];
|
|
349
486
|
const { x, y } = this.currentGeometry!; // Center is same
|
|
350
487
|
|
|
351
488
|
// Fabric Group objects have .getObjects() which returns children
|
|
@@ -354,7 +491,7 @@ export class FeatureTool implements Extension {
|
|
|
354
491
|
|
|
355
492
|
groupObj.getObjects().forEach((child, i) => {
|
|
356
493
|
const originalIndex = indices[i];
|
|
357
|
-
const feature = this.
|
|
494
|
+
const feature = this.workingFeatures[originalIndex];
|
|
358
495
|
const geometry = this.getGeometryForFeature(
|
|
359
496
|
this.currentGeometry!,
|
|
360
497
|
feature,
|
|
@@ -379,20 +516,8 @@ export class FeatureTool implements Extension {
|
|
|
379
516
|
};
|
|
380
517
|
});
|
|
381
518
|
|
|
382
|
-
this.
|
|
383
|
-
|
|
384
|
-
const configService =
|
|
385
|
-
this.context?.services.get<ConfigurationService>(
|
|
386
|
-
"ConfigurationService",
|
|
387
|
-
);
|
|
388
|
-
if (configService) {
|
|
389
|
-
this.isUpdatingConfig = true;
|
|
390
|
-
try {
|
|
391
|
-
configService.update("dieline.features", this.features);
|
|
392
|
-
} finally {
|
|
393
|
-
this.isUpdatingConfig = false;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
519
|
+
this.setWorkingFeatures(newFeatures);
|
|
520
|
+
this.emitWorkingChange();
|
|
396
521
|
} else {
|
|
397
522
|
// Single object
|
|
398
523
|
this.syncFeatureFromCanvas(target);
|
|
@@ -437,11 +562,9 @@ export class FeatureTool implements Extension {
|
|
|
437
562
|
feature?: DielineFeature
|
|
438
563
|
): { x: number; y: number } {
|
|
439
564
|
if (feature && feature.constraints) {
|
|
440
|
-
// Use Constraint Registry
|
|
441
|
-
// Convert to normalized coordinates
|
|
442
565
|
const minX = geometry.x - geometry.width / 2;
|
|
443
566
|
const minY = geometry.y - geometry.height / 2;
|
|
444
|
-
|
|
567
|
+
|
|
445
568
|
const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
|
|
446
569
|
const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
|
|
447
570
|
|
|
@@ -461,38 +584,33 @@ export class FeatureTool implements Extension {
|
|
|
461
584
|
}
|
|
462
585
|
|
|
463
586
|
if (feature && feature.placement === "internal") {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
y: Math.max(minY, Math.min(maxY, p.y))
|
|
474
|
-
};
|
|
587
|
+
const minX = geometry.x - geometry.width / 2;
|
|
588
|
+
const maxX = geometry.x + geometry.width / 2;
|
|
589
|
+
const minY = geometry.y - geometry.height / 2;
|
|
590
|
+
const maxY = geometry.y + geometry.height / 2;
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
x: Math.max(minX, Math.min(maxX, p.x)),
|
|
594
|
+
y: Math.max(minY, Math.min(maxY, p.y)),
|
|
595
|
+
};
|
|
475
596
|
}
|
|
476
597
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
598
|
+
const nearest = getNearestPointOnDieline(
|
|
599
|
+
{ x: p.x, y: p.y },
|
|
600
|
+
{
|
|
601
|
+
...geometry,
|
|
602
|
+
features: [],
|
|
603
|
+
} as any,
|
|
604
|
+
);
|
|
484
605
|
|
|
485
|
-
// Calculate vector from nearest point to current point
|
|
486
606
|
const dx = p.x - nearest.x;
|
|
487
607
|
const dy = p.y - nearest.y;
|
|
488
608
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
489
609
|
|
|
490
|
-
// If within limit, allow current position (offset from edge)
|
|
491
610
|
if (dist <= limit) {
|
|
492
611
|
return { x: p.x, y: p.y };
|
|
493
612
|
}
|
|
494
613
|
|
|
495
|
-
// Otherwise, clamp to limit
|
|
496
614
|
const scale = limit / dist;
|
|
497
615
|
return {
|
|
498
616
|
x: nearest.x + dx * scale,
|
|
@@ -504,10 +622,14 @@ export class FeatureTool implements Extension {
|
|
|
504
622
|
if (!this.currentGeometry || !this.context) return;
|
|
505
623
|
|
|
506
624
|
const index = target.data?.index;
|
|
507
|
-
if (
|
|
625
|
+
if (
|
|
626
|
+
index === undefined ||
|
|
627
|
+
index < 0 ||
|
|
628
|
+
index >= this.workingFeatures.length
|
|
629
|
+
)
|
|
508
630
|
return;
|
|
509
631
|
|
|
510
|
-
const feature = this.
|
|
632
|
+
const feature = this.workingFeatures[index];
|
|
511
633
|
const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
|
|
512
634
|
const { width, height, x, y } = geometry;
|
|
513
635
|
|
|
@@ -527,22 +649,10 @@ export class FeatureTool implements Extension {
|
|
|
527
649
|
// Could also update rotation if we allowed rotating markers
|
|
528
650
|
};
|
|
529
651
|
|
|
530
|
-
const newFeatures = [...this.
|
|
652
|
+
const newFeatures = [...this.workingFeatures];
|
|
531
653
|
newFeatures[index] = updatedFeature;
|
|
532
|
-
this.
|
|
533
|
-
|
|
534
|
-
// Save to config
|
|
535
|
-
const configService = this.context.services.get<ConfigurationService>(
|
|
536
|
-
"ConfigurationService",
|
|
537
|
-
);
|
|
538
|
-
if (configService) {
|
|
539
|
-
this.isUpdatingConfig = true;
|
|
540
|
-
try {
|
|
541
|
-
configService.update("dieline.features", this.features);
|
|
542
|
-
} finally {
|
|
543
|
-
this.isUpdatingConfig = false;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
654
|
+
this.setWorkingFeatures(newFeatures);
|
|
655
|
+
this.emitWorkingChange();
|
|
546
656
|
}
|
|
547
657
|
|
|
548
658
|
private redraw() {
|
|
@@ -556,7 +666,7 @@ export class FeatureTool implements Extension {
|
|
|
556
666
|
.filter((obj: any) => obj.data?.type === "feature-marker");
|
|
557
667
|
existing.forEach((obj) => canvas.remove(obj));
|
|
558
668
|
|
|
559
|
-
if (!this.
|
|
669
|
+
if (!this.workingFeatures || this.workingFeatures.length === 0) {
|
|
560
670
|
this.canvasService.requestRenderAll();
|
|
561
671
|
return;
|
|
562
672
|
}
|
|
@@ -569,7 +679,7 @@ export class FeatureTool implements Extension {
|
|
|
569
679
|
{};
|
|
570
680
|
const singles: { feature: DielineFeature; index: number }[] = [];
|
|
571
681
|
|
|
572
|
-
this.
|
|
682
|
+
this.workingFeatures.forEach((f: DielineFeature, i: number) => {
|
|
573
683
|
if (f.groupId) {
|
|
574
684
|
if (!groups[f.groupId]) groups[f.groupId] = [];
|
|
575
685
|
groups[f.groupId].push({ feature: f, index: i });
|
|
@@ -583,7 +693,6 @@ export class FeatureTool implements Extension {
|
|
|
583
693
|
feature: DielineFeature,
|
|
584
694
|
pos: { x: number; y: number },
|
|
585
695
|
) => {
|
|
586
|
-
// Features are in the same unit as geometry.unit
|
|
587
696
|
const featureScale = scale;
|
|
588
697
|
|
|
589
698
|
const visualWidth = (feature.width || 10) * featureScale;
|
|
@@ -653,27 +762,6 @@ export class FeatureTool implements Extension {
|
|
|
653
762
|
data: { type: "feature-marker", index, isGroup: false },
|
|
654
763
|
});
|
|
655
764
|
|
|
656
|
-
// Auto-hide logic
|
|
657
|
-
marker.set("opacity", 0);
|
|
658
|
-
marker.on("mouseover", () => {
|
|
659
|
-
marker.set("opacity", 1);
|
|
660
|
-
canvas.requestRenderAll();
|
|
661
|
-
});
|
|
662
|
-
marker.on("mouseout", () => {
|
|
663
|
-
if (canvas.getActiveObject() !== marker) {
|
|
664
|
-
marker.set("opacity", 0);
|
|
665
|
-
canvas.requestRenderAll();
|
|
666
|
-
}
|
|
667
|
-
});
|
|
668
|
-
marker.on("selected", () => {
|
|
669
|
-
marker.set("opacity", 1);
|
|
670
|
-
canvas.requestRenderAll();
|
|
671
|
-
});
|
|
672
|
-
marker.on("deselected", () => {
|
|
673
|
-
marker.set("opacity", 0);
|
|
674
|
-
canvas.requestRenderAll();
|
|
675
|
-
});
|
|
676
|
-
|
|
677
765
|
canvas.add(marker);
|
|
678
766
|
canvas.bringObjectToFront(marker);
|
|
679
767
|
});
|
|
@@ -718,27 +806,6 @@ export class FeatureTool implements Extension {
|
|
|
718
806
|
},
|
|
719
807
|
});
|
|
720
808
|
|
|
721
|
-
// Auto-hide logic for group
|
|
722
|
-
groupObj.set("opacity", 0);
|
|
723
|
-
groupObj.on("mouseover", () => {
|
|
724
|
-
groupObj.set("opacity", 1);
|
|
725
|
-
canvas.requestRenderAll();
|
|
726
|
-
});
|
|
727
|
-
groupObj.on("mouseout", () => {
|
|
728
|
-
if (canvas.getActiveObject() !== groupObj) {
|
|
729
|
-
groupObj.set("opacity", 0);
|
|
730
|
-
canvas.requestRenderAll();
|
|
731
|
-
}
|
|
732
|
-
});
|
|
733
|
-
groupObj.on("selected", () => {
|
|
734
|
-
groupObj.set("opacity", 1);
|
|
735
|
-
canvas.requestRenderAll();
|
|
736
|
-
});
|
|
737
|
-
groupObj.on("deselected", () => {
|
|
738
|
-
groupObj.set("opacity", 0);
|
|
739
|
-
canvas.requestRenderAll();
|
|
740
|
-
});
|
|
741
|
-
|
|
742
809
|
canvas.add(groupObj);
|
|
743
810
|
canvas.bringObjectToFront(groupObj);
|
|
744
811
|
});
|
|
@@ -760,12 +827,12 @@ export class FeatureTool implements Extension {
|
|
|
760
827
|
if (marker.data?.isGroup) {
|
|
761
828
|
const indices = marker.data?.indices as number[];
|
|
762
829
|
if (indices && indices.length > 0) {
|
|
763
|
-
feature = this.
|
|
830
|
+
feature = this.workingFeatures[indices[0]];
|
|
764
831
|
}
|
|
765
832
|
} else {
|
|
766
833
|
const index = marker.data?.index;
|
|
767
834
|
if (index !== undefined) {
|
|
768
|
-
feature = this.
|
|
835
|
+
feature = this.workingFeatures[index];
|
|
769
836
|
}
|
|
770
837
|
}
|
|
771
838
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ConstraintContext, ConstraintFeature, ConstraintRegistry } from "./constraints";
|
|
2
|
+
|
|
3
|
+
export type FeatureCompleteIssue = {
|
|
4
|
+
featureId: string;
|
|
5
|
+
groupId?: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function validateFeaturesStrict(
|
|
10
|
+
features: ConstraintFeature[],
|
|
11
|
+
context: ConstraintContext,
|
|
12
|
+
): { ok: boolean; issues?: FeatureCompleteIssue[] } {
|
|
13
|
+
const eps = 1e-6;
|
|
14
|
+
const issues: FeatureCompleteIssue[] = [];
|
|
15
|
+
|
|
16
|
+
for (const f of features) {
|
|
17
|
+
if (!f.constraints?.type) continue;
|
|
18
|
+
const constrained = ConstraintRegistry.apply(f.x, f.y, f, context);
|
|
19
|
+
if (
|
|
20
|
+
Math.abs(constrained.x - f.x) > eps ||
|
|
21
|
+
Math.abs(constrained.y - f.y) > eps
|
|
22
|
+
) {
|
|
23
|
+
issues.push({
|
|
24
|
+
featureId: f.id,
|
|
25
|
+
groupId: f.groupId,
|
|
26
|
+
reason: "Position violates constraint strategy",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { ok: issues.length === 0, issues: issues.length ? issues : undefined };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function completeFeaturesStrict(
|
|
35
|
+
features: ConstraintFeature[],
|
|
36
|
+
context: ConstraintContext,
|
|
37
|
+
update: (nextFeatures: ConstraintFeature[]) => void,
|
|
38
|
+
): { ok: boolean; issues?: FeatureCompleteIssue[] } {
|
|
39
|
+
const validation = validateFeaturesStrict(features, context);
|
|
40
|
+
if (!validation.ok) return validation;
|
|
41
|
+
const next = JSON.parse(JSON.stringify(features || [])) as ConstraintFeature[];
|
|
42
|
+
update(next);
|
|
43
|
+
return { ok: true };
|
|
44
|
+
}
|
|
45
|
+
|