@design-core/core 0.0.6 → 0.0.7

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 (145) hide show
  1. package/.eslintrc.json +4 -0
  2. package/TODO.md +49 -6
  3. package/core/core/core.js +0 -3
  4. package/core/dimensions/alignedDimension.js +5 -4
  5. package/core/dimensions/angularDimension.js +6 -5
  6. package/core/dimensions/baseCircularDimension.js +3 -2
  7. package/core/dimensions/baseDimension.js +59 -93
  8. package/core/dimensions/baseLinearDimension.js +6 -5
  9. package/core/dimensions/diametricDimension.js +4 -3
  10. package/core/dimensions/dimension.js +21 -14
  11. package/core/dimensions/radialDimension.js +4 -3
  12. package/core/dimensions/rotatedDimension.js +12 -11
  13. package/core/entities/arc.js +112 -145
  14. package/core/entities/arctext.js +212 -174
  15. package/core/entities/basePolyline.js +30 -282
  16. package/core/entities/circle.js +77 -99
  17. package/core/entities/entity.js +114 -93
  18. package/core/entities/hatch.js +322 -249
  19. package/core/entities/insert.js +66 -81
  20. package/core/entities/line.js +21 -142
  21. package/core/entities/lwpolyline.js +16 -11
  22. package/core/entities/polyline.js +4 -4
  23. package/core/entities/rectangle.js +3 -1
  24. package/core/entities/solid.js +6 -15
  25. package/core/entities/text.js +188 -199
  26. package/core/lib/auxiliary/rubberBand.js +32 -0
  27. package/core/lib/auxiliary/selectionWindow.js +63 -0
  28. package/core/lib/auxiliary/snapPoint.js +113 -0
  29. package/core/lib/auxiliary/trackingLine.js +76 -0
  30. package/core/lib/canvas.js +228 -166
  31. package/core/lib/clipboard.js +7 -1
  32. package/core/lib/colour.js +16 -0
  33. package/core/lib/commandLine.js +1 -6
  34. package/core/lib/commandManager.js +18 -8
  35. package/core/lib/constants.js +28 -0
  36. package/core/lib/dxf/dxf.js +4 -3
  37. package/core/lib/entityManager.js +18 -11
  38. package/core/lib/inputManager.js +35 -11
  39. package/core/lib/intersect.js +25 -0
  40. package/core/lib/patterns.js +8 -1
  41. package/core/lib/plotOptions.js +67 -0
  42. package/core/lib/polylineUtils.js +75 -0
  43. package/core/lib/renderers/cairoRenderer.js +371 -0
  44. package/core/lib/renderers/canvasRenderer.js +315 -0
  45. package/core/lib/renderers/pdfRenderer.js +366 -0
  46. package/core/lib/renderers/rendererBase.js +284 -0
  47. package/core/lib/renderers/svgRenderer.js +343 -0
  48. package/core/lib/scene.js +27 -5
  49. package/core/lib/selectionManager.js +2 -2
  50. package/core/lib/settings.js +4 -2
  51. package/core/lib/snapping.js +4 -191
  52. package/core/lib/stateManager.js +30 -1
  53. package/core/lib/utils.js +21 -2
  54. package/core/properties/entityProperties.js +104 -0
  55. package/core/properties/property.js +106 -2
  56. package/core/properties/propertyManager.js +29 -13
  57. package/core/tables/block.js +4 -37
  58. package/core/tables/endblk.js +1 -1
  59. package/core/tables/layer.js +8 -0
  60. package/core/tables/styleManagerBase.js +3 -2
  61. package/core/tables/tableManagerBase.js +8 -3
  62. package/core/tools/chamfer.js +111 -137
  63. package/core/tools/chamferFilletBase.js +135 -34
  64. package/core/tools/copy.js +3 -3
  65. package/core/tools/explode.js +2 -2
  66. package/core/tools/extend.js +132 -19
  67. package/core/tools/fillet.js +86 -183
  68. package/core/tools/matchProp.js +2 -11
  69. package/core/tools/mirror.js +7 -6
  70. package/core/tools/move.js +3 -3
  71. package/core/tools/offset.js +3 -5
  72. package/core/tools/pasteclip.js +1 -1
  73. package/core/tools/rotate.js +6 -3
  74. package/core/tools/scale.js +1 -1
  75. package/core/tools/trim.js +265 -23
  76. package/core/tools/zoom.js +1 -1
  77. package/jest.config.js +6 -0
  78. package/package.json +5 -3
  79. package/test/dimensions/alignedDimension.test.js +14 -7
  80. package/test/dimensions/angularDimension.test.js +6 -6
  81. package/test/dimensions/baseDimension.test.js +69 -8
  82. package/test/dimensions/baseLinearDimension.test.js +62 -0
  83. package/test/dimensions/diametricDimension.test.js +6 -6
  84. package/test/dimensions/radialDimension.test.js +6 -6
  85. package/test/dimensions/rotatedDimension.test.js +14 -7
  86. package/test/entities/arc.test.js +80 -83
  87. package/test/entities/arctext.test.js +180 -53
  88. package/test/entities/basePolyline.test.js +68 -2
  89. package/test/entities/circle.test.js +57 -85
  90. package/test/entities/entity.test.js +42 -8
  91. package/test/entities/hatch.test.js +543 -70
  92. package/test/entities/insert.test.js +96 -1
  93. package/test/entities/line.test.js +15 -41
  94. package/test/entities/lwpolyline.test.js +104 -0
  95. package/test/entities/polyline.test.js +11 -0
  96. package/test/entities/rectangle.test.js +11 -0
  97. package/test/entities/solid.test.js +1 -1
  98. package/test/entities/text.test.js +294 -69
  99. package/test/integration/exportIntegration.reference.dxf +9 -9
  100. package/test/integration/exportIntegration.test.js +2 -2
  101. package/test/integration/intersectionIntegration.test.js +4 -5
  102. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  103. package/test/integration/pdfRendererIntegration.test.js +61 -0
  104. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  105. package/test/integration/svgRendererIntegration.test.js +61 -0
  106. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  107. package/test/integration/userInputIntegration.test.js +239 -132
  108. package/test/lib/canvas.test.js +742 -123
  109. package/test/lib/constants.test.js +27 -0
  110. package/test/lib/entityManager.test.js +4 -4
  111. package/test/lib/inputManager.test.js +80 -19
  112. package/test/lib/intersect.test.js +83 -0
  113. package/test/lib/plotOptions.test.js +179 -0
  114. package/test/lib/rubberBand.test.js +84 -0
  115. package/test/lib/settings.test.js +5 -6
  116. package/test/lib/snapping.test.js +71 -129
  117. package/test/lib/stateManager.test.js +7 -7
  118. package/test/lib/utils.test.js +36 -2
  119. package/test/properties/entityProperties.test.js +132 -0
  120. package/test/properties/property.test.js +53 -0
  121. package/test/properties/propertyManager.test.js +3 -3
  122. package/test/renderers/canvasRenderer.test.js +317 -0
  123. package/test/renderers/pdfRenderer.test.js +271 -0
  124. package/test/renderers/rendererBase.test.js +213 -0
  125. package/test/renderers/svgRenderer.test.js +326 -0
  126. package/test/tables/block.test.js +31 -1
  127. package/test/tables/endblk.test.js +2 -2
  128. package/test/tables/layer.test.js +30 -0
  129. package/test/test-helpers/hatchGridRenderer.js +180 -0
  130. package/test/test-helpers/test-helpers.js +152 -0
  131. package/test/tools/chamfer.test.js +227 -5
  132. package/test/tools/chamferFilletBase.test.js +98 -9
  133. package/test/tools/copy.test.js +2 -2
  134. package/test/tools/explode.test.js +3 -3
  135. package/test/tools/extend.test.js +331 -0
  136. package/test/tools/fillet.test.js +181 -0
  137. package/test/tools/matchProp.test.js +5 -5
  138. package/test/tools/mirror.test.js +13 -13
  139. package/test/tools/offset.test.js +170 -3
  140. package/test/tools/pasteclip.test.js +5 -5
  141. package/test/tools/rotate.test.js +32 -2
  142. package/test/tools/scale.test.js +5 -5
  143. package/test/tools/trim.test.js +526 -0
  144. package/test/tools/zoom.test.js +7 -7
  145. package/core/lib/selectionWindow.js +0 -83
@@ -10,7 +10,8 @@ import { Flags } from '../properties/flags.js';
10
10
  import { Property } from '../properties/property.js';
11
11
 
12
12
  import { DesignCore } from '../designCore.js';
13
- import { SnapPoint } from '../lib/snapping.js';
13
+ import { SnapPoint } from '../lib/auxiliary/snapPoint.js';
14
+ import { RubberBand } from '../lib/auxiliary/rubberBand.js';
14
15
 
15
16
  /**
16
17
  * Text Entity Class
@@ -19,35 +20,31 @@ import { SnapPoint } from '../lib/snapping.js';
19
20
  export class Text extends Entity {
20
21
  static type = 'Text';
21
22
 
23
+ static #charWidths = {
24
+ ' ': 0.28, '!': 0.28, '"': 0.35, '#': 0.56, '$': 0.56, '%': 0.89, '&': 0.67, '\'': 0.19,
25
+ '(': 0.33, ')': 0.33, '*': 0.39, '+': 0.58, ',': 0.28, '-': 0.33, '.': 0.28, '/': 0.31,
26
+ '0': 0.56, '1': 0.56, '2': 0.56, '3': 0.56, '4': 0.56, '5': 0.56, '6': 0.56, '7': 0.56,
27
+ '8': 0.56, '9': 0.56, ':': 0.28, ';': 0.28, '<': 0.58, '=': 0.58, '>': 0.58, '?': 0.56,
28
+ '@': 1.02, 'A': 0.67, 'B': 0.67, 'C': 0.72, 'D': 0.72, 'E': 0.61, 'F': 0.56, 'G': 0.78,
29
+ 'H': 0.72, 'I': 0.28, 'J': 0.39, 'K': 0.67, 'L': 0.56, 'M': 0.83, 'N': 0.72, 'O': 0.78,
30
+ 'P': 0.61, 'Q': 0.78, 'R': 0.67, 'S': 0.56, 'T': 0.61, 'U': 0.72, 'V': 0.67, 'W': 0.94,
31
+ 'X': 0.67, 'Y': 0.67, 'Z': 0.61, '[': 0.28, '\\': 0.31, ']': 0.28, '^': 0.47, '_': 0.56,
32
+ '`': 0.33, 'a': 0.56, 'b': 0.56, 'c': 0.50, 'd': 0.56, 'e': 0.56, 'f': 0.28, 'g': 0.56,
33
+ 'h': 0.56, 'i': 0.22, 'j': 0.22, 'k': 0.50, 'l': 0.22, 'm': 0.83, 'n': 0.56, 'o': 0.56,
34
+ 'p': 0.56, 'q': 0.56, 'r': 0.33, 's': 0.50, 't': 0.33, 'u': 0.56, 'v': 0.50, 'w': 0.72,
35
+ 'x': 0.50, 'y': 0.50, 'z': 0.44, '{': 0.33, '|': 0.26, '}': 0.33, '~': 0.58,
36
+ };
37
+
22
38
  /**
23
39
  * Create a Text Entity
24
40
  * @param {Array} data
25
41
  */
26
42
  constructor(data) {
27
43
  super(data);
28
- this.string = '';
29
- this.height = 2.5;
30
- this.horizontalAlignment = 0;
31
- this.verticalAlignment = 0;
32
- this.styleName = 'STANDARD';
33
-
34
- // hide inherited properties
35
- // needs to be enumerable=false to not appear in the object props
36
- Object.defineProperty(this, 'lineType', {
37
- enumerable: false,
38
- });
39
44
 
40
- Object.defineProperty(this, 'lineWidth', {
41
- enumerable: false,
42
- });
43
-
44
- // add rotation property with getter and setter
45
- // needs to be enumerable to appear in the object props
46
- Object.defineProperty(this, 'rotation', {
47
- get: this.getRotation,
48
- set: this.setRotation,
49
- enumerable: true,
50
- });
45
+ // Remove inherited properties not applicable to text
46
+ this.properties.remove(Property.Names.LINETYPE);
47
+ this.properties.remove(Property.Names.LINEWIDTH);
51
48
 
52
49
  // needs to be non-enumerable as to not appear in the object props
53
50
  Object.defineProperty(this, 'boundingRect', {
@@ -61,75 +58,71 @@ export class Text extends Entity {
61
58
  writable: true,
62
59
  });
63
60
 
64
- // add backwards property with getter and setter
65
- // needs to be enumerable to appear in the object props
66
- Object.defineProperty(this, 'backwards', {
67
- get: this.getBackwards,
68
- set: this.setBackwards,
69
- enumerable: true,
61
+ // DXF Groupcode 1 - Text String
62
+ this.properties.add(Property.Names.STRING, {
63
+ type: Property.Type.STRING,
64
+ value: Property.loadValue([data?.string, data?.[1]], ''),
65
+ dxfCode: 1,
70
66
  });
71
-
72
-
73
- // add upsidedown property with getter and setter
74
- // needs to be enumerable to appear in the object props
75
- Object.defineProperty(this, 'upsideDown', {
76
- get: this.getUpsideDown,
77
- set: this.setUpsideDown,
78
- enumerable: true,
67
+ // DXF Groupcode 40 - Text Height
68
+ this.properties.add(Property.Names.HEIGHT, {
69
+ type: Property.Type.NUMBER,
70
+ value: Property.loadValue([data?.height, data?.[40]], 2.5),
71
+ dxfCode: 40,
79
72
  });
80
-
81
- if (data) {
82
- // DXF Groupcode 11,21,31 - Second alignment point (optional, used for aligned, middle, or fit text)
83
- if (data?.points?.length > 1) {
84
- if (data.points[1].sequence == 11) {
85
- this.points = [];
86
- this.points.push(new Point(data.points[1].x, data.points[1].y));
87
- }
88
- }
89
- }
90
-
91
- // DXF Groupcode 1 - Text String
92
- this.string = Property.loadValue([data?.string, data?.[1]], '');
93
73
  // DXF Groupcode 7 - Text Style Name
94
- this.styleName = Property.loadValue([data?.styleName, data?.[7]], 'STANDARD');
95
- // DXF Groupcode 40 - Text Height
96
- this.height = Property.loadValue([data?.height, data?.[40]], 2.5);
97
-
98
- if (data) {
99
- if (data.hasOwnProperty('rotation') || data.hasOwnProperty('50')) {
100
- // DXF Groupcode 50 - Text Rotation, angle in degrees
101
- // if we get rotation data store this as a point[1] at an angle from point[0]
102
- // this allows all the entities to be rotated by rotating the points i.e. not all entities have a rotation property
103
- this.setRotation(Property.loadValue([data.rotation, data[50]], 0));
104
- } else {
105
- // create points[1] used to determine the text rotation
106
- if (this.points.length && this.height !== undefined) {
107
- this.points[1] = this.points[0].add(new Point(this.height, 0));
108
- }
109
- }
110
- }
111
-
74
+ this.properties.add(Property.Names.STYLENAME, {
75
+ type: Property.Type.LIST,
76
+ value: Property.loadValue([data?.styleName, data?.[7]], 'STANDARD'),
77
+ dxfCode: 7,
78
+ options: () => DesignCore.StyleManager.getItems().map((s) => ({ display: s.name, value: s.name })),
79
+ });
112
80
  // DXF Groupcode 72 - Horizontal Alignment
113
- // 0 = Left
114
- // 1 = Center
115
- // 2 = Right
116
- // 3 = Aligned (if vertical alignment = 0) not supported, treated as center aligned
117
- // 4 = Middle (if vertical alignment = 0) not supported, treated as center aligned
118
- // 5 = Fit (if vertical alignment = 0) not supported, treated as center aligned
119
- this.horizontalAlignment = Property.loadValue([data?.horizontalAlignment, data?.[72]], 0);
120
-
121
- if (this.horizontalAlignment > 2) {
122
- this.horizontalAlignment = 1; // unsupported alignment types treated as center aligned
123
- }
81
+ const hAlign = Property.loadValue([data?.horizontalAlignment, data?.[72]], 0);
82
+ this.properties.add(Property.Names.HORIZONTALALIGNMENT, {
83
+ type: Property.Type.LIST,
84
+ value: hAlign > 2 ? 1 : hAlign,
85
+ dxfCode: 72,
86
+ options: () => [{ display: 'Left', value: 0 }, { display: 'Center', value: 1 }, { display: 'Right', value: 2 }],
87
+ });
88
+ // DXF Groupcode 73 - Vertical Alignment (0=Baseline; 1=Bottom; 2=Middle; 3=Top)
89
+ this.properties.add(Property.Names.VERTICALALIGNMENT, {
90
+ type: Property.Type.LIST,
91
+ value: Property.loadValue([data?.verticalAlignment, data?.[73]], 0),
92
+ dxfCode: 73,
93
+ options: () => [{ display: 'Baseline', value: 0 }, { display: 'Bottom', value: 1 }, { display: 'Middle', value: 2 }, { display: 'Top', value: 3 }],
94
+ });
95
+ // DXF Groupcode 71 - flags (bit-coded): 2=backward, 4=upsideDown
96
+ this.flags.setFlagValue(Property.loadValue([data?.flags, data?.[71]], 0));
124
97
 
125
- // DXF Groupcode 73 - Vertical Alignment
126
- // 0 = Baseline; 1 = Bottom; 2 = Middle; 3 = Top
127
- this.verticalAlignment = Property.loadValue([data?.verticalAlignment, data?.[73]], 0);
98
+ // rotation: computed from angle between points[0] and points[1]
99
+ this.properties.add(Property.Names.ROTATION, {
100
+ type: Property.Type.NUMBER,
101
+ get: (entity) => entity.getRotation(),
102
+ set: (entity, angle) => entity.setRotation(angle),
103
+ dxfCode: 50,
104
+ });
105
+ // backwards: computed from flags bit 2
106
+ this.properties.add(Property.Names.BACKWARDS, {
107
+ type: Property.Type.BOOLEAN,
108
+ get: (entity) => entity.getBackwards(),
109
+ set: (entity, bool) => entity.setBackwards(bool),
110
+ dxfCode: 71,
111
+ });
112
+ // upsideDown: computed from flags bit 4
113
+ this.properties.add(Property.Names.UPSIDEDOWN, {
114
+ type: Property.Type.BOOLEAN,
115
+ get: (entity) => entity.getUpsideDown(),
116
+ set: (entity, bool) => entity.setUpsideDown(bool),
117
+ dxfCode: 71,
118
+ });
128
119
 
129
- // DXF Groupcode 71 - flags (bit-coded values):
130
- // 2 = Text is backward (mirrored in X).
131
- // 4 = Text is upside down (mirrored in Y).
132
- this.flags.setFlagValue(Property.loadValue([data?.flags, data?.[71]], 0));
120
+ // Named scalar rotation for internal API usage (DXF group code 50 is handled by fromDxf)
121
+ if (data?.rotation !== undefined && !this.points[1]) {
122
+ this.setRotation(data.rotation);
123
+ } else if (!this.points[1] && this.points.length && this.getProperty(Property.Names.HEIGHT) !== undefined) {
124
+ this.points[1] = this.points[0].add(new Point(this.getProperty(Property.Names.HEIGHT), 0));
125
+ }
133
126
  }
134
127
 
135
128
  /**
@@ -144,6 +137,34 @@ export class Text extends Entity {
144
137
  return command;
145
138
  }
146
139
 
140
+ /**
141
+ * Normalise raw DXF data before construction
142
+ * @param {Object} data - raw DXF group code object
143
+ * @return {Object} normalised data with full points array and rotation encoded as points[1]
144
+ *
145
+ * Handles:
146
+ * - Group codes 11,21,31: when a second alignment point with sequence 11 is present it
147
+ * replaces points[0] as the text insertion point
148
+ * - Group code 50: rotation angle (degrees) is projected into points[1] so that all
149
+ * rotation logic in the entity works uniformly through the points array
150
+ */
151
+ static fromDxf(data) {
152
+ // DXF group codes 11,21,31 - second alignment point overrides first when present
153
+ let points = data.points ? [...data.points] : [];
154
+ if (points.length > 1 && points[1]?.sequence == 11) {
155
+ points = [new Point(points[1].x, points[1].y)];
156
+ }
157
+
158
+ // DXF group code 50 - text rotation angle in degrees
159
+ // project points[1] from the insertion point to encode rotation
160
+ if (data[50] !== undefined && points[0]) {
161
+ const height = data[40] ?? data.height ?? 2.5;
162
+ points[1] = points[0].project(Utils.degrees2radians(data[50]), height);
163
+ }
164
+
165
+ return { ...data, points };
166
+ }
167
+
147
168
  /**
148
169
  * Get the approximate width of the text
149
170
  * @param {string} string - text string
@@ -153,9 +174,11 @@ export class Text extends Entity {
153
174
  * Actual width depends on the font and style used
154
175
  */
155
176
  static getApproximateWidth(string, textHeight) {
156
- // Approximate width of the text based on the string length and height
157
- // This is a rough estimate, as actual width depends on the font and style
158
- return string.length * textHeight * 0.6; // 0.6 is an approximation factor for average character width
177
+ let width = 0;
178
+ for (const ch of string) {
179
+ width += (Text.#charWidths[ch] ?? 0.56) * textHeight;
180
+ }
181
+ return width;
159
182
  }
160
183
 
161
184
  /**
@@ -171,20 +194,20 @@ export class Text extends Entity {
171
194
 
172
195
  // set the text style to the current style
173
196
  const currentStyle = DesignCore.StyleManager.getCstyle();
174
- this.styleName = currentStyle;
197
+ this.setProperty(Property.Names.STYLENAME, currentStyle);
175
198
 
176
199
  // get properties from style
177
- const style = DesignCore.StyleManager.getItemByName(this.styleName);
200
+ const style = DesignCore.StyleManager.getItemByName(this.getProperty(Property.Names.STYLENAME));
178
201
  if (style?.textHeight) {
179
- this.height = style.textHeight;
202
+ this.setProperty(Property.Names.HEIGHT, style.textHeight);
180
203
  }
181
204
 
182
- this.backwards = style.backwards;
183
- this.upsideDown = style.upsideDown;
205
+ this.setProperty(Property.Names.BACKWARDS, style.backwards);
206
+ this.setProperty(Property.Names.UPSIDEDOWN, style.upsideDown);
184
207
 
185
208
  // Get the font size when the style has a variable height (textHeight === 0)
186
209
  if (!style?.textHeight) {
187
- const op2 = new PromptOptions(Strings.Input.HEIGHT, [Input.Type.NUMBER], [], this.height);
210
+ const op2 = new PromptOptions(Strings.Input.HEIGHT, [Input.Type.NUMBER], [], this.getProperty(Property.Names.HEIGHT));
188
211
  while (true) {
189
212
  const height = await DesignCore.Scene.inputManager.requestInput(op2);
190
213
  if (height === undefined) return;
@@ -192,7 +215,7 @@ export class Text extends Entity {
192
215
  DesignCore.Core.notify(`${this.type} - ${Strings.Error.NONZERO}`);
193
216
  continue;
194
217
  }
195
- this.height = height;
218
+ this.setProperty(Property.Names.HEIGHT, height);
196
219
  break;
197
220
  }
198
221
  }
@@ -209,7 +232,7 @@ export class Text extends Entity {
209
232
  DesignCore.Scene.inputManager.reset();
210
233
  return;
211
234
  }
212
- this.string = String(string);
235
+ this.setProperty(Property.Names.STRING, String(string));
213
236
 
214
237
  DesignCore.Scene.inputManager.executeCommand(this);
215
238
  } catch (err) {
@@ -225,17 +248,17 @@ export class Text extends Entity {
225
248
  if (DesignCore.Scene.inputManager.promptOption.types.includes(Input.Type.STRING)) {
226
249
  const data = {
227
250
  points: this.points,
228
- height: this.height,
229
- rotation: this.rotation,
230
- styleName: this.styleName,
251
+ height: this.getProperty(Property.Names.HEIGHT),
252
+ rotation: this.getProperty(Property.Names.ROTATION),
253
+ styleName: this.getProperty(Property.Names.STYLENAME),
231
254
  string: DesignCore.CommandLine.command,
232
255
  };
233
256
 
234
- DesignCore.Scene.tempEntities.create(this.type, data);
257
+ DesignCore.Scene.previewEntities.create(this.type, data);
235
258
  } else {
236
259
  const mousePoint = DesignCore.Mouse.pointOnScene();
237
260
  const points = [this.points.at(-1), mousePoint];
238
- DesignCore.Scene.tempEntities.create('Line', { points: points });
261
+ DesignCore.Scene.auxiliaryEntities.add(new RubberBand(points));
239
262
  }
240
263
  }
241
264
  }
@@ -251,8 +274,8 @@ export class Text extends Entity {
251
274
  return;
252
275
  }
253
276
 
254
- if (this.height > 0) {
255
- this.points[1] = this.points[0].project(Utils.degrees2radians(angle), this.height);
277
+ if (this.getProperty(Property.Names.HEIGHT) > 0) {
278
+ this.points[1] = this.points[0].project(Utils.degrees2radians(angle), this.getProperty(Property.Names.HEIGHT));
256
279
  }
257
280
  }
258
281
 
@@ -327,7 +350,7 @@ export class Text extends Entity {
327
350
  5 = Fit (if vertical alignment = 0)
328
351
  */
329
352
 
330
- switch (this.horizontalAlignment) {
353
+ switch (this.getProperty(Property.Names.HORIZONTALALIGNMENT)) {
331
354
  case 0:
332
355
  return 'left';
333
356
  case 1:
@@ -335,11 +358,11 @@ export class Text extends Entity {
335
358
  case 2:
336
359
  return 'right';
337
360
  case 3:
338
- return (this.verticalAlignment === 0 ? 'aligned' : 'left'); // (if vertical alignment = 0)
361
+ return (this.getProperty(Property.Names.VERTICALALIGNMENT) === 0 ? 'aligned' : 'left');
339
362
  case 4:
340
- return (this.verticalAlignment === 0 ? 'center' : 'left'); // (if vertical alignment = 0)
363
+ return (this.getProperty(Property.Names.VERTICALALIGNMENT) === 0 ? 'center' : 'left');
341
364
  case 5:
342
- return (this.verticalAlignment === 0 ? 'fit' : 'left'); // (if vertical alignment = 0)
365
+ return (this.getProperty(Property.Names.VERTICALALIGNMENT) === 0 ? 'fit' : 'left');
343
366
  default:
344
367
  return 'left';
345
368
  }
@@ -356,7 +379,7 @@ export class Text extends Entity {
356
379
  See the Group 72 and 73 integer codes table for clarification.
357
380
  */
358
381
 
359
- switch (this.verticalAlignment) {
382
+ switch (this.getProperty(Property.Names.VERTICALALIGNMENT)) {
360
383
  case 0:
361
384
  return 'alphabetic';
362
385
  case 1:
@@ -380,88 +403,54 @@ export class Text extends Entity {
380
403
  }
381
404
 
382
405
  /**
383
- * Draw the entity
384
- * @param {Object} ctx - context
385
- * @param {number} scale
406
+ * Return a character descriptor array for use with renderer.drawText().
407
+ * Coordinates are in scene space (Y-up). The renderer handles the Y-flip.
408
+ * upsideDownOffset and backwardsOffset are optional DXF-flag offsets in
409
+ * the local (post-rotation) text frame.
410
+ * @return {Array}
386
411
  */
387
- draw(ctx, scale) {
388
- if (this.string.length === 0) {
389
- return;
390
- }
391
-
392
- ctx.save(); // save current context before scale and translate
393
- ctx.scale(1, -1);
394
- const corners = this.getTextFrameCorners();
395
- const bottomLeft = corners[0];
396
- ctx.translate(bottomLeft.x, -bottomLeft.y);
397
-
398
- const style = DesignCore.StyleManager.getItemByName(this.styleName);
399
- const rotation = Utils.degrees2radians(this.rotation);
400
- ctx.rotate(-rotation);
401
-
402
- if (this.upsideDown) {
403
- const upsideDownOffset = corners[0].distance(corners[3]);
404
- ctx.translate(0, -upsideDownOffset);
405
- ctx.scale(1, -1);
406
- }
407
-
408
- if (this.backwards) {
409
- const backwardsOffset = corners[0].distance(corners[1]);
410
- ctx.translate(backwardsOffset, 0);
411
- ctx.scale(-1, 1);
412
- }
412
+ toCharacters() {
413
+ if (this.getProperty(Property.Names.STRING).length === 0) return [];
414
+ const [bottomLeft, bottomRight, /* topRight */, topLeft] = this.getTextFrameCorners();
415
+ return [{
416
+ x: bottomLeft.x,
417
+ y: bottomLeft.y,
418
+ rotation: Utils.degrees2radians(this.getRotation()),
419
+ char: this.getProperty(Property.Names.STRING),
420
+ upsideDownOffset: this.getUpsideDown() ? bottomLeft.distance(topLeft) : 0,
421
+ backwardsOffset: this.getBackwards() ? bottomLeft.distance(bottomRight) : 0,
422
+ }];
423
+ }
413
424
 
414
- try { // HTML
415
- ctx.font = this.height + 'pt ' + style.font;
416
- ctx.fillText(this.string, 0, 0);
417
- // TODO: find a better way to define the boundingRect
418
- const metrics = ctx.measureText(String(this.string));
419
- this.boundingRect = { width: metrics.width, height: this.height };
420
- } catch { // Cairo
421
- ctx.selectFontFace(style.font, null, null); // (FontName, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL);
422
-
423
- // Hack to test the text height vs bounding box height to find a scale factor to make the drawn text match the specified height.
424
- // This is needed because Cairo's font size is not the same as the actual drawn text height, and can vary based on the font used.
425
- // This is a rough approximation and may not be accurate for all fonts or sizes.
426
- ctx.setFontSize(this.height);
427
- const drawTextRect = ctx.textExtents(String(this.string));
428
-
429
- /* GJS Cairo needs additional support for font handling
430
- https://www.cairographics.org/tutorial/#L1understandingtext
431
- https://www.cairographics.org/manual/cairo-cairo-scaled-font-t.html#cairo-font-extents-t
432
-
433
- This needs to be implemented in GJS:
434
- https://gitlab.gnome.org/GNOME/gjs/-/blob/master/modules/cairo-context.cpp?ref_type=heads#L773
435
- https://gitlab.gnome.org/GNOME/gjs/-/blob/master/modules/cairo-context.cpp?ref_type=heads#L780
436
- */
437
- // Adjust the font size by the ratio of the desired height to the drawn height to get closer to the desired text height.
438
- ctx.setFontSize(this.height * this.height / drawTextRect.height);
439
- this.boundingRect = ctx.textExtents(String(this.string));
440
-
441
- ctx.showText(String(this.string));
425
+ /**
426
+ * Draw the text entity
427
+ * @param {Object} renderer
428
+ */
429
+ draw(renderer) {
430
+ if (this.getProperty(Property.Names.STRING).length === 0) return;
431
+ const style = DesignCore.StyleManager.getItemByName(this.getProperty(Property.Names.STYLENAME));
432
+ // Measure with the correct font before toCharacters() so that boundingRect
433
+ // (used for alignment offsets) is accurate on every paint pass.
434
+ const measured = renderer.measureText(this.getProperty(Property.Names.STRING), style?.font, this.getProperty(Property.Names.HEIGHT));
435
+ if (measured?.width) {
436
+ this.boundingRect = { width: measured.width, height: this.getProperty(Property.Names.HEIGHT) };
442
437
  }
443
-
444
- ctx.stroke();
445
- ctx.restore(); // restore context before scale and translate
438
+ renderer.drawText(this.toCharacters(), style?.font, this.getProperty(Property.Names.HEIGHT));
446
439
 
447
440
  /*
448
441
  // debug draw the bounding box
449
442
  const bb = this.boundingBox();
450
- ctx.moveTo(bb.xMin, bb.yMin);
451
- ctx.lineTo(bb.xMax, bb.yMin);
452
- ctx.lineTo(bb.xMax, bb.yMax);
453
- ctx.lineTo(bb.xMin, bb.yMax);
454
- ctx.lineTo(bb.xMin, bb.yMin);
455
- ctx.stroke();
443
+ renderer.drawShape([
444
+ new Point(bb.xMin, bb.yMin),
445
+ new Point(bb.xMax, bb.yMin),
446
+ new Point(bb.xMax, bb.yMax),
447
+ new Point(bb.xMin, bb.yMax),
448
+ new Point(bb.xMin, bb.yMin),
449
+ ]);
456
450
 
457
451
  // debug draw the text frame
458
- const frameCorners = this.getTextFrameCorners();
459
- ctx.moveTo(frameCorners[0].x, frameCorners[0].y);
460
- ctx.lineTo(frameCorners[1].x, frameCorners[1].y);
461
- ctx.lineTo(frameCorners[2].x, frameCorners[2].y);
462
- ctx.lineTo(frameCorners[3].x, frameCorners[3].y);
463
- ctx.lineTo(frameCorners[0].x, frameCorners[0].y);
464
- ctx.stroke();
452
+ const [fc0, fc1, fc2, fc3] = this.getTextFrameCorners();
453
+ renderer.drawShape([fc0, fc1, fc2, fc3, fc0]);
465
454
  */
466
455
  }
467
456
 
@@ -471,9 +460,9 @@ export class Text extends Entity {
471
460
  */
472
461
  dxf(file) {
473
462
  file.writeGroupCode('0', 'TEXT');
474
- file.writeGroupCode('5', this.handle, DXFFile.Version.R2000); // Handle
463
+ file.writeGroupCode('5', this.getProperty(Property.Names.HANDLE), DXFFile.Version.R2000); // Handle
475
464
  file.writeGroupCode('100', 'AcDbEntity', DXFFile.Version.R2000);
476
- file.writeGroupCode('8', this.layer);
465
+ file.writeGroupCode('8', this.getProperty(Property.Names.LAYER));
477
466
  file.writeGroupCode('100', 'AcDbText', DXFFile.Version.R2000);
478
467
 
479
468
  const frameCorners = this.getTextFrameCorners();
@@ -483,22 +472,22 @@ export class Text extends Entity {
483
472
  file.writeGroupCode('20', bottomLeft.y);
484
473
  file.writeGroupCode('30', '0.0');
485
474
 
486
- file.writeGroupCode('40', this.height);
487
- file.writeGroupCode('1', this.string);
488
- file.writeGroupCode('50', this.rotation);
475
+ file.writeGroupCode('40', this.getProperty(Property.Names.HEIGHT));
476
+ file.writeGroupCode('1', this.getProperty(Property.Names.STRING));
477
+ file.writeGroupCode('50', this.getRotation());
489
478
  // file.writeGroupCode('7', 'STANDARD'); // TEXT STYLE
490
479
  file.writeGroupCode('71', this.flags.getFlagValue()); // Text generation flags
491
- file.writeGroupCode('72', this.horizontalAlignment); // Horizontal alignment
480
+ file.writeGroupCode('72', this.getProperty(Property.Names.HORIZONTALALIGNMENT)); // Horizontal alignment
492
481
 
493
- if (this.horizontalAlignment > 0 || this.verticalAlignment > 0) {
482
+ if (this.getProperty(Property.Names.HORIZONTALALIGNMENT) > 0 || this.getProperty(Property.Names.VERTICALALIGNMENT) > 0) {
494
483
  file.writeGroupCode('11', this.points[0].x);
495
484
  file.writeGroupCode('21', this.points[0].y);
496
485
  file.writeGroupCode('31', '0.0');
497
486
  }
498
487
 
499
488
  file.writeGroupCode('100', 'AcDbText', DXFFile.Version.R2000);
500
- if (this.verticalAlignment !== 0) {
501
- file.writeGroupCode('73', this.verticalAlignment); // Vertical alignment
489
+ if (this.getProperty(Property.Names.VERTICALALIGNMENT) !== 0) {
490
+ file.writeGroupCode('73', this.getProperty(Property.Names.VERTICALALIGNMENT)); // Vertical alignment
502
491
  }
503
492
  }
504
493
 
@@ -546,7 +535,7 @@ export class Text extends Entity {
546
535
  const rect = this.getBoundingRect();
547
536
 
548
537
  let offsetX = 0;
549
- switch (this.horizontalAlignment) {
538
+ switch (this.getProperty(Property.Names.HORIZONTALALIGNMENT)) {
550
539
  case 0: // left
551
540
  offsetX = 0;
552
541
  break;
@@ -564,7 +553,7 @@ export class Text extends Entity {
564
553
  }
565
554
 
566
555
  let offsetY = 0;
567
- switch (this.verticalAlignment) {
556
+ switch (this.getProperty(Property.Names.VERTICALALIGNMENT)) {
568
557
  case 0: // baseline
569
558
  offsetY = 0; // see comments in draw method
570
559
  break;
@@ -586,18 +575,18 @@ export class Text extends Entity {
586
575
  const y0 = rect.y + offsetY;
587
576
 
588
577
  // compute min/max depending on backwards/upsideDown (text direction)
589
- const xmin = Math.min(x0, this.backwards ? x0 - rect.width : x0 + rect.width);
590
- const xmax = Math.max(x0, this.backwards ? x0 - rect.width : x0 + rect.width);
591
- const ymin = Math.min(y0, this.upsideDown ? y0 - rect.height : y0 + rect.height);
592
- const ymax = Math.max(y0, this.upsideDown ? y0 - rect.height : y0 + rect.height);
578
+ const xmin = Math.min(x0, this.getBackwards() ? x0 - rect.width : x0 + rect.width);
579
+ const xmax = Math.max(x0, this.getBackwards() ? x0 - rect.width : x0 + rect.width);
580
+ const ymin = Math.min(y0, this.getUpsideDown() ? y0 - rect.height : y0 + rect.height);
581
+ const ymax = Math.max(y0, this.getUpsideDown() ? y0 - rect.height : y0 + rect.height);
593
582
 
594
583
  let bottomLeft = new Point(xmin, ymin);
595
584
  let bottomRight = new Point(xmax, ymin);
596
585
  let topLeft = new Point(xmin, ymax);
597
586
  let topRight = new Point(xmax, ymax);
598
587
 
599
- if (this.rotation !== 0) {
600
- const angle = Utils.degrees2radians(this.rotation);
588
+ if (this.getRotation() !== 0) {
589
+ const angle = Utils.degrees2radians(this.getRotation());
601
590
  bottomLeft = bottomLeft.rotate(this.points[0], angle);
602
591
  bottomRight = bottomRight.rotate(this.points[0], angle);
603
592
  topLeft = topLeft.rotate(this.points[0], angle);
@@ -0,0 +1,32 @@
1
+ import { DesignCore } from '../../designCore.js';
2
+
3
+ /** RubberBand Class - rubber-band indicator line drawn between two or more points */
4
+ export class RubberBand {
5
+ /**
6
+ * Create a RubberBand
7
+ * @param {Point[]} points - two or more points defining the polyline
8
+ */
9
+ constructor(points) {
10
+ this.points = points;
11
+ this.lineWidth = 1.5;
12
+ this.dashPattern = [12, 6];
13
+ }
14
+
15
+ /**
16
+ * Draw the rubber-band line
17
+ * @param {Object} renderer
18
+ * @param {number} scale
19
+ */
20
+ draw(renderer, scale) {
21
+ if (this.points.length < 2) return;
22
+
23
+ const colour = DesignCore.Settings.rubberbandcolour;
24
+ const lineWidth = this.lineWidth / scale;
25
+ const scaledDash = this.dashPattern.map((x) => x / scale);
26
+
27
+ renderer.setColour(colour);
28
+ renderer.setLineWidth(lineWidth);
29
+ renderer.setDash(scaledDash, 0);
30
+ renderer.drawShape(this.points);
31
+ }
32
+ }
@@ -0,0 +1,63 @@
1
+ import { DesignCore } from '../../designCore.js';
2
+ import { Point } from '../../entities/point.js';
3
+
4
+ /** SelectionWindow Class */
5
+ export class SelectionWindow {
6
+ /**
7
+ * Create a SelectionWindow
8
+ * @param {Object} data
9
+ */
10
+ constructor(data) {
11
+ this.colour = DesignCore.Settings.selectionWindow;
12
+ this.lineWidth = 1;
13
+ this.dashPattern = [];
14
+
15
+ if (data) {
16
+ if (data.hasOwnProperty('points')) {
17
+ if (data.points[1].y > data.points[0].y) {
18
+ this.colour = DesignCore.Settings.selectionWindow;
19
+ this.dashPattern = [5];
20
+ } else {
21
+ this.colour = DesignCore.Settings.crossingWindow;
22
+ }
23
+
24
+ this.points = [];
25
+ const point1 = new Point(data.points[0].x, data.points[0].y);
26
+ const point2 = new Point(data.points[1].x, data.points[1].y);
27
+
28
+ this.points.push(point1);
29
+ this.points.push(point2);
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Draw the entity
36
+ * @param {Object} renderer
37
+ * @param {number} scale
38
+ */
39
+ draw(renderer, scale) {
40
+ const colour = this.colour;
41
+ const scaledPattern = this.dashPattern.map((x) => x / scale);
42
+
43
+ const p0 = this.points[0];
44
+ const p1 = this.points[1];
45
+ const rectPoints = [
46
+ new Point(p0.x, p0.y),
47
+ new Point(p1.x, p0.y),
48
+ new Point(p1.x, p1.y),
49
+ new Point(p0.x, p1.y),
50
+ ];
51
+
52
+ renderer.setColour(colour);
53
+ renderer.setLineWidth(this.lineWidth / scale);
54
+
55
+ // Semi-transparent fill
56
+ renderer.setDash([], 0);
57
+ renderer.drawShape(rectPoints, { closed: true, fill: true, stroke: false, alpha: 0.2 });
58
+
59
+ // Dashed outline
60
+ renderer.setDash(scaledPattern, 0);
61
+ renderer.drawShape(rectPoints, { closed: true, fill: false, stroke: true });
62
+ }
63
+ }