@design-core/core 51.2.0 → 52.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.
Files changed (51) hide show
  1. package/core/dimensions/alignedDimension.js +3 -1
  2. package/core/dimensions/angularDimension.js +118 -36
  3. package/core/dimensions/circularDimensionBase.js +32 -12
  4. package/core/dimensions/diametricDimension.js +29 -1
  5. package/core/dimensions/dimension.js +58 -5
  6. package/core/dimensions/dimensionBase.js +107 -4
  7. package/core/dimensions/linearDimensionBase.js +75 -37
  8. package/core/dimensions/radialDimension.js +26 -1
  9. package/core/dimensions/rotatedDimension.js +6 -3
  10. package/core/entities/entity.js +9 -0
  11. package/core/lib/auxiliary/snapPoint.js +9 -0
  12. package/core/lib/canvas.js +22 -0
  13. package/core/lib/commandManager.js +4 -4
  14. package/core/lib/dxf/dxf.js +8 -2
  15. package/core/lib/inputManager.js +23 -3
  16. package/core/lib/renderers/pdfRenderer.js +17 -1
  17. package/core/lib/scene.js +2 -0
  18. package/core/lib/selectionManager.js +28 -0
  19. package/core/lib/strings.js +1 -0
  20. package/core/properties/dimType.js +14 -0
  21. package/core/properties/property.js +1 -0
  22. package/core/tables/dimStyle.js +32 -2
  23. package/core/tables/dimStyleManager.js +2 -0
  24. package/core/tables/styleManagerBase.js +17 -3
  25. package/core/tables/tableManagerBase.js +24 -2
  26. package/core/tools/gripStretch.js +97 -0
  27. package/core/tools/offset.js +7 -1
  28. package/core/tools/tool.js +10 -0
  29. package/cspell.json +3 -0
  30. package/package.json +1 -1
  31. package/test/dimensions/alignedDimension.test.js +165 -0
  32. package/test/dimensions/angularDimension.test.js +149 -0
  33. package/test/dimensions/diametricDimension.test.js +77 -4
  34. package/test/dimensions/dimension.test.js +130 -0
  35. package/test/dimensions/dimensionBase.test.js +97 -4
  36. package/test/dimensions/radialDimension.test.js +34 -0
  37. package/test/dimensions/rotatedDimension.test.js +80 -0
  38. package/test/integration/exportIntegration.reference.dxf +3 -9
  39. package/test/integration/exportIntegration.test.js +1 -0
  40. package/test/integration/userInputIntegration.reference.dxf +3 -9
  41. package/test/integration/userInputIntegration.test.js +1 -0
  42. package/test/lib/inputManager.test.js +316 -1
  43. package/test/lib/selectionManager.test.js +42 -0
  44. package/test/renderers/pdfRenderer.test.js +26 -0
  45. package/test/tables/dimStyleManager.test.js +90 -0
  46. package/test/tables/dimstyle.test.js +32 -0
  47. package/test/tables/tableManagerBase.test.js +167 -0
  48. package/test/tools/gripStretch.test.js +108 -0
  49. package/test/tools/offset.test.js +2 -0
  50. package/test/tools/purge.test.js +3 -1
  51. package/test/tools/tool.test.js +25 -0
@@ -6,6 +6,8 @@ import { Point } from '../../core/entities/point.js';
6
6
  import { PromptOptions } from '../../core/lib/inputManager.js';
7
7
  import { Input, MouseDown, MouseUp } from '../../core/lib/inputManager.js';
8
8
  import { SingleSelection } from '../../core/lib/selectionManager.js';
9
+ import { DimensionBase } from '../../core/dimensions/dimensionBase.js';
10
+ import { GripStretch } from '../../core/tools/gripStretch.js';
9
11
  import { expect, jest } from '@jest/globals';
10
12
 
11
13
  const core = new Core();
@@ -269,6 +271,30 @@ test('starting a tool command from idle preserves the selection', () => {
269
271
  inputManager.reset();
270
272
  });
271
273
 
274
+ // Regression test: startCommand() generalises cancelling a previously active command to
275
+ // `activeCommand.cancel()` for any Tool, instead of special-casing GripStretch - Tool's default
276
+ // cancel() just delegates to the same reset() the old `else` branch called directly, so this
277
+ // must behave identically for every other Tool (Move, Erase, Offset, etc.)
278
+ test('starting a new command calls cancel() on a previously active Tool', () => {
279
+ inputManager.reset();
280
+ const Erase = DesignCore.CommandManager.createNew('Erase').constructor;
281
+ jest.spyOn(Erase.prototype, 'execute').mockImplementation(() => {});
282
+ inputManager.onCommand('E');
283
+ expect(inputManager.activeCommand).toBeInstanceOf(Erase);
284
+
285
+ const cancelSpy = jest.spyOn(inputManager.activeCommand, 'cancel');
286
+ const resetSpy = jest.spyOn(inputManager, 'reset');
287
+
288
+ inputManager.onCommand('M');
289
+
290
+ expect(cancelSpy).toHaveBeenCalledTimes(1);
291
+ // Tool's default cancel() delegates to the very same reset()
292
+ expect(resetSpy).toHaveBeenCalled();
293
+
294
+ jest.restoreAllMocks();
295
+ inputManager.reset();
296
+ });
297
+
272
298
  // ─── onCommand - command shortcut while non-accepting prompt is active ─────────
273
299
 
274
300
  test('onCommand with valid shortcut while POINT prompt active - switches command without notifying', () => {
@@ -877,6 +903,45 @@ test('mouseMoved skips highlightEntityUnderMouse when snapping is active and sna
877
903
  inputManager.reset();
878
904
  });
879
905
 
906
+ test('mouseMoved still calls highlightEntityUnderMouse when a command preview exists but the prompt also accepts SINGLESELECTION', () => {
907
+ // Reproduces the DIM command's second selection prompt: a candidate dimension preview is
908
+ // shown (from the first selected entity) while the prompt still accepts a second selection.
909
+ inputManager.reset();
910
+ inputManager.activeCommand = new Line();
911
+ const po = new PromptOptions('', [Input.Type.POINT, Input.Type.SINGLESELECTION]);
912
+ inputManager.requestInput(po);
913
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(undefined);
914
+ jest.spyOn(DesignCore.Scene.previewEntities, 'count').mockReturnValue(1);
915
+ jest.spyOn(core.scene.selectionManager, 'findClosestItem').mockReturnValue(0);
916
+ jest.spyOn(core.scene.entities, 'get').mockReturnValue(new Line());
917
+ core.mouse.buttonOneDown = false;
918
+
919
+ inputManager.mouseMoved();
920
+
921
+ expect(core.scene.hoverEntities.count()).toBe(1);
922
+ jest.restoreAllMocks();
923
+ po.cancel();
924
+ inputManager.reset();
925
+ });
926
+
927
+ test('mouseMoved skips highlightEntityUnderMouse when a command preview exists and the prompt only accepts POINT', () => {
928
+ inputManager.reset();
929
+ inputManager.activeCommand = new Line();
930
+ const po = new PromptOptions('', [Input.Type.POINT]);
931
+ inputManager.requestInput(po);
932
+ jest.spyOn(inputManager.snapping, 'snap').mockReturnValue(undefined);
933
+ jest.spyOn(DesignCore.Scene.previewEntities, 'count').mockReturnValue(1);
934
+ const highlightSpy = jest.spyOn(inputManager, 'highlightEntityUnderMouse');
935
+ core.mouse.buttonOneDown = false;
936
+
937
+ inputManager.mouseMoved();
938
+
939
+ expect(highlightSpy).not.toHaveBeenCalled();
940
+ jest.restoreAllMocks();
941
+ po.cancel();
942
+ inputManager.reset();
943
+ });
944
+
880
945
  // ─── mouseDown – no MOUSEDOWN prompt ─────────────────────────────────────────
881
946
 
882
947
  test('mouseDown case 0 without MOUSEDOWN prompt does not attempt to resolve a prompt', () => {
@@ -929,6 +994,37 @@ test('mouseUp case 0 with no prompt and no active command and mouse not moved -
929
994
  windowSelectSpy.mockRestore();
930
995
  });
931
996
 
997
+ // Regression test: grip editing has neither an active command nor a prompt of its own, so
998
+ // mouseUp(0)'s (promptOption || activeCommand) guard used to skip over it and call windowSelect()
999
+ // whenever the mouse moved since mousedown - i.e. every click-drag-release used to grab a grip
1000
+ // (mousedown grabs it, drag moves it, mouseup released over a different point) also started a
1001
+ // crossing/window selection. GripStretch now marks grip editing as the active command, so the
1002
+ // existing guard correctly skips window-select during an in-progress drag too
1003
+ test('mouseUp case 0 does not call windowSelect while GripStretch is active', () => {
1004
+ inputManager.reset();
1005
+ selectDimensionWithGrip(10, 10);
1006
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(10, 10));
1007
+ inputManager.onLeftClick(new Point(10, 10));
1008
+ expect(inputManager.activeCommand).toBeInstanceOf(GripStretch);
1009
+
1010
+ const windowSelectSpy = jest.spyOn(inputManager, 'windowSelect').mockImplementation(() => {});
1011
+ core.mouse.mouseDownCanvasPoint = new Point(0, 0);
1012
+ core.mouse.x = 50;
1013
+ core.mouse.y = 50;
1014
+
1015
+ inputManager.mouseUp(0);
1016
+
1017
+ expect(windowSelectSpy).not.toHaveBeenCalled();
1018
+ // the grip is still stuck to the mouse - mouseUp must not interfere with the in-progress drag
1019
+ expect(inputManager.activeCommand).toBeInstanceOf(GripStretch);
1020
+
1021
+ windowSelectSpy.mockRestore();
1022
+ core.mouse.x = 0;
1023
+ core.mouse.y = 0;
1024
+ jest.restoreAllMocks();
1025
+ inputManager.reset();
1026
+ });
1027
+
932
1028
  // ─── highlightEntityUnderMouse – selection prompts ───────────────────────────
933
1029
 
934
1030
  test('highlightEntityUnderMouse returns true with SINGLESELECTION prompt and entity found', () => {
@@ -1006,7 +1102,226 @@ test('onLeftClick with SINGLESELECTION prompt and entity nearby - selects the en
1006
1102
  inputManager.reset();
1007
1103
  });
1008
1104
 
1009
- // ─── onEnterPressed – SELECTIONSET already accepted ──────────────────────────
1105
+ // ─── grip editing ─────────────────────────────────────────────────────────────
1106
+
1107
+ /**
1108
+ * Select a fresh DimensionBase with a Pt11 (text position) grip at the given coordinates.
1109
+ * @param {number} x
1110
+ * @param {number} y
1111
+ * @return {number} index of the entity in DesignCore.Scene.entities
1112
+ */
1113
+ function selectDimensionWithGrip(x, y) {
1114
+ core.scene.clear();
1115
+ const dim = new DimensionBase({});
1116
+ dim.points = [
1117
+ Object.assign(new Point(0, 0), { sequence: 13 }),
1118
+ Object.assign(new Point(x, y), { sequence: 11 }),
1119
+ ];
1120
+ DesignCore.Scene.entities.add(dim);
1121
+ core.scene.selectionManager.addToSelectionSet(0);
1122
+ return 0;
1123
+ }
1124
+
1125
+ // GripStretch's execute() resolves its "stretch point" prompt asynchronously (same as any
1126
+ // other Tool's requestInput) - flush the microtask queue after a drop click before asserting.
1127
+ const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
1128
+
1129
+ test('onLeftClick grabs a grip under the mouse and starts GripStretch', () => {
1130
+ inputManager.reset();
1131
+ selectDimensionWithGrip(10, 10);
1132
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(10, 10));
1133
+ const singleSelectSpy = jest.spyOn(core.scene.selectionManager, 'singleSelect');
1134
+
1135
+ inputManager.onLeftClick(new Point(10, 10));
1136
+
1137
+ expect(inputManager.activeCommand).toBeInstanceOf(GripStretch);
1138
+ expect(inputManager.activeCommand.entityIndex).toBe(0);
1139
+ expect(singleSelectSpy).not.toHaveBeenCalled();
1140
+ jest.restoreAllMocks();
1141
+ inputManager.reset();
1142
+ });
1143
+
1144
+ test('mouseMoved updates the selected-entity preview clone while GripStretch is active', () => {
1145
+ inputManager.reset();
1146
+ selectDimensionWithGrip(10, 10);
1147
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(10, 10));
1148
+ inputManager.onLeftClick(new Point(10, 10));
1149
+ expect(inputManager.activeCommand).toBeInstanceOf(GripStretch);
1150
+
1151
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(50, 60));
1152
+ inputManager.mouseMoved();
1153
+
1154
+ const previewEntity = core.scene.selectionManager.selectedEntities[0];
1155
+ const previewPt11 = previewEntity.getPointBySequence(previewEntity.points, 11);
1156
+ expect(previewPt11.x).toBe(50);
1157
+ expect(previewPt11.y).toBe(60);
1158
+
1159
+ // the real scene entity is untouched until drop
1160
+ const realPt11 = core.scene.entities.get(0).getPointBySequence(core.scene.entities.get(0).points, 11);
1161
+ expect(realPt11.x).toBe(10);
1162
+ expect(realPt11.y).toBe(10);
1163
+
1164
+ jest.restoreAllMocks();
1165
+ inputManager.reset();
1166
+ });
1167
+
1168
+ test('onLeftClick drops an active grip, committing the new point to the real entity', async () => {
1169
+ inputManager.reset();
1170
+ selectDimensionWithGrip(10, 10);
1171
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(10, 10));
1172
+ inputManager.onLeftClick(new Point(10, 10));
1173
+ expect(inputManager.activeCommand).toBeInstanceOf(GripStretch);
1174
+
1175
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(50, 60));
1176
+ inputManager.onLeftClick(new Point(50, 60));
1177
+ await flushPromises();
1178
+
1179
+ expect(inputManager.activeCommand).toBeUndefined();
1180
+ const realEntity = core.scene.entities.get(0);
1181
+ const realPt11 = realEntity.getPointBySequence(realEntity.points, 11);
1182
+ expect(realPt11.x).toBe(50);
1183
+ expect(realPt11.y).toBe(60);
1184
+
1185
+ jest.restoreAllMocks();
1186
+ inputManager.reset();
1187
+ });
1188
+
1189
+ // Regression test: requestInput() sets inputManager.inputPoint to the dropped point (needed for
1190
+ // polar/ortho tracking), but GripStretch's completion path bypasses the standard reset() that
1191
+ // normally clears it - leaving the next command's first point (or the next grip drag's tracking
1192
+ // line) anchored to this stale drop position instead of starting fresh
1193
+ test('onLeftClick drops an active grip clears inputPoint, not just activeCommand', async () => {
1194
+ inputManager.reset();
1195
+ selectDimensionWithGrip(10, 10);
1196
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(10, 10));
1197
+ inputManager.onLeftClick(new Point(10, 10));
1198
+
1199
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(50, 60));
1200
+ inputManager.onLeftClick(new Point(50, 60));
1201
+ await flushPromises();
1202
+
1203
+ expect(inputManager.inputPoint).toBeNull();
1204
+
1205
+ jest.restoreAllMocks();
1206
+ inputManager.reset();
1207
+ });
1208
+
1209
+ // Regression test: the grip drop commits only 'points' via UpdateState, but DimensionBase's
1210
+ // points setter also flips dimType.userPositionedText as a side effect. Since State.do() only
1211
+ // snapshots the properties explicitly listed in the commit, undo used to restore the old points
1212
+ // while leaving the flag true - reverting the points but not the (now stale) automatic-placement
1213
+ // text layout. userPositionedText must now be committed explicitly alongside points so its
1214
+ // pre-drag value (false, for an initially auto-positioned dimension) is captured for undo too.
1215
+ // Also covers Scene.undo()/redo() reloading the retained selection preview clone, which used to
1216
+ // stay stuck at the dropped position while findGripUnderMouse() hit-tested the restored geometry
1217
+ test('undo after a grip drop restores both points and the original auto-positioned state', async () => {
1218
+ inputManager.reset();
1219
+ const entityIndex = selectDimensionWithGrip(10, 10);
1220
+ const entity = core.scene.entities.get(entityIndex);
1221
+ expect(entity.dimType.hasUserPositionedText()).toBe(false);
1222
+
1223
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(10, 10));
1224
+ inputManager.onLeftClick(new Point(10, 10));
1225
+
1226
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(50, 60));
1227
+ inputManager.onLeftClick(new Point(50, 60));
1228
+ await flushPromises();
1229
+
1230
+ const draggedEntity = core.scene.entities.get(entityIndex);
1231
+ expect(draggedEntity.dimType.hasUserPositionedText()).toBe(true);
1232
+ const draggedPt11 = draggedEntity.getPointBySequence(draggedEntity.points, 11);
1233
+ expect(draggedPt11.x).toBe(50);
1234
+ expect(draggedPt11.y).toBe(60);
1235
+
1236
+ core.scene.undo();
1237
+
1238
+ const restoredEntity = core.scene.entities.get(entityIndex);
1239
+ const restoredPt11 = restoredEntity.getPointBySequence(restoredEntity.points, 11);
1240
+ expect(restoredPt11.x).toBe(10);
1241
+ expect(restoredPt11.y).toBe(10);
1242
+ expect(restoredEntity.dimType.hasUserPositionedText()).toBe(false);
1243
+
1244
+ // the retained selection preview clone must be reloaded too, not just the real entity -
1245
+ // otherwise Canvas keeps drawing the dropped clone while the grip hit-test uses the restored one
1246
+ const restoredClone = core.scene.selectionManager.selectedEntities[0];
1247
+ const restoredClonePt11 = restoredClone.getPointBySequence(restoredClone.points, 11);
1248
+ expect(restoredClonePt11.x).toBe(10);
1249
+ expect(restoredClonePt11.y).toBe(10);
1250
+ expect(core.scene.selectionManager.findGripUnderMouse(new Point(10, 10))).toBeDefined();
1251
+
1252
+ core.scene.redo();
1253
+
1254
+ const redoneEntity = core.scene.entities.get(entityIndex);
1255
+ expect(redoneEntity.dimType.hasUserPositionedText()).toBe(true);
1256
+ const redonePt11 = redoneEntity.getPointBySequence(redoneEntity.points, 11);
1257
+ expect(redonePt11.x).toBe(50);
1258
+ expect(redonePt11.y).toBe(60);
1259
+
1260
+ const redoneClone = core.scene.selectionManager.selectedEntities[0];
1261
+ const redoneClonePt11 = redoneClone.getPointBySequence(redoneClone.points, 11);
1262
+ expect(redoneClonePt11.x).toBe(50);
1263
+ expect(redoneClonePt11.y).toBe(60);
1264
+ expect(core.scene.selectionManager.findGripUnderMouse(new Point(50, 60))).toBeDefined();
1265
+
1266
+ jest.restoreAllMocks();
1267
+ inputManager.reset();
1268
+ });
1269
+
1270
+ // Regression test: starting a command while a grip is stuck to the mouse used to leave
1271
+ // activeGrip set, because startCommand() only reset state when activeCommand was already
1272
+ // defined - but a grip can only be grabbed while idle (activeCommand undefined). The dangling
1273
+ // grip then suppressed the new command's preview during mouse movement, and its next click
1274
+ // dropped the grip instead of answering the command's prompt
1275
+ test('startCommand cancels an active grip and reloads the selection previews', () => {
1276
+ inputManager.reset();
1277
+ selectDimensionWithGrip(10, 10);
1278
+
1279
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(10, 10));
1280
+ inputManager.onLeftClick(new Point(10, 10));
1281
+ expect(inputManager.activeCommand).toBeInstanceOf(GripStretch);
1282
+
1283
+ // drag the grip - this mutates only the live preview clone's Pt11, not the real entity
1284
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(80, 90));
1285
+ inputManager.mouseMoved();
1286
+ const previewEntity = core.scene.selectionManager.selectedEntities[0];
1287
+ const draggedPreviewPt11 = previewEntity.getPointBySequence(previewEntity.points, 11);
1288
+ expect(draggedPreviewPt11.x).toBe(80);
1289
+
1290
+ // Prevent Move.execute() from running so we can isolate startCommand's own behaviour.
1291
+ const Move = DesignCore.CommandManager.createNew('Move').constructor;
1292
+ jest.spyOn(Move.prototype, 'execute').mockImplementation(() => {});
1293
+
1294
+ inputManager.startCommand('Move');
1295
+
1296
+ expect(inputManager.activeCommand).toBeInstanceOf(Move);
1297
+
1298
+ // the preview clone must be reloaded from the real (undragged) entity, not left dragged
1299
+ const reloadedEntity = core.scene.selectionManager.selectedEntities[0];
1300
+ const reloadedPt11 = reloadedEntity.getPointBySequence(reloadedEntity.points, 11);
1301
+ expect(reloadedPt11.x).toBe(10);
1302
+ expect(reloadedPt11.y).toBe(10);
1303
+
1304
+ jest.restoreAllMocks();
1305
+ inputManager.reset();
1306
+ });
1307
+
1308
+ // Regression test: pressing Enter while a grip is active ends GripStretch via the normal
1309
+ // reset() path, deselecting the entity - this matches ending any other command on Enter
1310
+ test('onEnterPressed ends an active grip and deselects the entity', () => {
1311
+ inputManager.reset();
1312
+ selectDimensionWithGrip(10, 10);
1313
+ jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(10, 10));
1314
+ inputManager.onLeftClick(new Point(10, 10));
1315
+ expect(inputManager.activeCommand).toBeInstanceOf(GripStretch);
1316
+
1317
+ inputManager.onEnterPressed();
1318
+
1319
+ expect(inputManager.activeCommand).toBeUndefined();
1320
+ expect(core.scene.selectionManager.selectionSet.selectionSet.length).toBe(0);
1321
+
1322
+ jest.restoreAllMocks();
1323
+ inputManager.reset();
1324
+ });
1010
1325
 
1011
1326
  test('onEnterPressed with SELECTIONSET prompt already accepted - resets', () => {
1012
1327
  inputManager.reset();
@@ -1,5 +1,6 @@
1
1
  import { Core } from '../../core/core/core.js';
2
2
  import { Point } from '../../core/entities/point.js';
3
+ import { DimensionBase } from '../../core/dimensions/dimensionBase.js';
3
4
 
4
5
  const core = new Core();
5
6
  const selectionManager = core.scene.selectionManager;
@@ -71,3 +72,44 @@ test('Test SelectionManager.selectAll()', () => {
71
72
  expect(selectionManager.selectionSet.selectionSet).toEqual([0, 1, 2]);
72
73
  });
73
74
 
75
+ // ─── findGripUnderMouse ───────────────────────────────────────────────────────
76
+
77
+ /**
78
+ * Select a fresh DimensionBase with a Pt11 (text position) grip at the given coordinates.
79
+ * @param {number} x
80
+ * @param {number} y
81
+ * @return {number} index of the entity in core.scene.entities
82
+ */
83
+ function selectDimensionWithGrip(x, y) {
84
+ selectionManager.reset();
85
+ const dim = new DimensionBase({});
86
+ dim.points = [
87
+ Object.assign(new Point(0, 0), { sequence: 13 }),
88
+ Object.assign(new Point(x, y), { sequence: 11 }),
89
+ ];
90
+ core.scene.entities.add(dim);
91
+ const entityIndex = core.scene.entities.count() - 1;
92
+ selectionManager.addToSelectionSet(entityIndex);
93
+ return entityIndex;
94
+ }
95
+
96
+ test('findGripUnderMouse returns undefined when no entity is selected', () => {
97
+ selectionManager.reset();
98
+ expect(selectionManager.findGripUnderMouse(new Point(0, 0))).toBeUndefined();
99
+ });
100
+
101
+ test('findGripUnderMouse returns the grip when the point is within tolerance', () => {
102
+ const entityIndex = selectDimensionWithGrip(10, 10);
103
+ const found = selectionManager.findGripUnderMouse(new Point(10, 10));
104
+ expect(found).toBeDefined();
105
+ expect(found.entityIndex).toBe(entityIndex);
106
+ expect(found.grip.point.x).toBe(10);
107
+ expect(found.grip.point.y).toBe(10);
108
+ });
109
+
110
+ test('findGripUnderMouse returns undefined when the point is far from any grip', () => {
111
+ selectDimensionWithGrip(10, 10);
112
+ expect(selectionManager.findGripUnderMouse(new Point(1000, 1000))).toBeUndefined();
113
+ });
114
+
115
+
@@ -178,6 +178,32 @@ describe('PdfRenderer — drawing', () => {
178
178
  // cos(π/4) ≈ 0.7071, sin(π/4) ≈ 0.7071
179
179
  expect(output).toContain('0.7071');
180
180
  });
181
+
182
+ test('drawText encodes non-ASCII characters as WinAnsi octal escapes', () => {
183
+ const renderer = new PdfRenderer(100, 100);
184
+ renderer.drawText([
185
+ { char: '\u00B0', x: 0, y: 0, rotation: 0 },
186
+ { char: '\u00D8', x: 0, y: 0, rotation: 0 },
187
+ ], 'Helvetica', 5);
188
+ const output = renderer.getOutput();
189
+ expect(output).toContain('(\\260)');
190
+ expect(output).toContain('(\\330)');
191
+ expect(output).not.toMatch(/[^\x00-\x7f]/);
192
+ });
193
+
194
+ test.each([
195
+ ['\u2019', '\\222'], // right single quote
196
+ ['\u201C', '\\223'], // left double quote
197
+ ['\u2013', '\\226'], // en dash
198
+ ['\u2014', '\\227'], // em dash
199
+ ['\u20AC', '\\200'], // euro sign
200
+ ['\u2122', '\\231'], // trademark
201
+ ['\u4E2D', '\\077'], // unsupported, replaced with ?
202
+ ])('drawText maps %s to WinAnsi %s', (char, escape) => {
203
+ const renderer = new PdfRenderer(100, 100);
204
+ renderer.drawText([{ char, x: 0, y: 0, rotation: 0 }], 'Helvetica', 5);
205
+ expect(renderer.getOutput()).toContain(`(${escape})`);
206
+ });
181
207
  });
182
208
 
183
209
  describe('PdfRenderer — state methods', () => {
@@ -0,0 +1,90 @@
1
+ import { Core } from '../../core/core/core.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+
4
+ const core = new Core();
5
+ const dimStyleManager = core.dimStyleManager;
6
+
7
+ const dimensionData = (dimensionStyle) => ({
8
+ points: [new Point(), new Point(10, 0), new Point(5, 5)],
9
+ dimensionStyle: dimensionStyle,
10
+ });
11
+
12
+ beforeEach(() => {
13
+ while (core.scene.entities.count() > 0) {
14
+ core.scene.entities.remove(0);
15
+ }
16
+ });
17
+
18
+ test('Test DimStyleManager.deleteStyle removes only dimensions using the style', () => {
19
+ dimStyleManager.addItem({ name: 'CUSTOM' });
20
+
21
+ core.scene.addEntity('Line', { points: [new Point(), new Point(10, 10)] });
22
+ core.scene.addEntity('Circle', { points: [new Point()], radius: 5 });
23
+ core.scene.addEntity('RotatedDimension', dimensionData('CUSTOM'));
24
+ core.scene.addEntity('RotatedDimension', dimensionData('STANDARD'));
25
+ expect(core.scene.entities.count()).toBe(4);
26
+
27
+ dimStyleManager.deleteStyle(dimStyleManager.getItemIndex('CUSTOM'));
28
+
29
+ expect(dimStyleManager.itemExists('CUSTOM')).toBe(false);
30
+ const types = core.scene.entities.get(0).type + ',' + core.scene.entities.get(1).type + ',' + core.scene.entities.get(2).type;
31
+ expect(core.scene.entities.count()).toBe(3);
32
+ expect(types).toBe('Line,Circle,RotatedDimension');
33
+ expect(core.scene.entities.get(2).getProperty('dimensionStyle')).toBe('STANDARD');
34
+ });
35
+
36
+ test('Test DimStyleManager.deleteStyle matches style names case-insensitively', () => {
37
+ dimStyleManager.addItem({ name: 'MixedCase' });
38
+ core.scene.addEntity('RotatedDimension', dimensionData('MIXEDCASE'));
39
+
40
+ dimStyleManager.deleteStyle(dimStyleManager.getItemIndex('MixedCase'));
41
+
42
+ expect(core.scene.entities.count()).toBe(0);
43
+ });
44
+
45
+ test('Test DimStyleManager.deleteStyle resets the current style when it is deleted', () => {
46
+ dimStyleManager.addItem({ name: 'CURRENT' });
47
+ dimStyleManager.setCstyle('CURRENT');
48
+
49
+ dimStyleManager.deleteStyle(dimStyleManager.getItemIndex('CURRENT'));
50
+
51
+ expect(dimStyleManager.getCstyle()).toBe(dimStyleManager.getItems()[0].name);
52
+ });
53
+
54
+ test('Test DimStyleManager.deleteStyle keeps indelible styles and their dimensions', () => {
55
+ core.scene.addEntity('RotatedDimension', dimensionData('STANDARD'));
56
+
57
+ dimStyleManager.deleteStyle(dimStyleManager.getItemIndex('STANDARD'), false);
58
+
59
+ expect(dimStyleManager.itemExists('STANDARD')).toBe(true);
60
+ expect(core.scene.entities.count()).toBe(1);
61
+ });
62
+
63
+ test('Test DimStyleManager.renameStyle updates dimensions through the property API', () => {
64
+ dimStyleManager.addItem({ name: 'OLDNAME' });
65
+ core.scene.addEntity('Line', { points: [new Point(), new Point(10, 10)] });
66
+ core.scene.addEntity('RotatedDimension', dimensionData('OLDNAME'));
67
+ core.scene.addEntity('RotatedDimension', dimensionData('STANDARD'));
68
+
69
+ dimStyleManager.renameStyle(dimStyleManager.getItemIndex('OLDNAME'), 'NEWNAME');
70
+
71
+ const dimension = core.scene.entities.get(1);
72
+ expect(dimension.getProperty('dimensionStyle')).toBe('NEWNAME');
73
+ expect(Object.hasOwn(dimension, 'dimensionStyle')).toBe(false);
74
+ expect(core.scene.entities.get(2).getProperty('dimensionStyle')).toBe('STANDARD');
75
+ expect(dimension.getDimensionStyle()?.name).toBe('NEWNAME');
76
+ });
77
+
78
+ test('Test DimStyleManager defaults to the ISO-25 current style', () => {
79
+ expect(new Core().dimStyleManager.getCstyle()).toBe('ISO-25');
80
+ });
81
+
82
+ test('Test DimStyleManager.purge keeps the current style', () => {
83
+ dimStyleManager.setCstyle('ISO-25');
84
+
85
+ dimStyleManager.purge();
86
+
87
+ expect(dimStyleManager.itemExists('ISO-25')).toBe(true);
88
+ expect(dimStyleManager.getCstyle()).toBe('ISO-25');
89
+ expect(dimStyleManager.itemExists('ANNOTATIVE')).toBe(false);
90
+ });
@@ -1,4 +1,5 @@
1
1
  import { DimStyle } from '../../core/tables/dimStyle';
2
+ import { DesignCore } from '../../core/designCore.js';
2
3
 
3
4
  describe('DimStyle', () => {
4
5
  let style;
@@ -67,6 +68,37 @@ describe('DimStyle', () => {
67
68
  style.setValue('customProp', 'hello');
68
69
  expect(style.customProp).toBeUndefined();
69
70
  });
71
+
72
+ describe('getTextStyleHandle', () => {
73
+ beforeEach(() => {
74
+ DesignCore.Core = {
75
+ styleManager: {
76
+ getItems: () => [
77
+ { name: 'STANDARD', handle: '19' },
78
+ { name: '19', handle: '2A' },
79
+ ],
80
+ },
81
+ };
82
+ });
83
+
84
+ test('resolves a style name by name', () => {
85
+ expect(new DimStyle({ DIMTXSTY: '19' }).getTextStyleHandle()).toBe('2A');
86
+ });
87
+
88
+ test('resolves an imported group 340 value strictly as a handle', () => {
89
+ expect(new DimStyle({ 340: '19' }).getTextStyleHandle()).toBe('19');
90
+ });
91
+
92
+ test('falls back to the Standard style when the reference is unknown', () => {
93
+ expect(new DimStyle({ 340: 'FF' }).getTextStyleHandle()).toBe('19');
94
+ });
95
+
96
+ test('setValue switches DIMTXSTY back to a name reference', () => {
97
+ const dimStyle = new DimStyle({ 340: '19' });
98
+ dimStyle.setValue('DIMTXSTY', '19');
99
+ expect(dimStyle.getTextStyleHandle()).toBe('2A');
100
+ });
101
+ });
70
102
  });
71
103
 
72
104