@xrn07/figure-renderer 0.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/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/parse.d.ts +11 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +39 -0
- package/dist/react.d.ts +14 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +21 -0
- package/dist/render.d.ts +10 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +41 -0
- package/dist/renderers/bioTable.d.ts +3 -0
- package/dist/renderers/bioTable.d.ts.map +1 -0
- package/dist/renderers/bioTable.js +28 -0
- package/dist/renderers/circuit.d.ts +3 -0
- package/dist/renderers/circuit.d.ts.map +1 -0
- package/dist/renderers/circuit.js +237 -0
- package/dist/renderers/coordinatePlane.d.ts +3 -0
- package/dist/renderers/coordinatePlane.d.ts.map +1 -0
- package/dist/renderers/coordinatePlane.js +142 -0
- package/dist/renderers/forceDiagram.d.ts +3 -0
- package/dist/renderers/forceDiagram.d.ts.map +1 -0
- package/dist/renderers/forceDiagram.js +130 -0
- package/dist/renderers/geometry.d.ts +3 -0
- package/dist/renderers/geometry.d.ts.map +1 -0
- package/dist/renderers/geometry.js +173 -0
- package/dist/renderers/graph.d.ts +3 -0
- package/dist/renderers/graph.d.ts.map +1 -0
- package/dist/renderers/graph.js +137 -0
- package/dist/renderers/numberLine.d.ts +3 -0
- package/dist/renderers/numberLine.d.ts.map +1 -0
- package/dist/renderers/numberLine.js +98 -0
- package/dist/renderers/rayDiagram.d.ts +3 -0
- package/dist/renderers/rayDiagram.d.ts.map +1 -0
- package/dist/renderers/rayDiagram.js +181 -0
- package/dist/renderers/venn.d.ts +3 -0
- package/dist/renderers/venn.d.ts.map +1 -0
- package/dist/renderers/venn.js +60 -0
- package/dist/types.d.ts +225 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/utils/arrowMarker.d.ts +13 -0
- package/dist/utils/arrowMarker.d.ts.map +1 -0
- package/dist/utils/arrowMarker.js +48 -0
- package/dist/utils/coordTransform.d.ts +29 -0
- package/dist/utils/coordTransform.d.ts.map +1 -0
- package/dist/utils/coordTransform.js +38 -0
- package/dist/utils/gridLines.d.ts +23 -0
- package/dist/utils/gridLines.d.ts.map +1 -0
- package/dist/utils/gridLines.js +124 -0
- package/dist/utils/label.d.ts +30 -0
- package/dist/utils/label.d.ts.map +1 -0
- package/dist/utils/label.js +82 -0
- package/dist/utils/path.d.ts +21 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +72 -0
- package/dist/utils/shapes.d.ts +29 -0
- package/dist/utils/shapes.d.ts.map +1 -0
- package/dist/utils/shapes.js +78 -0
- package/dist/utils/svgTag.d.ts +11 -0
- package/dist/utils/svgTag.d.ts.map +1 -0
- package/dist/utils/svgTag.js +29 -0
- package/package.json +70 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Coordinate plane renderer
|
|
2
|
+
import { svgTag } from '../utils/svgTag';
|
|
3
|
+
import { gridLines, axes } from '../utils/gridLines';
|
|
4
|
+
import { createCoordTransform } from '../utils/coordTransform';
|
|
5
|
+
import { circle, line, path, text } from '../utils/shapes';
|
|
6
|
+
export function renderCoordinatePlane(dsl) {
|
|
7
|
+
const { width, height, xRange, yRange, showGrid = true, showLabels = true, elements, title } = dsl;
|
|
8
|
+
const svgElements = [];
|
|
9
|
+
// Add arrow markers
|
|
10
|
+
svgElements.push(createAxisMarkers());
|
|
11
|
+
// Draw grid
|
|
12
|
+
if (showGrid) {
|
|
13
|
+
svgElements.push(gridLines({
|
|
14
|
+
xRange,
|
|
15
|
+
yRange,
|
|
16
|
+
width,
|
|
17
|
+
height,
|
|
18
|
+
xSteps: 10,
|
|
19
|
+
ySteps: 10,
|
|
20
|
+
stroke: '#e0e0e0',
|
|
21
|
+
strokeWidth: 1,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
// Draw axes
|
|
25
|
+
svgElements.push(axes({
|
|
26
|
+
xRange,
|
|
27
|
+
yRange,
|
|
28
|
+
width,
|
|
29
|
+
height,
|
|
30
|
+
showLabels,
|
|
31
|
+
}));
|
|
32
|
+
// Create coordinate transform
|
|
33
|
+
const transform = createCoordTransform({ xRange, yRange, width, height });
|
|
34
|
+
// Render elements
|
|
35
|
+
elements.forEach(element => {
|
|
36
|
+
svgElements.push(renderElement(element, transform));
|
|
37
|
+
});
|
|
38
|
+
const svg = svgTag('svg', {
|
|
39
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
40
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
41
|
+
}, svgElements.join(''));
|
|
42
|
+
return svg;
|
|
43
|
+
}
|
|
44
|
+
function renderElement(element, transform) {
|
|
45
|
+
switch (element.type) {
|
|
46
|
+
case 'point':
|
|
47
|
+
return renderPoint(element, transform);
|
|
48
|
+
case 'line':
|
|
49
|
+
return renderPlaneLine(element, transform);
|
|
50
|
+
case 'curve':
|
|
51
|
+
return renderCurve(element, transform);
|
|
52
|
+
case 'label':
|
|
53
|
+
return renderPlaneLabel(element, transform);
|
|
54
|
+
default:
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function renderPoint(element, transform) {
|
|
59
|
+
const data = element.data;
|
|
60
|
+
const { x, y, label, color = '#e53e3e' } = data;
|
|
61
|
+
const svgX = transform.x(x);
|
|
62
|
+
const svgY = transform.y(y);
|
|
63
|
+
const elements = [];
|
|
64
|
+
// Draw point
|
|
65
|
+
elements.push(circle(svgX, svgY, 5, {
|
|
66
|
+
fill: color,
|
|
67
|
+
stroke: '#fff',
|
|
68
|
+
'stroke-width': 2,
|
|
69
|
+
}));
|
|
70
|
+
// Add label
|
|
71
|
+
if (label) {
|
|
72
|
+
elements.push(text(svgX + 8, svgY - 8, label, {
|
|
73
|
+
'text-anchor': 'start',
|
|
74
|
+
'font-size': 12,
|
|
75
|
+
'font-weight': 'bold',
|
|
76
|
+
fill: color,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
return svgTag('g', { class: 'point' }, elements.join(''));
|
|
80
|
+
}
|
|
81
|
+
function renderPlaneLine(element, transform) {
|
|
82
|
+
const data = element.data;
|
|
83
|
+
const { x1, y1, x2, y2, color = '#333', style = 'solid' } = data;
|
|
84
|
+
const svgX1 = transform.x(x1);
|
|
85
|
+
const svgY1 = transform.y(y1);
|
|
86
|
+
const svgX2 = transform.x(x2);
|
|
87
|
+
const svgY2 = transform.y(y2);
|
|
88
|
+
const strokeDasharray = style === 'dashed' ? '8,4' : style === 'dotted' ? '2,2' : 'none';
|
|
89
|
+
return svgTag('g', { class: 'line' }, line(svgX1, svgY1, svgX2, svgY2, {
|
|
90
|
+
stroke: color,
|
|
91
|
+
'stroke-width': 2,
|
|
92
|
+
'stroke-dasharray': strokeDasharray,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
function renderCurve(element, transform) {
|
|
96
|
+
const data = element.data;
|
|
97
|
+
const { points, color = '#3182ce', style = 'solid' } = data;
|
|
98
|
+
// Transform all points
|
|
99
|
+
const svgPoints = points.map(([x, y]) => {
|
|
100
|
+
const sx = transform.x(x);
|
|
101
|
+
const sy = transform.y(y);
|
|
102
|
+
return [sx, sy];
|
|
103
|
+
});
|
|
104
|
+
// Create smooth path through points
|
|
105
|
+
let d = '';
|
|
106
|
+
if (svgPoints.length > 0) {
|
|
107
|
+
d = `M ${svgPoints[0][0]} ${svgPoints[0][1]}`;
|
|
108
|
+
for (let i = 1; i < svgPoints.length; i++) {
|
|
109
|
+
d += ` L ${svgPoints[i][0]} ${svgPoints[i][1]}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const strokeDasharray = style === 'dashed' ? '8,4' : style === 'dotted' ? '2,2' : 'none';
|
|
113
|
+
return svgTag('g', { class: 'curve' }, path(d, {
|
|
114
|
+
fill: 'none',
|
|
115
|
+
stroke: color,
|
|
116
|
+
'stroke-width': 2.5,
|
|
117
|
+
'stroke-linecap': 'round',
|
|
118
|
+
'stroke-linejoin': 'round',
|
|
119
|
+
'stroke-dasharray': strokeDasharray,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
function renderPlaneLabel(element, transform) {
|
|
123
|
+
const data = element.data;
|
|
124
|
+
const { x, y, text: labelText, align = 'center' } = data;
|
|
125
|
+
const svgX = transform.x(x);
|
|
126
|
+
const svgY = transform.y(y);
|
|
127
|
+
return text(svgX, svgY, labelText, {
|
|
128
|
+
'text-anchor': align,
|
|
129
|
+
'dominant-baseline': 'middle',
|
|
130
|
+
'font-size': 14,
|
|
131
|
+
fill: '#333',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function createAxisMarkers() {
|
|
135
|
+
return `
|
|
136
|
+
<defs>
|
|
137
|
+
<marker id="axis-arrow" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
138
|
+
<path d="M0,0 L10,5 L0,10" fill="#333" />
|
|
139
|
+
</marker>
|
|
140
|
+
</defs>
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forceDiagram.d.ts","sourceRoot":"","sources":["../../src/renderers/forceDiagram.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAyB,MAAM,UAAU,CAAC;AAKvE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAsB/D"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Force diagram renderer
|
|
2
|
+
import { svgTag } from '../utils/svgTag';
|
|
3
|
+
import { rect, circle, text, line } from '../utils/shapes';
|
|
4
|
+
export function renderForceDiagram(dsl) {
|
|
5
|
+
const { width, height, object, forces, title } = dsl;
|
|
6
|
+
const elements = [];
|
|
7
|
+
// Add arrow markers
|
|
8
|
+
elements.push(createForceMarkers());
|
|
9
|
+
// Render object
|
|
10
|
+
elements.push(renderObject(object));
|
|
11
|
+
// Render forces
|
|
12
|
+
forces.forEach(force => {
|
|
13
|
+
elements.push(renderForce(force));
|
|
14
|
+
});
|
|
15
|
+
const svg = svgTag('svg', {
|
|
16
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
17
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
18
|
+
}, elements.join(''));
|
|
19
|
+
return svg;
|
|
20
|
+
}
|
|
21
|
+
function renderObject(obj) {
|
|
22
|
+
const elements = [];
|
|
23
|
+
switch (obj.type) {
|
|
24
|
+
case 'box': {
|
|
25
|
+
const { x, y, width: w = 60, height: h = 60, label } = obj;
|
|
26
|
+
elements.push(rect(x - w / 2, y - h / 2, w, h, {
|
|
27
|
+
fill: '#f0f0f0',
|
|
28
|
+
stroke: '#333',
|
|
29
|
+
'stroke-width': 2,
|
|
30
|
+
}));
|
|
31
|
+
if (label) {
|
|
32
|
+
elements.push(text(x, y, label, {
|
|
33
|
+
'text-anchor': 'middle',
|
|
34
|
+
'dominant-baseline': 'middle',
|
|
35
|
+
'font-size': 14,
|
|
36
|
+
fill: '#333',
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'sphere': {
|
|
42
|
+
const { x, y, label } = obj;
|
|
43
|
+
const radius = 30;
|
|
44
|
+
elements.push(circle(x, y, radius, {
|
|
45
|
+
fill: '#e6f7ff',
|
|
46
|
+
stroke: '#333',
|
|
47
|
+
'stroke-width': 2,
|
|
48
|
+
}));
|
|
49
|
+
if (label) {
|
|
50
|
+
elements.push(text(x, y, label, {
|
|
51
|
+
'text-anchor': 'middle',
|
|
52
|
+
'dominant-baseline': 'middle',
|
|
53
|
+
'font-size': 12,
|
|
54
|
+
fill: '#333',
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case 'point': {
|
|
60
|
+
const { x, y, label } = obj;
|
|
61
|
+
elements.push(circle(x, y, 5, {
|
|
62
|
+
fill: '#333',
|
|
63
|
+
}));
|
|
64
|
+
if (label) {
|
|
65
|
+
elements.push(text(x + 10, y, label, {
|
|
66
|
+
'text-anchor': 'start',
|
|
67
|
+
'dominant-baseline': 'middle',
|
|
68
|
+
'font-size': 12,
|
|
69
|
+
fill: '#333',
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return svgTag('g', { class: 'object' }, elements.join(''));
|
|
76
|
+
}
|
|
77
|
+
function renderForce(force) {
|
|
78
|
+
const { originX, originY, magnitude, angle, label, color = '#e53e3e' } = force;
|
|
79
|
+
const elements = [];
|
|
80
|
+
// Convert angle to radians and calculate endpoint
|
|
81
|
+
const angleRad = (angle * Math.PI) / 180;
|
|
82
|
+
const scale = 2; // Scale factor for visibility
|
|
83
|
+
const endX = originX + magnitude * Math.cos(angleRad) * scale;
|
|
84
|
+
const endY = originY - magnitude * Math.sin(angleRad) * scale; // SVG y is inverted
|
|
85
|
+
// Draw force vector with arrow
|
|
86
|
+
elements.push(line(originX, originY, endX, endY, {
|
|
87
|
+
stroke: color,
|
|
88
|
+
'stroke-width': 3,
|
|
89
|
+
'marker-end': `url(#arrow-${color})`,
|
|
90
|
+
}));
|
|
91
|
+
// Draw label
|
|
92
|
+
if (label) {
|
|
93
|
+
// Position label slightly offset from the arrow
|
|
94
|
+
const labelOffset = 15;
|
|
95
|
+
const labelX = endX + labelOffset * Math.cos(angleRad);
|
|
96
|
+
const labelY = endY - labelOffset * Math.sin(angleRad);
|
|
97
|
+
elements.push(text(labelX, labelY, label, {
|
|
98
|
+
'text-anchor': 'start',
|
|
99
|
+
'dominant-baseline': 'middle',
|
|
100
|
+
'font-size': 14,
|
|
101
|
+
'font-weight': 'bold',
|
|
102
|
+
fill: color,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
return svgTag('g', { class: 'force' }, elements.join(''));
|
|
106
|
+
}
|
|
107
|
+
function createForceMarkers() {
|
|
108
|
+
return `
|
|
109
|
+
<defs>
|
|
110
|
+
<marker id="arrow-#e53e3e" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
111
|
+
<path d="M0,0 L10,5 L0,10" fill="#e53e3e" />
|
|
112
|
+
</marker>
|
|
113
|
+
<marker id="arrow-#3182ce" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
114
|
+
<path d="M0,0 L10,5 L0,10" fill="#3182ce" />
|
|
115
|
+
</marker>
|
|
116
|
+
<marker id="arrow-#38a169" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
117
|
+
<path d="M0,0 L10,5 L0,10" fill="#38a169" />
|
|
118
|
+
</marker>
|
|
119
|
+
<marker id="arrow-#d69e2e" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
120
|
+
<path d="M0,0 L10,5 L0,10" fill="#d69e2e" />
|
|
121
|
+
</marker>
|
|
122
|
+
<marker id="arrow-#805ad5" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
123
|
+
<path d="M0,0 L10,5 L0,10" fill="#805ad5" />
|
|
124
|
+
</marker>
|
|
125
|
+
<marker id="arrow-#333" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
126
|
+
<path d="M0,0 L10,5 L0,10" fill="#333" />
|
|
127
|
+
</marker>
|
|
128
|
+
</defs>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../../src/renderers/geometry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,UAAU,CAAC;AAK3D,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAgBvD"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Geometry figure renderer
|
|
2
|
+
import { svgTag } from '../utils/svgTag';
|
|
3
|
+
import { line, circle, rect, polygon, path, text } from '../utils/shapes';
|
|
4
|
+
import { arcPath } from '../utils/path';
|
|
5
|
+
export function renderGeometry(dsl) {
|
|
6
|
+
const { width, height, shapes, title } = dsl;
|
|
7
|
+
const elements = [];
|
|
8
|
+
// Render shapes
|
|
9
|
+
shapes.forEach(shape => {
|
|
10
|
+
elements.push(renderShape(shape));
|
|
11
|
+
});
|
|
12
|
+
const svg = svgTag('svg', {
|
|
13
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
14
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
15
|
+
}, elements.join(''));
|
|
16
|
+
return svg;
|
|
17
|
+
}
|
|
18
|
+
function renderShape(shape) {
|
|
19
|
+
switch (shape.type) {
|
|
20
|
+
case 'line':
|
|
21
|
+
return renderLine(shape);
|
|
22
|
+
case 'circle':
|
|
23
|
+
return renderCircle(shape);
|
|
24
|
+
case 'rect':
|
|
25
|
+
return renderRect(shape);
|
|
26
|
+
case 'triangle':
|
|
27
|
+
return renderTriangle(shape);
|
|
28
|
+
case 'angle':
|
|
29
|
+
return renderAngle(shape);
|
|
30
|
+
default:
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function renderLine(shape) {
|
|
35
|
+
const { x1, y1, x2, y2, label } = shape;
|
|
36
|
+
const elements = [];
|
|
37
|
+
// Draw line
|
|
38
|
+
elements.push(line(x1, y1, x2, y2, {
|
|
39
|
+
stroke: '#333',
|
|
40
|
+
'stroke-width': 2,
|
|
41
|
+
}));
|
|
42
|
+
// Add endpoints
|
|
43
|
+
elements.push(circle(x1, y1, 3, { fill: '#333' }));
|
|
44
|
+
elements.push(circle(x2, y2, 3, { fill: '#333' }));
|
|
45
|
+
// Add label if provided
|
|
46
|
+
if (label) {
|
|
47
|
+
const midX = (x1 + x2) / 2;
|
|
48
|
+
const midY = (y1 + y2) / 2;
|
|
49
|
+
elements.push(text(midX + 5, midY - 5, label, {
|
|
50
|
+
'text-anchor': 'start',
|
|
51
|
+
'font-size': 12,
|
|
52
|
+
fill: '#333',
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
return svgTag('g', { class: 'line' }, elements.join(''));
|
|
56
|
+
}
|
|
57
|
+
function renderCircle(shape) {
|
|
58
|
+
const { cx, cy, r, label } = shape;
|
|
59
|
+
const elements = [];
|
|
60
|
+
// Draw circle
|
|
61
|
+
elements.push(circle(cx, cy, r, {
|
|
62
|
+
fill: 'none',
|
|
63
|
+
stroke: '#333',
|
|
64
|
+
'stroke-width': 2,
|
|
65
|
+
}));
|
|
66
|
+
// Draw center point
|
|
67
|
+
elements.push(circle(cx, cy, 3, { fill: '#333' }));
|
|
68
|
+
// Add label if provided
|
|
69
|
+
if (label) {
|
|
70
|
+
elements.push(text(cx, cy - r - 10, label, {
|
|
71
|
+
'text-anchor': 'middle',
|
|
72
|
+
'font-size': 12,
|
|
73
|
+
fill: '#333',
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
return svgTag('g', { class: 'circle' }, elements.join(''));
|
|
77
|
+
}
|
|
78
|
+
function renderRect(shape) {
|
|
79
|
+
const { x, y, width, height, label } = shape;
|
|
80
|
+
const elements = [];
|
|
81
|
+
// Draw rectangle
|
|
82
|
+
elements.push(rect(x, y, width, height, {
|
|
83
|
+
fill: 'none',
|
|
84
|
+
stroke: '#333',
|
|
85
|
+
'stroke-width': 2,
|
|
86
|
+
}));
|
|
87
|
+
// Add corner points
|
|
88
|
+
elements.push(circle(x, y, 3, { fill: '#333' }));
|
|
89
|
+
elements.push(circle(x + width, y, 3, { fill: '#333' }));
|
|
90
|
+
elements.push(circle(x + width, y + height, 3, { fill: '#333' }));
|
|
91
|
+
elements.push(circle(x, y + height, 3, { fill: '#333' }));
|
|
92
|
+
// Add label if provided
|
|
93
|
+
if (label) {
|
|
94
|
+
elements.push(text(x + width / 2, y - 10, label, {
|
|
95
|
+
'text-anchor': 'middle',
|
|
96
|
+
'font-size': 12,
|
|
97
|
+
fill: '#333',
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
return svgTag('g', { class: 'rectangle' }, elements.join(''));
|
|
101
|
+
}
|
|
102
|
+
function renderTriangle(shape) {
|
|
103
|
+
const { points, label } = shape;
|
|
104
|
+
const elements = [];
|
|
105
|
+
// Draw triangle
|
|
106
|
+
elements.push(polygon(points, {
|
|
107
|
+
fill: 'none',
|
|
108
|
+
stroke: '#333',
|
|
109
|
+
'stroke-width': 2,
|
|
110
|
+
}));
|
|
111
|
+
// Add vertices
|
|
112
|
+
points.forEach(([px, py]) => {
|
|
113
|
+
elements.push(circle(px, py, 3, { fill: '#333' }));
|
|
114
|
+
});
|
|
115
|
+
// Add label if provided
|
|
116
|
+
if (label) {
|
|
117
|
+
// Calculate centroid
|
|
118
|
+
const cx = points.reduce((sum, [x]) => sum + x, 0) / points.length;
|
|
119
|
+
const cy = points.reduce((sum, [, y]) => sum + y, 0) / points.length;
|
|
120
|
+
elements.push(text(cx, cy, label, {
|
|
121
|
+
'text-anchor': 'middle',
|
|
122
|
+
'dominant-baseline': 'middle',
|
|
123
|
+
'font-size': 12,
|
|
124
|
+
fill: '#333',
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
return svgTag('g', { class: 'triangle' }, elements.join(''));
|
|
128
|
+
}
|
|
129
|
+
function renderAngle(shape) {
|
|
130
|
+
const { vertex, rays, label } = shape;
|
|
131
|
+
const [vx, vy] = vertex;
|
|
132
|
+
const [[r1x, r1y], [r2x, r2y]] = rays;
|
|
133
|
+
const elements = [];
|
|
134
|
+
// Calculate angle
|
|
135
|
+
const angle1 = Math.atan2(vy - r1y, r1x - vx) * 180 / Math.PI;
|
|
136
|
+
const angle2 = Math.atan2(vy - r2y, r2x - vx) * 180 / Math.PI;
|
|
137
|
+
// Draw rays
|
|
138
|
+
const rayLength = 50;
|
|
139
|
+
elements.push(line(vx, vy, r1x, r1y, {
|
|
140
|
+
stroke: '#333',
|
|
141
|
+
'stroke-width': 2,
|
|
142
|
+
}));
|
|
143
|
+
elements.push(line(vx, vy, r2x, r2y, {
|
|
144
|
+
stroke: '#333',
|
|
145
|
+
'stroke-width': 2,
|
|
146
|
+
}));
|
|
147
|
+
// Draw angle arc
|
|
148
|
+
const arcRadius = 20;
|
|
149
|
+
const startAngle = Math.atan2(r1y - vy, r1x - vx) * 180 / Math.PI;
|
|
150
|
+
const endAngle = Math.atan2(r2y - vy, r2x - vx) * 180 / Math.PI;
|
|
151
|
+
elements.push(path(arcPath(vx, vy, arcRadius, startAngle, endAngle), {
|
|
152
|
+
fill: 'rgba(100, 149, 237, 0.2)',
|
|
153
|
+
stroke: '#6495ED',
|
|
154
|
+
'stroke-width': 1.5,
|
|
155
|
+
}));
|
|
156
|
+
// Add vertex point
|
|
157
|
+
elements.push(circle(vx, vy, 4, { fill: '#333' }));
|
|
158
|
+
// Add label if provided
|
|
159
|
+
if (label) {
|
|
160
|
+
// Position label at middle of angle
|
|
161
|
+
const midAngle = ((startAngle + endAngle) / 2) * Math.PI / 180;
|
|
162
|
+
const labelRadius = arcRadius + 15;
|
|
163
|
+
const labelX = vx + labelRadius * Math.cos(midAngle);
|
|
164
|
+
const labelY = vy - labelRadius * Math.sin(midAngle);
|
|
165
|
+
elements.push(text(labelX, labelY, label, {
|
|
166
|
+
'text-anchor': 'middle',
|
|
167
|
+
'dominant-baseline': 'middle',
|
|
168
|
+
'font-size': 14,
|
|
169
|
+
fill: '#333',
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
return svgTag('g', { class: 'angle' }, elements.join(''));
|
|
173
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/renderers/graph.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAiB,MAAM,UAAU,CAAC;AAMxD,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CA2CjD"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Graph renderer
|
|
2
|
+
import { svgTag } from '../utils/svgTag';
|
|
3
|
+
import { gridLines, axes } from '../utils/gridLines';
|
|
4
|
+
import { createCoordTransform } from '../utils/coordTransform';
|
|
5
|
+
import { path } from '../utils/shapes';
|
|
6
|
+
export function renderGraph(dsl) {
|
|
7
|
+
const { width, height, xRange, yRange, functions, title } = dsl;
|
|
8
|
+
const elements = [];
|
|
9
|
+
// Add arrow markers
|
|
10
|
+
elements.push(createGraphMarkers());
|
|
11
|
+
// Draw grid
|
|
12
|
+
elements.push(gridLines({
|
|
13
|
+
xRange,
|
|
14
|
+
yRange,
|
|
15
|
+
width,
|
|
16
|
+
height,
|
|
17
|
+
xSteps: 10,
|
|
18
|
+
ySteps: 10,
|
|
19
|
+
stroke: '#e0e0e0',
|
|
20
|
+
strokeWidth: 1,
|
|
21
|
+
}));
|
|
22
|
+
// Draw axes
|
|
23
|
+
elements.push(axes({
|
|
24
|
+
xRange,
|
|
25
|
+
yRange,
|
|
26
|
+
width,
|
|
27
|
+
height,
|
|
28
|
+
showLabels: true,
|
|
29
|
+
}));
|
|
30
|
+
// Create coordinate transform
|
|
31
|
+
const transform = createCoordTransform({ xRange, yRange, width, height });
|
|
32
|
+
// Render functions
|
|
33
|
+
functions.forEach(fn => {
|
|
34
|
+
elements.push(renderFunction(fn, transform, xRange));
|
|
35
|
+
});
|
|
36
|
+
const svg = svgTag('svg', {
|
|
37
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
38
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
39
|
+
}, elements.join(''));
|
|
40
|
+
return svg;
|
|
41
|
+
}
|
|
42
|
+
function renderFunction(fn, transform, xRange) {
|
|
43
|
+
const { equation, color, style = 'solid' } = fn;
|
|
44
|
+
// Generate points along the curve
|
|
45
|
+
const step = (xRange[1] - xRange[0]) / 100;
|
|
46
|
+
const points = [];
|
|
47
|
+
for (let x = xRange[0]; x <= xRange[1]; x += step) {
|
|
48
|
+
try {
|
|
49
|
+
// Safely evaluate the function
|
|
50
|
+
const y = evaluateFunction(equation, x);
|
|
51
|
+
// Check if y is finite and within reasonable bounds
|
|
52
|
+
if (isFinite(y) && Math.abs(y) < 1e6) {
|
|
53
|
+
points.push([x, y]);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Break the path if we hit infinity or NaN
|
|
57
|
+
if (points.length > 1) {
|
|
58
|
+
// We'll handle discontinuities by creating separate paths
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
// Skip invalid points
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Transform points to SVG coordinates
|
|
67
|
+
const svgPoints = points.map(([x, y]) => {
|
|
68
|
+
const sx = transform.x(x);
|
|
69
|
+
const sy = transform.y(y);
|
|
70
|
+
return [sx, sy];
|
|
71
|
+
});
|
|
72
|
+
// Create path data
|
|
73
|
+
let d = '';
|
|
74
|
+
if (svgPoints.length > 0) {
|
|
75
|
+
d = `M ${svgPoints[0][0]} ${svgPoints[0][1]}`;
|
|
76
|
+
for (let i = 1; i < svgPoints.length; i++) {
|
|
77
|
+
d += ` L ${svgPoints[i][0]} ${svgPoints[i][1]}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const strokeDasharray = style === 'dashed' ? '8,4' : style === 'dotted' ? '2,2' : 'none';
|
|
81
|
+
return path(d, {
|
|
82
|
+
fill: 'none',
|
|
83
|
+
stroke: color,
|
|
84
|
+
'stroke-width': 2.5,
|
|
85
|
+
'stroke-linecap': 'round',
|
|
86
|
+
'stroke-linejoin': 'round',
|
|
87
|
+
'stroke-dasharray': strokeDasharray,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Safely evaluate a mathematical function
|
|
92
|
+
* Supports: basic arithmetic, Math functions, powers
|
|
93
|
+
*/
|
|
94
|
+
function evaluateFunction(equation, x) {
|
|
95
|
+
// Create a safe evaluation context
|
|
96
|
+
const safeMath = {
|
|
97
|
+
sin: Math.sin,
|
|
98
|
+
cos: Math.cos,
|
|
99
|
+
tan: Math.tan,
|
|
100
|
+
asin: Math.asin,
|
|
101
|
+
acos: Math.acos,
|
|
102
|
+
atan: Math.atan,
|
|
103
|
+
sqrt: Math.sqrt,
|
|
104
|
+
abs: Math.abs,
|
|
105
|
+
log: Math.log,
|
|
106
|
+
exp: Math.exp,
|
|
107
|
+
pow: Math.pow,
|
|
108
|
+
PI: Math.PI,
|
|
109
|
+
E: Math.E,
|
|
110
|
+
};
|
|
111
|
+
// Replace common notation with JavaScript equivalents
|
|
112
|
+
let expr = equation
|
|
113
|
+
.replace(/\^/g, '**') // x^2 -> x**2
|
|
114
|
+
.replace(/ln/g, 'log') // ln -> log
|
|
115
|
+
.replace(/(\d)([a-zA-Z])/g, '$1*$2'); // 2x -> 2*x
|
|
116
|
+
// Create a function with x as parameter
|
|
117
|
+
try {
|
|
118
|
+
const func = new Function('x', 'Math', `
|
|
119
|
+
with (Math) {
|
|
120
|
+
return ${expr};
|
|
121
|
+
}
|
|
122
|
+
`);
|
|
123
|
+
return func(x, safeMath);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
return NaN;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function createGraphMarkers() {
|
|
130
|
+
return `
|
|
131
|
+
<defs>
|
|
132
|
+
<marker id="graph-arrow" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
|
|
133
|
+
<path d="M0,0 L10,5 L0,10" fill="#333" />
|
|
134
|
+
</marker>
|
|
135
|
+
</defs>
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numberLine.d.ts","sourceRoot":"","sources":["../../src/renderers/numberLine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,UAAU,CAAC;AAI9D,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CA+F3D"}
|