@pixel-normal-edit/mcp 2.0.6 → 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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
const perspShapes = require('./perspective_shapes');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lesson: 1-Point Perspective
|
|
6
|
+
*/
|
|
7
|
+
function lesson1PointPerspective(canvasSize = 32, color = '#2196f3') {
|
|
8
|
+
const commands = [];
|
|
9
|
+
const horizonY = Math.floor(canvasSize * 0.4);
|
|
10
|
+
const vpX = Math.floor(canvasSize * 0.5);
|
|
11
|
+
const vpY = horizonY;
|
|
12
|
+
|
|
13
|
+
// Horizon & VP
|
|
14
|
+
commands.push(...perspShapes.drawHorizon(canvasSize, horizonY));
|
|
15
|
+
commands.push(...perspShapes.drawVP(vpX, vpY));
|
|
16
|
+
|
|
17
|
+
// Box below horizon (seeing top face)
|
|
18
|
+
const w = Math.floor(canvasSize * 0.2);
|
|
19
|
+
const h = Math.floor(canvasSize * 0.15);
|
|
20
|
+
commands.push(...perspShapes.draw1PointBox(vpX, vpY, Math.floor(canvasSize * 0.2), horizonY + Math.floor(canvasSize * 0.2), w, h, 0.4, color));
|
|
21
|
+
|
|
22
|
+
// Box above horizon (seeing bottom face)
|
|
23
|
+
commands.push(...perspShapes.draw1PointBox(vpX, vpY, Math.floor(canvasSize * 0.6), horizonY - Math.floor(canvasSize * 0.3), w, h, 0.4, '#e91e63'));
|
|
24
|
+
|
|
25
|
+
return commands;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lesson: 2-Point Perspective
|
|
30
|
+
*/
|
|
31
|
+
function lesson2PointPerspective(canvasSize = 32, color = '#4caf50') {
|
|
32
|
+
const commands = [];
|
|
33
|
+
const horizonY = Math.floor(canvasSize * 0.5);
|
|
34
|
+
const vp1X = Math.floor(canvasSize * 0.1);
|
|
35
|
+
const vp2X = Math.floor(canvasSize * 0.9);
|
|
36
|
+
|
|
37
|
+
// Horizon & VPs
|
|
38
|
+
commands.push(...perspShapes.drawHorizon(canvasSize, horizonY));
|
|
39
|
+
commands.push(...perspShapes.drawVP(vp1X, horizonY));
|
|
40
|
+
commands.push(...perspShapes.drawVP(vp2X, horizonY));
|
|
41
|
+
|
|
42
|
+
// 2-Point Box straddling the horizon
|
|
43
|
+
const startX = Math.floor(canvasSize * 0.4);
|
|
44
|
+
const startY = Math.floor(canvasSize * 0.3);
|
|
45
|
+
const h = Math.floor(canvasSize * 0.4);
|
|
46
|
+
|
|
47
|
+
commands.push(...perspShapes.draw2PointBox(vp1X, horizonY, vp2X, horizonY, startX, startY, h, 0.3, 0.4, color));
|
|
48
|
+
|
|
49
|
+
return commands;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Lesson: 3-Point Perspective (Bird's eye view)
|
|
54
|
+
*/
|
|
55
|
+
function lesson3PointPerspective(canvasSize = 32, color = '#ff9800') {
|
|
56
|
+
const commands = [];
|
|
57
|
+
const horizonY = Math.floor(canvasSize * 0.2);
|
|
58
|
+
const vp1X = Math.floor(canvasSize * 0.1);
|
|
59
|
+
const vp2X = Math.floor(canvasSize * 0.9);
|
|
60
|
+
const vp3X = Math.floor(canvasSize * 0.5);
|
|
61
|
+
const vp3Y = Math.floor(canvasSize * 1.5); // VP3 far below (Bird's eye)
|
|
62
|
+
|
|
63
|
+
// Horizon & VPs
|
|
64
|
+
commands.push(...perspShapes.drawHorizon(canvasSize, horizonY));
|
|
65
|
+
commands.push(...perspShapes.drawVP(vp1X, horizonY));
|
|
66
|
+
commands.push(...perspShapes.drawVP(vp2X, horizonY));
|
|
67
|
+
// VP3 is usually off-canvas, but we draw a line to it to show direction
|
|
68
|
+
|
|
69
|
+
// We approximate a 3-point box by drawing converging vertical lines to VP3
|
|
70
|
+
const topY = Math.floor(canvasSize * 0.4);
|
|
71
|
+
const h = Math.floor(canvasSize * 0.4);
|
|
72
|
+
|
|
73
|
+
// Center edge converging to VP3
|
|
74
|
+
commands.push(shapes.drawLine(vp3X, topY, vp3X, topY+h, color)); // simplified
|
|
75
|
+
commands.push(shapes.drawLine(vp3X, topY+h, vp3X, vp3Y, '#e0e0e0')); // Guide to VP3
|
|
76
|
+
|
|
77
|
+
// Left and Right top edges to VP1, VP2
|
|
78
|
+
commands.push(shapes.drawLine(vp3X, topY, vp1X, horizonY, '#e0e0e0'));
|
|
79
|
+
commands.push(shapes.drawLine(vp3X, topY, vp2X, horizonY, '#e0e0e0'));
|
|
80
|
+
|
|
81
|
+
// Draw the rest of the box structure to simulate 3-point
|
|
82
|
+
const lX = vp3X - Math.floor(canvasSize * 0.15);
|
|
83
|
+
const rX = vp3X + Math.floor(canvasSize * 0.2);
|
|
84
|
+
// Vertical edges also converge to VP3 slightly (foreshortening)
|
|
85
|
+
commands.push(shapes.drawLine(lX, topY + Math.floor(canvasSize * 0.1), lX + 2, topY + h, color));
|
|
86
|
+
commands.push(shapes.drawLine(rX, topY + Math.floor(canvasSize * 0.15), rX - 3, topY + h - 5, color));
|
|
87
|
+
|
|
88
|
+
return commands;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Lesson: Foreshortening (Sự rút ngắn theo chiều sâu)
|
|
93
|
+
* Drawing a cylinder pointing directly at the viewer
|
|
94
|
+
*/
|
|
95
|
+
function lessonForeshortening(canvasSize = 32, color = '#9c27b0') {
|
|
96
|
+
const commands = [];
|
|
97
|
+
const cx = Math.floor(canvasSize / 2);
|
|
98
|
+
const cy = Math.floor(canvasSize / 2);
|
|
99
|
+
|
|
100
|
+
// Draw overlapping circles getting larger (coming towards viewer)
|
|
101
|
+
const numCircles = 5;
|
|
102
|
+
const maxR = Math.floor(canvasSize * 0.4);
|
|
103
|
+
const minR = Math.floor(canvasSize * 0.15);
|
|
104
|
+
|
|
105
|
+
for(let i=0; i<numCircles; i++) {
|
|
106
|
+
const t = i / (numCircles - 1); // 0 to 1
|
|
107
|
+
// Radius grows exponentially to simulate perspective
|
|
108
|
+
const r = minR + (maxR - minR) * (t * t);
|
|
109
|
+
// Y position shifts slightly to show overlap
|
|
110
|
+
const y = cy - Math.floor(canvasSize * 0.2) + Math.floor(canvasSize * 0.4 * t);
|
|
111
|
+
|
|
112
|
+
// Draw the circle
|
|
113
|
+
commands.push(shapes.drawCircle(cx, y, Math.floor(r), color, false));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Draw bounding lines converging
|
|
117
|
+
commands.push(shapes.drawLine(cx - minR, cy - Math.floor(canvasSize * 0.2), cx - maxR, cy + Math.floor(canvasSize * 0.2), '#bdbdbd'));
|
|
118
|
+
commands.push(shapes.drawLine(cx + minR, cy - Math.floor(canvasSize * 0.2), cx + maxR, cy + Math.floor(canvasSize * 0.2), '#bdbdbd'));
|
|
119
|
+
|
|
120
|
+
return commands;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getPerspectiveLessonCatalog() {
|
|
124
|
+
return [
|
|
125
|
+
{ id: 'persp_1point', name: '1-Point Perspective', description: 'Horizon, 1 VP, Front vs Receding planes', fn: lesson1PointPerspective },
|
|
126
|
+
{ id: 'persp_2point', name: '2-Point Perspective', description: 'Horizon, 2 VPs, Edges facing viewer', fn: lesson2PointPerspective },
|
|
127
|
+
{ id: 'persp_3point', name: '3-Point Perspective', description: '3 VPs (Bird\'s eye view)', fn: lesson3PointPerspective },
|
|
128
|
+
{ id: 'persp_foreshorten', name: 'Foreshortening', description: 'Cylinder pointing at viewer', fn: lessonForeshortening },
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
lesson1PointPerspective,
|
|
134
|
+
lesson2PointPerspective,
|
|
135
|
+
lesson3PointPerspective,
|
|
136
|
+
lessonForeshortening,
|
|
137
|
+
getPerspectiveLessonCatalog
|
|
138
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
|
|
3
|
+
function drawHorizon(canvasSize, yPos) {
|
|
4
|
+
const commands = [];
|
|
5
|
+
// Horizon line (Eye level)
|
|
6
|
+
commands.push(shapes.drawLine(0, yPos, canvasSize, yPos, '#9e9e9e'));
|
|
7
|
+
return commands;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function drawVP(x, y) {
|
|
11
|
+
// Red dot for vanishing point
|
|
12
|
+
return [shapes.drawCircle(x, y, 2, '#ff0000', true)];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function draw1PointBox(vpX, vpY, fX, fY, w, h, depthFactor, color) {
|
|
16
|
+
const commands = [];
|
|
17
|
+
// Front face (Front-facing plane)
|
|
18
|
+
commands.push(shapes.drawRectangle(fX, fY, w, h, color, false));
|
|
19
|
+
|
|
20
|
+
// Orthogonal lines to VP (Depth lines)
|
|
21
|
+
const guideColor = '#e0e0e0';
|
|
22
|
+
commands.push(shapes.drawLine(fX, fY, vpX, vpY, guideColor));
|
|
23
|
+
commands.push(shapes.drawLine(fX+w, fY, vpX, vpY, guideColor));
|
|
24
|
+
commands.push(shapes.drawLine(fX, fY+h, vpX, vpY, guideColor));
|
|
25
|
+
commands.push(shapes.drawLine(fX+w, fY+h, vpX, vpY, guideColor));
|
|
26
|
+
|
|
27
|
+
// Back face (Receding plane calculation)
|
|
28
|
+
// Distance from front face to VP
|
|
29
|
+
const dTLx = vpX - fX;
|
|
30
|
+
const dTLy = vpY - fY;
|
|
31
|
+
|
|
32
|
+
// Back face coordinates based on depth factor (0 to 1)
|
|
33
|
+
const bX = fX + Math.floor(dTLx * depthFactor);
|
|
34
|
+
const bY = fY + Math.floor(dTLy * depthFactor);
|
|
35
|
+
const bW = Math.floor(w * (1 - depthFactor));
|
|
36
|
+
const bH = Math.floor(h * (1 - depthFactor));
|
|
37
|
+
|
|
38
|
+
commands.push(shapes.drawRectangle(bX, bY, bW, bH, color, false));
|
|
39
|
+
|
|
40
|
+
// Connect corners
|
|
41
|
+
commands.push(shapes.drawLine(fX, fY, bX, bY, color));
|
|
42
|
+
commands.push(shapes.drawLine(fX+w, fY, bX+bW, bY, color));
|
|
43
|
+
commands.push(shapes.drawLine(fX, fY+h, bX, bY+bH, color));
|
|
44
|
+
commands.push(shapes.drawLine(fX+w, fY+h, bX+bW, bY+bH, color));
|
|
45
|
+
|
|
46
|
+
return commands;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function draw2PointBox(vp1X, vp1Y, vp2X, vp2Y, startX, startY, h, depth1, depth2, color) {
|
|
50
|
+
const commands = [];
|
|
51
|
+
const guideColor = '#e0e0e0';
|
|
52
|
+
|
|
53
|
+
// Center vertical edge (faces viewer)
|
|
54
|
+
commands.push(shapes.drawLine(startX, startY, startX, startY + h, color));
|
|
55
|
+
|
|
56
|
+
// Orthogonals to VP1 (Left)
|
|
57
|
+
commands.push(shapes.drawLine(startX, startY, vp1X, vp1Y, guideColor));
|
|
58
|
+
commands.push(shapes.drawLine(startX, startY + h, vp1X, vp1Y, guideColor));
|
|
59
|
+
|
|
60
|
+
// Orthogonals to VP2 (Right)
|
|
61
|
+
commands.push(shapes.drawLine(startX, startY, vp2X, vp2Y, guideColor));
|
|
62
|
+
commands.push(shapes.drawLine(startX, startY + h, vp2X, vp2Y, guideColor));
|
|
63
|
+
|
|
64
|
+
// Left vertical edge
|
|
65
|
+
const lX = startX + Math.floor((vp1X - startX) * depth1);
|
|
66
|
+
const lY1 = startY + Math.floor((vp1Y - startY) * depth1);
|
|
67
|
+
const lY2 = (startY + h) + Math.floor((vp1Y - (startY + h)) * depth1);
|
|
68
|
+
commands.push(shapes.drawLine(lX, lY1, lX, lY2, color));
|
|
69
|
+
commands.push(shapes.drawLine(startX, startY, lX, lY1, color));
|
|
70
|
+
commands.push(shapes.drawLine(startX, startY+h, lX, lY2, color));
|
|
71
|
+
|
|
72
|
+
// Right vertical edge
|
|
73
|
+
const rX = startX + Math.floor((vp2X - startX) * depth2);
|
|
74
|
+
const rY1 = startY + Math.floor((vp2Y - startY) * depth2);
|
|
75
|
+
const rY2 = (startY + h) + Math.floor((vp2Y - (startY + h)) * depth2);
|
|
76
|
+
commands.push(shapes.drawLine(rX, rY1, rX, rY2, color));
|
|
77
|
+
commands.push(shapes.drawLine(startX, startY, rX, rY1, color));
|
|
78
|
+
commands.push(shapes.drawLine(startX, startY+h, rX, rY2, color));
|
|
79
|
+
|
|
80
|
+
// Back vertical edge (intersection of lines from left/right edges to opposite VPs)
|
|
81
|
+
// For simplicity, we just draw the top and bottom back lines converging to VPs
|
|
82
|
+
commands.push(shapes.drawLine(lX, lY1, vp2X, vp2Y, guideColor));
|
|
83
|
+
commands.push(shapes.drawLine(rX, rY1, vp1X, vp1Y, guideColor));
|
|
84
|
+
|
|
85
|
+
// Approximate intersection for top back corner
|
|
86
|
+
// (In a full implementation, we'd calculate line intersection, here we just show the structure)
|
|
87
|
+
|
|
88
|
+
return commands;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
drawHorizon,
|
|
93
|
+
drawVP,
|
|
94
|
+
draw1PointBox,
|
|
95
|
+
draw2PointBox
|
|
96
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const { registerTool, sendCommand } = require('../../core/server');
|
|
3
|
+
const perspLessons = require('./perspective_lessons');
|
|
4
|
+
|
|
5
|
+
const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/).describe('Hex color e.g. #ff0000');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register all perspective tools on the MCP server.
|
|
9
|
+
* @param {McpServer} server
|
|
10
|
+
*/
|
|
11
|
+
function register(server) {
|
|
12
|
+
registerTool(server, 'art_tree_lesson_persp_1point',
|
|
13
|
+
'Lesson: Perspective - 1-Point Perspective (Horizon, 1 VP, Front vs Receding)',
|
|
14
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#2196f3') },
|
|
15
|
+
async (p) => {
|
|
16
|
+
const commands = perspLessons.lesson1PointPerspective(p.canvasSize, p.color);
|
|
17
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
18
|
+
return { content: [{ type: 'text', text: `✓ 1-Point Perspective lesson completed` }] };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
registerTool(server, 'art_tree_lesson_persp_2point',
|
|
22
|
+
'Lesson: Perspective - 2-Point Perspective (Horizon, 2 VPs)',
|
|
23
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#4caf50') },
|
|
24
|
+
async (p) => {
|
|
25
|
+
const commands = perspLessons.lesson2PointPerspective(p.canvasSize, p.color);
|
|
26
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
27
|
+
return { content: [{ type: 'text', text: `✓ 2-Point Perspective lesson completed` }] };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
registerTool(server, 'art_tree_lesson_persp_3point',
|
|
31
|
+
'Lesson: Perspective - 3-Point Perspective (Bird\'s eye view)',
|
|
32
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#ff9800') },
|
|
33
|
+
async (p) => {
|
|
34
|
+
const commands = perspLessons.lesson3PointPerspective(p.canvasSize, p.color);
|
|
35
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
36
|
+
return { content: [{ type: 'text', text: `✓ 3-Point Perspective lesson completed` }] };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
registerTool(server, 'art_tree_lesson_persp_foreshorten',
|
|
40
|
+
'Lesson: Perspective - Foreshortening (Rút ngắn theo chiều sâu)',
|
|
41
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#9c27b0') },
|
|
42
|
+
async (p) => {
|
|
43
|
+
const commands = perspLessons.lessonForeshortening(p.canvasSize, p.color);
|
|
44
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
45
|
+
return { content: [{ type: 'text', text: `✓ Foreshortening lesson completed` }] };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
registerTool(server, 'art_tree_lesson_persp_catalog',
|
|
49
|
+
'Get the catalog of all available Perspective lessons',
|
|
50
|
+
{},
|
|
51
|
+
() => {
|
|
52
|
+
const catalog = perspLessons.getPerspectiveLessonCatalog();
|
|
53
|
+
const text = catalog.map(l =>
|
|
54
|
+
` • ${l.id}: ${l.name} — ${l.description}`
|
|
55
|
+
).join('\n');
|
|
56
|
+
return { content: [{ type: 'text', text: `📚 Perspective Lessons:\n\n${text}` }] };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { register };
|
|
@@ -182,9 +182,24 @@ function drawGrid(startX, startY, cols, rows, cellSize, color) {
|
|
|
182
182
|
return commands;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Draw a continuous polyline from a list of points.
|
|
187
|
+
* @param {Array<{x: number, y: number}>} points - Array of vertices
|
|
188
|
+
* @param {string} color - Hex color
|
|
189
|
+
* @returns {Array<Object>} Array of Command payloads
|
|
190
|
+
*/
|
|
191
|
+
function drawPolyline(points, color) {
|
|
192
|
+
const commands = [];
|
|
193
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
194
|
+
commands.push(drawLine(points[i].x, points[i].y, points[i+1].x, points[i+1].y, color));
|
|
195
|
+
}
|
|
196
|
+
return commands;
|
|
197
|
+
}
|
|
198
|
+
|
|
185
199
|
module.exports = {
|
|
186
200
|
PRIMITIVE_ACTIONS,
|
|
187
201
|
drawLine,
|
|
202
|
+
drawPolyline,
|
|
188
203
|
drawSquare,
|
|
189
204
|
drawRectangle,
|
|
190
205
|
drawCircle,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
const structureShapes = require('./structure_shapes');
|
|
3
|
+
const shapes3d = require('./3d_shapes');
|
|
4
|
+
const ellipseShapes = require('./ellipse_shapes');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lesson: XYZ Axes (Vertical, Horizontal, Depth)
|
|
8
|
+
*/
|
|
9
|
+
function lessonXYZAxes(canvasSize = 32, color = '#2196f3') {
|
|
10
|
+
const commands = [];
|
|
11
|
+
const cx = Math.floor(canvasSize / 2);
|
|
12
|
+
const cy = Math.floor(canvasSize / 2);
|
|
13
|
+
const length = Math.floor(canvasSize * 0.4);
|
|
14
|
+
|
|
15
|
+
// Draw the axes
|
|
16
|
+
commands.push(...structureShapes.drawXYZAxes(cx, cy, length));
|
|
17
|
+
|
|
18
|
+
// Draw a simple box aligned with these axes to show how objects follow them
|
|
19
|
+
const w = Math.floor(length * 0.5);
|
|
20
|
+
const h = Math.floor(length * 0.5);
|
|
21
|
+
const d = Math.floor(length * 0.5);
|
|
22
|
+
// Using a lighter color for the reference box
|
|
23
|
+
commands.push(...shapes3d.drawWireframeBox(cx + Math.floor(w/2), cy - Math.floor(h/2), w, h, d, '#e0e0e0'));
|
|
24
|
+
|
|
25
|
+
return commands;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lesson: Complex Structure Analysis (e.g. A Bottle)
|
|
30
|
+
*/
|
|
31
|
+
function lessonComplexStructure(canvasSize = 32, color = '#9c27b0') {
|
|
32
|
+
const commands = [];
|
|
33
|
+
const cx = Math.floor(canvasSize / 2);
|
|
34
|
+
const cy = Math.floor(canvasSize / 2);
|
|
35
|
+
const totalHeight = Math.floor(canvasSize * 0.8);
|
|
36
|
+
|
|
37
|
+
// Draw bottle structure showing cross-sections and central axis
|
|
38
|
+
commands.push(...structureShapes.drawBottleAnatomy(cx, cy, totalHeight, color));
|
|
39
|
+
|
|
40
|
+
return commands;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Lesson: Axis Orientation (Object pointing along different axes)
|
|
45
|
+
*/
|
|
46
|
+
function lessonAxisOrientation(canvasSize = 32, color = '#00bcd4') {
|
|
47
|
+
const commands = [];
|
|
48
|
+
const r = Math.floor(canvasSize * 0.1);
|
|
49
|
+
const h = Math.floor(canvasSize * 0.25);
|
|
50
|
+
const midY = Math.floor(canvasSize / 2);
|
|
51
|
+
|
|
52
|
+
// 1. Standing Cylinder (Oriented along Y axis)
|
|
53
|
+
const q1 = Math.floor(canvasSize * 0.2);
|
|
54
|
+
// Draw vertical axis
|
|
55
|
+
commands.push(shapes.drawLine(q1, midY - h + 5, q1, midY + 5, '#4caf50'));
|
|
56
|
+
commands.push(shapes.drawEllipse(q1, midY, r, Math.floor(r * 0.3), color, false)); // Base
|
|
57
|
+
commands.push(shapes.drawEllipse(q1, midY - h, r, Math.floor(r * 0.3), color, false)); // Top
|
|
58
|
+
commands.push(shapes.drawLine(q1 - r, midY, q1 - r, midY - h, color));
|
|
59
|
+
commands.push(shapes.drawLine(q1 + r, midY, q1 + r, midY - h, color));
|
|
60
|
+
|
|
61
|
+
// 2. Lying Cylinder (Oriented along X axis)
|
|
62
|
+
const q2 = Math.floor(canvasSize * 0.5);
|
|
63
|
+
// Draw horizontal axis
|
|
64
|
+
commands.push(shapes.drawLine(q2 - 5, midY + h - r, q2 + h + 5, midY + h - r, '#f44336'));
|
|
65
|
+
commands.push(shapes.drawEllipse(q2, midY + h - r, Math.floor(r * 0.3), r, color, false)); // Left base
|
|
66
|
+
commands.push(shapes.drawEllipse(q2 + h, midY + h - r, Math.floor(r * 0.3), r, color, false)); // Right base
|
|
67
|
+
commands.push(shapes.drawLine(q2, midY + h - r - r, q2 + h, midY + h - r - r, color));
|
|
68
|
+
commands.push(shapes.drawLine(q2, midY + h - r + r, q2 + h, midY + h - r + r, color));
|
|
69
|
+
|
|
70
|
+
// 3. Tilted/Depth Cylinder (Oriented along an arbitrary angle / Z axis)
|
|
71
|
+
const q3 = Math.floor(canvasSize * 0.75);
|
|
72
|
+
const angle = Math.PI / 6; // 30 degrees tilt
|
|
73
|
+
// Draw tilted axis
|
|
74
|
+
const dx = Math.floor(h * Math.cos(angle));
|
|
75
|
+
const dy = Math.floor(h * Math.sin(angle));
|
|
76
|
+
commands.push(shapes.drawLine(q3 - dx/2 - 5, midY - dy/2 - 5, q3 + dx/2 + 5, midY + dy/2 + 5, '#2196f3'));
|
|
77
|
+
// Draw rotated ellipses for bases
|
|
78
|
+
commands.push(...ellipseShapes.drawRotatedEllipse(q3 - dx/2, midY - dy/2, Math.floor(r * 0.3), r, angle, color));
|
|
79
|
+
commands.push(...ellipseShapes.drawRotatedEllipse(q3 + dx/2, midY + dy/2, Math.floor(r * 0.3), r, angle, color));
|
|
80
|
+
// Tangent lines for tilted cylinder (approximate)
|
|
81
|
+
const tx = Math.floor(r * Math.sin(angle));
|
|
82
|
+
const ty = Math.floor(r * Math.cos(angle));
|
|
83
|
+
commands.push(shapes.drawLine(q3 - dx/2 - tx, midY - dy/2 + ty, q3 + dx/2 - tx, midY + dy/2 + ty, color));
|
|
84
|
+
commands.push(shapes.drawLine(q3 - dx/2 + tx, midY - dy/2 - ty, q3 + dx/2 + tx, midY + dy/2 - ty, color));
|
|
85
|
+
|
|
86
|
+
return commands;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getStructureLessonCatalog() {
|
|
90
|
+
return [
|
|
91
|
+
{ id: 'structure_xyz', name: 'XYZ Axes', description: 'Understanding 3D space directions (Vertical, Horizontal, Depth)', fn: lessonXYZAxes },
|
|
92
|
+
{ id: 'structure_bottle', name: 'Complex Structure (Bottle)', description: 'Analyzing an object using a central axis and multiple cross-sections', fn: lessonComplexStructure },
|
|
93
|
+
{ id: 'structure_orientation', name: 'Axis Orientation', description: 'Identifying which axis an object is oriented along', fn: lessonAxisOrientation },
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
lessonXYZAxes,
|
|
99
|
+
lessonComplexStructure,
|
|
100
|
+
lessonAxisOrientation,
|
|
101
|
+
getStructureLessonCatalog
|
|
102
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
const ellipseShapes = require('./ellipse_shapes');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Draw a 3D coordinate system (X, Y, Z axes) originating from a center point
|
|
6
|
+
*/
|
|
7
|
+
function drawXYZAxes(cx, cy, length) {
|
|
8
|
+
const commands = [];
|
|
9
|
+
|
|
10
|
+
// Y-axis (Vertical) - Green
|
|
11
|
+
commands.push(shapes.drawLine(cx, cy, cx, cy - length, '#4caf50'));
|
|
12
|
+
|
|
13
|
+
// X-axis (Horizontal) - Red
|
|
14
|
+
commands.push(shapes.drawLine(cx, cy, cx + length, cy, '#f44336'));
|
|
15
|
+
|
|
16
|
+
// Z-axis (Depth - Oblique projection) - Blue
|
|
17
|
+
const dx = Math.floor(length * 0.7 * Math.cos(Math.PI / 6));
|
|
18
|
+
const dy = Math.floor(length * 0.7 * Math.sin(Math.PI / 6));
|
|
19
|
+
commands.push(shapes.drawLine(cx, cy, cx - dx, cy + dy, '#2196f3'));
|
|
20
|
+
|
|
21
|
+
// Center point
|
|
22
|
+
commands.push(shapes.drawCircle(cx, cy, 2, '#000000', true));
|
|
23
|
+
|
|
24
|
+
return commands;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Draw a complex bottle structure using cross-sections and central axis
|
|
29
|
+
*/
|
|
30
|
+
function drawBottleAnatomy(cx, cy, totalHeight, color) {
|
|
31
|
+
const commands = [];
|
|
32
|
+
|
|
33
|
+
const bottomY = cy + Math.floor(totalHeight / 2);
|
|
34
|
+
const topY = cy - Math.floor(totalHeight / 2);
|
|
35
|
+
|
|
36
|
+
// Central vertical axis
|
|
37
|
+
commands.push(shapes.drawLine(cx, topY - 10, cx, bottomY + 10, '#bdbdbd'));
|
|
38
|
+
|
|
39
|
+
// Define cross sections (y position from bottom, radius)
|
|
40
|
+
const sections = [
|
|
41
|
+
{ y: bottomY, r: Math.floor(totalHeight * 0.2) }, // Base
|
|
42
|
+
{ y: bottomY - Math.floor(totalHeight * 0.4), r: Math.floor(totalHeight * 0.2) }, // Body
|
|
43
|
+
{ y: bottomY - Math.floor(totalHeight * 0.7), r: Math.floor(totalHeight * 0.08) }, // Neck start
|
|
44
|
+
{ y: topY, r: Math.floor(totalHeight * 0.08) } // Mouth
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Draw cross section ellipses
|
|
48
|
+
for (const sec of sections) {
|
|
49
|
+
// ry is based on perspective (lower = wider perspective, but keep it simple here)
|
|
50
|
+
const ry = Math.floor(sec.r * 0.3);
|
|
51
|
+
commands.push(shapes.drawEllipse(cx, sec.y, sec.r, ry, color, false));
|
|
52
|
+
|
|
53
|
+
// Cross point on the axis
|
|
54
|
+
commands.push(shapes.drawCircle(cx, sec.y, 1, '#ff0000', true));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Draw outer contour lines
|
|
58
|
+
for (let i = 0; i < sections.length - 1; i++) {
|
|
59
|
+
const s1 = sections[i];
|
|
60
|
+
const s2 = sections[i+1];
|
|
61
|
+
|
|
62
|
+
// Left contour
|
|
63
|
+
commands.push(shapes.drawLine(cx - s1.r, s1.y, cx - s2.r, s2.y, color));
|
|
64
|
+
// Right contour
|
|
65
|
+
commands.push(shapes.drawLine(cx + s1.r, s1.y, cx + s2.r, s2.y, color));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return commands;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
drawXYZAxes,
|
|
73
|
+
drawBottleAnatomy
|
|
74
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const { registerTool, sendCommand } = require('../../core/server');
|
|
3
|
+
const structureLessons = require('./structure_lessons');
|
|
4
|
+
|
|
5
|
+
const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/).describe('Hex color e.g. #ff0000');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register all structure and axis tools on the MCP server.
|
|
9
|
+
* @param {McpServer} server
|
|
10
|
+
*/
|
|
11
|
+
function register(server) {
|
|
12
|
+
registerTool(server, 'art_tree_lesson_structure_xyz',
|
|
13
|
+
'Lesson: Structure - XYZ Axes (Vertical, Horizontal, Depth directions)',
|
|
14
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#2196f3') },
|
|
15
|
+
async (p) => {
|
|
16
|
+
const commands = structureLessons.lessonXYZAxes(p.canvasSize, p.color);
|
|
17
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
18
|
+
return { content: [{ type: 'text', text: `✓ XYZ Axes lesson completed` }] };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
registerTool(server, 'art_tree_lesson_structure_bottle',
|
|
22
|
+
'Lesson: Structure - Complex Analysis (Bottle with cross-sections)',
|
|
23
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#9c27b0') },
|
|
24
|
+
async (p) => {
|
|
25
|
+
const commands = structureLessons.lessonComplexStructure(p.canvasSize, p.color);
|
|
26
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
27
|
+
return { content: [{ type: 'text', text: `✓ Complex Structure (Bottle) lesson completed` }] };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
registerTool(server, 'art_tree_lesson_structure_orientation',
|
|
31
|
+
'Lesson: Structure - Axis Orientation (Which axis is it oriented along?)',
|
|
32
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#00bcd4') },
|
|
33
|
+
async (p) => {
|
|
34
|
+
const commands = structureLessons.lessonAxisOrientation(p.canvasSize, p.color);
|
|
35
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
36
|
+
return { content: [{ type: 'text', text: `✓ Axis Orientation lesson completed` }] };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
registerTool(server, 'art_tree_lesson_structure_catalog',
|
|
40
|
+
'Get the catalog of all available Structure lessons',
|
|
41
|
+
{},
|
|
42
|
+
() => {
|
|
43
|
+
const catalog = structureLessons.getStructureLessonCatalog();
|
|
44
|
+
const text = catalog.map(l =>
|
|
45
|
+
` • ${l.id}: ${l.name} — ${l.description}`
|
|
46
|
+
).join('\n');
|
|
47
|
+
return { content: [{ type: 'text', text: `📚 Structure Lessons:\n\n${text}` }] };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { register };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
const surfaceShapes = require('./surface_shapes');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lesson: Surface Types (Mặt phẳng, lồi, lõm)
|
|
6
|
+
*/
|
|
7
|
+
function lessonSurfaceTypes(canvasSize = 32, color = '#2196f3') {
|
|
8
|
+
const commands = [];
|
|
9
|
+
const midY = Math.floor(canvasSize / 2);
|
|
10
|
+
const qX = Math.floor(canvasSize / 4);
|
|
11
|
+
const r = Math.floor(canvasSize * 0.15);
|
|
12
|
+
|
|
13
|
+
// 1. Flat Plane (Mặt phẳng)
|
|
14
|
+
const w = Math.floor(canvasSize * 0.2);
|
|
15
|
+
commands.push(shapes.drawRectangle(qX - w/2, midY - w/2, w, w, '#4caf50', false));
|
|
16
|
+
// Add some grid lines to show it's flat
|
|
17
|
+
commands.push(shapes.drawLine(qX, midY - w/2, qX, midY + w/2, '#4caf50'));
|
|
18
|
+
commands.push(shapes.drawLine(qX - w/2, midY, qX + w/2, midY, '#4caf50'));
|
|
19
|
+
|
|
20
|
+
// 2. Convex Surface (Mặt lồi - e.g. Dome/Sphere)
|
|
21
|
+
// We use an ellipse that curves outwards (downwards)
|
|
22
|
+
commands.push(shapes.drawEllipse(qX * 2, midY, r, r, '#ff9800', false));
|
|
23
|
+
// Cross contour curving DOWN (convex towards viewer)
|
|
24
|
+
commands.push(shapes.drawEllipse(qX * 2, midY + Math.floor(r*0.2), r, Math.floor(r*0.3), '#ff9800', false));
|
|
25
|
+
|
|
26
|
+
// 3. Concave Surface (Mặt lõm - e.g. Bowl)
|
|
27
|
+
// Draw the top rim of a bowl
|
|
28
|
+
commands.push(shapes.drawEllipse(qX * 3, midY, r, Math.floor(r*0.3), '#9c27b0', false));
|
|
29
|
+
// Draw the bottom curve (the inside of the bowl)
|
|
30
|
+
const bowlPoints = [];
|
|
31
|
+
for(let i=0; i<=18; i++) {
|
|
32
|
+
const a = (i/18) * Math.PI; // 0 to 180 degrees (bottom half)
|
|
33
|
+
bowlPoints.push({ x: Math.round(qX * 3 + r * Math.cos(a)), y: Math.round(midY + r * Math.sin(a)) });
|
|
34
|
+
}
|
|
35
|
+
commands.push(shapes.drawPolyline(bowlPoints, '#9c27b0'));
|
|
36
|
+
|
|
37
|
+
return commands;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lesson: Edge Types (Cạnh cứng vuông vức vs Cạnh mềm bo góc)
|
|
42
|
+
*/
|
|
43
|
+
function lessonEdgeTypes(canvasSize = 32, color = '#ff5722') {
|
|
44
|
+
const commands = [];
|
|
45
|
+
const qX1 = Math.floor(canvasSize * 0.3);
|
|
46
|
+
const qX2 = Math.floor(canvasSize * 0.7);
|
|
47
|
+
const midY = Math.floor(canvasSize / 2);
|
|
48
|
+
const w = Math.floor(canvasSize * 0.25);
|
|
49
|
+
|
|
50
|
+
// 1. Hard Edge (Cạnh cứng - Sharp corners)
|
|
51
|
+
commands.push(shapes.drawRectangle(qX1 - w/2, midY - w/2, w, w, '#e91e63', false));
|
|
52
|
+
|
|
53
|
+
// 2. Soft Edge / Transitional (Cạnh mềm / Mặt chuyển tiếp)
|
|
54
|
+
// Draw a box with rounded corners (fillet)
|
|
55
|
+
const r = Math.floor(w * 0.2); // Corner radius
|
|
56
|
+
const rx = qX2 - w/2;
|
|
57
|
+
const ry = midY - w/2;
|
|
58
|
+
|
|
59
|
+
// Straight segments
|
|
60
|
+
commands.push(shapes.drawLine(rx + r, ry, rx + w - r, ry, '#00bcd4')); // Top
|
|
61
|
+
commands.push(shapes.drawLine(rx + w, ry + r, rx + w, ry + w - r, '#00bcd4')); // Right
|
|
62
|
+
commands.push(shapes.drawLine(rx + r, ry + w, rx + w - r, ry + w, '#00bcd4')); // Bottom
|
|
63
|
+
commands.push(shapes.drawLine(rx, ry + r, rx, ry + w - r, '#00bcd4')); // Left
|
|
64
|
+
|
|
65
|
+
// Rounded corners (approximated with small circles for visual simplicity)
|
|
66
|
+
commands.push(shapes.drawCircle(rx + r, ry + r, r, '#00bcd4', false)); // TL
|
|
67
|
+
commands.push(shapes.drawCircle(rx + w - r, ry + r, r, '#00bcd4', false)); // TR
|
|
68
|
+
commands.push(shapes.drawCircle(rx + w - r, ry + w - r, r, '#00bcd4', false)); // BR
|
|
69
|
+
commands.push(shapes.drawCircle(rx + r, ry + w - r, r, '#00bcd4', false)); // BL
|
|
70
|
+
|
|
71
|
+
return commands;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Lesson: Cup Analysis (Analyzing surfaces of a cup)
|
|
76
|
+
*/
|
|
77
|
+
function lessonCupAnalysis(canvasSize = 32, color = '#607d8b') {
|
|
78
|
+
const cx = Math.floor(canvasSize / 2);
|
|
79
|
+
const cy = Math.floor(canvasSize / 2);
|
|
80
|
+
const r = Math.floor(canvasSize * 0.2);
|
|
81
|
+
const h = Math.floor(canvasSize * 0.4);
|
|
82
|
+
|
|
83
|
+
return surfaceShapes.drawCupAnalysis(cx, cy, r, h);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Lesson: Chair Analysis (Analyzing surfaces and edges of a chair)
|
|
88
|
+
*/
|
|
89
|
+
function lessonChairAnalysis(canvasSize = 32, color = '#795548') {
|
|
90
|
+
const cx = Math.floor(canvasSize / 2);
|
|
91
|
+
const cy = Math.floor(canvasSize / 2);
|
|
92
|
+
const w = Math.floor(canvasSize * 0.3);
|
|
93
|
+
|
|
94
|
+
return surfaceShapes.drawChairAnalysis(cx, cy, w);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getSurfaceLessonCatalog() {
|
|
98
|
+
return [
|
|
99
|
+
{ id: 'surface_types', name: 'Surface Types', description: 'Flat, Convex, Concave planes', fn: lessonSurfaceTypes },
|
|
100
|
+
{ id: 'edge_types', name: 'Edge Types', description: 'Hard vs Soft (Transitional) edges', fn: lessonEdgeTypes },
|
|
101
|
+
{ id: 'surface_cup', name: 'Cup Analysis', description: 'Real object: Convex/Concave walls, Rim, Tube handle', fn: lessonCupAnalysis },
|
|
102
|
+
{ id: 'surface_chair', name: 'Chair Analysis', description: 'Real object: Flat planes, Hard edges, Cylindrical legs', fn: lessonChairAnalysis },
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
lessonSurfaceTypes,
|
|
108
|
+
lessonEdgeTypes,
|
|
109
|
+
lessonCupAnalysis,
|
|
110
|
+
lessonChairAnalysis,
|
|
111
|
+
getSurfaceLessonCatalog
|
|
112
|
+
};
|