@design-core/core 51.2.0 → 52.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/dimensions/alignedDimension.js +3 -1
- package/core/dimensions/angularDimension.js +118 -36
- package/core/dimensions/circularDimensionBase.js +32 -12
- package/core/dimensions/diametricDimension.js +29 -1
- package/core/dimensions/dimension.js +58 -5
- package/core/dimensions/dimensionBase.js +107 -4
- package/core/dimensions/linearDimensionBase.js +75 -37
- package/core/dimensions/radialDimension.js +26 -1
- package/core/dimensions/rotatedDimension.js +6 -3
- package/core/entities/entity.js +9 -0
- package/core/lib/auxiliary/snapPoint.js +9 -0
- package/core/lib/canvas.js +22 -0
- package/core/lib/commandManager.js +4 -4
- package/core/lib/dxf/dxf.js +8 -2
- package/core/lib/inputManager.js +23 -3
- package/core/lib/renderers/pdfRenderer.js +17 -1
- package/core/lib/scene.js +2 -0
- package/core/lib/selectionManager.js +28 -0
- package/core/lib/strings.js +1 -0
- package/core/properties/dimType.js +14 -0
- package/core/properties/property.js +1 -0
- package/core/tables/dimStyle.js +32 -2
- package/core/tools/gripStretch.js +97 -0
- package/core/tools/offset.js +7 -1
- package/core/tools/tool.js +10 -0
- package/cspell.json +3 -0
- package/package.json +1 -1
- package/test/dimensions/alignedDimension.test.js +165 -0
- package/test/dimensions/angularDimension.test.js +149 -0
- package/test/dimensions/diametricDimension.test.js +76 -4
- package/test/dimensions/dimension.test.js +130 -0
- package/test/dimensions/dimensionBase.test.js +97 -4
- package/test/dimensions/radialDimension.test.js +33 -0
- package/test/dimensions/rotatedDimension.test.js +80 -0
- package/test/integration/exportIntegration.reference.dxf +3 -9
- package/test/integration/userInputIntegration.reference.dxf +3 -9
- package/test/lib/inputManager.test.js +316 -1
- package/test/lib/selectionManager.test.js +42 -0
- package/test/renderers/pdfRenderer.test.js +26 -0
- package/test/tables/dimstyle.test.js +32 -0
- package/test/tools/gripStretch.test.js +108 -0
- package/test/tools/offset.test.js +2 -0
- 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
|
-
// ───
|
|
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', () => {
|
|
@@ -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
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Tool } from '../../core/tools/tool.js';
|
|
2
|
+
import { GripStretch } from '../../core/tools/gripStretch.js';
|
|
3
|
+
import { Core } from '../../core/core/core.js';
|
|
4
|
+
import { DesignCore } from '../../core/designCore.js';
|
|
5
|
+
import { DimensionBase } from '../../core/dimensions/dimensionBase.js';
|
|
6
|
+
import { Point } from '../../core/entities/point.js';
|
|
7
|
+
import { jest } from '@jest/globals';
|
|
8
|
+
|
|
9
|
+
const core = new Core();
|
|
10
|
+
|
|
11
|
+
test('GripStretch is a Tool with type GripStretch', () => {
|
|
12
|
+
const gripStretch = new GripStretch();
|
|
13
|
+
expect(gripStretch).toBeInstanceOf(Tool);
|
|
14
|
+
expect(gripStretch.type).toBe('GripStretch');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('GripStretch is not registered as a typeable command', () => {
|
|
18
|
+
expect(typeof GripStretch.register).toBe('undefined');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('GripStretch.action commits the dropped grip position to the real entity', () => {
|
|
22
|
+
core.scene.clear();
|
|
23
|
+
const dim = new DimensionBase({});
|
|
24
|
+
dim.points = [
|
|
25
|
+
Object.assign(new Point(0, 0), { sequence: 13 }),
|
|
26
|
+
Object.assign(new Point(10, 10), { sequence: 11 }),
|
|
27
|
+
];
|
|
28
|
+
DesignCore.Scene.entities.add(dim);
|
|
29
|
+
core.scene.selectionManager.addToSelectionSet(0);
|
|
30
|
+
|
|
31
|
+
const grip = dim.getGrips()[0];
|
|
32
|
+
const gripStretch = new GripStretch(0, grip);
|
|
33
|
+
gripStretch.points.push(new Point(50, 60));
|
|
34
|
+
|
|
35
|
+
gripStretch.action();
|
|
36
|
+
|
|
37
|
+
const entity = core.scene.entities.get(0);
|
|
38
|
+
const pt11 = entity.getPointBySequence(entity.points, 11);
|
|
39
|
+
expect(pt11.x).toBe(50);
|
|
40
|
+
expect(pt11.y).toBe(60);
|
|
41
|
+
expect(entity.dimType.hasUserPositionedText()).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('GripStretch.preview live-updates the selected-entity preview clone, not the real entity', () => {
|
|
45
|
+
core.scene.clear();
|
|
46
|
+
const dim = new DimensionBase({});
|
|
47
|
+
dim.points = [
|
|
48
|
+
Object.assign(new Point(0, 0), { sequence: 13 }),
|
|
49
|
+
Object.assign(new Point(10, 10), { sequence: 11 }),
|
|
50
|
+
];
|
|
51
|
+
DesignCore.Scene.entities.add(dim);
|
|
52
|
+
core.scene.selectionManager.addToSelectionSet(0);
|
|
53
|
+
|
|
54
|
+
const grip = dim.getGrips()[0];
|
|
55
|
+
const gripStretch = new GripStretch(0, grip);
|
|
56
|
+
jest.spyOn(DesignCore.Mouse, 'pointOnScene').mockReturnValue(new Point(50, 60));
|
|
57
|
+
|
|
58
|
+
gripStretch.preview();
|
|
59
|
+
|
|
60
|
+
const previewEntity = core.scene.selectionManager.selectedEntities[0];
|
|
61
|
+
const previewPt11 = previewEntity.getPointBySequence(previewEntity.points, 11);
|
|
62
|
+
expect(previewPt11.x).toBe(50);
|
|
63
|
+
expect(previewPt11.y).toBe(60);
|
|
64
|
+
|
|
65
|
+
const realPt11 = core.scene.entities.get(0).getPointBySequence(core.scene.entities.get(0).points, 11);
|
|
66
|
+
expect(realPt11.x).toBe(10);
|
|
67
|
+
expect(realPt11.y).toBe(10);
|
|
68
|
+
|
|
69
|
+
jest.restoreAllMocks();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('GripStretch.execute requests the stretch point then commits, keeping the entity selected', async () => {
|
|
73
|
+
core.scene.clear();
|
|
74
|
+
const dim = new DimensionBase({});
|
|
75
|
+
dim.points = [
|
|
76
|
+
Object.assign(new Point(0, 0), { sequence: 13 }),
|
|
77
|
+
Object.assign(new Point(10, 10), { sequence: 11 }),
|
|
78
|
+
];
|
|
79
|
+
DesignCore.Scene.entities.add(dim);
|
|
80
|
+
core.scene.selectionManager.addToSelectionSet(0);
|
|
81
|
+
|
|
82
|
+
const grip = dim.getGrips()[0];
|
|
83
|
+
const gripStretch = new GripStretch(0, grip);
|
|
84
|
+
|
|
85
|
+
const origInputManager = DesignCore.Scene.inputManager;
|
|
86
|
+
DesignCore.Scene.inputManager = {
|
|
87
|
+
requestInput: async (op) => {
|
|
88
|
+
expect(op.promptMessage).toBe('Specify stretch point');
|
|
89
|
+
return new Point(50, 60);
|
|
90
|
+
},
|
|
91
|
+
actionCommand: () => gripStretch.action(),
|
|
92
|
+
refreshPreview: () => {},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await gripStretch.execute();
|
|
97
|
+
} finally {
|
|
98
|
+
DesignCore.Scene.inputManager = origInputManager;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const entity = core.scene.entities.get(0);
|
|
102
|
+
const pt11 = entity.getPointBySequence(entity.points, 11);
|
|
103
|
+
expect(pt11.x).toBe(50);
|
|
104
|
+
expect(pt11.y).toBe(60);
|
|
105
|
+
// grip editing must leave the entity selected afterwards, unlike most commands
|
|
106
|
+
expect(core.scene.selectionManager.selectionSet.selectionSet).toContain(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
@@ -269,6 +269,7 @@ test('Test Offset.action - offset circle outward', () => {
|
|
|
269
269
|
expect(newCircle.points[0].y).toBeCloseTo(0);
|
|
270
270
|
// New radius should be 15
|
|
271
271
|
expect(newCircle.getProperty('radius')).toBeCloseTo(15);
|
|
272
|
+
expect(newCircle.type).toBe('Circle'); // not an Arc
|
|
272
273
|
});
|
|
273
274
|
|
|
274
275
|
test('Test Offset.action - offset circle inward', () => {
|
|
@@ -288,6 +289,7 @@ test('Test Offset.action - offset circle inward', () => {
|
|
|
288
289
|
expect(newCircle.points[0].x).toBeCloseTo(0);
|
|
289
290
|
expect(newCircle.points[0].y).toBeCloseTo(0);
|
|
290
291
|
expect(newCircle.getProperty('radius')).toBeCloseTo(7);
|
|
292
|
+
expect(newCircle.type).toBe('Circle'); // not an Arc
|
|
291
293
|
});
|
|
292
294
|
|
|
293
295
|
test('Test Offset.action - offset circle inward returns null when radius would be zero', () => {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Core } from '../../core/core/core.js';
|
|
2
|
+
import { DesignCore } from '../../core/designCore.js';
|
|
3
|
+
import { Tool } from '../../core/tools/tool.js';
|
|
4
|
+
import { jest } from '@jest/globals';
|
|
5
|
+
|
|
6
|
+
new Core();
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
jest.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('Tool.cancel default delegates to inputManager.reset()', () => {
|
|
13
|
+
const tool = new Tool();
|
|
14
|
+
const resetSpy = jest.spyOn(DesignCore.Scene.inputManager, 'reset').mockImplementation(() => {});
|
|
15
|
+
|
|
16
|
+
tool.cancel();
|
|
17
|
+
|
|
18
|
+
expect(resetSpy).toHaveBeenCalledTimes(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('Tool.preview and Tool.action are no-ops by default', () => {
|
|
22
|
+
const tool = new Tool();
|
|
23
|
+
expect(() => tool.preview()).not.toThrow();
|
|
24
|
+
expect(() => tool.action()).not.toThrow();
|
|
25
|
+
});
|