@design-core/core 0.0.6 → 50.1.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 (173) hide show
  1. package/.eslintrc.json +4 -0
  2. package/.github/workflows/DesignCoreQA.yml +3 -0
  3. package/TODO.md +49 -6
  4. package/core/core/core.js +0 -3
  5. package/core/designCore.js +1 -1
  6. package/core/dimensions/alignedDimension.js +12 -11
  7. package/core/dimensions/angularDimension.js +38 -37
  8. package/core/dimensions/{baseCircularDimension.js → circularDimensionBase.js} +22 -21
  9. package/core/dimensions/diametricDimension.js +14 -13
  10. package/core/dimensions/dimension.js +62 -55
  11. package/core/dimensions/{baseDimension.js → dimensionBase.js} +73 -107
  12. package/core/dimensions/{baseLinearDimension.js → linearDimensionBase.js} +15 -14
  13. package/core/dimensions/radialDimension.js +12 -11
  14. package/core/dimensions/rotatedDimension.js +20 -19
  15. package/core/entities/arc.js +123 -156
  16. package/core/entities/arctext.js +222 -184
  17. package/core/entities/circle.js +77 -99
  18. package/core/entities/entity.js +114 -93
  19. package/core/entities/hatch.js +346 -273
  20. package/core/entities/insert.js +68 -83
  21. package/core/entities/line.js +21 -142
  22. package/core/entities/lwpolyline.js +16 -11
  23. package/core/entities/point.js +19 -19
  24. package/core/entities/polyline.js +7 -7
  25. package/core/entities/{basePolyline.js → polylineBase.js} +47 -299
  26. package/core/entities/rectangle.js +3 -1
  27. package/core/entities/solid.js +6 -15
  28. package/core/entities/text.js +190 -201
  29. package/core/lib/auxiliary/rubberBand.js +32 -0
  30. package/core/lib/auxiliary/selectionWindow.js +63 -0
  31. package/core/lib/auxiliary/snapPoint.js +113 -0
  32. package/core/lib/auxiliary/trackingLine.js +76 -0
  33. package/core/lib/boundingBox.js +11 -11
  34. package/core/lib/canvas.js +232 -168
  35. package/core/lib/clipboard.js +9 -3
  36. package/core/lib/colour.js +16 -0
  37. package/core/lib/commandLine.js +1 -6
  38. package/core/lib/commandManager.js +18 -8
  39. package/core/lib/constants.js +28 -0
  40. package/core/lib/dxf/dxf.js +15 -14
  41. package/core/lib/dxf/dxfIterator.js +2 -2
  42. package/core/lib/entityManager.js +33 -26
  43. package/core/lib/inputManager.js +36 -12
  44. package/core/lib/intersect.js +25 -0
  45. package/core/lib/matrix.js +1 -1
  46. package/core/lib/patterns.js +9 -2
  47. package/core/lib/plotOptions.js +67 -0
  48. package/core/lib/polylineUtils.js +75 -0
  49. package/core/lib/renderers/cairoRenderer.js +371 -0
  50. package/core/lib/renderers/canvasRenderer.js +315 -0
  51. package/core/lib/renderers/pdfRenderer.js +366 -0
  52. package/core/lib/renderers/rendererBase.js +284 -0
  53. package/core/lib/renderers/svgRenderer.js +343 -0
  54. package/core/lib/scene.js +28 -6
  55. package/core/lib/selectionManager.js +20 -20
  56. package/core/lib/settings.js +4 -2
  57. package/core/lib/snapping.js +4 -191
  58. package/core/lib/stateManager.js +30 -1
  59. package/core/lib/utils.js +26 -7
  60. package/core/properties/entityProperties.js +104 -0
  61. package/core/properties/property.js +106 -2
  62. package/core/properties/propertyManager.js +60 -44
  63. package/core/tables/block.js +50 -81
  64. package/core/tables/dimStyle.js +5 -5
  65. package/core/tables/endblk.js +1 -1
  66. package/core/tables/layer.js +8 -0
  67. package/core/tables/ltypeManager.js +1 -1
  68. package/core/tables/styleManagerBase.js +3 -2
  69. package/core/tables/tableManagerBase.js +8 -3
  70. package/core/tables/vport.js +2 -2
  71. package/core/tools/chamfer.js +117 -143
  72. package/core/tools/chamferFilletBase.js +138 -37
  73. package/core/tools/copy.js +11 -11
  74. package/core/tools/copybase.js +3 -3
  75. package/core/tools/copyclip.js +3 -3
  76. package/core/tools/cornerEntity.js +2 -2
  77. package/core/tools/cutclip.js +3 -3
  78. package/core/tools/explode.js +7 -7
  79. package/core/tools/extend.js +138 -25
  80. package/core/tools/fillet.js +92 -189
  81. package/core/tools/matchProp.js +3 -12
  82. package/core/tools/mirror.js +15 -14
  83. package/core/tools/move.js +8 -8
  84. package/core/tools/offset.js +31 -33
  85. package/core/tools/pasteclip.js +1 -1
  86. package/core/tools/purge.js +10 -10
  87. package/core/tools/rotate.js +19 -16
  88. package/core/tools/scale.js +7 -7
  89. package/core/tools/trim.js +271 -29
  90. package/core/tools/zoom.js +1 -1
  91. package/cspell.json +321 -0
  92. package/jest.config.js +6 -0
  93. package/package.json +5 -3
  94. package/test/dimensions/alignedDimension.test.js +17 -10
  95. package/test/dimensions/angularDimension.test.js +13 -13
  96. package/test/dimensions/diametricDimension.test.js +15 -15
  97. package/test/dimensions/dimension.test.js +12 -12
  98. package/test/dimensions/{baseDimension.test.js → dimensionBase.test.js} +73 -12
  99. package/test/dimensions/linearDimensionBase.test.js +62 -0
  100. package/test/dimensions/radialDimension.test.js +15 -15
  101. package/test/dimensions/rotatedDimension.test.js +18 -11
  102. package/test/entities/arc.test.js +83 -86
  103. package/test/entities/arctext.test.js +184 -57
  104. package/test/entities/circle.test.js +68 -96
  105. package/test/entities/entity.test.js +42 -8
  106. package/test/entities/hatch.test.js +569 -96
  107. package/test/entities/insert.test.js +99 -4
  108. package/test/entities/line.test.js +15 -41
  109. package/test/entities/lwpolyline.test.js +104 -0
  110. package/test/entities/point.test.js +3 -3
  111. package/test/entities/polyline.test.js +11 -0
  112. package/test/entities/{basePolyline.test.js → polylineBase.test.js} +192 -57
  113. package/test/entities/rectangle.test.js +11 -0
  114. package/test/entities/solid.test.js +1 -1
  115. package/test/entities/text.test.js +302 -77
  116. package/test/integration/exportIntegration.reference.dxf +9 -9
  117. package/test/integration/exportIntegration.test.js +5 -5
  118. package/test/integration/intersectionIntegration.test.js +4 -5
  119. package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
  120. package/test/integration/pdfRendererIntegration.test.js +61 -0
  121. package/test/integration/svgRendererIntegration.reference.svg +3343 -0
  122. package/test/integration/svgRendererIntegration.test.js +61 -0
  123. package/test/integration/userInputIntegration.reference.dxf +1165 -105
  124. package/test/integration/userInputIntegration.test.js +239 -132
  125. package/test/lib/boundingBox.test.js +3 -3
  126. package/test/lib/canvas.test.js +744 -125
  127. package/test/lib/constants.test.js +27 -0
  128. package/test/lib/entityManager.test.js +4 -4
  129. package/test/lib/inputManager.test.js +81 -20
  130. package/test/lib/intersect.test.js +83 -0
  131. package/test/lib/plotOptions.test.js +179 -0
  132. package/test/lib/rubberBand.test.js +84 -0
  133. package/test/lib/scene.test.js +9 -9
  134. package/test/lib/selectionManager.test.js +3 -3
  135. package/test/lib/settings.test.js +5 -6
  136. package/test/lib/snapping.test.js +72 -130
  137. package/test/lib/stateManager.test.js +7 -7
  138. package/test/lib/utils.test.js +36 -2
  139. package/test/properties/entityProperties.test.js +132 -0
  140. package/test/properties/property.test.js +53 -0
  141. package/test/properties/propertyManager.test.js +25 -25
  142. package/test/renderers/canvasRenderer.test.js +317 -0
  143. package/test/renderers/pdfRenderer.test.js +271 -0
  144. package/test/renderers/rendererBase.test.js +213 -0
  145. package/test/renderers/svgRenderer.test.js +326 -0
  146. package/test/tables/block.test.js +92 -13
  147. package/test/tables/endblk.test.js +2 -2
  148. package/test/tables/layer.test.js +30 -0
  149. package/test/tables/layerManager.test.js +2 -2
  150. package/test/test-helpers/hatchGridRenderer.js +180 -0
  151. package/test/test-helpers/test-helpers.js +158 -6
  152. package/test/tools/chamfer.test.js +273 -51
  153. package/test/tools/chamferFilletBase.test.js +125 -36
  154. package/test/tools/copy.test.js +8 -8
  155. package/test/tools/copybase.test.js +2 -2
  156. package/test/tools/copyclip.test.js +2 -2
  157. package/test/tools/cornerEntity.test.js +8 -8
  158. package/test/tools/cutclip.test.js +2 -2
  159. package/test/tools/erase.test.js +6 -6
  160. package/test/tools/explode.test.js +5 -5
  161. package/test/tools/extend.test.js +373 -42
  162. package/test/tools/fillet.test.js +230 -49
  163. package/test/tools/matchProp.test.js +11 -11
  164. package/test/tools/mirror.test.js +39 -39
  165. package/test/tools/move.test.js +7 -7
  166. package/test/tools/offset.test.js +222 -55
  167. package/test/tools/pasteclip.test.js +5 -5
  168. package/test/tools/purge.test.js +4 -4
  169. package/test/tools/rotate.test.js +39 -9
  170. package/test/tools/scale.test.js +33 -33
  171. package/test/tools/trim.test.js +579 -53
  172. package/test/tools/zoom.test.js +12 -12
  173. package/core/lib/selectionWindow.js +0 -83
@@ -0,0 +1,326 @@
1
+ import { SvgRenderer } from '../../core/lib/renderers/svgRenderer.js';
2
+ import { RendererBase } from '../../core/lib/renderers/rendererBase.js';
3
+ import { Point } from '../../core/entities/point.js';
4
+ import { Text } from '../../core/entities/text.js';
5
+
6
+ describe('SvgRenderer — getOutput()', () => {
7
+ test('produces valid SVG with XML declaration', () => {
8
+ const renderer = new SvgRenderer(100, 100);
9
+ expect(renderer.getOutput().startsWith('<?xml version="1.0" encoding="UTF-8"?>')).toBe(true);
10
+ });
11
+
12
+ test('contains svg element with correct dimensions', () => {
13
+ const renderer = new SvgRenderer(595, 842);
14
+ const output = renderer.getOutput();
15
+ expect(output).toContain('<svg xmlns="http://www.w3.org/2000/svg" width="595" height="842"');
16
+ expect(output).toContain('viewBox="0 0 595 842"');
17
+ });
18
+
19
+ test('contains Y-flip group for scene coordinate conversion', () => {
20
+ const renderer = new SvgRenderer(100, 200);
21
+ expect(renderer.getOutput()).toContain('translate(0,200) scale(1,-1)');
22
+ });
23
+
24
+ test('ends with </svg>', () => {
25
+ const renderer = new SvgRenderer(100, 100);
26
+ expect(renderer.getOutput().trim().endsWith('</svg>')).toBe(true);
27
+ });
28
+
29
+ test('fillBackground emits rect before Y-flip group', () => {
30
+ const renderer = new SvgRenderer(200, 100);
31
+ renderer.setBackgroundColour({ r: 30, g: 30, b: 30 });
32
+ renderer.fillBackground();
33
+ const output = renderer.getOutput();
34
+ const rectPos = output.indexOf('<rect');
35
+ const flipPos = output.indexOf('scale(1,-1)');
36
+ expect(rectPos).toBeGreaterThan(-1);
37
+ expect(flipPos).toBeGreaterThan(rectPos);
38
+ expect(output).toContain('fill="rgb(30,30,30)"');
39
+ });
40
+ });
41
+
42
+ describe('SvgRenderer — drawing', () => {
43
+ test('drawShape emits path element with M and L commands', () => {
44
+ const renderer = new SvgRenderer(100, 100);
45
+ renderer.setColour({ r: 0, g: 0, b: 0 });
46
+ renderer.setLineWidth(1);
47
+ renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 20 }], {});
48
+ const output = renderer.getOutput();
49
+ expect(output).toContain('<path');
50
+ expect(output).toContain('M 0 0');
51
+ expect(output).toContain('L 10 20');
52
+ });
53
+
54
+ test('drawShape with closed=true emits Z in path d', () => {
55
+ const renderer = new SvgRenderer(100, 100);
56
+ renderer.setColour({ r: 0, g: 0, b: 0 });
57
+ renderer.setLineWidth(1);
58
+ renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 5, y: 10 }], { closed: true });
59
+ expect(renderer.getOutput()).toContain('Z');
60
+ });
61
+
62
+ test('drawShape defaults to stroke=true with fill="none"', () => {
63
+ const renderer = new SvgRenderer(100, 100);
64
+ renderer.setColour({ r: 255, g: 0, b: 0 });
65
+ renderer.setLineWidth(1);
66
+ renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], {});
67
+ const output = renderer.getOutput();
68
+ expect(output).toContain('stroke="rgb(255,0,0)"');
69
+ expect(output).toContain('fill="none"');
70
+ });
71
+
72
+ test('drawShape with fill=true, stroke=false emits fill colour and stroke="none"', () => {
73
+ const renderer = new SvgRenderer(100, 100);
74
+ renderer.setColour({ r: 0, g: 128, b: 0 });
75
+ renderer.setLineWidth(1);
76
+ renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: false });
77
+ const output = renderer.getOutput();
78
+ expect(output).toContain('fill="rgb(0,128,0)"');
79
+ expect(output).toContain('stroke="none"');
80
+ });
81
+
82
+ test('drawShape with bulge emits A arc command', () => {
83
+ const renderer = new SvgRenderer(100, 100);
84
+ renderer.setColour({ r: 0, g: 0, b: 0 });
85
+ renderer.setLineWidth(1);
86
+ // bulge = 1 → semicircle CCW
87
+ const p1 = new Point(100, 50, 1);
88
+ const p2 = new Point(-100, 50);
89
+ renderer.drawShape([p1, p2], {});
90
+ expect(renderer.getOutput()).toContain(' A ');
91
+ });
92
+
93
+ test('drawShape arc: large-arc-flag is 0 for |bulge| <= 1', () => {
94
+ const renderer = new SvgRenderer(100, 100);
95
+ renderer.setColour({ r: 0, g: 0, b: 0 });
96
+ renderer.setLineWidth(1);
97
+ const p1 = new Point(10, 0, 1); // bulge=1 → semicircle, |bulge| not > 1
98
+ const p2 = new Point(-10, 0);
99
+ renderer.drawShape([p1, p2], {});
100
+ // A rx ry 0 0 sweep x y — large-arc-flag = 0
101
+ expect(renderer.getOutput()).toMatch(/A [\d.]+ [\d.]+ 0 0 /);
102
+ });
103
+
104
+ test('drawShape arc: large-arc-flag is 1 for |bulge| > 1', () => {
105
+ const renderer = new SvgRenderer(100, 100);
106
+ renderer.setColour({ r: 0, g: 0, b: 0 });
107
+ renderer.setLineWidth(1);
108
+ const p1 = new Point(10, 0, 2); // |bulge| > 1 → major arc
109
+ const p2 = new Point(-10, 0);
110
+ renderer.drawShape([p1, p2], {});
111
+ // A rx ry 0 1 sweep x y — large-arc-flag = 1
112
+ expect(renderer.getOutput()).toMatch(/A [\d.]+ [\d.]+ 0 1 /);
113
+ });
114
+
115
+ test('drawShape arc: sweep-flag is 1 for bulge > 0 (CCW in Y-up)', () => {
116
+ const renderer = new SvgRenderer(100, 100);
117
+ renderer.setColour({ r: 0, g: 0, b: 0 });
118
+ renderer.setLineWidth(1);
119
+ const p1 = new Point(10, 0, 1); // CCW
120
+ const p2 = new Point(-10, 0);
121
+ renderer.drawShape([p1, p2], {});
122
+ // A rx ry 0 large-arc 1 x y
123
+ expect(renderer.getOutput()).toMatch(/A [\d.]+ [\d.]+ 0 \d 1 /);
124
+ });
125
+
126
+ test('drawShape arc: sweep-flag is 0 for bulge < 0 (CW in Y-up)', () => {
127
+ const renderer = new SvgRenderer(100, 100);
128
+ renderer.setColour({ r: 0, g: 0, b: 0 });
129
+ renderer.setLineWidth(1);
130
+ const p1 = new Point(10, 0, -1); // CW
131
+ const p2 = new Point(-10, 0);
132
+ renderer.drawShape([p1, p2], {});
133
+ // A rx ry 0 large-arc 0 x y
134
+ expect(renderer.getOutput()).toMatch(/A [\d.]+ [\d.]+ 0 \d 0 /);
135
+ });
136
+
137
+ test('drawSegments solid emits one path element', () => {
138
+ const renderer = new SvgRenderer(100, 100);
139
+ renderer.setColour({ r: 0, g: 0, b: 0 });
140
+ renderer.setLineWidth(1);
141
+ renderer.drawSegments([
142
+ { x1: 0, y1: 0, x2: 10, y2: 0 },
143
+ { x1: 20, y1: 0, x2: 30, y2: 0 },
144
+ ], []);
145
+ const output = renderer.getOutput();
146
+ expect(output).toContain('M 0 0 L 10 0');
147
+ expect(output).toContain('M 20 0 L 30 0');
148
+ // Both segments in one <path>
149
+ expect((output.match(/<path/g) ?? []).length).toBe(1);
150
+ });
151
+
152
+ test('drawSegments dashed emits one path per segment with stroke-dasharray', () => {
153
+ const renderer = new SvgRenderer(100, 100);
154
+ renderer.setColour({ r: 0, g: 0, b: 0 });
155
+ renderer.setLineWidth(1);
156
+ renderer.drawSegments([
157
+ { x1: 0, y1: 0, x2: 10, y2: 0, dashPhase: 0 },
158
+ { x1: 20, y1: 0, x2: 30, y2: 0, dashPhase: 5 },
159
+ ], [5, 3]);
160
+ const output = renderer.getOutput();
161
+ expect(output).toContain('stroke-dasharray="5,3"');
162
+ expect(output).toContain('stroke-dashoffset="0"');
163
+ expect(output).toContain('stroke-dashoffset="5"');
164
+ expect((output.match(/<path/g) ?? []).length).toBe(2);
165
+ });
166
+
167
+ test('drawText emits text element with character and transform', () => {
168
+ const renderer = new SvgRenderer(100, 100);
169
+ renderer.setColour({ r: 0, g: 0, b: 0 });
170
+ renderer.drawText([{ char: 'A', x: 10, y: 20, rotation: 0 }], 'Helvetica', 5);
171
+ const output = renderer.getOutput();
172
+ expect(output).toContain('<text');
173
+ expect(output).toContain('>A<');
174
+ expect(output).toContain('font-family="Helvetica"');
175
+ expect(output).toContain('font-size="5"');
176
+ expect(output).toContain('translate(10,20)');
177
+ });
178
+
179
+ test('drawText with rotation includes rotate in transform', () => {
180
+ const renderer = new SvgRenderer(100, 100);
181
+ renderer.setColour({ r: 0, g: 0, b: 0 });
182
+ const r = Math.PI / 4; // 45 degrees
183
+ renderer.drawText([{ char: 'X', x: 0, y: 0, rotation: r }], 'Helvetica', 5);
184
+ // -45 degrees in transform (negated for Y-flip compensation)
185
+ expect(renderer.getOutput()).toContain('rotate(-45)');
186
+ });
187
+
188
+ test('drawText escapes XML special characters', () => {
189
+ const renderer = new SvgRenderer(100, 100);
190
+ renderer.setColour({ r: 0, g: 0, b: 0 });
191
+ renderer.drawText([{ char: '<', x: 0, y: 0, rotation: 0 }], 'Helvetica', 5);
192
+ expect(renderer.getOutput()).toContain('&lt;');
193
+ });
194
+ });
195
+
196
+ describe('SvgRenderer — state methods', () => {
197
+ test('setColour affects stroke and fill on subsequent drawShape', () => {
198
+ const renderer = new SvgRenderer(100, 100);
199
+ renderer.setColour({ r: 100, g: 150, b: 200 });
200
+ renderer.setLineWidth(1);
201
+ renderer.drawShape([{ x: 0, y: 0 }, { x: 1, y: 0 }], {});
202
+ expect(renderer.getOutput()).toContain('rgb(100,150,200)');
203
+ });
204
+
205
+ test('setLineWidth affects stroke-width on drawShape', () => {
206
+ const renderer = new SvgRenderer(100, 100);
207
+ renderer.setColour({ r: 0, g: 0, b: 0 });
208
+ renderer.setLineWidth(3.5);
209
+ renderer.drawShape([{ x: 0, y: 0 }, { x: 1, y: 0 }], {});
210
+ expect(renderer.getOutput()).toContain('stroke-width="3.5"');
211
+ });
212
+
213
+ test('setDash with array emits stroke-dasharray on drawShape', () => {
214
+ const renderer = new SvgRenderer(100, 100);
215
+ renderer.setColour({ r: 0, g: 0, b: 0 });
216
+ renderer.setLineWidth(1);
217
+ renderer.setDash([5, 3], 1);
218
+ renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], {});
219
+ const output = renderer.getOutput();
220
+ expect(output).toContain('stroke-dasharray="5,3"');
221
+ expect(output).toContain('stroke-dashoffset="1"');
222
+ });
223
+
224
+ test('setHighlight is a no-op (does not add content)', () => {
225
+ const renderer = new SvgRenderer(100, 100);
226
+ renderer.setHighlight(true, { r: 255, g: 0, b: 0 }, 5);
227
+ const output = renderer.getOutput();
228
+ // Only the structural SVG tags — no extra elements
229
+ expect((output.match(/<path/g) ?? []).length).toBe(0);
230
+ expect((output.match(/<text/g) ?? []).length).toBe(0);
231
+ });
232
+ });
233
+
234
+ describe('SvgRenderer — transform methods', () => {
235
+ test('setTransform emits matrix group', () => {
236
+ const renderer = new SvgRenderer(100, 100);
237
+ renderer.setTransform({ a: 2, d: 2, e: 10, f: 20 });
238
+ expect(renderer.getOutput()).toContain('matrix(2 0 0 2 10 20)');
239
+ });
240
+
241
+ test('setTransform group is automatically closed in getOutput()', () => {
242
+ const renderer = new SvgRenderer(100, 100);
243
+ renderer.setTransform({ a: 1, d: 1, e: 0, f: 0 });
244
+ const output = renderer.getOutput();
245
+ // Every <g must have a matching </g>
246
+ const opens = (output.match(/<g[\s>]/g) ?? []).length;
247
+ const closes = (output.match(/<\/g>/g) ?? []).length;
248
+ expect(opens).toBe(closes);
249
+ });
250
+
251
+ test('save and restore emit balanced <g> and </g>', () => {
252
+ const renderer = new SvgRenderer(100, 100);
253
+ renderer.save();
254
+ renderer.setColour({ r: 0, g: 0, b: 0 });
255
+ renderer.setLineWidth(1);
256
+ renderer.drawShape([{ x: 0, y: 0 }, { x: 1, y: 0 }], {});
257
+ renderer.restore();
258
+ const output = renderer.getOutput();
259
+ const opens = (output.match(/<g[\s>]/g) ?? []).length;
260
+ const closes = (output.match(/<\/g>/g) ?? []).length;
261
+ expect(opens).toBe(closes);
262
+ });
263
+
264
+ test('applyTransform updates innermost plain <g> with transform attribute', () => {
265
+ const renderer = new SvgRenderer(100, 100);
266
+ renderer.save();
267
+ renderer.applyTransform({ x: 5, y: 10, rotation: 0 });
268
+ renderer.restore();
269
+ expect(renderer.getOutput()).toContain('translate(5,10)');
270
+ });
271
+ });
272
+
273
+ describe('SvgRenderer — measurement', () => {
274
+ test('measureText returns width matching Text.getApproximateWidth', () => {
275
+ const renderer = new SvgRenderer(100, 100);
276
+ const result = renderer.measureText('Hello', 'Helvetica', 10);
277
+ expect(result.width).toBeCloseTo(Text.getApproximateWidth('Hello', 10), 5);
278
+ });
279
+
280
+ test('measureCharWidth returns per-character width', () => {
281
+ const renderer = new SvgRenderer(100, 100);
282
+ expect(renderer.measureCharWidth('M')).toBeCloseTo(Text.getApproximateWidth('M', 1), 5);
283
+ });
284
+ });
285
+
286
+ describe('SvgRenderer — plot styles', () => {
287
+ /**
288
+ * @param {SvgRenderer} renderer
289
+ */
290
+ function drawLine(renderer) {
291
+ renderer.setLineWidth(1);
292
+ renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], {});
293
+ }
294
+
295
+ test('default style NONE renders colour unchanged', () => {
296
+ const renderer = new SvgRenderer(100, 100);
297
+ renderer.setColour({ r: 255, g: 0, b: 0 });
298
+ drawLine(renderer);
299
+ expect(renderer.getOutput()).toContain('stroke="rgb(255,0,0)"');
300
+ });
301
+
302
+ test('style MONOCHROME maps all colours to black', () => {
303
+ const renderer = new SvgRenderer(100, 100);
304
+ renderer.setStyle(RendererBase.Styles.MONOCHROME);
305
+ renderer.setColour({ r: 255, g: 0, b: 0 });
306
+ drawLine(renderer);
307
+ expect(renderer.getOutput()).toContain('stroke="rgb(0,0,0)"');
308
+ });
309
+
310
+ test('style GREYSCALE maps colour to grey', () => {
311
+ const renderer = new SvgRenderer(100, 100);
312
+ renderer.setStyle(RendererBase.Styles.GREYSCALE);
313
+ renderer.setColour({ r: 255, g: 0, b: 0 }); // 0.299*255 ≈ 76
314
+ drawLine(renderer);
315
+ expect(renderer.getOutput()).toContain('stroke="rgb(76,76,76)"');
316
+ });
317
+
318
+ test('setStyle with null falls back to NONE', () => {
319
+ const renderer = new SvgRenderer(100, 100);
320
+ renderer.setStyle(RendererBase.Styles.MONOCHROME);
321
+ renderer.setStyle(null); // reset to NONE
322
+ renderer.setColour({ r: 0, g: 128, b: 255 });
323
+ drawLine(renderer);
324
+ expect(renderer.getOutput()).toContain('stroke="rgb(0,128,255)"');
325
+ });
326
+ });
@@ -5,6 +5,22 @@ import { Point } from '../../core/entities/point.js';
5
5
 
6
6
  import { File } from '../test-helpers/test-helpers.js';
7
7
  import { Flags } from '../../core/properties/flags.js';
8
+ import { DesignCore } from '../../core/designCore.js';
9
+ import { Utils } from '../../core/lib/utils.js';
10
+ import { DXFFile } from '../../core/lib/dxf/dxfFile.js';
11
+
12
+ /**
13
+ * Create a mock requestInput function that returns values from the provided array in order.
14
+ * @param {Array} inputs
15
+ * @return {Function}
16
+ */
17
+ function mockRequestInput(inputs) {
18
+ let i = 0;
19
+ return async () => {
20
+ if (i < inputs.length) return inputs[i++];
21
+ throw new Error('cancelled');
22
+ };
23
+ }
8
24
 
9
25
  new Core();
10
26
 
@@ -12,19 +28,19 @@ test('Test Block', () => {
12
28
  const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
13
29
  const flags = new Flags();
14
30
  flags.addValue(1);
15
- const block = new Block({ items: [line], flags: flags });
31
+ const block = new Block({ entities: [line], flags: flags });
16
32
 
17
- expect(block.items.length).toBe(1);
33
+ expect(block.entities.length).toBe(1);
18
34
  expect(block.flags.getFlagValue()).toBe(1);
19
35
  });
20
36
 
21
37
  test('Test Block.clearItems', () => {
22
38
  const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
23
- const block = new Block({ items: [line] });
24
- expect(block.items.length).toBe(1);
39
+ const block = new Block({ entities: [line] });
40
+ expect(block.entities.length).toBe(1);
25
41
 
26
- block.clearItems();
27
- expect(block.items.length).toBe(0);
42
+ block.clearEntities();
43
+ expect(block.entities.length).toBe(0);
28
44
  });
29
45
 
30
46
  test('Test Block.snaps', () => {
@@ -33,7 +49,7 @@ test('Test Block.snaps', () => {
33
49
  expect(block.snaps()).toHaveLength(0);
34
50
 
35
51
  const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
36
- block.addItem(line);
52
+ block.addEntity(line);
37
53
  expect(block.snaps()).toBeInstanceOf(Array);
38
54
  expect(block.snaps()).not.toHaveLength(0);
39
55
  });
@@ -42,9 +58,9 @@ test('Test Block', () => {
42
58
  const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
43
59
  const flags = new Flags();
44
60
  flags.addValue(1);
45
- const block = new Block({ items: [line], flags: flags });
61
+ const block = new Block({ entities: [line], flags: flags });
46
62
 
47
- expect(block.items.length).toBe(1);
63
+ expect(block.entities.length).toBe(1);
48
64
  expect(block.flags.getFlagValue()).toBe(1);
49
65
  });
50
66
 
@@ -60,7 +76,7 @@ test('Test Block.closestPoint', () => {
60
76
  expect(block.closestPoint(point)[1]).toBe(Infinity);
61
77
 
62
78
  const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
63
- block.addItem(line);
79
+ block.addEntity(line);
64
80
  expect(block.closestPoint(point)).toBeInstanceOf(Array);
65
81
  expect(block.closestPoint(point)).toHaveLength(2);
66
82
  expect(block.closestPoint(point)[0]).toBeInstanceOf(Point);
@@ -72,7 +88,7 @@ test('Test Block.closestPoint', () => {
72
88
  test('Test Block.dxf', () => {
73
89
  const block = new Block({ handle: '1', endblkHandle: '1' });
74
90
  const line = new Line({ handle: '1', points: [new Point(101, 102), new Point(201, 202)] });
75
- block.addItem(line);
91
+ block.addEntity(line);
76
92
 
77
93
  const file = new File();
78
94
  block.dxf(file);
@@ -127,7 +143,7 @@ ByLayer
127
143
  31
128
144
  0.0
129
145
  39
130
- 2
146
+ 1
131
147
  0
132
148
  ENDBLK
133
149
  5
@@ -151,7 +167,70 @@ test('Test Block.touched', () => {
151
167
  expect(block.touched(selectionExtremesTrue)).toBe(false);
152
168
 
153
169
  const line = new Line({ points: [new Point(101, 102), new Point(201, 202)] });
154
- block.addItem(line);
170
+ block.addEntity(line);
155
171
  expect(block.touched(selectionExtremesTrue)).toBe(true);
156
172
  expect(block.touched(selectionExtremesFalse)).toBe(false);
157
173
  });
174
+
175
+ test('Test Block.dxf throws when a block item has an undefined handle', () => {
176
+ const block = new Block({ handle: '1', endblkHandle: '2' });
177
+ // cloneObject clears the handle, simulating an item added without a handle assignment
178
+ const line = new Line({ handle: '3', points: [new Point(0, 0), new Point(1, 1)] });
179
+ const clone = Utils.cloneObject(line);
180
+ expect(clone.getProperty('handle')).toBeUndefined();
181
+ block.addEntity(clone);
182
+
183
+ // Use a real DXFFile (R2000+) so the version-gated handle validation fires
184
+ const file = new DXFFile('R2000');
185
+ expect(() => block.dxf(file)).toThrow();
186
+ });
187
+
188
+ test('Test Block.dxf succeeds when block items have valid handles', () => {
189
+ const block = new Block({ handle: '1', endblkHandle: '2' });
190
+ const line = new Line({ handle: '3', points: [new Point(0, 0), new Point(1, 1)] });
191
+ const clone = Utils.cloneObject(line);
192
+ // simulate the handle assignment done in Block.execute()
193
+ clone.setProperty('handle', DesignCore.HandleManager.next());
194
+ expect(clone.getProperty('handle')).toMatch(/^[0-9A-F]+$/i);
195
+ block.addEntity(clone);
196
+
197
+ const file = new DXFFile('R2000');
198
+ expect(() => block.dxf(file)).not.toThrow();
199
+ expect(file.contents).toContain(clone.getProperty('handle'));
200
+ });
201
+
202
+ test('Test Block.execute offsets entity points by the insert point', async () => {
203
+ new Core();
204
+
205
+ // Add a line to the scene at world coordinates (100,200) → (300,400)
206
+ const line = new Line({ points: [new Point(100, 200), new Point(300, 400)] });
207
+ DesignCore.Scene.entities.add(line);
208
+ const lineIndex = DesignCore.Scene.entities.count() - 1;
209
+
210
+ // Pre-select the line so Block.execute() skips the selection-set prompt
211
+ DesignCore.Scene.selectionManager.reset();
212
+ DesignCore.Scene.selectionManager.addToSelectionSet(lineIndex);
213
+
214
+ // Feed: block name, then insert point (50, 75)
215
+ const insertPoint = new Point(50, 75);
216
+ const origRequestInput = DesignCore.Scene.inputManager.requestInput.bind(DesignCore.Scene.inputManager);
217
+ DesignCore.Scene.inputManager.requestInput = mockRequestInput(['TestBlock', insertPoint]);
218
+
219
+ await new Block().execute();
220
+
221
+ DesignCore.Scene.inputManager.requestInput = origRequestInput;
222
+
223
+ // The block should exist in the block manager
224
+ const block = DesignCore.Scene.blockManager.getItemByName('TestBlock');
225
+ expect(block).toBeDefined();
226
+ expect(block.entities).toHaveLength(1);
227
+
228
+ // Points inside the block must be offset by -insertPoint so that
229
+ // when Insert renders them via applyTransform(+insertPoint) they land
230
+ // back at their original world coordinates.
231
+ const blockLine = block.entities[0];
232
+ expect(blockLine.points[0].x).toBeCloseTo(100 - insertPoint.x);
233
+ expect(blockLine.points[0].y).toBeCloseTo(200 - insertPoint.y);
234
+ expect(blockLine.points[1].x).toBeCloseTo(300 - insertPoint.x);
235
+ expect(blockLine.points[1].y).toBeCloseTo(400 - insertPoint.y);
236
+ });
@@ -3,12 +3,12 @@ import { File } from '../test-helpers/test-helpers.js';
3
3
 
4
4
  test('Test EndBlock constructor defaults', () => {
5
5
  const endblk = new EndBlock();
6
- expect(endblk.layer).toBe('0');
6
+ expect(endblk.getProperty('layer')).toBe('0');
7
7
  });
8
8
 
9
9
  test('Test EndBlock constructor with layer', () => {
10
10
  const endblk = new EndBlock({ 8: 'TestLayer' });
11
- expect(endblk.layer).toBe('TestLayer');
11
+ expect(endblk.getProperty('layer')).toBe('TestLayer');
12
12
  });
13
13
 
14
14
  test('Test EndBlock.dxf', () => {
@@ -62,6 +62,36 @@ test('Test Layer.isSelectable', () => {
62
62
  expect(layer.isSelectable).toBe(true);
63
63
  });
64
64
 
65
+ test('Test Layer.isPlottable - defaults to true', () => {
66
+ const layer = new Layer();
67
+ expect(layer.isPlottable).toBe(true);
68
+ });
69
+
70
+ test('Test Layer.isPlottable - false when plotting is false', () => {
71
+ const layer = new Layer();
72
+ layer.plotting = false;
73
+ expect(layer.isPlottable).toBe(false);
74
+ });
75
+
76
+ test('Test Layer.isPlottable - false when layer is frozen', () => {
77
+ const layer = new Layer();
78
+ layer.frozen = true;
79
+ expect(layer.isPlottable).toBe(false);
80
+ });
81
+
82
+ test('Test Layer.isPlottable - false when layer is off', () => {
83
+ const layer = new Layer();
84
+ layer.on = false;
85
+ expect(layer.isPlottable).toBe(false);
86
+ });
87
+
88
+ test('Test Layer.isPlottable - false when both plotting false and frozen', () => {
89
+ const layer = new Layer();
90
+ layer.plotting = false;
91
+ layer.frozen = true;
92
+ expect(layer.isPlottable).toBe(false);
93
+ });
94
+
65
95
  test('Test Layer.dxf', () => {
66
96
  const layer = new Layer({ handle: 'A', name: 'TestLayer' });
67
97
  const file = new File();
@@ -91,7 +91,7 @@ test('Test LayerManager.checkStyles', () => {
91
91
  layer: newItemName,
92
92
  };
93
93
 
94
- core.scene.addItem('Line', data);
94
+ core.scene.addEntity('Line', data);
95
95
 
96
96
  layerManager.checkStyles();
97
97
  const layers = layerManager.getItems();
@@ -115,7 +115,7 @@ test('Test LayerManager.checkItems', () => {
115
115
  layer: newItemName,
116
116
  };
117
117
 
118
- core.scene.addItem('Line', data);
118
+ core.scene.addEntity('Line', data);
119
119
 
120
120
  layerManager.checkItems();
121
121
  const layers = layerManager.getItems();