@pixel-normal-edit/mcp 2.0.5 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/domains/art-tree/3d_lessons.js +114 -0
- package/domains/art-tree/3d_shapes.js +116 -0
- package/domains/art-tree/3d_tools.js +60 -0
- package/domains/art-tree/advanced_lessons.js +124 -0
- package/domains/art-tree/advanced_shapes.js +73 -0
- package/domains/art-tree/advanced_tools.js +69 -0
- package/domains/art-tree/cross_section_lessons.js +65 -0
- package/domains/art-tree/cross_section_shapes.js +84 -0
- package/domains/art-tree/cross_section_tools.js +51 -0
- package/domains/art-tree/curve_lessons.js +138 -0
- package/domains/art-tree/curve_shapes.js +90 -0
- package/domains/art-tree/curve_tools.js +51 -0
- package/domains/art-tree/ellipse_lessons.js +105 -0
- package/domains/art-tree/ellipse_shapes.js +63 -0
- package/domains/art-tree/ellipse_tools.js +60 -0
- package/domains/art-tree/hidden_lessons.js +55 -0
- package/domains/art-tree/hidden_shapes.js +97 -0
- package/domains/art-tree/hidden_tools.js +51 -0
- package/domains/art-tree/index.js +22 -0
- package/domains/art-tree/lessons.js +112 -0
- package/domains/art-tree/light_lessons.js +92 -0
- package/domains/art-tree/light_shapes.js +136 -0
- package/domains/art-tree/light_tools.js +49 -0
- package/domains/art-tree/perspective_lessons.js +138 -0
- package/domains/art-tree/perspective_shapes.js +96 -0
- package/domains/art-tree/perspective_tools.js +60 -0
- package/domains/art-tree/shapes.js +15 -0
- package/domains/art-tree/structure_lessons.js +102 -0
- package/domains/art-tree/structure_shapes.js +74 -0
- package/domains/art-tree/structure_tools.js +51 -0
- package/domains/art-tree/surface_lessons.js +112 -0
- package/domains/art-tree/surface_shapes.js +87 -0
- package/domains/art-tree/surface_tools.js +60 -0
- package/domains/art-tree/tools.js +54 -0
- package/domains/art-tree/transform_lessons_1.js +108 -0
- package/domains/art-tree/transform_lessons_2.js +102 -0
- package/domains/art-tree/transform_shapes.js +109 -0
- package/domains/art-tree/transform_tools.js +104 -0
- package/package.json +1 -1
- package/tools/workspace.js +14 -4
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
const ellipseShapes = require('./ellipse_shapes');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Draw a lofted 3D form by providing a central axis and cross-sections.
|
|
6
|
+
* @param {number} cx Center X
|
|
7
|
+
* @param {number} topY Top Y of the axis
|
|
8
|
+
* @param {number} bottomY Bottom Y of the axis
|
|
9
|
+
* @param {Array<{y: number, r: number}>} sections Slices sorted from top to bottom (or bottom to top)
|
|
10
|
+
* @param {string} color
|
|
11
|
+
* @param {boolean} drawAxis Whether to draw the central vertical axis
|
|
12
|
+
*/
|
|
13
|
+
function drawLoftedForm(cx, topY, bottomY, sections, color, drawAxis = true) {
|
|
14
|
+
const commands = [];
|
|
15
|
+
|
|
16
|
+
if (drawAxis) {
|
|
17
|
+
commands.push(shapes.drawLine(cx, topY - 5, cx, bottomY + 5, '#bdbdbd'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Sort sections by y to ensure we can connect them properly
|
|
21
|
+
const sorted = [...sections].sort((a, b) => a.y - b.y);
|
|
22
|
+
|
|
23
|
+
// Draw ellipses and connect edges
|
|
24
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
25
|
+
const sec = sorted[i];
|
|
26
|
+
const ry = Math.max(2, Math.floor(sec.r * 0.3)); // perspective foreshortening
|
|
27
|
+
|
|
28
|
+
// Draw cross-section ellipse
|
|
29
|
+
commands.push(shapes.drawEllipse(cx, sec.y, sec.r, ry, color, false));
|
|
30
|
+
|
|
31
|
+
// Connect to next section's edges
|
|
32
|
+
if (i < sorted.length - 1) {
|
|
33
|
+
const nextSec = sorted[i + 1];
|
|
34
|
+
// Left contour
|
|
35
|
+
commands.push(shapes.drawLine(cx - sec.r, sec.y, cx - nextSec.r, nextSec.y, color));
|
|
36
|
+
// Right contour
|
|
37
|
+
commands.push(shapes.drawLine(cx + sec.r, sec.y, cx + nextSec.r, nextSec.y, color));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return commands;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Draw the structural volume of a head using cross sections
|
|
46
|
+
*/
|
|
47
|
+
function drawHeadStructure(cx, cy, height, color) {
|
|
48
|
+
const commands = [];
|
|
49
|
+
|
|
50
|
+
const topY = cy - Math.floor(height / 2);
|
|
51
|
+
const bottomY = cy + Math.floor(height / 2);
|
|
52
|
+
|
|
53
|
+
// 1. Cranium (large sphere at the top)
|
|
54
|
+
const craniumR = Math.floor(height * 0.35);
|
|
55
|
+
const craniumY = topY + craniumR;
|
|
56
|
+
commands.push(shapes.drawCircle(cx, craniumY, craniumR, color, false));
|
|
57
|
+
|
|
58
|
+
// Cranium cross sections (horizontal and vertical)
|
|
59
|
+
commands.push(shapes.drawEllipse(cx, craniumY, craniumR, Math.floor(craniumR * 0.4), '#03a9f4', false));
|
|
60
|
+
commands.push(shapes.drawEllipse(cx, craniumY, Math.floor(craniumR * 0.3), craniumR, '#03a9f4', false));
|
|
61
|
+
|
|
62
|
+
// 2. Face & Jaw (narrowing down)
|
|
63
|
+
const jawR = Math.floor(craniumR * 0.6);
|
|
64
|
+
const jawY = bottomY - Math.floor(jawR * 0.5);
|
|
65
|
+
|
|
66
|
+
// Jaw cross section
|
|
67
|
+
commands.push(shapes.drawEllipse(cx, jawY, jawR, Math.floor(jawR * 0.3), '#e91e63', false));
|
|
68
|
+
|
|
69
|
+
// Connect Cranium to Jaw (Cheekbones/Jawline)
|
|
70
|
+
// Left side
|
|
71
|
+
commands.push(shapes.drawLine(cx - craniumR, craniumY, cx - jawR, jawY, color));
|
|
72
|
+
// Right side
|
|
73
|
+
commands.push(shapes.drawLine(cx + craniumR, craniumY, cx + jawR, jawY, color));
|
|
74
|
+
|
|
75
|
+
// Central facial axis (Center line of the face)
|
|
76
|
+
commands.push(shapes.drawLine(cx, topY, cx, bottomY, '#4caf50'));
|
|
77
|
+
|
|
78
|
+
return commands;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
drawLoftedForm,
|
|
83
|
+
drawHeadStructure
|
|
84
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const { registerTool, sendCommand } = require('../../core/server');
|
|
3
|
+
const csLessons = require('./cross_section_lessons');
|
|
4
|
+
|
|
5
|
+
const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/).describe('Hex color e.g. #ff0000');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register all cross-section tools on the MCP server.
|
|
9
|
+
* @param {McpServer} server
|
|
10
|
+
*/
|
|
11
|
+
function register(server) {
|
|
12
|
+
registerTool(server, 'art_tree_lesson_cs_bottle',
|
|
13
|
+
'Lesson: Cross-sections - Bottle Analysis (Base, body, shoulder, neck, mouth)',
|
|
14
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#2196f3') },
|
|
15
|
+
async (p) => {
|
|
16
|
+
const commands = csLessons.lessonBottleSections(p.canvasSize, p.color);
|
|
17
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
18
|
+
return { content: [{ type: 'text', text: `✓ Bottle Cross-sections lesson completed` }] };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
registerTool(server, 'art_tree_lesson_cs_glass',
|
|
22
|
+
'Lesson: Cross-sections - Glass Analysis (Base, mid, mouth)',
|
|
23
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#9c27b0') },
|
|
24
|
+
async (p) => {
|
|
25
|
+
const commands = csLessons.lessonGlassSections(p.canvasSize, p.color);
|
|
26
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
27
|
+
return { content: [{ type: 'text', text: `✓ Glass Cross-sections lesson completed` }] };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
registerTool(server, 'art_tree_lesson_cs_head',
|
|
31
|
+
'Lesson: Cross-sections - Head Analysis (Cranium, face, jaw)',
|
|
32
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#ff9800') },
|
|
33
|
+
async (p) => {
|
|
34
|
+
const commands = csLessons.lessonHeadSections(p.canvasSize, p.color);
|
|
35
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
36
|
+
return { content: [{ type: 'text', text: `✓ Head Cross-sections lesson completed` }] };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
registerTool(server, 'art_tree_lesson_cs_catalog',
|
|
40
|
+
'Get the catalog of all available Cross-section lessons',
|
|
41
|
+
{},
|
|
42
|
+
() => {
|
|
43
|
+
const catalog = csLessons.getCrossSectionLessonCatalog();
|
|
44
|
+
const text = catalog.map(l =>
|
|
45
|
+
` • ${l.id}: ${l.name} — ${l.description}`
|
|
46
|
+
).join('\n');
|
|
47
|
+
return { content: [{ type: 'text', text: `📚 Cross-section Lessons:\n\n${text}` }] };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { register };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
const curveShapes = require('./curve_shapes');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lesson 1: Curve Types (C, S, Convex, Concave)
|
|
6
|
+
*/
|
|
7
|
+
function lessonCurveTypes(canvasSize = 32, color = '#2196f3') {
|
|
8
|
+
const margin = Math.floor(canvasSize * 0.1);
|
|
9
|
+
const commands = [];
|
|
10
|
+
|
|
11
|
+
const w = Math.floor(canvasSize / 2) - margin * 2;
|
|
12
|
+
const h = Math.floor(canvasSize / 2) - margin * 2;
|
|
13
|
+
|
|
14
|
+
// C Curve (Top Left)
|
|
15
|
+
const cP0 = { x: margin + w, y: margin };
|
|
16
|
+
const cP1 = { x: margin - w, y: margin + Math.floor(h/2) }; // Pulls curve left
|
|
17
|
+
const cP2 = { x: margin + w, y: margin + h };
|
|
18
|
+
commands.push(...curveShapes.drawQuadraticCurve(cP0, cP1, cP2, color));
|
|
19
|
+
|
|
20
|
+
// S Curve (Top Right)
|
|
21
|
+
const sP0 = { x: canvasSize - margin, y: margin };
|
|
22
|
+
const sP1 = { x: canvasSize - margin - w*2, y: margin };
|
|
23
|
+
const sP2 = { x: canvasSize - margin + w, y: margin + h };
|
|
24
|
+
const sP3 = { x: canvasSize - margin - w, y: margin + h };
|
|
25
|
+
commands.push(...curveShapes.drawCubicCurve(sP0, sP1, sP2, sP3, '#e91e63'));
|
|
26
|
+
|
|
27
|
+
// Convex (Bottom Left, viewed from bottom)
|
|
28
|
+
const cvxP0 = { x: margin, y: canvasSize - margin };
|
|
29
|
+
const cvxP1 = { x: margin + Math.floor(w/2), y: canvasSize - margin - h };
|
|
30
|
+
const cvxP2 = { x: margin + w, y: canvasSize - margin };
|
|
31
|
+
commands.push(...curveShapes.drawQuadraticCurve(cvxP0, cvxP1, cvxP2, '#4caf50'));
|
|
32
|
+
|
|
33
|
+
// Concave (Bottom Right, viewed from bottom)
|
|
34
|
+
const cnvP0 = { x: canvasSize - margin - w, y: canvasSize - margin - Math.floor(h/2) };
|
|
35
|
+
const cnvP1 = { x: canvasSize - margin - Math.floor(w/2), y: canvasSize - margin + h };
|
|
36
|
+
const cnvP2 = { x: canvasSize - margin, y: canvasSize - margin - Math.floor(h/2) };
|
|
37
|
+
commands.push(...curveShapes.drawQuadraticCurve(cnvP0, cnvP1, cnvP2, '#ff9800'));
|
|
38
|
+
|
|
39
|
+
return commands;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Lesson 2: Curve Topology (Closed, Open, Symmetrical, Asymmetrical)
|
|
44
|
+
*/
|
|
45
|
+
function lessonCurveTopology(canvasSize = 32, color = '#9c27b0') {
|
|
46
|
+
const mid = Math.floor(canvasSize / 2);
|
|
47
|
+
const q = Math.floor(canvasSize / 4);
|
|
48
|
+
const commands = [];
|
|
49
|
+
|
|
50
|
+
// Closed Curve (Circle-like using Bezier for demonstration)
|
|
51
|
+
// Since we already have drawCircle, we just draw a circle to represent a closed curve
|
|
52
|
+
commands.push(shapes.drawCircle(q, q, Math.floor(q*0.8), color, false));
|
|
53
|
+
|
|
54
|
+
// Open Curve (Quadratic)
|
|
55
|
+
commands.push(...curveShapes.drawQuadraticCurve(
|
|
56
|
+
{ x: canvasSize - q*1.5, y: q },
|
|
57
|
+
{ x: canvasSize - q*0.5, y: q - q/2 },
|
|
58
|
+
{ x: canvasSize - q*0.5, y: q + q/2 },
|
|
59
|
+
'#3f51b5'
|
|
60
|
+
));
|
|
61
|
+
|
|
62
|
+
// Symmetrical Curve (Parabola-like)
|
|
63
|
+
commands.push(...curveShapes.drawQuadraticCurve(
|
|
64
|
+
{ x: q*0.5, y: canvasSize - q*0.5 },
|
|
65
|
+
{ x: q, y: canvasSize - q*1.5 },
|
|
66
|
+
{ x: q*1.5, y: canvasSize - q*0.5 },
|
|
67
|
+
'#00bcd4'
|
|
68
|
+
));
|
|
69
|
+
|
|
70
|
+
// Asymmetrical Curve
|
|
71
|
+
commands.push(...curveShapes.drawQuadraticCurve(
|
|
72
|
+
{ x: canvasSize - q*1.5, y: canvasSize - q*0.5 },
|
|
73
|
+
{ x: canvasSize - q*0.2, y: canvasSize - q*1.8 }, // Pull point is skewed
|
|
74
|
+
{ x: canvasSize - q*0.5, y: canvasSize - q*0.5 },
|
|
75
|
+
'#ff5722'
|
|
76
|
+
));
|
|
77
|
+
|
|
78
|
+
return commands;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Lesson 3: Curve Properties (Start, Direction, Curvature, Inflection, Transition)
|
|
83
|
+
*/
|
|
84
|
+
function lessonCurveProperties(canvasSize = 32, color = '#607d8b') {
|
|
85
|
+
const margin = Math.floor(canvasSize * 0.1);
|
|
86
|
+
const commands = [];
|
|
87
|
+
|
|
88
|
+
// 1. Start point and Direction (Top Left Curve)
|
|
89
|
+
const p0 = { x: margin, y: margin * 2 };
|
|
90
|
+
const p1 = { x: margin + canvasSize*0.3, y: margin };
|
|
91
|
+
const p2 = { x: margin + canvasSize*0.3, y: margin + canvasSize*0.3 };
|
|
92
|
+
commands.push(...curveShapes.drawQuadraticCurve(p0, p1, p2, color));
|
|
93
|
+
commands.push(curveShapes.drawStartMarker(p0.x, p0.y, '#00ff00')); // Green dot at start
|
|
94
|
+
commands.push(...curveShapes.drawDirectionArrow(p0, p1, p2, null, '#ff0000')); // Red arrow
|
|
95
|
+
|
|
96
|
+
// 2. Curvature comparison (Bottom Left)
|
|
97
|
+
// Strong vs Weak curve with same endpoints
|
|
98
|
+
const bp0 = { x: margin, y: canvasSize - margin };
|
|
99
|
+
const bp2 = { x: margin + canvasSize*0.4, y: canvasSize - margin };
|
|
100
|
+
// Weak curve
|
|
101
|
+
commands.push(...curveShapes.drawQuadraticCurve(bp0, { x: margin + canvasSize*0.2, y: canvasSize - margin - canvasSize*0.1 }, bp2, '#8bc34a'));
|
|
102
|
+
// Strong curve
|
|
103
|
+
commands.push(...curveShapes.drawQuadraticCurve(bp0, { x: margin + canvasSize*0.2, y: canvasSize - margin - canvasSize*0.4 }, bp2, '#388e3c'));
|
|
104
|
+
|
|
105
|
+
// 3. Inflection point (Top Right S Curve)
|
|
106
|
+
const sP0 = { x: canvasSize - margin - canvasSize*0.3, y: margin };
|
|
107
|
+
const sP1 = { x: canvasSize - margin, y: margin };
|
|
108
|
+
const sP2 = { x: canvasSize - margin - canvasSize*0.4, y: margin + canvasSize*0.3 };
|
|
109
|
+
const sP3 = { x: canvasSize - margin, y: margin + canvasSize*0.3 };
|
|
110
|
+
commands.push(...curveShapes.drawCubicCurve(sP0, sP1, sP2, sP3, '#03a9f4'));
|
|
111
|
+
// Inflection point roughly at t=0.5
|
|
112
|
+
const inflection = curveShapes.cubicBezier(sP0, sP1, sP2, sP3, 0.5);
|
|
113
|
+
commands.push(curveShapes.drawInflectionMarker(inflection.x, inflection.y, '#ff9800'));
|
|
114
|
+
|
|
115
|
+
// 4. Transition (Bottom Right - line transitioning to curve)
|
|
116
|
+
const startLine = { x: canvasSize - margin - canvasSize*0.3, y: canvasSize - margin - canvasSize*0.2 };
|
|
117
|
+
const transPt = { x: canvasSize - margin - canvasSize*0.1, y: canvasSize - margin - canvasSize*0.2 };
|
|
118
|
+
commands.push(shapes.drawLine(startLine.x, startLine.y, transPt.x, transPt.y, '#9e9e9e'));
|
|
119
|
+
// Curve starting tangentially
|
|
120
|
+
commands.push(...curveShapes.drawQuadraticCurve(transPt, { x: canvasSize - margin, y: canvasSize - margin - canvasSize*0.2 }, { x: canvasSize - margin, y: canvasSize - margin }, '#795548'));
|
|
121
|
+
|
|
122
|
+
return commands;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getCurveLessonCatalog() {
|
|
126
|
+
return [
|
|
127
|
+
{ id: 'curve_types', name: 'Curve Types', description: 'C, S, Convex, and Concave curves', fn: lessonCurveTypes },
|
|
128
|
+
{ id: 'curve_topology', name: 'Curve Topology', description: 'Closed, Open, Symmetrical, Asymmetrical curves', fn: lessonCurveTopology },
|
|
129
|
+
{ id: 'curve_properties', name: 'Curve Properties', description: 'Start point, Direction, Curvature, Inflection, and Transitions', fn: lessonCurveProperties },
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
lessonCurveTypes,
|
|
135
|
+
lessonCurveTopology,
|
|
136
|
+
lessonCurveProperties,
|
|
137
|
+
getCurveLessonCatalog
|
|
138
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Draw a marker at the start of a curve
|
|
5
|
+
*/
|
|
6
|
+
function drawStartMarker(x, y, color = '#00ff00') {
|
|
7
|
+
return shapes.drawCircle(x, y, 2, color, true);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Draw an inflection point marker
|
|
12
|
+
*/
|
|
13
|
+
function drawInflectionMarker(x, y, color = '#ff0000') {
|
|
14
|
+
return shapes.drawCircle(x, y, 2, color, true);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calculate quadratic bezier point
|
|
19
|
+
*/
|
|
20
|
+
function quadraticBezier(p0, p1, p2, t) {
|
|
21
|
+
const x = Math.round((1 - t) * (1 - t) * p0.x + 2 * (1 - t) * t * p1.x + t * t * p2.x);
|
|
22
|
+
const y = Math.round((1 - t) * (1 - t) * p0.y + 2 * (1 - t) * t * p1.y + t * t * p2.y);
|
|
23
|
+
return { x, y };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Draw a quadratic bezier curve as a polyline
|
|
28
|
+
*/
|
|
29
|
+
function drawQuadraticCurve(p0, p1, p2, color, segments = 20) {
|
|
30
|
+
const points = [];
|
|
31
|
+
for (let i = 0; i <= segments; i++) {
|
|
32
|
+
const t = i / segments;
|
|
33
|
+
points.push(quadraticBezier(p0, p1, p2, t));
|
|
34
|
+
}
|
|
35
|
+
return shapes.drawPolyline(points, color);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Calculate cubic bezier point
|
|
40
|
+
*/
|
|
41
|
+
function cubicBezier(p0, p1, p2, p3, t) {
|
|
42
|
+
const x = Math.round(Math.pow(1 - t, 3) * p0.x + 3 * Math.pow(1 - t, 2) * t * p1.x + 3 * (1 - t) * t * t * p2.x + t * t * t * p3.x);
|
|
43
|
+
const y = Math.round(Math.pow(1 - t, 3) * p0.y + 3 * Math.pow(1 - t, 2) * t * p1.y + 3 * (1 - t) * t * t * p2.y + t * t * t * p3.y);
|
|
44
|
+
return { x, y };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Draw a cubic bezier curve as a polyline
|
|
49
|
+
*/
|
|
50
|
+
function drawCubicCurve(p0, p1, p2, p3, color, segments = 30) {
|
|
51
|
+
const points = [];
|
|
52
|
+
for (let i = 0; i <= segments; i++) {
|
|
53
|
+
const t = i / segments;
|
|
54
|
+
points.push(cubicBezier(p0, p1, p2, p3, t));
|
|
55
|
+
}
|
|
56
|
+
return shapes.drawPolyline(points, color);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Draw a direction arrow roughly along the curve at t=0.5
|
|
61
|
+
*/
|
|
62
|
+
function drawDirectionArrow(p0, p1, p2, p3 = null, color = '#ff0000') {
|
|
63
|
+
const commands = [];
|
|
64
|
+
const t = 0.5;
|
|
65
|
+
const pt = p3 ? cubicBezier(p0, p1, p2, p3, t) : quadraticBezier(p0, p1, p2, t);
|
|
66
|
+
const ptNext = p3 ? cubicBezier(p0, p1, p2, p3, t + 0.1) : quadraticBezier(p0, p1, p2, t + 0.1);
|
|
67
|
+
|
|
68
|
+
// Calculate angle
|
|
69
|
+
const angle = Math.atan2(ptNext.y - pt.y, ptNext.x - pt.x);
|
|
70
|
+
|
|
71
|
+
// Draw arrow head
|
|
72
|
+
const arrowLen = 4;
|
|
73
|
+
const a1 = angle + Math.PI * 0.8;
|
|
74
|
+
const a2 = angle - Math.PI * 0.8;
|
|
75
|
+
|
|
76
|
+
commands.push(shapes.drawLine(pt.x, pt.y, Math.round(pt.x + arrowLen * Math.cos(a1)), Math.round(pt.y + arrowLen * Math.sin(a1)), color));
|
|
77
|
+
commands.push(shapes.drawLine(pt.x, pt.y, Math.round(pt.x + arrowLen * Math.cos(a2)), Math.round(pt.y + arrowLen * Math.sin(a2)), color));
|
|
78
|
+
|
|
79
|
+
return commands;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
drawStartMarker,
|
|
84
|
+
drawInflectionMarker,
|
|
85
|
+
quadraticBezier,
|
|
86
|
+
drawQuadraticCurve,
|
|
87
|
+
cubicBezier,
|
|
88
|
+
drawCubicCurve,
|
|
89
|
+
drawDirectionArrow
|
|
90
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const { registerTool, sendCommand } = require('../../core/server');
|
|
3
|
+
const curveLessons = require('./curve_lessons');
|
|
4
|
+
|
|
5
|
+
const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/).describe('Hex color e.g. #ff0000');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register all advanced curve tools on the MCP server.
|
|
9
|
+
* @param {McpServer} server
|
|
10
|
+
*/
|
|
11
|
+
function register(server) {
|
|
12
|
+
registerTool(server, 'art_tree_lesson_curve_types',
|
|
13
|
+
'Lesson: Advanced Curves - Types (C, S, Convex, Concave)',
|
|
14
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#2196f3') },
|
|
15
|
+
async (p) => {
|
|
16
|
+
const commands = curveLessons.lessonCurveTypes(p.canvasSize, p.color);
|
|
17
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
18
|
+
return { content: [{ type: 'text', text: `✓ Curve Types lesson completed` }] };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
registerTool(server, 'art_tree_lesson_curve_topology',
|
|
22
|
+
'Lesson: Advanced Curves - Topology (Closed, Open, Symmetrical, Asymmetrical)',
|
|
23
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#9c27b0') },
|
|
24
|
+
async (p) => {
|
|
25
|
+
const commands = curveLessons.lessonCurveTopology(p.canvasSize, p.color);
|
|
26
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
27
|
+
return { content: [{ type: 'text', text: `✓ Curve Topology lesson completed` }] };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
registerTool(server, 'art_tree_lesson_curve_properties',
|
|
31
|
+
'Lesson: Advanced Curves - Properties (Start, Direction, Curvature, Inflection, Transitions)',
|
|
32
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#607d8b') },
|
|
33
|
+
async (p) => {
|
|
34
|
+
const commands = curveLessons.lessonCurveProperties(p.canvasSize, p.color);
|
|
35
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
36
|
+
return { content: [{ type: 'text', text: `✓ Curve Properties lesson completed` }] };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
registerTool(server, 'art_tree_lesson_curve_catalog',
|
|
40
|
+
'Get the catalog of all available Advanced Curve lessons',
|
|
41
|
+
{},
|
|
42
|
+
() => {
|
|
43
|
+
const catalog = curveLessons.getCurveLessonCatalog();
|
|
44
|
+
const text = catalog.map(l =>
|
|
45
|
+
` • ${l.id}: ${l.name} — ${l.description}`
|
|
46
|
+
).join('\n');
|
|
47
|
+
return { content: [{ type: 'text', text: `📚 Advanced Curve Lessons:\n\n${text}` }] };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { register };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
const ellipseShapes = require('./ellipse_shapes');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lesson: Ellipse Orientations (Horizontal, Vertical, Tilted)
|
|
6
|
+
*/
|
|
7
|
+
function lessonEllipseOrientations(canvasSize = 32, color = '#2196f3') {
|
|
8
|
+
const commands = [];
|
|
9
|
+
const midY = Math.floor(canvasSize / 2);
|
|
10
|
+
const qX = Math.floor(canvasSize / 4);
|
|
11
|
+
const rLong = Math.floor(canvasSize * 0.2);
|
|
12
|
+
const rShort = Math.floor(canvasSize * 0.1);
|
|
13
|
+
|
|
14
|
+
// Horizontal (Left)
|
|
15
|
+
commands.push(shapes.drawEllipse(qX, midY, rLong, rShort, color, false));
|
|
16
|
+
|
|
17
|
+
// Vertical (Middle)
|
|
18
|
+
commands.push(shapes.drawEllipse(qX * 2, midY, rShort, rLong, '#e91e63', false));
|
|
19
|
+
|
|
20
|
+
// Tilted (Right) - 45 degrees
|
|
21
|
+
commands.push(...ellipseShapes.drawRotatedEllipse(qX * 3, midY, rLong, rShort, Math.PI / 4, '#4caf50'));
|
|
22
|
+
|
|
23
|
+
return commands;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Lesson: Ellipse Proportions (Wide vs Narrow)
|
|
28
|
+
*/
|
|
29
|
+
function lessonEllipseProportions(canvasSize = 32, color = '#ff9800') {
|
|
30
|
+
const commands = [];
|
|
31
|
+
const midY = Math.floor(canvasSize / 2);
|
|
32
|
+
const qX = Math.floor(canvasSize / 4);
|
|
33
|
+
const rx = Math.floor(canvasSize * 0.2);
|
|
34
|
+
|
|
35
|
+
// Wide Ellipse (Closer to a circle)
|
|
36
|
+
commands.push(shapes.drawEllipse(qX, midY, rx, Math.floor(rx * 0.8), color, false));
|
|
37
|
+
|
|
38
|
+
// Medium Ellipse
|
|
39
|
+
commands.push(shapes.drawEllipse(qX * 2, midY, rx, Math.floor(rx * 0.4), '#9c27b0', false));
|
|
40
|
+
|
|
41
|
+
// Narrow Ellipse (Almost a line)
|
|
42
|
+
commands.push(shapes.drawEllipse(qX * 3, midY, rx, Math.floor(rx * 0.1), '#f44336', false));
|
|
43
|
+
|
|
44
|
+
return commands;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Lesson: Ellipse Anatomy (Symmetry, Major/Minor axes, Center)
|
|
49
|
+
*/
|
|
50
|
+
function lessonEllipseAnatomy(canvasSize = 32, color = '#00bcd4') {
|
|
51
|
+
const commands = [];
|
|
52
|
+
const cx = Math.floor(canvasSize / 2);
|
|
53
|
+
const cy = Math.floor(canvasSize / 2);
|
|
54
|
+
const rx = Math.floor(canvasSize * 0.35);
|
|
55
|
+
const ry = Math.floor(canvasSize * 0.2);
|
|
56
|
+
const angle = Math.PI / 6; // 30 degrees tilt to show axes clearly
|
|
57
|
+
|
|
58
|
+
// Draw ellipse
|
|
59
|
+
commands.push(...ellipseShapes.drawRotatedEllipse(cx, cy, rx, ry, angle, color));
|
|
60
|
+
|
|
61
|
+
// Draw anatomy (axes and center)
|
|
62
|
+
commands.push(...ellipseShapes.drawEllipseAxes(cx, cy, rx, ry, angle));
|
|
63
|
+
|
|
64
|
+
return commands;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Lesson: Coaxial Ellipses (Multiple ellipses on the same axis)
|
|
69
|
+
*/
|
|
70
|
+
function lessonCoaxialEllipses(canvasSize = 32, color = '#795548') {
|
|
71
|
+
const commands = [];
|
|
72
|
+
const cx = Math.floor(canvasSize / 2);
|
|
73
|
+
const rx = Math.floor(canvasSize * 0.3);
|
|
74
|
+
|
|
75
|
+
// Central axis line
|
|
76
|
+
commands.push(shapes.drawLine(cx, Math.floor(canvasSize * 0.1), cx, canvasSize - Math.floor(canvasSize * 0.1), '#bdbdbd'));
|
|
77
|
+
|
|
78
|
+
// Top ellipse (narrower, viewed from steeper angle)
|
|
79
|
+
commands.push(shapes.drawEllipse(cx, Math.floor(canvasSize * 0.2), rx, Math.floor(rx * 0.2), color, false));
|
|
80
|
+
|
|
81
|
+
// Middle ellipse
|
|
82
|
+
commands.push(shapes.drawEllipse(cx, Math.floor(canvasSize * 0.5), rx, Math.floor(rx * 0.3), color, false));
|
|
83
|
+
|
|
84
|
+
// Bottom ellipse (wider, viewed from less steep angle)
|
|
85
|
+
commands.push(shapes.drawEllipse(cx, canvasSize - Math.floor(canvasSize * 0.2), rx, Math.floor(rx * 0.4), color, false));
|
|
86
|
+
|
|
87
|
+
return commands;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getEllipseLessonCatalog() {
|
|
91
|
+
return [
|
|
92
|
+
{ id: 'ellipse_orientations', name: 'Ellipse Orientations', description: 'Horizontal, Vertical, and Tilted ellipses', fn: lessonEllipseOrientations },
|
|
93
|
+
{ id: 'ellipse_proportions', name: 'Ellipse Proportions', description: 'Wide vs Narrow ellipses', fn: lessonEllipseProportions },
|
|
94
|
+
{ id: 'ellipse_anatomy', name: 'Ellipse Anatomy', description: 'Symmetry, Major/Minor axes, and Center', fn: lessonEllipseAnatomy },
|
|
95
|
+
{ id: 'ellipse_coaxial', name: 'Coaxial Ellipses', description: 'Multiple ellipses sharing the same axis', fn: lessonCoaxialEllipses },
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
lessonEllipseOrientations,
|
|
101
|
+
lessonEllipseProportions,
|
|
102
|
+
lessonEllipseAnatomy,
|
|
103
|
+
lessonCoaxialEllipses,
|
|
104
|
+
getEllipseLessonCatalog
|
|
105
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Draw a rotated ellipse using a polygon approximation
|
|
5
|
+
* @param {number} cx Center X
|
|
6
|
+
* @param {number} cy Center Y
|
|
7
|
+
* @param {number} rx X radius (major axis if rx > ry)
|
|
8
|
+
* @param {number} ry Y radius
|
|
9
|
+
* @param {number} angle Rotation angle in radians
|
|
10
|
+
* @param {string} color Hex color
|
|
11
|
+
* @param {number} segments Number of polygon segments
|
|
12
|
+
*/
|
|
13
|
+
function drawRotatedEllipse(cx, cy, rx, ry, angle, color, segments = 36) {
|
|
14
|
+
const points = [];
|
|
15
|
+
const cosA = Math.cos(angle);
|
|
16
|
+
const sinA = Math.sin(angle);
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i <= segments; i++) {
|
|
19
|
+
const t = (i / segments) * 2 * Math.PI;
|
|
20
|
+
const px = rx * Math.cos(t);
|
|
21
|
+
const py = ry * Math.sin(t);
|
|
22
|
+
|
|
23
|
+
const x = Math.round(cx + px * cosA - py * sinA);
|
|
24
|
+
const y = Math.round(cy + px * sinA + py * cosA);
|
|
25
|
+
points.push({ x, y });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return shapes.drawPolyline(points, color);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Draw major and minor axes for an ellipse
|
|
33
|
+
*/
|
|
34
|
+
function drawEllipseAxes(cx, cy, rx, ry, angle = 0) {
|
|
35
|
+
const commands = [];
|
|
36
|
+
const cosA = Math.cos(angle);
|
|
37
|
+
const sinA = Math.sin(angle);
|
|
38
|
+
|
|
39
|
+
// Major/Minor Axis calculation based on rotation
|
|
40
|
+
const x1 = Math.round(cx - rx * cosA);
|
|
41
|
+
const y1 = Math.round(cy - rx * sinA);
|
|
42
|
+
const x2 = Math.round(cx + rx * cosA);
|
|
43
|
+
const y2 = Math.round(cy + rx * sinA);
|
|
44
|
+
|
|
45
|
+
const y3_x = Math.round(cx + ry * sinA);
|
|
46
|
+
const y3_y = Math.round(cy - ry * cosA);
|
|
47
|
+
const y4_x = Math.round(cx - ry * sinA);
|
|
48
|
+
const y4_y = Math.round(cy + ry * cosA);
|
|
49
|
+
|
|
50
|
+
// Red for Major, Blue for Minor
|
|
51
|
+
commands.push(shapes.drawLine(x1, y1, x2, y2, '#ff0000'));
|
|
52
|
+
commands.push(shapes.drawLine(y3_x, y3_y, y4_x, y4_y, '#2196f3'));
|
|
53
|
+
|
|
54
|
+
// Center dot
|
|
55
|
+
commands.push(shapes.drawCircle(cx, cy, 1, '#000000', true));
|
|
56
|
+
|
|
57
|
+
return commands;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
drawRotatedEllipse,
|
|
62
|
+
drawEllipseAxes
|
|
63
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const { registerTool, sendCommand } = require('../../core/server');
|
|
3
|
+
const ellipseLessons = require('./ellipse_lessons');
|
|
4
|
+
|
|
5
|
+
const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/).describe('Hex color e.g. #ff0000');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register all ellipse tools on the MCP server.
|
|
9
|
+
* @param {McpServer} server
|
|
10
|
+
*/
|
|
11
|
+
function register(server) {
|
|
12
|
+
registerTool(server, 'art_tree_lesson_ellipse_orientations',
|
|
13
|
+
'Lesson: Ellipses - Orientations (Horizontal, Vertical, Tilted)',
|
|
14
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#2196f3') },
|
|
15
|
+
async (p) => {
|
|
16
|
+
const commands = ellipseLessons.lessonEllipseOrientations(p.canvasSize, p.color);
|
|
17
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
18
|
+
return { content: [{ type: 'text', text: `✓ Ellipse Orientations lesson completed` }] };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
registerTool(server, 'art_tree_lesson_ellipse_proportions',
|
|
22
|
+
'Lesson: Ellipses - Proportions (Wide vs Narrow)',
|
|
23
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#ff9800') },
|
|
24
|
+
async (p) => {
|
|
25
|
+
const commands = ellipseLessons.lessonEllipseProportions(p.canvasSize, p.color);
|
|
26
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
27
|
+
return { content: [{ type: 'text', text: `✓ Ellipse Proportions lesson completed` }] };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
registerTool(server, 'art_tree_lesson_ellipse_anatomy',
|
|
31
|
+
'Lesson: Ellipses - Anatomy (Symmetry, Axes, Center)',
|
|
32
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#00bcd4') },
|
|
33
|
+
async (p) => {
|
|
34
|
+
const commands = ellipseLessons.lessonEllipseAnatomy(p.canvasSize, p.color);
|
|
35
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
36
|
+
return { content: [{ type: 'text', text: `✓ Ellipse Anatomy lesson completed` }] };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
registerTool(server, 'art_tree_lesson_coaxial_ellipses',
|
|
40
|
+
'Lesson: Ellipses - Coaxial (Multiple ellipses on the same axis)',
|
|
41
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#795548') },
|
|
42
|
+
async (p) => {
|
|
43
|
+
const commands = ellipseLessons.lessonCoaxialEllipses(p.canvasSize, p.color);
|
|
44
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
45
|
+
return { content: [{ type: 'text', text: `✓ Coaxial Ellipses lesson completed` }] };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
registerTool(server, 'art_tree_lesson_ellipse_catalog',
|
|
49
|
+
'Get the catalog of all available Ellipse lessons',
|
|
50
|
+
{},
|
|
51
|
+
() => {
|
|
52
|
+
const catalog = ellipseLessons.getEllipseLessonCatalog();
|
|
53
|
+
const text = catalog.map(l =>
|
|
54
|
+
` • ${l.id}: ${l.name} — ${l.description}`
|
|
55
|
+
).join('\n');
|
|
56
|
+
return { content: [{ type: 'text', text: `📚 Ellipse Lessons:\n\n${text}` }] };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { register };
|