@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.
- package/.eslintrc.json +4 -0
- package/.github/workflows/DesignCoreQA.yml +3 -0
- package/TODO.md +49 -6
- package/core/core/core.js +0 -3
- package/core/designCore.js +1 -1
- package/core/dimensions/alignedDimension.js +12 -11
- package/core/dimensions/angularDimension.js +38 -37
- package/core/dimensions/{baseCircularDimension.js → circularDimensionBase.js} +22 -21
- package/core/dimensions/diametricDimension.js +14 -13
- package/core/dimensions/dimension.js +62 -55
- package/core/dimensions/{baseDimension.js → dimensionBase.js} +73 -107
- package/core/dimensions/{baseLinearDimension.js → linearDimensionBase.js} +15 -14
- package/core/dimensions/radialDimension.js +12 -11
- package/core/dimensions/rotatedDimension.js +20 -19
- package/core/entities/arc.js +123 -156
- package/core/entities/arctext.js +222 -184
- package/core/entities/circle.js +77 -99
- package/core/entities/entity.js +114 -93
- package/core/entities/hatch.js +346 -273
- package/core/entities/insert.js +68 -83
- package/core/entities/line.js +21 -142
- package/core/entities/lwpolyline.js +16 -11
- package/core/entities/point.js +19 -19
- package/core/entities/polyline.js +7 -7
- package/core/entities/{basePolyline.js → polylineBase.js} +47 -299
- package/core/entities/rectangle.js +3 -1
- package/core/entities/solid.js +6 -15
- package/core/entities/text.js +190 -201
- package/core/lib/auxiliary/rubberBand.js +32 -0
- package/core/lib/auxiliary/selectionWindow.js +63 -0
- package/core/lib/auxiliary/snapPoint.js +113 -0
- package/core/lib/auxiliary/trackingLine.js +76 -0
- package/core/lib/boundingBox.js +11 -11
- package/core/lib/canvas.js +232 -168
- package/core/lib/clipboard.js +9 -3
- package/core/lib/colour.js +16 -0
- package/core/lib/commandLine.js +1 -6
- package/core/lib/commandManager.js +18 -8
- package/core/lib/constants.js +28 -0
- package/core/lib/dxf/dxf.js +15 -14
- package/core/lib/dxf/dxfIterator.js +2 -2
- package/core/lib/entityManager.js +33 -26
- package/core/lib/inputManager.js +36 -12
- package/core/lib/intersect.js +25 -0
- package/core/lib/matrix.js +1 -1
- package/core/lib/patterns.js +9 -2
- package/core/lib/plotOptions.js +67 -0
- package/core/lib/polylineUtils.js +75 -0
- package/core/lib/renderers/cairoRenderer.js +371 -0
- package/core/lib/renderers/canvasRenderer.js +315 -0
- package/core/lib/renderers/pdfRenderer.js +366 -0
- package/core/lib/renderers/rendererBase.js +284 -0
- package/core/lib/renderers/svgRenderer.js +343 -0
- package/core/lib/scene.js +28 -6
- package/core/lib/selectionManager.js +20 -20
- package/core/lib/settings.js +4 -2
- package/core/lib/snapping.js +4 -191
- package/core/lib/stateManager.js +30 -1
- package/core/lib/utils.js +26 -7
- package/core/properties/entityProperties.js +104 -0
- package/core/properties/property.js +106 -2
- package/core/properties/propertyManager.js +60 -44
- package/core/tables/block.js +50 -81
- package/core/tables/dimStyle.js +5 -5
- package/core/tables/endblk.js +1 -1
- package/core/tables/layer.js +8 -0
- package/core/tables/ltypeManager.js +1 -1
- package/core/tables/styleManagerBase.js +3 -2
- package/core/tables/tableManagerBase.js +8 -3
- package/core/tables/vport.js +2 -2
- package/core/tools/chamfer.js +117 -143
- package/core/tools/chamferFilletBase.js +138 -37
- package/core/tools/copy.js +11 -11
- package/core/tools/copybase.js +3 -3
- package/core/tools/copyclip.js +3 -3
- package/core/tools/cornerEntity.js +2 -2
- package/core/tools/cutclip.js +3 -3
- package/core/tools/explode.js +7 -7
- package/core/tools/extend.js +138 -25
- package/core/tools/fillet.js +92 -189
- package/core/tools/matchProp.js +3 -12
- package/core/tools/mirror.js +15 -14
- package/core/tools/move.js +8 -8
- package/core/tools/offset.js +31 -33
- package/core/tools/pasteclip.js +1 -1
- package/core/tools/purge.js +10 -10
- package/core/tools/rotate.js +19 -16
- package/core/tools/scale.js +7 -7
- package/core/tools/trim.js +271 -29
- package/core/tools/zoom.js +1 -1
- package/cspell.json +321 -0
- package/jest.config.js +6 -0
- package/package.json +5 -3
- package/test/dimensions/alignedDimension.test.js +17 -10
- package/test/dimensions/angularDimension.test.js +13 -13
- package/test/dimensions/diametricDimension.test.js +15 -15
- package/test/dimensions/dimension.test.js +12 -12
- package/test/dimensions/{baseDimension.test.js → dimensionBase.test.js} +73 -12
- package/test/dimensions/linearDimensionBase.test.js +62 -0
- package/test/dimensions/radialDimension.test.js +15 -15
- package/test/dimensions/rotatedDimension.test.js +18 -11
- package/test/entities/arc.test.js +83 -86
- package/test/entities/arctext.test.js +184 -57
- package/test/entities/circle.test.js +68 -96
- package/test/entities/entity.test.js +42 -8
- package/test/entities/hatch.test.js +569 -96
- package/test/entities/insert.test.js +99 -4
- package/test/entities/line.test.js +15 -41
- package/test/entities/lwpolyline.test.js +104 -0
- package/test/entities/point.test.js +3 -3
- package/test/entities/polyline.test.js +11 -0
- package/test/entities/{basePolyline.test.js → polylineBase.test.js} +192 -57
- package/test/entities/rectangle.test.js +11 -0
- package/test/entities/solid.test.js +1 -1
- package/test/entities/text.test.js +302 -77
- package/test/integration/exportIntegration.reference.dxf +9 -9
- package/test/integration/exportIntegration.test.js +5 -5
- package/test/integration/intersectionIntegration.test.js +4 -5
- package/test/integration/pdfRendererIntegration.reference.pdf +17709 -0
- package/test/integration/pdfRendererIntegration.test.js +61 -0
- package/test/integration/svgRendererIntegration.reference.svg +3343 -0
- package/test/integration/svgRendererIntegration.test.js +61 -0
- package/test/integration/userInputIntegration.reference.dxf +1165 -105
- package/test/integration/userInputIntegration.test.js +239 -132
- package/test/lib/boundingBox.test.js +3 -3
- package/test/lib/canvas.test.js +744 -125
- package/test/lib/constants.test.js +27 -0
- package/test/lib/entityManager.test.js +4 -4
- package/test/lib/inputManager.test.js +81 -20
- package/test/lib/intersect.test.js +83 -0
- package/test/lib/plotOptions.test.js +179 -0
- package/test/lib/rubberBand.test.js +84 -0
- package/test/lib/scene.test.js +9 -9
- package/test/lib/selectionManager.test.js +3 -3
- package/test/lib/settings.test.js +5 -6
- package/test/lib/snapping.test.js +72 -130
- package/test/lib/stateManager.test.js +7 -7
- package/test/lib/utils.test.js +36 -2
- package/test/properties/entityProperties.test.js +132 -0
- package/test/properties/property.test.js +53 -0
- package/test/properties/propertyManager.test.js +25 -25
- package/test/renderers/canvasRenderer.test.js +317 -0
- package/test/renderers/pdfRenderer.test.js +271 -0
- package/test/renderers/rendererBase.test.js +213 -0
- package/test/renderers/svgRenderer.test.js +326 -0
- package/test/tables/block.test.js +92 -13
- package/test/tables/endblk.test.js +2 -2
- package/test/tables/layer.test.js +30 -0
- package/test/tables/layerManager.test.js +2 -2
- package/test/test-helpers/hatchGridRenderer.js +180 -0
- package/test/test-helpers/test-helpers.js +158 -6
- package/test/tools/chamfer.test.js +273 -51
- package/test/tools/chamferFilletBase.test.js +125 -36
- package/test/tools/copy.test.js +8 -8
- package/test/tools/copybase.test.js +2 -2
- package/test/tools/copyclip.test.js +2 -2
- package/test/tools/cornerEntity.test.js +8 -8
- package/test/tools/cutclip.test.js +2 -2
- package/test/tools/erase.test.js +6 -6
- package/test/tools/explode.test.js +5 -5
- package/test/tools/extend.test.js +373 -42
- package/test/tools/fillet.test.js +230 -49
- package/test/tools/matchProp.test.js +11 -11
- package/test/tools/mirror.test.js +39 -39
- package/test/tools/move.test.js +7 -7
- package/test/tools/offset.test.js +222 -55
- package/test/tools/pasteclip.test.js +5 -5
- package/test/tools/purge.test.js +4 -4
- package/test/tools/rotate.test.js +39 -9
- package/test/tools/scale.test.js +33 -33
- package/test/tools/trim.test.js +579 -53
- package/test/tools/zoom.test.js +12 -12
- 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.
|
|
18
|
-
DesignCore.Scene.
|
|
19
|
-
DesignCore.Scene.
|
|
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.
|
|
21
|
+
DesignCore.Scene.addEntity('Arc', { points: [point1, point2], colour: { r: 130, g: 130, b: 130 } });
|
|
22
22
|
|
|
23
|
-
test('Test propertyManager.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
81
|
+
properties = propertiesManager.getEntityProperties('Line');
|
|
82
82
|
expect(properties.length).toBeGreaterThan(0);
|
|
83
83
|
|
|
84
84
|
// check properties for All itemTypes
|
|
85
|
-
properties = propertiesManager.
|
|
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.
|
|
89
|
+
properties = propertiesManager.getEntityProperties('Circle');
|
|
90
90
|
expect(properties.length).toBe(0);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
test('Test propertyManager.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|