@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
@@ -14,45 +14,45 @@ const data = {
14
14
  colour: { r: 100, g: 100, b: 100 },
15
15
  };
16
16
 
17
- DesignCore.Scene.addItem('Line', data);
18
- DesignCore.Scene.addItem('Circle', data);
19
- DesignCore.Scene.addItem('Text', data);
17
+ DesignCore.Scene.addEntity('Line', data);
18
+ DesignCore.Scene.addEntity('Circle', data);
19
+ DesignCore.Scene.addEntity('Text', data);
20
20
  // Add Arc with a different Colour
21
- DesignCore.Scene.addItem('Arc', { points: [point1, point2], colour: { r: 130, g: 130, b: 130 } });
21
+ DesignCore.Scene.addEntity('Arc', { points: [point1, point2], colour: { r: 130, g: 130, b: 130 } });
22
22
 
23
- test('Test propertyManager.getItemTypes', () => {
23
+ test('Test propertyManager.getEntityTypes', () => {
24
24
  // Add an item to the selectionSet
25
25
  DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(0);
26
- let types = propertiesManager.getItemTypes();
26
+ let types = propertiesManager.getEntityTypes();
27
27
  expect(types.length).toBe(1);
28
28
 
29
29
  // Add the same item to the selectionSet - shouldn't change the count
30
30
  DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(0);
31
- types = propertiesManager.getItemTypes();
31
+ types = propertiesManager.getEntityTypes();
32
32
  expect(types.length).toBe(1);
33
33
 
34
34
  // Add a new item to the selectionSet - result should include 'All'
35
35
  DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(1);
36
- types = propertiesManager.getItemTypes();
36
+ types = propertiesManager.getEntityTypes();
37
37
  expect(types.length).toBe(3);
38
38
  expect(types[0]).toBe('All');
39
39
  });
40
40
 
41
- test('Test propertyManager.setItemProperties', () => {
41
+ test('Test propertyManager.setEntityProperties', () => {
42
42
  // clear the selection set
43
43
  DesignCore.Scene.reset();
44
44
  // add the text entity
45
45
  DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(2);
46
46
  // set the string attribute of the text entity
47
47
  const string = 'test text';
48
- propertiesManager.setItemProperties('string', string);
48
+ propertiesManager.setEntityProperties('string', string);
49
49
 
50
50
  let text = DesignCore.Scene.entities.get(2);
51
51
  expect(text.type).toBe('Text');
52
- expect(text.string).toBe(string);
52
+ expect(text.getProperty('string')).toBe(string);
53
53
 
54
54
  // try and set a non-existent property
55
- propertiesManager.setItemProperties('faux-prop', string);
55
+ propertiesManager.setEntityProperties('faux-prop', string);
56
56
  text = DesignCore.Scene.entities.get(2);
57
57
  expect(text['faux-prop']).toBeUndefined();
58
58
 
@@ -61,55 +61,55 @@ test('Test propertyManager.setItemProperties', () => {
61
61
  // Select the circle element
62
62
  DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(1);
63
63
  // get the current radius
64
- const radius = DesignCore.Scene.entities.get(1).radius;
64
+ const radius = DesignCore.Scene.entities.get(1).getProperty('radius');
65
65
  // try and set an incorrect value
66
- propertiesManager.setItemProperties('radius', string);
66
+ propertiesManager.setEntityProperties('radius', string);
67
67
  const circle = DesignCore.Scene.entities.get(1);
68
- expect(circle.radius).toBe(radius);
68
+ expect(circle.getProperty('radius')).toBe(radius);
69
69
  });
70
70
 
71
- test('Test propertyManager.getItemProperties', () => {
71
+ test('Test propertyManager.getEntityProperties', () => {
72
72
  // clear the selection set
73
73
  DesignCore.Scene.reset();
74
74
 
75
75
  // get props with nothing selected - should be undefined
76
- let properties = propertiesManager.getItemProperties();
76
+ let properties = propertiesManager.getEntityProperties();
77
77
  expect(properties).toBeUndefined();
78
78
 
79
79
  // Add the line entity to the selectionSet
80
80
  DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(0);
81
- properties = propertiesManager.getItemProperties('Line');
81
+ properties = propertiesManager.getEntityProperties('Line');
82
82
  expect(properties.length).toBeGreaterThan(0);
83
83
 
84
84
  // check properties for All itemTypes
85
- properties = propertiesManager.getItemProperties('All');
85
+ properties = propertiesManager.getEntityProperties('All');
86
86
  expect(properties.length).toBeGreaterThan(0);
87
87
 
88
88
  // get circle props with a line selected - should be 0
89
- properties = propertiesManager.getItemProperties('Circle');
89
+ properties = propertiesManager.getEntityProperties('Circle');
90
90
  expect(properties.length).toBe(0);
91
91
  });
92
92
 
93
- test('Test propertyManager.getItemPropertyValue', () => {
93
+ test('Test propertyManager.getEntityPropertyValue', () => {
94
94
  // clear the selection set
95
95
  DesignCore.Scene.reset();
96
96
 
97
97
  // get props with nothing selected - should be undefined
98
- let propertyValues = propertiesManager.getItemPropertyValue();
98
+ let propertyValues = propertiesManager.getEntityPropertyValue();
99
99
  expect(propertyValues).toBeUndefined();
100
100
 
101
101
  // Add the line entity to the selectionSet
102
102
  DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(0);
103
103
  // get the line colour
104
- propertyValues = propertiesManager.getItemPropertyValue('Line', 'colour');
104
+ propertyValues = propertiesManager.getEntityPropertyValue('Line', 'colour');
105
105
  expect(propertyValues).toEqual(data.colour);
106
106
 
107
107
  // get circle props with a line selected - should be undefined
108
- propertyValues = propertiesManager.getItemPropertyValue('Circle', 'colour');
108
+ propertyValues = propertiesManager.getEntityPropertyValue('Circle', 'colour');
109
109
  expect(propertyValues).toBeUndefined();
110
110
 
111
111
  // get props for all selected types - index 0 and 3 colour should be Varies
112
112
  DesignCore.Scene.selectionManager.selectionSet.selectionSet.push(3);
113
- propertyValues = propertiesManager.getItemPropertyValue('All', 'colour');
113
+ propertyValues = propertiesManager.getEntityPropertyValue('All', 'colour');
114
114
  expect(propertyValues).toBe('Varies');
115
115
  });
@@ -0,0 +1,317 @@
1
+ import { CanvasRenderer } from '../../core/lib/renderers/canvasRenderer.js';
2
+ import { Point } from '../../core/entities/point.js';
3
+
4
+ // ─── Mock context factory ─────────────────────────────────────────────────────
5
+
6
+ /**
7
+ * Build a minimal mock CanvasRenderingContext2D that records every call.
8
+ * @return {Object} mock context with a `calls` array
9
+ */
10
+ function makeMockCtx() {
11
+ const calls = [];
12
+ const record = (name) => (...args) => calls.push({ name, args });
13
+ return {
14
+ calls,
15
+ save: record('save'),
16
+ restore: record('restore'),
17
+ beginPath: record('beginPath'),
18
+ closePath: record('closePath'),
19
+ moveTo: record('moveTo'),
20
+ lineTo: record('lineTo'),
21
+ arc: record('arc'),
22
+ stroke: record('stroke'),
23
+ fill: record('fill'),
24
+ clip: record('clip'),
25
+ fillText: record('fillText'),
26
+ strokeText: record('strokeText'),
27
+ translate: record('translate'),
28
+ rotate: record('rotate'),
29
+ scale: record('scale'),
30
+ setLineDash: record('setLineDash'),
31
+ setTransform: record('setTransform'),
32
+ fillRect: record('fillRect'),
33
+ measureText: (s) => ({ width: s.length * 8 }),
34
+ strokeStyle: '', fillStyle: '', lineWidth: 0, lineDashOffset: 0, globalAlpha: 1, font: '',
35
+ };
36
+ }
37
+
38
+ // ─── drawShape ────────────────────────────────────────────────────────────────
39
+
40
+ describe('CanvasRenderer — drawShape', () => {
41
+ test('returns early and emits nothing for empty points', () => {
42
+ const ctx = makeMockCtx();
43
+ new CanvasRenderer(ctx).drawShape([]);
44
+ expect(ctx.calls).toHaveLength(0);
45
+ });
46
+
47
+ test('calls beginPath, moveTo, lineTo, stroke for a basic shape', () => {
48
+ const ctx = makeMockCtx();
49
+ new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 20 }]);
50
+ const names = ctx.calls.map((c) => c.name);
51
+ expect(names).toContain('beginPath');
52
+ expect(names).toContain('moveTo');
53
+ expect(names).toContain('lineTo');
54
+ expect(names).toContain('stroke');
55
+ expect(names).not.toContain('fill');
56
+ });
57
+
58
+ test('calls closePath before stroke when closed=true', () => {
59
+ const ctx = makeMockCtx();
60
+ new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { closed: true });
61
+ const names = ctx.calls.map((c) => c.name);
62
+ const closeIdx = names.lastIndexOf('closePath');
63
+ const strokeIdx = names.lastIndexOf('stroke');
64
+ expect(closeIdx).toBeGreaterThan(-1);
65
+ expect(strokeIdx).toBeGreaterThan(closeIdx);
66
+ });
67
+
68
+ test('calls fill with nonzero when fill=true', () => {
69
+ const ctx = makeMockCtx();
70
+ new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: false });
71
+ const fillCall = ctx.calls.find((c) => c.name === 'fill');
72
+ expect(fillCall).toBeDefined();
73
+ expect(fillCall.args[0]).toBe('nonzero');
74
+ expect(ctx.calls.map((c) => c.name)).not.toContain('stroke');
75
+ });
76
+
77
+ test('calls fill with evenodd when fillRule=evenodd', () => {
78
+ const ctx = makeMockCtx();
79
+ new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: false, fillRule: 'evenodd' });
80
+ const fillCall = ctx.calls.find((c) => c.name === 'fill');
81
+ expect(fillCall.args[0]).toBe('evenodd');
82
+ });
83
+
84
+ test('calls both fill and stroke when fill=true and stroke=true', () => {
85
+ const ctx = makeMockCtx();
86
+ new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: true });
87
+ const names = ctx.calls.map((c) => c.name);
88
+ expect(names).toContain('fill');
89
+ expect(names).toContain('stroke');
90
+ });
91
+
92
+ test('calls clip with nonzero when clip=true', () => {
93
+ const ctx = makeMockCtx();
94
+ new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { clip: true });
95
+ const clipCall = ctx.calls.find((c) => c.name === 'clip');
96
+ expect(clipCall).toBeDefined();
97
+ expect(clipCall.args[0]).toBe('nonzero');
98
+ expect(ctx.calls.map((c) => c.name)).not.toContain('stroke');
99
+ });
100
+
101
+ test('calls clip with evenodd when clip=true and fillRule=evenodd', () => {
102
+ const ctx = makeMockCtx();
103
+ new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { clip: true, fillRule: 'evenodd' });
104
+ const clipCall = ctx.calls.find((c) => c.name === 'clip');
105
+ expect(clipCall.args[0]).toBe('evenodd');
106
+ });
107
+
108
+ test('wraps fill in save/restore when alpha < 1', () => {
109
+ const ctx = makeMockCtx();
110
+ new CanvasRenderer(ctx).drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }], { fill: true, stroke: false, alpha: 0.5 });
111
+ const names = ctx.calls.map((c) => c.name);
112
+ const saveIdx = names.lastIndexOf('save');
113
+ const fillIdx = names.lastIndexOf('fill');
114
+ const restoreIdx = names.lastIndexOf('restore');
115
+ expect(saveIdx).toBeLessThan(fillIdx);
116
+ expect(fillIdx).toBeLessThan(restoreIdx);
117
+ expect(ctx.globalAlpha).toBe(0.5);
118
+ });
119
+
120
+ test('draws highlight halo before normal path when highlighted', () => {
121
+ const ctx = makeMockCtx();
122
+ const renderer = new CanvasRenderer(ctx);
123
+ renderer.setHighlight(true, { r: 255, g: 0, b: 0 }, 3);
124
+ renderer.drawShape([{ x: 0, y: 0 }, { x: 10, y: 0 }]);
125
+ // Two save/restore pairs: one for the highlight pass, none extra for normal
126
+ const saves = ctx.calls.filter((c) => c.name === 'save');
127
+ const strokes = ctx.calls.filter((c) => c.name === 'stroke');
128
+ expect(saves.length).toBeGreaterThanOrEqual(1);
129
+ expect(strokes.length).toBe(2); // halo stroke + normal stroke
130
+ });
131
+ });
132
+
133
+ // ─── tracePath ────────────────────────────────────────────────────────────────
134
+
135
+ describe('CanvasRenderer — tracePath', () => {
136
+ test('returns early for empty points', () => {
137
+ const ctx = makeMockCtx();
138
+ new CanvasRenderer(ctx).tracePath([]);
139
+ expect(ctx.calls).toHaveLength(0);
140
+ });
141
+
142
+ test('calls moveTo for first point and lineTo for each subsequent straight segment', () => {
143
+ const ctx = makeMockCtx();
144
+ new CanvasRenderer(ctx).tracePath([{ x: 0, y: 0, bulge: 0 }, { x: 5, y: 0, bulge: 0 }, { x: 10, y: 5, bulge: 0 }]);
145
+ expect(ctx.calls[0]).toEqual({ name: 'moveTo', args: [0, 0] });
146
+ expect(ctx.calls[1]).toEqual({ name: 'lineTo', args: [5, 0] });
147
+ expect(ctx.calls[2]).toEqual({ name: 'lineTo', args: [10, 5] });
148
+ });
149
+
150
+ test('calls arc for a bulge segment', () => {
151
+ const ctx = makeMockCtx();
152
+ const p1 = new Point(0, 0, 1); // bulge=1 → semicircle CCW
153
+ const p2 = new Point(10, 0);
154
+ new CanvasRenderer(ctx).tracePath([p1, p2]);
155
+ const arcCall = ctx.calls.find((c) => c.name === 'arc');
156
+ expect(arcCall).toBeDefined();
157
+ expect(arcCall.args).toHaveLength(6);
158
+ });
159
+
160
+ test('passes anticlockwise=true to arc for negative bulge', () => {
161
+ const ctx = makeMockCtx();
162
+ const p1 = new Point(0, 0, -1); // negative bulge → CW in DXF = anticlockwise=true in canvas
163
+ const p2 = new Point(10, 0);
164
+ new CanvasRenderer(ctx).tracePath([p1, p2]);
165
+ const arcCall = ctx.calls.find((c) => c.name === 'arc');
166
+ expect(arcCall.args[5]).toBe(true);
167
+ });
168
+
169
+ test('passes anticlockwise=false to arc for positive bulge', () => {
170
+ const ctx = makeMockCtx();
171
+ const p1 = new Point(0, 0, 1);
172
+ const p2 = new Point(10, 0);
173
+ new CanvasRenderer(ctx).tracePath([p1, p2]);
174
+ const arcCall = ctx.calls.find((c) => c.name === 'arc');
175
+ expect(arcCall.args[5]).toBe(false);
176
+ });
177
+ });
178
+
179
+ // ─── drawSegments ─────────────────────────────────────────────────────────────
180
+
181
+ describe('CanvasRenderer — drawSegments', () => {
182
+ test('returns early for empty segments', () => {
183
+ const ctx = makeMockCtx();
184
+ new CanvasRenderer(ctx).drawSegments([]);
185
+ expect(ctx.calls).toHaveLength(0);
186
+ });
187
+
188
+ test('calls stroke without setLineDash for solid segments', () => {
189
+ const ctx = makeMockCtx();
190
+ new CanvasRenderer(ctx).drawSegments([{ x1: 0, y1: 0, x2: 10, y2: 0 }]);
191
+ expect(ctx.calls.map((c) => c.name)).not.toContain('setLineDash');
192
+ expect(ctx.calls.map((c) => c.name)).toContain('stroke');
193
+ });
194
+
195
+ test('calls setLineDash per segment when dashes provided', () => {
196
+ const ctx = makeMockCtx();
197
+ new CanvasRenderer(ctx).drawSegments(
198
+ [{ x1: 0, y1: 0, x2: 10, y2: 0, dashPhase: 2 }],
199
+ [5, 3],
200
+ );
201
+ const dashCall = ctx.calls.find((c) => c.name === 'setLineDash');
202
+ expect(dashCall).toBeDefined();
203
+ expect(dashCall.args[0]).toEqual([5, 3]);
204
+ expect(ctx.lineDashOffset).toBe(2);
205
+ });
206
+
207
+ test('draws highlight halo before normal pass when highlighted', () => {
208
+ const ctx = makeMockCtx();
209
+ const renderer = new CanvasRenderer(ctx);
210
+ renderer.setHighlight(true, { r: 0, g: 255, b: 0 }, 2);
211
+ renderer.drawSegments([{ x1: 0, y1: 0, x2: 10, y2: 0 }]);
212
+ const strokes = ctx.calls.filter((c) => c.name === 'stroke');
213
+ expect(strokes.length).toBe(2);
214
+ });
215
+ });
216
+
217
+ // ─── drawText ─────────────────────────────────────────────────────────────────
218
+
219
+ describe('CanvasRenderer — drawText', () => {
220
+ test('returns early for null characters', () => {
221
+ const ctx = makeMockCtx();
222
+ new CanvasRenderer(ctx).drawText(null, 'Arial', 12);
223
+ expect(ctx.calls).toHaveLength(0);
224
+ });
225
+
226
+ test('returns early for empty characters array', () => {
227
+ const ctx = makeMockCtx();
228
+ new CanvasRenderer(ctx).drawText([], 'Arial', 12);
229
+ expect(ctx.calls).toHaveLength(0);
230
+ });
231
+
232
+ test('sets ctx.font from height and fontName', () => {
233
+ const ctx = makeMockCtx();
234
+ new CanvasRenderer(ctx).drawText([{ x: 0, y: 0, char: 'A' }], 'Courier', 14);
235
+ expect(ctx.font).toBe('14pt Courier');
236
+ });
237
+
238
+ test('calls translate, scale(1,-1), fillText for each character', () => {
239
+ const ctx = makeMockCtx();
240
+ new CanvasRenderer(ctx).drawText([{ x: 5, y: 10, char: 'X' }], 'Arial', 12);
241
+ const translateCall = ctx.calls.find((c) => c.name === 'translate');
242
+ const scaleCall = ctx.calls.find((c) => c.name === 'scale');
243
+ const fillTextCall = ctx.calls.find((c) => c.name === 'fillText');
244
+ expect(translateCall.args).toEqual([5, 10]);
245
+ expect(scaleCall.args).toEqual([1, -1]);
246
+ expect(fillTextCall.args).toEqual(['X', 0, 0]);
247
+ });
248
+
249
+ test('calls rotate with negated rotation when rotation is set', () => {
250
+ const ctx = makeMockCtx();
251
+ new CanvasRenderer(ctx).drawText([{ x: 0, y: 0, char: 'A', rotation: Math.PI / 4 }], 'Arial', 12);
252
+ const rotateCall = ctx.calls.find((c) => c.name === 'rotate');
253
+ expect(rotateCall).toBeDefined();
254
+ expect(rotateCall.args[0]).toBeCloseTo(-Math.PI / 4);
255
+ });
256
+
257
+ test('uses strokeText for highlight pass', () => {
258
+ const ctx = makeMockCtx();
259
+ const renderer = new CanvasRenderer(ctx);
260
+ renderer.setHighlight(true, { r: 255, g: 0, b: 0 }, 2);
261
+ renderer.drawText([{ x: 0, y: 0, char: 'A' }], 'Arial', 12);
262
+ expect(ctx.calls.map((c) => c.name)).toContain('strokeText');
263
+ expect(ctx.calls.map((c) => c.name)).toContain('fillText');
264
+ });
265
+ });
266
+
267
+ // ─── state setters ────────────────────────────────────────────────────────────
268
+
269
+ describe('CanvasRenderer — state setters', () => {
270
+ test('setColour sets strokeStyle and fillStyle', () => {
271
+ const ctx = makeMockCtx();
272
+ const renderer = new CanvasRenderer(ctx);
273
+ renderer.setColour({ r: 255, g: 0, b: 0 });
274
+ expect(ctx.strokeStyle).toBeTruthy();
275
+ expect(ctx.fillStyle).toBeTruthy();
276
+ expect(ctx.strokeStyle).toBe(ctx.fillStyle);
277
+ });
278
+
279
+ test('setLineWidth sets ctx.lineWidth and is reflected in next drawShape', () => {
280
+ const ctx = makeMockCtx();
281
+ const renderer = new CanvasRenderer(ctx);
282
+ renderer.setLineWidth(4);
283
+ expect(ctx.lineWidth).toBe(4);
284
+ });
285
+
286
+ test('setDash sets ctx.setLineDash and lineDashOffset', () => {
287
+ const ctx = makeMockCtx();
288
+ const renderer = new CanvasRenderer(ctx);
289
+ renderer.setDash([4, 2], 1);
290
+ const dashCall = ctx.calls.find((c) => c.name === 'setLineDash');
291
+ expect(dashCall.args[0]).toEqual([4, 2]);
292
+ expect(ctx.lineDashOffset).toBe(1);
293
+ });
294
+
295
+ test('save and restore delegate to ctx', () => {
296
+ const ctx = makeMockCtx();
297
+ const renderer = new CanvasRenderer(ctx);
298
+ renderer.save();
299
+ renderer.restore();
300
+ expect(ctx.calls.map((c) => c.name)).toEqual(['save', 'restore']);
301
+ });
302
+ });
303
+
304
+ // ─── measureText / measureCharWidth ──────────────────────────────────────────
305
+
306
+ describe('CanvasRenderer — measurement', () => {
307
+ test('measureText delegates to ctx.measureText', () => {
308
+ const ctx = makeMockCtx();
309
+ const result = new CanvasRenderer(ctx).measureText('hello', 'Arial', 12);
310
+ expect(result).toEqual({ width: 40 }); // 5 chars × 8
311
+ });
312
+
313
+ test('measureCharWidth returns width from ctx.measureText', () => {
314
+ const ctx = makeMockCtx();
315
+ expect(new CanvasRenderer(ctx).measureCharWidth('A')).toBe(8);
316
+ });
317
+ });