@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.
Files changed (43) 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/tools/gripStretch.js +97 -0
  24. package/core/tools/offset.js +7 -1
  25. package/core/tools/tool.js +10 -0
  26. package/cspell.json +3 -0
  27. package/package.json +1 -1
  28. package/test/dimensions/alignedDimension.test.js +165 -0
  29. package/test/dimensions/angularDimension.test.js +149 -0
  30. package/test/dimensions/diametricDimension.test.js +76 -4
  31. package/test/dimensions/dimension.test.js +130 -0
  32. package/test/dimensions/dimensionBase.test.js +97 -4
  33. package/test/dimensions/radialDimension.test.js +33 -0
  34. package/test/dimensions/rotatedDimension.test.js +80 -0
  35. package/test/integration/exportIntegration.reference.dxf +3 -9
  36. package/test/integration/userInputIntegration.reference.dxf +3 -9
  37. package/test/lib/inputManager.test.js +316 -1
  38. package/test/lib/selectionManager.test.js +42 -0
  39. package/test/renderers/pdfRenderer.test.js +26 -0
  40. package/test/tables/dimstyle.test.js +32 -0
  41. package/test/tools/gripStretch.test.js +108 -0
  42. package/test/tools/offset.test.js +2 -0
  43. package/test/tools/tool.test.js +25 -0
@@ -4,12 +4,12 @@ import { Text } from '../entities/text.js';
4
4
  import { Input, PromptOptions } from '../lib/inputManager.js';
5
5
  import { Logging } from '../lib/logging.js';
6
6
  import { DimensionBase } from './dimensionBase.js';
7
- import { Point } from '../entities/point.js';
8
7
 
9
8
  import { DesignCore } from '../designCore.js';
10
9
  import { Property } from '../properties/property.js';
11
10
  import { Utils } from '../lib/utils.js';
12
11
  import { RubberBand } from '../lib/auxiliary/rubberBand.js';
12
+ import { SnapPoint } from '../lib/auxiliary/snapPoint.js';
13
13
 
14
14
  /**
15
15
  * Aligned Dimension Entity Class
@@ -45,6 +45,8 @@ export class LinearDimensionBase extends DimensionBase {
45
45
  this.points.push(pt14);
46
46
 
47
47
  const op2 = new PromptOptions(Strings.Input.DIMENSION, [Input.Type.POINT]);
48
+ // avoid polar/ortho tracking a line from pt14 while placing the dimension
49
+ DesignCore.Scene.inputManager.inputPoint = null;
48
50
  const pt11 = await DesignCore.Scene.inputManager.requestInput(op2);
49
51
  pt11.sequence = 11;
50
52
  this.points.push(pt11);
@@ -76,6 +78,70 @@ export class LinearDimensionBase extends DimensionBase {
76
78
  }
77
79
  }
78
80
 
81
+ /**
82
+ * Get the grip points for interactive editing on the canvas.
83
+ * Pt10 (the dimension line's far end) is derived from Pt11 via getPointsFromSelection() -
84
+ * it must be recomputed, not left stale, when the Pt11 grip moves.
85
+ * @return {Array<{point: Point, apply: Function}>} grips
86
+ */
87
+ getGrips() {
88
+ const Pt13 = this.getPointBySequence(this.points, 13);
89
+ const Pt14 = this.getPointBySequence(this.points, 14);
90
+ const Pt11 = this.getPointBySequence(this.points, 11);
91
+
92
+ if (!Pt13 || !Pt14 || !Pt11) {
93
+ return [];
94
+ }
95
+
96
+ const apply = (newPoint) => {
97
+ const tempLine = new Line({ points: [Pt13, Pt14] });
98
+ const angle = this.getProperty(Property.Names.LINEARDIMANGLE);
99
+ return this.constructor.getPointsFromSelection([tempLine], newPoint, angle);
100
+ };
101
+
102
+ return [{ point: Pt11, apply: apply }];
103
+ }
104
+
105
+ /**
106
+ * Get the points where the extension lines meet the dimension line
107
+ * @return {Array<Point>} [Pt13e, Pt14e]
108
+ */
109
+ getDimensionLineEndPoints() {
110
+ const Pt10 = this.getPointBySequence(this.points, 10);
111
+ const Pt13 = this.getPointBySequence(this.points, 13);
112
+ const Pt14 = this.getPointBySequence(this.points, 14);
113
+
114
+ if (this.dimType.getBaseDimType() === 0) {
115
+ // Rotated dimension: the dimension line direction is a fixed, user-specified angle
116
+ // (LINEARDIMANGLE) independent of the measured points - project the extension
117
+ // line origin onto that fixed line, so it stays correct at any rotation, not just 0/90/180/270
118
+ const dimLineAngleRad = Utils.degrees2radians(this.getProperty(Property.Names.LINEARDIMANGLE));
119
+ const linePt2 = Pt10.project(dimLineAngleRad, 1);
120
+ return [Pt13.perpendicular(Pt10, linePt2), Pt10];
121
+ }
122
+
123
+ // Aligned dimension: the dimension line is parallel to the measured edge (Pt13 -> Pt14),
124
+ // derived dynamically from the points rather than a fixed angle
125
+ return [Pt13.project(Pt14.angle(Pt10), Pt14.distance(Pt10)), Pt10];
126
+ }
127
+
128
+ /**
129
+ * Get snap points
130
+ * @param {Point} mousePoint
131
+ * @param {number} delta
132
+ * @return {Array} - array of snap points
133
+ */
134
+ snaps(mousePoint, delta) {
135
+ const Pt10 = this.getPointBySequence(this.points, 10);
136
+ const Pt13 = this.getPointBySequence(this.points, 13);
137
+ const Pt14 = this.getPointBySequence(this.points, 14);
138
+
139
+ if (!Pt10 || !Pt13 || !Pt14) {
140
+ return [];
141
+ }
142
+
143
+ return this.getDimensionLineEndPoints().map((point) => new SnapPoint(point, SnapPoint.Type.END));
144
+ }
79
145
 
80
146
  /**
81
147
  * Build the dimension
@@ -86,7 +152,6 @@ export class LinearDimensionBase extends DimensionBase {
86
152
  let dimension = 0;
87
153
  const entities = [];
88
154
 
89
- const Pt10 = this.getPointBySequence(this.points, 10);
90
155
  const Pt13 = this.getPointBySequence(this.points, 13);
91
156
  const Pt14 = this.getPointBySequence(this.points, 14);
92
157
  const Pt11 = this.getPointBySequence(this.points, 11);
@@ -113,38 +178,8 @@ export class LinearDimensionBase extends DimensionBase {
113
178
  return;
114
179
  }
115
180
 
116
- let Pt13e = new Point();
117
- const Pt14e = Pt10;
118
-
119
- // generate the x and y delta values
120
- const dx = Pt14.x - Pt13.x;
121
- const dy = Pt14.y - Pt13.y;
122
-
123
- // check if Line Pt13 -> P14 is perpendicular to Line Pt14 -> Pt10
124
- const m1 = (Pt14.y - Pt13.y) / (Pt14.x - Pt13.x);
125
- const m2 = (Pt14.y - Pt10.y) / (Pt14.x - Pt10.x);
126
- const perpendicular = Number((m1 * m2).toFixed(1));
127
-
128
- const isAligned = perpendicular !== -1.0 ? false : true;
129
-
130
- if (isAligned || Utils.round(dx) === 0 || Utils.round(dy) === 0) {
131
- Pt13e = Pt13.project(Pt14.angle(Pt10), Pt14.distance(Pt10));
132
- dimension = Pt13.distance(Pt14);
133
- } else {
134
- // Not perpendicular to the selected line: get the primary axis (x or y) and calculate the dimension
135
- const iX = ((Math.abs(Pt10.x - Pt13.x) + Math.abs(Pt14.x - Pt10.x)) - Math.abs(dx));
136
- const iY = ((Math.abs(Pt10.y - Pt13.y) + Math.abs(Pt14.y - Pt10.y)) - Math.abs(dy));
137
-
138
- if (iX >= iY && dy !== 0) {
139
- Pt13e.x = Pt10.x;
140
- Pt13e.y = Pt13.y;
141
- dimension = dy;
142
- } else if (iX < iY && dx !== 0) {
143
- Pt13e.x = Pt13.x;
144
- Pt13e.y = Pt10.y;
145
- dimension = dx;
146
- }
147
- }
181
+ const [Pt13e, Pt14e] = this.getDimensionLineEndPoints();
182
+ dimension = this.dimType.getBaseDimType() === 0 ? Pt13e.distance(Pt14e) : Pt13.distance(Pt14);
148
183
 
149
184
  // ensure dimension is positive
150
185
  dimension = Math.abs(dimension);
@@ -155,9 +190,6 @@ export class LinearDimensionBase extends DimensionBase {
155
190
  const reverseDimLineAngle = Pt14e.angle(Pt13e);
156
191
  const midPoint = Pt13e.midPoint(Pt14e);
157
192
 
158
- // set the dimension line angle. This is required for rotated dimension dxf definition
159
- this.setProperty(Property.Names.LINEARDIMANGLE, Utils.radians2degrees(dimLineAngle));
160
-
161
193
  // Approximate text width based on height using the formatted dimension value i.e with units, precision and symbols
162
194
  const approxTextWidth = Text.getApproximateWidth(this.getDimensionValue(dimension), DIMTXT);
163
195
  const approxTextHalfWidth = approxTextWidth * 0.5;
@@ -221,6 +253,12 @@ export class LinearDimensionBase extends DimensionBase {
221
253
  break;
222
254
  }
223
255
 
256
+ // A grip-dragged text position overrides the DIMJUST auto-placement above with Pt11 as
257
+ // the anchor - DIMTAD's vertical offset below still applies on top, matching AutoCAD
258
+ if (this.dimType.hasUserPositionedText()) {
259
+ textPosition = Pt11;
260
+ }
261
+
224
262
  // Check if the text is aligned with the dimension line
225
263
  const textAndDimlineAligned = this.alignedOrOpposite(textRotation, dimLineAngle);
226
264
 
@@ -14,7 +14,7 @@ export class RadialDimension extends CircularDimensionBase {
14
14
  */
15
15
  constructor(data) {
16
16
  super(data);
17
- this.dimType.setDimType(4); // Radial dimension
17
+ this.dimType.setBaseType(4); // Radial dimension - preserve flags loaded from DXF group 70
18
18
  }
19
19
 
20
20
  /**
@@ -48,6 +48,31 @@ export class RadialDimension extends CircularDimensionBase {
48
48
  return points;
49
49
  }
50
50
 
51
+ /**
52
+ * Get the grip points for interactive editing on the canvas.
53
+ * Pt15 (the circle attach point) is derived from Pt11 via getPointsFromSelection() -
54
+ * must be recomputed from the unchanged centre (Pt10) and radius, not left stale.
55
+ * @return {Array<{point: Point, apply: Function}>} grips
56
+ */
57
+ getGrips() {
58
+ const Pt10 = this.getPointBySequence(this.points, 10); // centre
59
+ const Pt15 = this.getPointBySequence(this.points, 15); // point on circle
60
+ const Pt11 = this.getPointBySequence(this.points, 11); // text position
61
+
62
+ if (!Pt10 || !Pt15 || !Pt11) {
63
+ return [];
64
+ }
65
+
66
+ const radius = Pt10.distance(Pt15);
67
+
68
+ const apply = (newPoint) => {
69
+ const tempEntity = { points: [Pt10], getRadius: () => radius };
70
+ return this.constructor.getPointsFromSelection([tempEntity], newPoint);
71
+ };
72
+
73
+ return [{ point: Pt11, apply: apply }];
74
+ }
75
+
51
76
  /**
52
77
  * Write the entity to file in the dxf format
53
78
  * @param {DXFFile} file
@@ -22,7 +22,7 @@ export class RotatedDimension extends LinearDimensionBase {
22
22
  */
23
23
  constructor(data) {
24
24
  super(data);
25
- this.dimType.setDimType(0); // Rotated dimension
25
+ this.dimType.setBaseType(0); // Rotated dimension - preserve flags loaded from DXF group 70
26
26
  }
27
27
 
28
28
  /**
@@ -62,6 +62,8 @@ export class RotatedDimension extends LinearDimensionBase {
62
62
  this.points.push(pt14);
63
63
 
64
64
  const op2 = new PromptOptions(Strings.Input.DIMENSION, [Input.Type.POINT]);
65
+ // avoid polar/ortho tracking a line from pt14 while placing the dimension
66
+ DesignCore.Scene.inputManager.inputPoint = null;
65
67
  const pt11 = await DesignCore.Scene.inputManager.requestInput(op2);
66
68
  pt11.sequence = 11;
67
69
  this.points.push(pt11);
@@ -126,8 +128,9 @@ export class RotatedDimension extends LinearDimensionBase {
126
128
  const pt11 = DesignCore.Mouse.pointOnScene();
127
129
  pt11.sequence = 11;
128
130
  const tempLine = new Line({ points: [this.points[0], this.points[1]] });
129
- const points = RotatedDimension.getPointsFromSelection([tempLine], pt11, this.getProperty(Property.Names.LINEARDIMANGLE));
130
- DesignCore.Scene.previewEntities.create(this.type, { points: points, dimensionStyle: this.getProperty(Property.Names.DIMENSIONSTYLE) });
131
+ const angle = this.getProperty(Property.Names.LINEARDIMANGLE);
132
+ const points = RotatedDimension.getPointsFromSelection([tempLine], pt11, angle);
133
+ DesignCore.Scene.previewEntities.create(this.type, { points: points, dimensionStyle: this.getProperty(Property.Names.DIMENSIONSTYLE), linearDimAngle: angle });
131
134
  }
132
135
  }
133
136
 
@@ -109,6 +109,15 @@ export class Entity {
109
109
  this.properties.set(Property.Names.POINTS, value, this);
110
110
  }
111
111
 
112
+ /**
113
+ * Get the grip points for interactive editing on the canvas.
114
+ * Overridden by entities that expose editable grips (e.g. DimensionBase).
115
+ * @return {Array<{point: Point, apply: Function}>} grips - apply(newPoint) returns a new points array
116
+ */
117
+ getGrips() {
118
+ return [];
119
+ }
120
+
112
121
  /**
113
122
  * Normalise entity-level DXF group codes before construction.
114
123
  * Subclasses override this for geometric transformation; they receive data
@@ -13,6 +13,7 @@ export class SnapPoint {
13
13
  TANGENT: 'tangent',
14
14
  NODE: 'node',
15
15
  PERPENDICULAR: 'perpendicular',
16
+ GRIP: 'grip',
16
17
  });
17
18
 
18
19
  /**
@@ -57,6 +58,14 @@ export class SnapPoint {
57
58
  new Point(x - size, y + size),
58
59
  ], { closed: true });
59
60
  break;
61
+ case SnapPoint.Type.GRIP: // filled square
62
+ renderer.drawShape([
63
+ new Point(x - size, y - size),
64
+ new Point(x + size, y - size),
65
+ new Point(x + size, y + size),
66
+ new Point(x - size, y + size),
67
+ ], { closed: true, fill: true });
68
+ break;
60
69
  case SnapPoint.Type.MID: // triangle
61
70
  renderer.drawShape([
62
71
  new Point(x, y + size),
@@ -5,6 +5,7 @@ import { Point } from '../entities/point.js';
5
5
  import { Input } from './input.js';
6
6
  import { Logging } from './logging.js';
7
7
  import { PlotOptions } from './plotOptions.js';
8
+ import { SnapPoint } from './auxiliary/snapPoint.js';
8
9
  // import { RendererBase } from './renderers/rendererBase.js';
9
10
 
10
11
  import { DesignCore } from '../designCore.js';
@@ -399,6 +400,11 @@ export class Canvas {
399
400
  const selectedEntities = DesignCore.Scene.selectionManager.selectedEntities;
400
401
  this.#paintEntities(selectedEntities, renderer, { colour: selectionHaloColour, lineWidthDelta: selectionLineWidthDelta });
401
402
 
403
+ // Paint grips for selected entities - only when idle (no active command)
404
+ if (DesignCore.Scene.inputManager.activeCommand === undefined) {
405
+ this.#paintGrips(selectedEntities, renderer, scale);
406
+ }
407
+
402
408
  // Paint the auxiliary scene items
403
409
  // auxiliary items include things like the selection window, snap points etc
404
410
  // these items have their own draw routine
@@ -462,6 +468,22 @@ export class Canvas {
462
468
  }
463
469
  }
464
470
 
471
+ /**
472
+ * Draw grip markers for every grip exposed by the given entities.
473
+ * @param {Array} entities - selected entities to draw grips for
474
+ * @param {Object} renderer - renderer instance
475
+ * @param {number} scale - current canvas scale
476
+ */
477
+ #paintGrips(entities, renderer, scale) {
478
+ for (const entity of entities) {
479
+ for (const grip of entity.getGrips()) {
480
+ renderer.save();
481
+ new SnapPoint(grip.point, SnapPoint.Type.GRIP).draw(renderer, scale);
482
+ renderer.restore();
483
+ }
484
+ }
485
+ }
486
+
465
487
  /**
466
488
  * Set the scene context on the renderer before drawing an entity.
467
489
  * @param {Object} item - entity to draw
@@ -12,7 +12,7 @@ import { Text } from '../entities/text.js';
12
12
  import { Block } from '../tables/block.js';
13
13
  import { Insert } from '../entities/insert.js';
14
14
  import { Hatch } from '../entities/hatch.js';
15
- /*
15
+
16
16
  import { Dimension } from '../dimensions/dimension.js';
17
17
  import { AlignedDimension } from '../dimensions/alignedDimension.js';
18
18
  import { AngularDimension } from '../dimensions/angularDimension.js';
@@ -20,7 +20,7 @@ import { LinearDimension } from '../dimensions/linearDimension.js';
20
20
  import { DiametricDimension } from '../dimensions/diametricDimension.js';
21
21
  import { RadialDimension } from '../dimensions/radialDimension.js';
22
22
  import { RotatedDimension } from '../dimensions/rotatedDimension.js';
23
- */
23
+
24
24
 
25
25
  // import { Point } from "../entities/point.js"
26
26
 
@@ -70,7 +70,7 @@ const classes = {
70
70
  // Spline,
71
71
  ArcAlignedText,
72
72
  Text,
73
- /*
73
+
74
74
  Dimension,
75
75
  AngularDimension,
76
76
  AlignedDimension,
@@ -78,7 +78,7 @@ const classes = {
78
78
  LinearDimension,
79
79
  RadialDimension,
80
80
  RotatedDimension,
81
- */
81
+
82
82
  Block,
83
83
  Insert,
84
84
  Hatch,
@@ -388,8 +388,14 @@ export class DXF {
388
388
 
389
389
  const command = entity[0];
390
390
  if (DesignCore.CommandManager.isCommand(command)) {
391
- const newEntity = DesignCore.CommandManager.createNew(command, entity);
392
- DesignCore.Scene.entities.add(newEntity);
391
+ try {
392
+ const newEntity = DesignCore.CommandManager.createNew(command, entity);
393
+ DesignCore.Scene.entities.add(newEntity);
394
+ } catch (err) {
395
+ // a recognised command can still fail to build (e.g. unsupported dimension subtype) - skip it, don't abort the rest of the file
396
+ Logging.instance.warn(`${Strings.Message.UNKNOWNCOMMAND} ${command}: ${err}`);
397
+ this.unsupportedElements = true;
398
+ }
393
399
  } else {
394
400
  Logging.instance.warn(`${Strings.Message.UNKNOWNCOMMAND} ${command}`);
395
401
  this.unsupportedElements = true;
@@ -1,4 +1,5 @@
1
1
  import { Tool } from '../tools/tool.js';
2
+ import { GripStretch } from '../tools/gripStretch.js';
2
3
  import { Snapping } from './snapping.js';
3
4
 
4
5
  import { DesignCore } from '../designCore.js';
@@ -131,7 +132,9 @@ export class InputManager {
131
132
  * @param {string} command - command name or shortcut
132
133
  */
133
134
  startCommand(command) {
134
- if (this.activeCommand !== undefined) {
135
+ if (this.activeCommand instanceof Tool) {
136
+ this.activeCommand.cancel();
137
+ } else if (this.activeCommand !== undefined) {
135
138
  this.reset();
136
139
  }
137
140
 
@@ -196,6 +199,17 @@ export class InputManager {
196
199
  * @param {Point} point
197
200
  */
198
201
  onLeftClick(point) {
202
+ // Grip hit-test takes priority over normal entity selection when idle, so clicking a grip
203
+ // starts GripStretch (which requests the stretch point itself) instead of reselecting it
204
+ if (this.activeCommand === undefined) {
205
+ const grabbedGrip = DesignCore.Scene.selectionManager.findGripUnderMouse(DesignCore.Mouse.pointOnScene());
206
+ if (grabbedGrip) {
207
+ this.activeCommand = new GripStretch(grabbedGrip.entityIndex, grabbedGrip.grip);
208
+ this.activeCommand.execute();
209
+ return;
210
+ }
211
+ }
212
+
199
213
  // left clicks are used for selection of entities and points
200
214
  // Selection of entities can only occur if there is no active snap available
201
215
  // Selection of entities can only occur if the mouse is over an entity
@@ -282,8 +296,14 @@ export class InputManager {
282
296
  this.activeCommand.preview();
283
297
  }
284
298
 
285
- // Only show hover glow when no command preview is occupying previewEntities
286
- if (!snapped && DesignCore.Scene.previewEntities.count() === 0) {
299
+ // a command preview can coexist with an active selection prompt (e.g. a dimension
300
+ // previewing a point-based result while still accepting a second entity selection)
301
+ const canSelectEntity = this.promptOption?.types.includes(Input.Type.SINGLESELECTION) ||
302
+ this.promptOption?.types.includes(Input.Type.SELECTIONSET);
303
+
304
+ // Only show hover glow when no command preview is occupying previewEntities,
305
+ // unless the active prompt also accepts an entity selection
306
+ if (!snapped && (DesignCore.Scene.previewEntities.count() === 0 || canSelectEntity)) {
287
307
  this.highlightEntityUnderMouse();
288
308
  }
289
309
 
@@ -1,6 +1,16 @@
1
1
  import { RendererBase } from './rendererBase.js';
2
2
  import { Text } from '../../entities/text.js';
3
3
 
4
+ // Unicode code points mapped to WinAnsi bytes 0x80-0x9F (the range where WinAnsi differs from Latin-1)
5
+ const WIN_ANSI_EXTRAS = new Map([
6
+ [0x20ac, 0x80], [0x201a, 0x82], [0x0192, 0x83], [0x201e, 0x84], [0x2026, 0x85],
7
+ [0x2020, 0x86], [0x2021, 0x87], [0x02c6, 0x88], [0x2030, 0x89], [0x0160, 0x8a],
8
+ [0x2039, 0x8b], [0x0152, 0x8c], [0x017d, 0x8e], [0x2018, 0x91], [0x2019, 0x92],
9
+ [0x201c, 0x93], [0x201d, 0x94], [0x2022, 0x95], [0x2013, 0x96], [0x2014, 0x97],
10
+ [0x02dc, 0x98], [0x2122, 0x99], [0x0161, 0x9a], [0x203a, 0x9b], [0x0153, 0x9c],
11
+ [0x017e, 0x9e], [0x0178, 0x9f],
12
+ ]);
13
+
4
14
  /**
5
15
  * Pure-JS PDF renderer.
6
16
  * Implements the RendererBase interface by accumulating a PDF content stream in memory.
@@ -361,6 +371,12 @@ export class PdfRenderer extends RendererBase {
361
371
  return str
362
372
  .replace(/\\/g, '\\\\')
363
373
  .replace(/\(/g, '\\(')
364
- .replace(/\)/g, '\\)');
374
+ .replace(/\)/g, '\\)')
375
+ // Keep output ASCII: map Unicode to WinAnsi (Windows-1252) bytes, unsupported characters become '?'
376
+ .replace(/[^\x00-\x7f]/g, (c) => {
377
+ const code = c.charCodeAt(0);
378
+ const byte = WIN_ANSI_EXTRAS.get(code) ?? (code >= 0xa0 && code <= 0xff ? code : 0x3f);
379
+ return '\\' + byte.toString(8).padStart(3, '0');
380
+ });
365
381
  }
366
382
  }
package/core/lib/scene.js CHANGED
@@ -139,6 +139,7 @@ export class Scene {
139
139
  undo() {
140
140
  if (this.stateManager.canUndo()) {
141
141
  this.stateManager.undo();
142
+ this.selectionManager.reloadSelectedEntities();
142
143
  DesignCore.Canvas.requestPaint();
143
144
  } else {
144
145
  DesignCore.Core.notify(Strings.Message.NOUNDO);
@@ -151,6 +152,7 @@ export class Scene {
151
152
  redo() {
152
153
  if (this.stateManager.canRedo()) {
153
154
  this.stateManager.redo();
155
+ this.selectionManager.reloadSelectedEntities();
154
156
  DesignCore.Canvas.requestPaint();
155
157
  } else {
156
158
  DesignCore.Core.notify(Strings.Message.NOREDO);
@@ -156,6 +156,34 @@ export class SelectionManager {
156
156
  return DesignCore.Scene.entities.findClosest(point);
157
157
  }
158
158
 
159
+ /**
160
+ * Find the grip closest to point, within tolerance, across all selected entities.
161
+ * Hit-tests against the real scene entities (not the selectedEntities preview clones) so a
162
+ * grip's apply() always recomputes from the last-committed geometry.
163
+ * @param {Point} point
164
+ * @return {Object|undefined} { entityIndex, grip } or undefined if no grip is close enough
165
+ */
166
+ findGripUnderMouse(point) {
167
+ const gripAperture = 25; // capture radius in pixels (screen space), matches Snapping's snapAperture
168
+ const delta = gripAperture / DesignCore.Canvas.getScale();
169
+
170
+ let closestGrip;
171
+ let closestDistance = delta;
172
+
173
+ for (const entityIndex of this.selectionSet.selectionSet) {
174
+ const entity = DesignCore.Scene.entities.get(entityIndex);
175
+ for (const grip of entity.getGrips()) {
176
+ const distance = grip.point.distance(point);
177
+ if (distance < closestDistance) {
178
+ closestDistance = distance;
179
+ closestGrip = { entityIndex, grip };
180
+ }
181
+ }
182
+ }
183
+
184
+ return closestGrip;
185
+ }
186
+
159
187
  /**
160
188
  * Remove the item at index from the selectionSet and selectedEntities
161
189
  * @param {number} index - index of the item
@@ -116,5 +116,6 @@ export class Strings {
116
116
  SCALEFACTOR: 'Specify scale factor',
117
117
  REFERENCELENGTH: 'Specify reference length',
118
118
  NEWLENGTH: 'Specify new length',
119
+ STRETCHPOINT: 'Specify stretch point',
119
120
  };
120
121
  };
@@ -99,6 +99,20 @@ export class DimType {
99
99
  this.userPositionedText = (dimType & 128) !== 0;
100
100
  }
101
101
 
102
+ /**
103
+ * Set only the base dimension type (0-6), leaving the block reference, ordinate and
104
+ * user-positioned-text flags untouched - used by subclass constructors to pin their type
105
+ * without clobbering flags already loaded from a DXF group 70 value
106
+ * @param {number} baseType
107
+ */
108
+ setBaseType(baseType) {
109
+ if (!DimType.isValidDimensionType(baseType)) {
110
+ baseType = 0;
111
+ }
112
+
113
+ this.dimType = DimType.getBaseType(baseType);
114
+ }
115
+
102
116
  /**
103
117
  * Check if the dimension has a block reference
104
118
  * @return {boolean} true if the dimension has a block reference
@@ -112,6 +112,7 @@ export class Property {
112
112
  LEADERLENGTH: 'leaderLength',
113
113
  LINEARDIMANGLE: 'linearDimAngle',
114
114
  TEXTOVERRIDE: 'textOverride',
115
+ USERPOSITIONEDTEXT: 'userPositionedText',
115
116
  });
116
117
 
117
118
  /**
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { DXFFile } from '../lib/dxf/dxfFile.js';
3
+ import { DesignCore } from '../designCore.js';
3
4
  import { Logging } from '../lib/logging.js';
4
5
  import { Flags } from '../properties/flags.js';
5
6
  import { Property } from '../properties/property.js';
@@ -383,6 +384,8 @@ export class DimStyle {
383
384
 
384
385
  // DXF Groupcode 340 - (handle of referenced STYLE)
385
386
  this.DIMTXSTY = Property.loadValue([data.DIMTXSTY, data[340]], 'Standard');
387
+ // internal - true when DIMTXSTY holds a style handle (loaded from group 340) rather than a style name
388
+ this.DIMTXSTYISHANDLE = Property.loadValue([data.DIMTXSTYISHANDLE, data.DIMTXSTY === undefined && data[340] !== undefined], false);
386
389
 
387
390
  // DXF Groupcode 341 - (handle of referenced BLOCK)
388
391
  this.DIMLDRBLK = Property.loadValue([data.DIMLDRBLK, data[341]], 'ClosedFilled');
@@ -475,6 +478,9 @@ export class DimStyle {
475
478
 
476
479
  if (this.hasOwnProperty(valueName)) {
477
480
  this[valueName] = value;
481
+ if (valueName === 'DIMTXSTY') {
482
+ this.DIMTXSTYISHANDLE = false;
483
+ }
478
484
  } else {
479
485
  const err = `Setting Value - Property does not exist: ${valueName}`;
480
486
  Logging.instance.warn(`${this.type} - ${err}`);
@@ -544,6 +550,23 @@ export class DimStyle {
544
550
  this.DIMCENVALUE = Math.abs(value);
545
551
  }
546
552
 
553
+ /**
554
+ * Get the handle of the text style referenced by DIMTXSTY
555
+ * DIMTXSTY holds a style name, or a style handle when DIMTXSTYISHANDLE is set (loaded from a dxf file)
556
+ * @return {string} handle or undefined if no matching style exists
557
+ */
558
+ getTextStyleHandle() {
559
+ const styles = DesignCore.StyleManager.getItems();
560
+ const reference = String(this.DIMTXSTY).toUpperCase();
561
+
562
+ const style = styles.find((item) => this.DIMTXSTYISHANDLE ?
563
+ String(item.handle).toUpperCase() === reference :
564
+ item.name.toUpperCase() === reference) ??
565
+ styles.find((item) => item.name.toUpperCase() === 'STANDARD');
566
+
567
+ return style?.handle;
568
+ }
569
+
547
570
  /**
548
571
  * Write the entity to file in the dxf format
549
572
  * @param {DXFFile} file
@@ -616,8 +639,15 @@ export class DimStyle {
616
639
  file.writeGroupCode('287', this.DIMFIT);
617
640
  file.writeGroupCode('288', this.DIMUPT, DXFFile.Version.R13);
618
641
  file.writeGroupCode('289', this.DIMATFIT, DXFFile.Version.R2000);
619
- file.writeGroupCode('340', this.DIMTXSTY, DXFFile.Version.R13);
620
- file.writeGroupCode('341', this.DIMLDRBLK, DXFFile.Version.R2000);
642
+ // 340 and 341 are handles - writing a name leaves AutoCAD unable to resolve the text style (dimension text is not shown)
643
+ const textStyleHandle = this.getTextStyleHandle();
644
+ if (textStyleHandle !== undefined) {
645
+ file.writeGroupCode('340', textStyleHandle, DXFFile.Version.R13);
646
+ }
647
+ // 'ClosedFilled' is the default leader arrowhead and has no block handle, omitting the group selects it
648
+ if (this.DIMLDRBLK !== 'ClosedFilled') {
649
+ file.writeGroupCode('341', this.DIMLDRBLK, DXFFile.Version.R2000);
650
+ }
621
651
  // file.writeGroupCode('342', this.DIMBLK);
622
652
  // file.writeGroupCode('343', this.DIMBLK1);
623
653
  // file.writeGroupCode('344', this.DIMBLK2);