@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,55 @@
|
|
|
1
|
+
const hiddenShapes = require('./hidden_shapes');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lesson: Hidden Box (Khối hộp 6 mặt)
|
|
5
|
+
* You see 3 faces, but you must understand all 6.
|
|
6
|
+
*/
|
|
7
|
+
function lessonHiddenBox(canvasSize = 32, visibleColor = '#2196f3', hiddenColor = '#e0e0e0') {
|
|
8
|
+
const cx = Math.floor(canvasSize / 2);
|
|
9
|
+
const cy = Math.floor(canvasSize / 2);
|
|
10
|
+
const w = Math.floor(canvasSize * 0.4);
|
|
11
|
+
const h = Math.floor(canvasSize * 0.4);
|
|
12
|
+
const d = Math.floor(canvasSize * 0.4);
|
|
13
|
+
|
|
14
|
+
return hiddenShapes.drawXRayBox(cx, cy, w, h, d, visibleColor, hiddenColor);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Lesson: Hidden Head (Đầu người và hộp sọ)
|
|
19
|
+
* You don't see the whole skull, but must understand the structure behind it.
|
|
20
|
+
*/
|
|
21
|
+
function lessonHiddenHead(canvasSize = 32, visibleColor = '#ff5722', hiddenColor = '#e0e0e0') {
|
|
22
|
+
const cx = Math.floor(canvasSize / 2);
|
|
23
|
+
const cy = Math.floor(canvasSize * 0.4);
|
|
24
|
+
const r = Math.floor(canvasSize * 0.25);
|
|
25
|
+
|
|
26
|
+
return hiddenShapes.drawXRayHead(cx, cy, r, visibleColor, hiddenColor);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Lesson: Hidden Cup (Cái cốc và đáy bên trong)
|
|
31
|
+
* You don't see the full inside bottom, but must understand how the wall goes down.
|
|
32
|
+
*/
|
|
33
|
+
function lessonHiddenCup(canvasSize = 32, visibleColor = '#4caf50', hiddenColor = '#e0e0e0') {
|
|
34
|
+
const cx = Math.floor(canvasSize / 2);
|
|
35
|
+
const cy = Math.floor(canvasSize / 2);
|
|
36
|
+
const r = Math.floor(canvasSize * 0.25);
|
|
37
|
+
const h = Math.floor(canvasSize * 0.5);
|
|
38
|
+
|
|
39
|
+
return hiddenShapes.drawXRayCup(cx, cy, r, h, visibleColor, hiddenColor);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getHiddenLessonCatalog() {
|
|
43
|
+
return [
|
|
44
|
+
{ id: 'hidden_box', name: 'Hidden Box', description: 'See all 6 faces of a box, not just 3', fn: lessonHiddenBox },
|
|
45
|
+
{ id: 'hidden_head', name: 'Hidden Head', description: 'Understand the cranium sphere behind the face', fn: lessonHiddenHead },
|
|
46
|
+
{ id: 'hidden_cup', name: 'Hidden Cup', description: 'Understand the inner depth and bottom of a cup', fn: lessonHiddenCup },
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
lessonHiddenBox,
|
|
52
|
+
lessonHiddenHead,
|
|
53
|
+
lessonHiddenCup,
|
|
54
|
+
getHiddenLessonCatalog
|
|
55
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
const ellipseShapes = require('./ellipse_shapes');
|
|
3
|
+
|
|
4
|
+
function drawXRayBox(cx, cy, w, h, d, visibleColor, hiddenColor) {
|
|
5
|
+
const commands = [];
|
|
6
|
+
const dx = Math.floor(d * 0.7);
|
|
7
|
+
const dy = Math.floor(d * 0.5);
|
|
8
|
+
|
|
9
|
+
const fX = cx - Math.floor(w/2);
|
|
10
|
+
const fY = cy + Math.floor(h/2);
|
|
11
|
+
|
|
12
|
+
// Hidden edges (Back, Bottom, Left side)
|
|
13
|
+
// Back face
|
|
14
|
+
const bX = fX + dx;
|
|
15
|
+
const bY = fY - dy;
|
|
16
|
+
commands.push(shapes.drawLine(fX, fY, bX, bY, hiddenColor)); // Bottom left depth
|
|
17
|
+
commands.push(shapes.drawLine(bX, bY, bX + w, bY, hiddenColor)); // Back bottom
|
|
18
|
+
commands.push(shapes.drawLine(bX, bY, bX, bY - h, hiddenColor)); // Back left vertical
|
|
19
|
+
|
|
20
|
+
// Visible edges (Front, Top, Right side)
|
|
21
|
+
// Front face
|
|
22
|
+
commands.push(shapes.drawRectangle(fX, fY - h, w, h, visibleColor, false));
|
|
23
|
+
// Top face
|
|
24
|
+
commands.push(shapes.drawLine(fX, fY - h, bX, bY - h, visibleColor));
|
|
25
|
+
commands.push(shapes.drawLine(fX + w, fY - h, bX + w, bY - h, visibleColor));
|
|
26
|
+
commands.push(shapes.drawLine(bX, bY - h, bX + w, bY - h, visibleColor));
|
|
27
|
+
// Right side depth
|
|
28
|
+
commands.push(shapes.drawLine(fX + w, fY, bX + w, bY, visibleColor));
|
|
29
|
+
// Right back vertical
|
|
30
|
+
commands.push(shapes.drawLine(bX + w, bY, bX + w, bY - h, visibleColor));
|
|
31
|
+
|
|
32
|
+
return commands;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function drawXRayHead(cx, cy, r, visibleColor, hiddenColor) {
|
|
36
|
+
const commands = [];
|
|
37
|
+
|
|
38
|
+
// Hidden/Structural elements (Cranium sphere behind the face)
|
|
39
|
+
commands.push(shapes.drawCircle(cx, cy, r, hiddenColor, false));
|
|
40
|
+
// Cranium cross-contours (hidden structure)
|
|
41
|
+
commands.push(shapes.drawEllipse(cx, cy, r, Math.floor(r*0.3), hiddenColor, false)); // Equator
|
|
42
|
+
commands.push(shapes.drawEllipse(cx, cy, Math.floor(r*0.3), r, hiddenColor, false)); // Prime meridian
|
|
43
|
+
|
|
44
|
+
// Visible features (Face and Jaw)
|
|
45
|
+
const jawY = cy + Math.floor(r * 1.4);
|
|
46
|
+
const jawW = Math.floor(r * 0.6);
|
|
47
|
+
// Cheek lines dropping down
|
|
48
|
+
commands.push(shapes.drawLine(cx - r, cy, cx - jawW, jawY, visibleColor));
|
|
49
|
+
commands.push(shapes.drawLine(cx + r, cy, cx + jawW, jawY, visibleColor));
|
|
50
|
+
// Jaw line
|
|
51
|
+
commands.push(shapes.drawEllipse(cx, jawY, jawW, Math.floor(jawW * 0.4), visibleColor, false));
|
|
52
|
+
// Center facial axis
|
|
53
|
+
commands.push(shapes.drawLine(cx, cy - r, cx, jawY, visibleColor));
|
|
54
|
+
|
|
55
|
+
return commands;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function drawXRayCup(cx, cy, r, h, visibleColor, hiddenColor) {
|
|
59
|
+
const commands = [];
|
|
60
|
+
const topY = cy - Math.floor(h/2);
|
|
61
|
+
const botY = cy + Math.floor(h/2);
|
|
62
|
+
const ry = Math.floor(r * 0.3);
|
|
63
|
+
|
|
64
|
+
// Hidden elements (Inner bottom surface, back curve of outer base)
|
|
65
|
+
// Back curve of the outer base (hidden from viewer)
|
|
66
|
+
commands.push(shapes.drawEllipse(cx, botY, r, ry, hiddenColor, false));
|
|
67
|
+
|
|
68
|
+
// Inner bottom surface (showing the depth/thickness of the cup bottom)
|
|
69
|
+
const innerBotY = botY - Math.floor(h * 0.1);
|
|
70
|
+
const innerR = Math.floor(r * 0.85);
|
|
71
|
+
const innerRy = Math.floor(ry * 0.85);
|
|
72
|
+
commands.push(shapes.drawEllipse(cx, innerBotY, innerR, innerRy, hiddenColor, false));
|
|
73
|
+
|
|
74
|
+
// Visible elements (Outer walls, front curve of base, rim, inner wall)
|
|
75
|
+
// Walls
|
|
76
|
+
commands.push(shapes.drawLine(cx - r, topY, cx - r, botY, visibleColor));
|
|
77
|
+
commands.push(shapes.drawLine(cx + r, topY, cx + r, botY, visibleColor));
|
|
78
|
+
// Rim / Lip
|
|
79
|
+
commands.push(shapes.drawEllipse(cx, topY, r, ry, visibleColor, false));
|
|
80
|
+
commands.push(shapes.drawEllipse(cx, topY, innerR, innerRy, visibleColor, false));
|
|
81
|
+
|
|
82
|
+
// To draw just the front curve of the base in visible color, we approximate with a polyline
|
|
83
|
+
const frontCurve = [];
|
|
84
|
+
for (let i = 0; i <= 18; i++) {
|
|
85
|
+
const angle = (i / 18) * Math.PI; // 0 to 180 degrees (bottom half)
|
|
86
|
+
frontCurve.push({ x: Math.round(cx + r * Math.cos(angle)), y: Math.round(botY + ry * Math.sin(angle)) });
|
|
87
|
+
}
|
|
88
|
+
commands.push(shapes.drawPolyline(frontCurve, visibleColor));
|
|
89
|
+
|
|
90
|
+
return commands;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
drawXRayBox,
|
|
95
|
+
drawXRayHead,
|
|
96
|
+
drawXRayCup
|
|
97
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const { registerTool, sendCommand } = require('../../core/server');
|
|
3
|
+
const hiddenLessons = require('./hidden_lessons');
|
|
4
|
+
|
|
5
|
+
const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/).describe('Hex color e.g. #ff0000');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register all hidden shapes tools on the MCP server.
|
|
9
|
+
* @param {McpServer} server
|
|
10
|
+
*/
|
|
11
|
+
function register(server) {
|
|
12
|
+
registerTool(server, 'art_tree_lesson_hidden_box',
|
|
13
|
+
'Lesson: Hidden - Understand all 6 faces of a box (X-Ray vision)',
|
|
14
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), visibleColor: hexColor.default('#2196f3'), hiddenColor: hexColor.default('#e0e0e0') },
|
|
15
|
+
async (p) => {
|
|
16
|
+
const commands = hiddenLessons.lessonHiddenBox(p.canvasSize, p.visibleColor, p.hiddenColor);
|
|
17
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
18
|
+
return { content: [{ type: 'text', text: `✓ Hidden Box lesson completed` }] };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
registerTool(server, 'art_tree_lesson_hidden_head',
|
|
22
|
+
'Lesson: Hidden - Understand the cranium sphere behind the face (X-Ray vision)',
|
|
23
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), visibleColor: hexColor.default('#ff5722'), hiddenColor: hexColor.default('#e0e0e0') },
|
|
24
|
+
async (p) => {
|
|
25
|
+
const commands = hiddenLessons.lessonHiddenHead(p.canvasSize, p.visibleColor, p.hiddenColor);
|
|
26
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
27
|
+
return { content: [{ type: 'text', text: `✓ Hidden Head lesson completed` }] };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
registerTool(server, 'art_tree_lesson_hidden_cup',
|
|
31
|
+
'Lesson: Hidden - Understand the inner depth of a cup (X-Ray vision)',
|
|
32
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32), visibleColor: hexColor.default('#4caf50'), hiddenColor: hexColor.default('#e0e0e0') },
|
|
33
|
+
async (p) => {
|
|
34
|
+
const commands = hiddenLessons.lessonHiddenCup(p.canvasSize, p.visibleColor, p.hiddenColor);
|
|
35
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
36
|
+
return { content: [{ type: 'text', text: `✓ Hidden Cup lesson completed` }] };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
registerTool(server, 'art_tree_lesson_hidden_catalog',
|
|
40
|
+
'Get the catalog of all available Hidden Shapes lessons',
|
|
41
|
+
{},
|
|
42
|
+
() => {
|
|
43
|
+
const catalog = hiddenLessons.getHiddenLessonCatalog();
|
|
44
|
+
const text = catalog.map(l =>
|
|
45
|
+
` • ${l.id}: ${l.name} — ${l.description}`
|
|
46
|
+
).join('\n');
|
|
47
|
+
return { content: [{ type: 'text', text: `📚 Hidden Shapes Lessons:\n\n${text}` }] };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { register };
|
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
const shapes = require('./shapes');
|
|
11
11
|
const lessons = require('./lessons');
|
|
12
12
|
const tools = require('./tools');
|
|
13
|
+
const advTools = require('./advanced_tools');
|
|
14
|
+
const curveTools = require('./curve_tools');
|
|
15
|
+
const ellipseTools = require('./ellipse_tools');
|
|
16
|
+
const tools3d = require('./3d_tools');
|
|
17
|
+
const structureTools = require('./structure_tools');
|
|
18
|
+
const csTools = require('./cross_section_tools');
|
|
19
|
+
const transformTools = require('./transform_tools');
|
|
20
|
+
const surfaceTools = require('./surface_tools');
|
|
21
|
+
const perspTools = require('./perspective_tools');
|
|
22
|
+
const hiddenTools = require('./hidden_tools');
|
|
23
|
+
const lightTools = require('./light_tools');
|
|
13
24
|
|
|
14
25
|
/**
|
|
15
26
|
* Register all Art Tree MCP tools on the server
|
|
@@ -17,6 +28,17 @@ const tools = require('./tools');
|
|
|
17
28
|
*/
|
|
18
29
|
function registerAll(server) {
|
|
19
30
|
tools.register(server);
|
|
31
|
+
advTools.register(server);
|
|
32
|
+
curveTools.register(server);
|
|
33
|
+
ellipseTools.register(server);
|
|
34
|
+
tools3d.register(server);
|
|
35
|
+
structureTools.register(server);
|
|
36
|
+
csTools.register(server);
|
|
37
|
+
transformTools.register(server);
|
|
38
|
+
surfaceTools.register(server);
|
|
39
|
+
perspTools.register(server);
|
|
40
|
+
hiddenTools.register(server);
|
|
41
|
+
lightTools.register(server);
|
|
20
42
|
}
|
|
21
43
|
|
|
22
44
|
module.exports = {
|
|
@@ -165,6 +165,106 @@ function lessonComposition(canvasSize = 32) {
|
|
|
165
165
|
];
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Lesson 8: Freehand — Luyện vẽ bằng tay, mô phỏng nét rung.
|
|
170
|
+
*/
|
|
171
|
+
function lessonFreehand(canvasSize = 32, color = '#795548') {
|
|
172
|
+
const margin = Math.floor(canvasSize * 0.1);
|
|
173
|
+
const points = [];
|
|
174
|
+
let y = Math.floor(canvasSize / 2);
|
|
175
|
+
for (let x = margin; x < canvasSize - margin; x += 2) {
|
|
176
|
+
points.push({ x, y: y + Math.floor(Math.random() * 3) - 1 });
|
|
177
|
+
}
|
|
178
|
+
return shapes.drawPolyline(points, color);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Lesson 9: Proportions and Angles — Tập giữ tỉ lệ và góc.
|
|
183
|
+
*/
|
|
184
|
+
function lessonProportionsAndAngles(canvasSize = 32, color = '#3f51b5') {
|
|
185
|
+
const mid = Math.floor(canvasSize / 2);
|
|
186
|
+
const size1 = Math.floor(canvasSize * 0.4);
|
|
187
|
+
const size2 = Math.floor(size1 / 2);
|
|
188
|
+
return [
|
|
189
|
+
shapes.drawSquare(mid - Math.floor(size1/2), mid - Math.floor(size1/2), size1, color, false),
|
|
190
|
+
shapes.drawSquare(mid - Math.floor(size2/2), mid - Math.floor(size2/2), size2, '#e91e63', false),
|
|
191
|
+
shapes.drawLine(mid, mid, mid + size1, mid - size1, '#009688')
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Lesson 10: Curves — Đường cong C, S.
|
|
197
|
+
*/
|
|
198
|
+
function lessonCurves(canvasSize = 32, color = '#9c27b0') {
|
|
199
|
+
const commands = [];
|
|
200
|
+
const mid = Math.floor(canvasSize / 2);
|
|
201
|
+
const r = Math.floor(canvasSize * 0.15);
|
|
202
|
+
// C curve
|
|
203
|
+
const cPoints = [];
|
|
204
|
+
for (let i = -Math.PI/2; i <= Math.PI/2; i += 0.2) {
|
|
205
|
+
cPoints.push({ x: Math.round(mid - r - r * Math.cos(i)), y: Math.round(mid / 2 + r * Math.sin(i)) });
|
|
206
|
+
}
|
|
207
|
+
commands.push(...shapes.drawPolyline(cPoints, color));
|
|
208
|
+
// S curve
|
|
209
|
+
const sPoints = [];
|
|
210
|
+
for (let i = -Math.PI/2; i <= Math.PI*1.5; i += 0.2) {
|
|
211
|
+
const isTop = i < Math.PI/2;
|
|
212
|
+
const cy = isTop ? (mid + Math.floor(canvasSize*0.1)) : (mid + Math.floor(canvasSize*0.1) + r*2);
|
|
213
|
+
const signX = isTop ? -1 : 1;
|
|
214
|
+
sPoints.push({ x: Math.round(mid + r + signX * r * Math.cos(i)), y: Math.round(cy + r * Math.sin(i)) });
|
|
215
|
+
}
|
|
216
|
+
commands.push(...shapes.drawPolyline(sPoints, '#e91e63'));
|
|
217
|
+
return commands;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Lesson 11: Spiral — Đường xoắn ốc.
|
|
222
|
+
*/
|
|
223
|
+
function lessonSpiral(canvasSize = 32, color = '#ff9800') {
|
|
224
|
+
const mid = Math.floor(canvasSize / 2);
|
|
225
|
+
const points = [];
|
|
226
|
+
const maxRadius = Math.floor(canvasSize * 0.4);
|
|
227
|
+
const loops = 3;
|
|
228
|
+
for (let angle = 0; angle < Math.PI * 2 * loops; angle += 0.3) {
|
|
229
|
+
const r = (angle / (Math.PI * 2 * loops)) * maxRadius;
|
|
230
|
+
points.push({ x: Math.round(mid + r * Math.cos(angle)), y: Math.round(mid + r * Math.sin(angle)) });
|
|
231
|
+
}
|
|
232
|
+
return shapes.drawPolyline(points, color);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Lesson 12: Strokes — Nét dài, nét ngắn, nét đậm–nhạt.
|
|
237
|
+
*/
|
|
238
|
+
function lessonStrokes(canvasSize = 32, color = '#2196f3') {
|
|
239
|
+
const margin = Math.floor(canvasSize * 0.1);
|
|
240
|
+
const y1 = Math.floor(canvasSize * 0.3);
|
|
241
|
+
const y2 = Math.floor(canvasSize * 0.5);
|
|
242
|
+
const y3 = Math.floor(canvasSize * 0.7);
|
|
243
|
+
return [
|
|
244
|
+
shapes.drawLine(margin, y1, canvasSize - margin, y1, color),
|
|
245
|
+
shapes.drawLine(margin, y2, Math.floor(canvasSize / 2), y2, '#4caf50'),
|
|
246
|
+
shapes.drawLine(margin, y3, canvasSize - margin, y3, '#f44336'),
|
|
247
|
+
shapes.drawLine(margin, y3 + 1, canvasSize - margin, y3 + 1, '#f44336'),
|
|
248
|
+
shapes.drawLine(margin, y3 + 2, canvasSize - margin, y3 + 2, '#f44336'),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Lesson 13: Parallel & Intersecting — Các đường song song và giao nhau.
|
|
254
|
+
*/
|
|
255
|
+
function lessonParallelAndIntersecting(canvasSize = 32, color = '#00bcd4') {
|
|
256
|
+
const margin = Math.floor(canvasSize * 0.1);
|
|
257
|
+
const mid = Math.floor(canvasSize / 2);
|
|
258
|
+
const offset = 4;
|
|
259
|
+
return [
|
|
260
|
+
shapes.drawLine(margin, margin, canvasSize - margin, margin, color),
|
|
261
|
+
shapes.drawLine(margin, margin + offset, canvasSize - margin, margin + offset, color),
|
|
262
|
+
shapes.drawLine(margin, margin + offset * 2, canvasSize - margin, margin + offset * 2, color),
|
|
263
|
+
shapes.drawLine(mid - offset*2, mid, mid + offset*2, canvasSize - margin, '#ff5722'),
|
|
264
|
+
shapes.drawLine(mid + offset*2, mid, mid - offset*2, canvasSize - margin, '#ff5722')
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
|
|
168
268
|
/**
|
|
169
269
|
* Get all available lessons with metadata.
|
|
170
270
|
* @returns {Array<{id: string, name: string, description: string, fn: Function}>}
|
|
@@ -178,6 +278,12 @@ function getLessonCatalog() {
|
|
|
178
278
|
{ id: 'triangles', name: 'Hình tam giác', description: 'Hình ba cạnh và sự ổn định', fn: lessonTriangles },
|
|
179
279
|
{ id: 'polygons', name: 'Đa giác', description: 'Hình nhiều cạnh', fn: lessonPolygons },
|
|
180
280
|
{ id: 'composition', name: 'Tổng hợp', description: 'Kết hợp các hình cơ bản tạo vật thể đơn giản', fn: lessonComposition },
|
|
281
|
+
{ id: 'freehand', name: 'Vẽ bằng tay', description: 'Luyện vẽ bằng tay, hạn chế dùng thước', fn: lessonFreehand },
|
|
282
|
+
{ id: 'proportions', name: 'Tỉ lệ và góc', description: 'Tập giữ tỉ lệ và góc', fn: lessonProportionsAndAngles },
|
|
283
|
+
{ id: 'curves', name: 'Đường cong', description: 'Đường cong và nét: Đường cong C, S', fn: lessonCurves },
|
|
284
|
+
{ id: 'spiral', name: 'Xoắn ốc', description: 'Đường xoắn ốc', fn: lessonSpiral },
|
|
285
|
+
{ id: 'strokes', name: 'Các loại nét', description: 'Nét dài, nét ngắn, nét đậm–nhạt', fn: lessonStrokes },
|
|
286
|
+
{ id: 'parallel_intersecting', name: 'Song song & giao nhau', description: 'Vẽ các đường song song và đường giao nhau', fn: lessonParallelAndIntersecting },
|
|
181
287
|
];
|
|
182
288
|
}
|
|
183
289
|
|
|
@@ -189,5 +295,11 @@ module.exports = {
|
|
|
189
295
|
lessonTriangles,
|
|
190
296
|
lessonPolygons,
|
|
191
297
|
lessonComposition,
|
|
298
|
+
lessonFreehand,
|
|
299
|
+
lessonProportionsAndAngles,
|
|
300
|
+
lessonCurves,
|
|
301
|
+
lessonSpiral,
|
|
302
|
+
lessonStrokes,
|
|
303
|
+
lessonParallelAndIntersecting,
|
|
192
304
|
getLessonCatalog,
|
|
193
305
|
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
const lightShapes = require('./light_shapes');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lesson: 6 Zones of Light (Khối cầu)
|
|
6
|
+
* Highlight, Light, Halftone, Core shadow, Reflected light, Cast shadow.
|
|
7
|
+
*/
|
|
8
|
+
function lessonLightZones(canvasSize = 32) {
|
|
9
|
+
const commands = [];
|
|
10
|
+
const cx = Math.floor(canvasSize / 2);
|
|
11
|
+
const cy = Math.floor(canvasSize / 2);
|
|
12
|
+
const r = Math.floor(canvasSize * 0.25);
|
|
13
|
+
|
|
14
|
+
// Light from top-left (approx -45 degrees or -135 degrees mathematically)
|
|
15
|
+
const lightAngle = -Math.PI * 0.75;
|
|
16
|
+
|
|
17
|
+
// Draw light source
|
|
18
|
+
const lx = cx + Math.floor(r * 2 * Math.cos(lightAngle));
|
|
19
|
+
const ly = cy + Math.floor(r * 2 * Math.sin(lightAngle));
|
|
20
|
+
commands.push(...lightShapes.drawLightSource(lx, ly, Math.floor(canvasSize * 0.05)));
|
|
21
|
+
|
|
22
|
+
// Draw shaded sphere
|
|
23
|
+
commands.push(...lightShapes.drawShadedSphere(cx, cy, r, lightAngle));
|
|
24
|
+
|
|
25
|
+
return commands;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Lesson: Light Direction & Intensity (Hướng ánh sáng và mặt phẳng)
|
|
30
|
+
* Shows a box with planes facing/turning away from light.
|
|
31
|
+
*/
|
|
32
|
+
function lessonLightDirection(canvasSize = 32) {
|
|
33
|
+
const commands = [];
|
|
34
|
+
const cx = Math.floor(canvasSize / 2);
|
|
35
|
+
const cy = Math.floor(canvasSize / 2);
|
|
36
|
+
const w = Math.floor(canvasSize * 0.25);
|
|
37
|
+
const h = Math.floor(canvasSize * 0.25);
|
|
38
|
+
|
|
39
|
+
// Light from top-left
|
|
40
|
+
const lightAngle = -Math.PI * 0.8;
|
|
41
|
+
|
|
42
|
+
const lx = cx - Math.floor(canvasSize * 0.35);
|
|
43
|
+
const ly = cy - Math.floor(canvasSize * 0.35);
|
|
44
|
+
commands.push(...lightShapes.drawLightSource(lx, ly, Math.floor(canvasSize * 0.05)));
|
|
45
|
+
|
|
46
|
+
// Draw shaded box
|
|
47
|
+
commands.push(...lightShapes.drawShadedBox(cx, cy, w, h, lightAngle));
|
|
48
|
+
|
|
49
|
+
return commands;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Lesson: Contact Shadow (Bóng tiếp xúc)
|
|
54
|
+
* The darkest area where an object touches the ground.
|
|
55
|
+
*/
|
|
56
|
+
function lessonContactShadow(canvasSize = 32) {
|
|
57
|
+
const commands = [];
|
|
58
|
+
const cx = Math.floor(canvasSize / 2);
|
|
59
|
+
const cy = Math.floor(canvasSize / 2);
|
|
60
|
+
const r = Math.floor(canvasSize * 0.3);
|
|
61
|
+
|
|
62
|
+
// Ground line
|
|
63
|
+
commands.push(shapes.drawLine(0, cy + r, canvasSize, cy + r, '#9e9e9e'));
|
|
64
|
+
|
|
65
|
+
// Sphere outline
|
|
66
|
+
commands.push(shapes.drawCircle(cx, cy, r, '#bdbdbd', false));
|
|
67
|
+
|
|
68
|
+
// Cast shadow (faint)
|
|
69
|
+
commands.push(shapes.drawEllipse(cx + Math.floor(r*0.5), cy + r, Math.floor(r*1.2), Math.floor(r*0.2), '#757575', true));
|
|
70
|
+
|
|
71
|
+
// CONTACT SHADOW (Pitch black, right at the touching point)
|
|
72
|
+
// Draw a very small, very dark ellipse directly under the center
|
|
73
|
+
commands.push(shapes.drawEllipse(cx, cy + r, Math.floor(r * 0.4), 2, '#000000', true));
|
|
74
|
+
commands.push(shapes.drawEllipse(cx, cy + r, Math.floor(r * 0.2), 1, '#000000', true));
|
|
75
|
+
|
|
76
|
+
return commands;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getLightLessonCatalog() {
|
|
80
|
+
return [
|
|
81
|
+
{ id: 'light_zones', name: '6 Zones of Light', description: 'Highlight to Cast shadow on a sphere', fn: lessonLightZones },
|
|
82
|
+
{ id: 'light_dir', name: 'Light Direction', description: 'Planes facing vs turning away from light', fn: lessonLightDirection },
|
|
83
|
+
{ id: 'light_contact', name: 'Contact Shadow', description: 'The darkest point where object touches ground', fn: lessonContactShadow },
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
lessonLightZones,
|
|
89
|
+
lessonLightDirection,
|
|
90
|
+
lessonContactShadow,
|
|
91
|
+
getLightLessonCatalog
|
|
92
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const shapes = require('./shapes');
|
|
2
|
+
|
|
3
|
+
function drawLightSource(cx, cy, r) {
|
|
4
|
+
const commands = [];
|
|
5
|
+
const color = '#ffeb3b'; // Yellow sun
|
|
6
|
+
commands.push(shapes.drawCircle(cx, cy, r, color, true));
|
|
7
|
+
|
|
8
|
+
// Rays
|
|
9
|
+
const rayL = Math.floor(r * 1.5);
|
|
10
|
+
for (let i = 0; i < 8; i++) {
|
|
11
|
+
const a = (i / 8) * 2 * Math.PI;
|
|
12
|
+
const x1 = Math.round(cx + (r + 2) * Math.cos(a));
|
|
13
|
+
const y1 = Math.round(cy + (r + 2) * Math.sin(a));
|
|
14
|
+
const x2 = Math.round(cx + (r + rayL) * Math.cos(a));
|
|
15
|
+
const y2 = Math.round(cy + (r + rayL) * Math.sin(a));
|
|
16
|
+
commands.push(shapes.drawLine(x1, y1, x2, y2, color));
|
|
17
|
+
}
|
|
18
|
+
return commands;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function drawShadedSphere(cx, cy, r, lightAngle) {
|
|
22
|
+
const commands = [];
|
|
23
|
+
|
|
24
|
+
// We simulate shading using overlapping crescent shapes or offset circles
|
|
25
|
+
// Since we have limited primitives, we'll draw concentric offset circles
|
|
26
|
+
// from dark to light towards the light source.
|
|
27
|
+
|
|
28
|
+
const offX = Math.cos(lightAngle);
|
|
29
|
+
const offY = Math.sin(lightAngle);
|
|
30
|
+
|
|
31
|
+
// 1. Cast Shadow (Bóng đổ) on the ground
|
|
32
|
+
// Drawn first so it's behind the sphere
|
|
33
|
+
// Shift cast shadow away from light
|
|
34
|
+
const castR = Math.floor(r * 1.2);
|
|
35
|
+
const castRy = Math.floor(r * 0.3);
|
|
36
|
+
const castX = cx - Math.floor(offX * r * 0.8);
|
|
37
|
+
const castY = cy + r - Math.floor(castRy/2);
|
|
38
|
+
commands.push(shapes.drawEllipse(castX, castY, castR, castRy, '#424242', true));
|
|
39
|
+
|
|
40
|
+
// 2. Base Sphere (Reflected Light zone color)
|
|
41
|
+
// The very edge away from light gets some bounce light
|
|
42
|
+
commands.push(shapes.drawCircle(cx, cy, r, '#78909c', true));
|
|
43
|
+
|
|
44
|
+
// 3. Core Shadow (Vùng tối chính)
|
|
45
|
+
// Offset slightly away from light
|
|
46
|
+
const coreR = Math.floor(r * 0.9);
|
|
47
|
+
const coreX = cx - Math.floor(offX * r * 0.1);
|
|
48
|
+
const coreY = cy - Math.floor(offY * r * 0.1);
|
|
49
|
+
commands.push(shapes.drawCircle(coreX, coreY, coreR, '#263238', true));
|
|
50
|
+
|
|
51
|
+
// 4. Halftone (Vùng trung gian)
|
|
52
|
+
const halfR = Math.floor(r * 0.75);
|
|
53
|
+
const halfX = cx + Math.floor(offX * r * 0.1);
|
|
54
|
+
const halfY = cy + Math.floor(offY * r * 0.1);
|
|
55
|
+
commands.push(shapes.drawCircle(halfX, halfY, halfR, '#546e7a', true));
|
|
56
|
+
|
|
57
|
+
// 5. Light (Vùng sáng)
|
|
58
|
+
const lightR = Math.floor(r * 0.5);
|
|
59
|
+
const lightX = cx + Math.floor(offX * r * 0.3);
|
|
60
|
+
const lightY = cy + Math.floor(offY * r * 0.3);
|
|
61
|
+
commands.push(shapes.drawCircle(lightX, lightY, lightR, '#90a4ae', true));
|
|
62
|
+
|
|
63
|
+
// 6. Highlight (Điểm sáng)
|
|
64
|
+
const highR = Math.floor(r * 0.15);
|
|
65
|
+
const highX = cx + Math.floor(offX * r * 0.5);
|
|
66
|
+
const highY = cy + Math.floor(offY * r * 0.5);
|
|
67
|
+
commands.push(shapes.drawCircle(highX, highY, highR, '#ffffff', true));
|
|
68
|
+
|
|
69
|
+
return commands;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function drawShadedBox(cx, cy, w, h, lightAngle) {
|
|
73
|
+
const commands = [];
|
|
74
|
+
const d = Math.floor(w * 0.6);
|
|
75
|
+
const dx = Math.floor(d * 0.7);
|
|
76
|
+
const dy = Math.floor(d * 0.5);
|
|
77
|
+
|
|
78
|
+
const fX = cx - Math.floor(w/2);
|
|
79
|
+
const fY = cy + Math.floor(h/2);
|
|
80
|
+
const bX = fX + dx;
|
|
81
|
+
const bY = fY - dy;
|
|
82
|
+
|
|
83
|
+
// Assume light is from Top-Left or Top-Right
|
|
84
|
+
const isLightLeft = Math.cos(lightAngle) < 0;
|
|
85
|
+
|
|
86
|
+
// Determine values (colors) for planes based on light
|
|
87
|
+
// Top is usually Light (facing up towards light source)
|
|
88
|
+
const topColor = '#cfd8dc';
|
|
89
|
+
// Front and Side depend on left/right light
|
|
90
|
+
const frontColor = isLightLeft ? '#90a4ae' : '#455a64'; // If light is left, front gets some light. If right, front is darker.
|
|
91
|
+
const sideColor = isLightLeft ? '#455a64' : '#90a4ae';
|
|
92
|
+
|
|
93
|
+
// Cast Shadow
|
|
94
|
+
const castX = isLightLeft ? bX + w : fX - w;
|
|
95
|
+
const castY = fY;
|
|
96
|
+
// A simple polygon for cast shadow
|
|
97
|
+
const shadowPts = [
|
|
98
|
+
{x: fX, y: fY},
|
|
99
|
+
{x: fX + w, y: fY},
|
|
100
|
+
{x: castX, y: castY - dy},
|
|
101
|
+
{x: castX - w, y: castY - dy}
|
|
102
|
+
];
|
|
103
|
+
commands.push(shapes.drawPolyline(shadowPts, '#424242'));
|
|
104
|
+
|
|
105
|
+
// Draw solid planes
|
|
106
|
+
// Note: Since we don't have a fillPolygon tool natively exposed that supports all shapes easily,
|
|
107
|
+
// we will draw wireframes with heavy lines or crosshatching to represent values.
|
|
108
|
+
// Actually, we can just use drawRectangle for front.
|
|
109
|
+
|
|
110
|
+
// Front face (solid)
|
|
111
|
+
commands.push(shapes.drawRectangle(fX, fY - h, w, h, frontColor, true));
|
|
112
|
+
|
|
113
|
+
// Side face (Right) (wireframe/hatch for now, or just outline)
|
|
114
|
+
// Let's use outline with thick color
|
|
115
|
+
const rightSidePts = [
|
|
116
|
+
{x: fX+w, y: fY-h}, {x: bX+w, y: bY-h}, {x: bX+w, y: bY}, {x: fX+w, y: fY}
|
|
117
|
+
];
|
|
118
|
+
commands.push(shapes.drawPolyline(rightSidePts, sideColor));
|
|
119
|
+
// Add some hatch lines to simulate fill
|
|
120
|
+
commands.push(shapes.drawLine(fX+w, fY-Math.floor(h/2), bX+w, bY-Math.floor(h/2), sideColor));
|
|
121
|
+
|
|
122
|
+
// Top face
|
|
123
|
+
const topPts = [
|
|
124
|
+
{x: fX, y: fY-h}, {x: bX, y: bY-h}, {x: bX+w, y: bY-h}, {x: fX+w, y: fY-h}
|
|
125
|
+
];
|
|
126
|
+
commands.push(shapes.drawPolyline(topPts, topColor));
|
|
127
|
+
commands.push(shapes.drawLine(fX+Math.floor(w/2), fY-h, bX+Math.floor(w/2), bY-h, topColor));
|
|
128
|
+
|
|
129
|
+
return commands;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
drawLightSource,
|
|
134
|
+
drawShadedSphere,
|
|
135
|
+
drawShadedBox
|
|
136
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const { registerTool, sendCommand } = require('../../core/server');
|
|
3
|
+
const lightLessons = require('./light_lessons');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Register all lighting & shading tools on the MCP server.
|
|
7
|
+
* @param {McpServer} server
|
|
8
|
+
*/
|
|
9
|
+
function register(server) {
|
|
10
|
+
registerTool(server, 'art_tree_lesson_light_zones',
|
|
11
|
+
'Lesson: Lighting - 6 Zones of Light and Shadow on a Sphere',
|
|
12
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32) },
|
|
13
|
+
async (p) => {
|
|
14
|
+
const commands = lightLessons.lessonLightZones(p.canvasSize);
|
|
15
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
16
|
+
return { content: [{ type: 'text', text: `✓ 6 Zones of Light lesson completed` }] };
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
registerTool(server, 'art_tree_lesson_light_dir',
|
|
20
|
+
'Lesson: Lighting - Light Direction and Plane values on a Box',
|
|
21
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32) },
|
|
22
|
+
async (p) => {
|
|
23
|
+
const commands = lightLessons.lessonLightDirection(p.canvasSize);
|
|
24
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
25
|
+
return { content: [{ type: 'text', text: `✓ Light Direction lesson completed` }] };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
registerTool(server, 'art_tree_lesson_light_contact',
|
|
29
|
+
'Lesson: Lighting - Contact Shadow (Darkest point of intersection)',
|
|
30
|
+
{ canvasSize: z.number().int().min(16).max(64).default(32) },
|
|
31
|
+
async (p) => {
|
|
32
|
+
const commands = lightLessons.lessonContactShadow(p.canvasSize);
|
|
33
|
+
await Promise.all(commands.map(cmd => sendCommand(cmd)));
|
|
34
|
+
return { content: [{ type: 'text', text: `✓ Contact Shadow lesson completed` }] };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
registerTool(server, 'art_tree_lesson_light_catalog',
|
|
38
|
+
'Get the catalog of all available Lighting lessons',
|
|
39
|
+
{},
|
|
40
|
+
() => {
|
|
41
|
+
const catalog = lightLessons.getLightLessonCatalog();
|
|
42
|
+
const text = catalog.map(l =>
|
|
43
|
+
` • ${l.id}: ${l.name} — ${l.description}`
|
|
44
|
+
).join('\n');
|
|
45
|
+
return { content: [{ type: 'text', text: `📚 Lighting Lessons:\n\n${text}` }] };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { register };
|