@pixel-normal-edit/mcp 2.0.6 → 2.0.8

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.
Files changed (56) hide show
  1. package/domains/art-tree/3d_lessons.js +114 -0
  2. package/domains/art-tree/3d_shapes.js +116 -0
  3. package/domains/art-tree/3d_tools.js +60 -0
  4. package/domains/art-tree/advanced_lessons.js +124 -0
  5. package/domains/art-tree/advanced_shapes.js +73 -0
  6. package/domains/art-tree/advanced_tools.js +69 -0
  7. package/domains/art-tree/analysis_lessons.js +32 -0
  8. package/domains/art-tree/analysis_shapes.js +146 -0
  9. package/domains/art-tree/analysis_tools.js +49 -0
  10. package/domains/art-tree/cross_section_lessons.js +65 -0
  11. package/domains/art-tree/cross_section_shapes.js +84 -0
  12. package/domains/art-tree/cross_section_tools.js +51 -0
  13. package/domains/art-tree/curve_lessons.js +138 -0
  14. package/domains/art-tree/curve_shapes.js +90 -0
  15. package/domains/art-tree/curve_tools.js +51 -0
  16. package/domains/art-tree/ellipse_lessons.js +105 -0
  17. package/domains/art-tree/ellipse_shapes.js +63 -0
  18. package/domains/art-tree/ellipse_tools.js +60 -0
  19. package/domains/art-tree/hidden_lessons.js +55 -0
  20. package/domains/art-tree/hidden_shapes.js +97 -0
  21. package/domains/art-tree/hidden_tools.js +51 -0
  22. package/domains/art-tree/index.js +32 -0
  23. package/domains/art-tree/layer_lessons.js +32 -0
  24. package/domains/art-tree/layer_shapes.js +117 -0
  25. package/domains/art-tree/layer_tools.js +43 -0
  26. package/domains/art-tree/lessons.js +119 -7
  27. package/domains/art-tree/light_lessons.js +92 -0
  28. package/domains/art-tree/light_shapes.js +136 -0
  29. package/domains/art-tree/light_tools.js +49 -0
  30. package/domains/art-tree/material_lessons.js +71 -0
  31. package/domains/art-tree/material_shapes.js +130 -0
  32. package/domains/art-tree/material_tools.js +49 -0
  33. package/domains/art-tree/perspective_lessons.js +138 -0
  34. package/domains/art-tree/perspective_shapes.js +96 -0
  35. package/domains/art-tree/perspective_tools.js +60 -0
  36. package/domains/art-tree/shapes.js +15 -0
  37. package/domains/art-tree/sky_lessons_1.js +82 -0
  38. package/domains/art-tree/sky_shapes_1.js +68 -0
  39. package/domains/art-tree/sky_tools.js +51 -0
  40. package/domains/art-tree/structure_lessons.js +102 -0
  41. package/domains/art-tree/structure_shapes.js +74 -0
  42. package/domains/art-tree/structure_tools.js +51 -0
  43. package/domains/art-tree/surface_lessons.js +112 -0
  44. package/domains/art-tree/surface_shapes.js +87 -0
  45. package/domains/art-tree/surface_tools.js +60 -0
  46. package/domains/art-tree/tools.js +69 -15
  47. package/domains/art-tree/transform_lessons_1.js +108 -0
  48. package/domains/art-tree/transform_lessons_2.js +102 -0
  49. package/domains/art-tree/transform_shapes.js +109 -0
  50. package/domains/art-tree/transform_tools.js +104 -0
  51. package/domains/art-tree/vocab_lessons.js +32 -0
  52. package/domains/art-tree/vocab_shapes.js +116 -0
  53. package/domains/art-tree/vocab_tools.js +41 -0
  54. package/package.json +1 -1
  55. package/tools/index.js +2 -0
  56. package/tools/layer.js +80 -0
@@ -165,19 +165,125 @@ function lessonComposition(canvasSize = 32) {
165
165
  ];
166
166
  }
167
167
 
168
+ /**
169
+ * Lesson 8: Freehand — Practice freehand drawing with simulated hand tremor.
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 — Practice maintaining proportions and angles.
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 — C-curves and S-curves.
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 — Drawing spiral curves.
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 — Long strokes, short strokes, thick and thin lines.
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 Lines — Drawing parallel and intersecting lines.
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}>}
171
271
  */
172
272
  function getLessonCatalog() {
173
273
  return [
174
- { id: 'lines', name: 'Đường thẳng', description: 'Vẽ đường thẳng ngang, dọc và chéo', fn: lessonLines },
175
- { id: 'squares', name: 'Hình vuông & Hình chữ nhật', description: 'Hiểu về góc vuông và tỷ lệ', fn: lessonSquares },
176
- { id: 'circles', name: 'Hình tròn', description: 'Làm quen với đường cong và đối xứng', fn: lessonCircles },
177
- { id: 'ellipses', name: 'Hình elip', description: 'Hiểu về hình bầu dục và tỷ lệ', fn: lessonEllipses },
178
- { id: 'triangles', name: 'Hình tam giác', description: 'Hình ba cạnh và sự ổn định', fn: lessonTriangles },
179
- { id: 'polygons', name: 'Đa giác', description: 'Hình nhiều cạnh', fn: lessonPolygons },
180
- { id: 'composition', name: 'Tổng hợp', description: 'Kết hợp các hình bản tạo vật thể đơn giản', fn: lessonComposition },
274
+ { id: 'lines', name: 'Lines', description: 'Draw horizontal, vertical and diagonal lines', fn: lessonLines },
275
+ { id: 'squares', name: 'Squares & Rectangles', description: 'Understand right angles and proportions', fn: lessonSquares },
276
+ { id: 'circles', name: 'Circles', description: 'Learn curves and symmetry', fn: lessonCircles },
277
+ { id: 'ellipses', name: 'Ellipses', description: 'Understand oval shapes and proportions', fn: lessonEllipses },
278
+ { id: 'triangles', name: 'Triangles', description: 'Three-point shapes and stability', fn: lessonTriangles },
279
+ { id: 'polygons', name: 'Polygons', description: 'Multi-sided shapes', fn: lessonPolygons },
280
+ { id: 'composition', name: 'Composition', description: 'Combine basic shapes to create simple objects', fn: lessonComposition },
281
+ { id: 'freehand', name: 'Freehand Drawing', description: 'Practice freehand drawing without rulers', fn: lessonFreehand },
282
+ { id: 'proportions', name: 'Proportions & Angles', description: 'Practice maintaining proportions and angles', fn: lessonProportionsAndAngles },
283
+ { id: 'curves', name: 'Curves', description: 'C-curves and S-curves', fn: lessonCurves },
284
+ { id: 'spiral', name: 'Spiral', description: 'Drawing spiral curves', fn: lessonSpiral },
285
+ { id: 'strokes', name: 'Stroke Types', description: 'Long strokes, short strokes, thick and thin lines', fn: lessonStrokes },
286
+ { id: 'parallel_intersecting', name: 'Parallel & Intersecting', description: 'Draw parallel and intersecting lines', 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 (Sphere)
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 (Light direction and plane values)
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 (Point of contact shadow)
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 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 (main shadow zone)
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 (mid-tone zone)
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 zone
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 (bright spot)
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 };
@@ -0,0 +1,71 @@
1
+ const materialShapes = require('./material_shapes');
2
+
3
+ /**
4
+ * Lesson: Shiny vs Matte (Metal vs Plastic/Rubber)
5
+ * Compares high contrast sharp highlights vs low contrast diffused highlights.
6
+ */
7
+ function lessonMaterialShiny(canvasSize = 32) {
8
+ const commands = [];
9
+ const cx1 = Math.floor(canvasSize * 0.3);
10
+ const cx2 = Math.floor(canvasSize * 0.7);
11
+ const cy = Math.floor(canvasSize / 2);
12
+ const r = Math.floor(canvasSize * 0.2);
13
+
14
+ // 1. Metal Sphere (Left)
15
+ commands.push(...materialShapes.drawMetalSphere(cx1, cy, r));
16
+
17
+ // 2. Matte/Plastic Sphere (Right)
18
+ commands.push(...materialShapes.drawMatteSphere(cx2, cy, r, '#e65100'));
19
+
20
+ return commands;
21
+ }
22
+
23
+ /**
24
+ * Lesson: Transparent (Glass)
25
+ * Demonstrates Fresnel effect, sharp reflections, and light transmission.
26
+ */
27
+ function lessonMaterialGlass(canvasSize = 32) {
28
+ const commands = [];
29
+ const cx = Math.floor(canvasSize / 2);
30
+ const cy = Math.floor(canvasSize / 2);
31
+ const r = Math.floor(canvasSize * 0.35);
32
+
33
+ commands.push(...materialShapes.drawGlassSphere(cx, cy, r));
34
+
35
+ return commands;
36
+ }
37
+
38
+ /**
39
+ * Lesson: Texture (Wood vs Stone)
40
+ * Compares organic lines vs rough cracks.
41
+ */
42
+ function lessonMaterialTexture(canvasSize = 32) {
43
+ const commands = [];
44
+ const cx1 = Math.floor(canvasSize * 0.3);
45
+ const cx2 = Math.floor(canvasSize * 0.7);
46
+ const cy = Math.floor(canvasSize / 2);
47
+ const r = Math.floor(canvasSize * 0.2);
48
+
49
+ // 1. Wood Sphere (Left)
50
+ commands.push(...materialShapes.drawTexturedSphere(cx1, cy, r, 'wood'));
51
+
52
+ // 2. Stone Sphere (Right)
53
+ commands.push(...materialShapes.drawTexturedSphere(cx2, cy, r, 'stone'));
54
+
55
+ return commands;
56
+ }
57
+
58
+ function getMaterialLessonCatalog() {
59
+ return [
60
+ { id: 'material_shiny', name: 'Metal vs Matte', description: 'Sharp highlights (Metal) vs Soft highlights (Plastic)', fn: lessonMaterialShiny },
61
+ { id: 'material_glass', name: 'Glass & Transparency', description: 'Fresnel effect and transmitted light', fn: lessonMaterialGlass },
62
+ { id: 'material_texture', name: 'Texture: Wood vs Stone', description: 'Grain lines vs cracks on a 3D form', fn: lessonMaterialTexture },
63
+ ];
64
+ }
65
+
66
+ module.exports = {
67
+ lessonMaterialShiny,
68
+ lessonMaterialGlass,
69
+ lessonMaterialTexture,
70
+ getMaterialLessonCatalog
71
+ };
@@ -0,0 +1,130 @@
1
+ const shapes = require('./shapes');
2
+
3
+ function drawMetalSphere(cx, cy, r) {
4
+ const commands = [];
5
+
6
+ // Base sphere (darkest shadow/environment reflection)
7
+ commands.push(shapes.drawCircle(cx, cy, r, '#263238', true));
8
+
9
+ // High contrast core shadow
10
+ const coreR = Math.floor(r * 0.9);
11
+ commands.push(shapes.drawCircle(cx, cy + Math.floor(r*0.1), coreR, '#1a1a1a', true));
12
+
13
+ // Environment reflection (Ground bounce)
14
+ const envR = Math.floor(r * 0.7);
15
+ commands.push(shapes.drawCircle(cx, cy + Math.floor(r*0.3), envR, '#546e7a', true));
16
+
17
+ // Sharp light zone
18
+ const lightR = Math.floor(r * 0.6);
19
+ commands.push(shapes.drawCircle(cx - Math.floor(r*0.2), cy - Math.floor(r*0.2), lightR, '#cfd8dc', true));
20
+
21
+ // VERY sharp highlight (Tiny and pure white)
22
+ const highR = Math.floor(r * 0.1);
23
+ commands.push(shapes.drawCircle(cx - Math.floor(r*0.4), cy - Math.floor(r*0.4), highR, '#ffffff', true));
24
+
25
+ return commands;
26
+ }
27
+
28
+ function drawMatteSphere(cx, cy, r, colorBase) {
29
+ const commands = [];
30
+
31
+ // Base sphere (shadow)
32
+ commands.push(shapes.drawCircle(cx, cy, r, '#795548', true)); // Dark brownish shadow
33
+
34
+ // Smooth gradation (Halftone)
35
+ const halfR = Math.floor(r * 0.85);
36
+ commands.push(shapes.drawCircle(cx - Math.floor(r*0.05), cy - Math.floor(r*0.05), halfR, colorBase, true));
37
+
38
+ // Light area
39
+ const lightR = Math.floor(r * 0.6);
40
+ commands.push(shapes.drawCircle(cx - Math.floor(r*0.15), cy - Math.floor(r*0.15), lightR, '#ffcc80', true)); // Lighter tone
41
+
42
+ // Soft highlight (Large and diffused)
43
+ const highR = Math.floor(r * 0.3);
44
+ commands.push(shapes.drawCircle(cx - Math.floor(r*0.25), cy - Math.floor(r*0.25), highR, '#ffe0b2', true));
45
+
46
+ return commands;
47
+ }
48
+
49
+ function drawGlassSphere(cx, cy, r) {
50
+ const commands = [];
51
+
52
+ // Fresnel effect: Edges are brighter/more reflective than the center
53
+ // Base (Outline & Rim light)
54
+ commands.push(shapes.drawCircle(cx, cy, r, '#80deea', true));
55
+
56
+ // Darker inner area (light passes through)
57
+ const innerR = Math.floor(r * 0.9);
58
+ commands.push(shapes.drawCircle(cx, cy, innerR, '#006064', true));
59
+
60
+ // Transmitted light hitting the back bottom
61
+ const transR = Math.floor(r * 0.7);
62
+ commands.push(shapes.drawCircle(cx + Math.floor(r*0.1), cy + Math.floor(r*0.2), transR, '#4dd0e1', true));
63
+
64
+ // Very center is darkest (least reflection)
65
+ const centerR = Math.floor(r * 0.5);
66
+ commands.push(shapes.drawCircle(cx, cy, centerR, '#00363a', true));
67
+
68
+ // Sharp Highlights (Primary and secondary reflections)
69
+ const highR1 = Math.floor(r * 0.15);
70
+ commands.push(shapes.drawCircle(cx - Math.floor(r*0.4), cy - Math.floor(r*0.4), highR1, '#ffffff', true));
71
+
72
+ const highR2 = Math.floor(r * 0.08);
73
+ commands.push(shapes.drawCircle(cx + Math.floor(r*0.3), cy + Math.floor(r*0.4), highR2, '#e0f7fa', true));
74
+
75
+ return commands;
76
+ }
77
+
78
+ function drawTexturedSphere(cx, cy, r, type) {
79
+ const commands = [];
80
+
81
+ // Base sphere
82
+ const baseColor = type === 'wood' ? '#8d6e63' : '#9e9e9e';
83
+ commands.push(shapes.drawCircle(cx, cy, r, baseColor, true));
84
+
85
+ // Shadow
86
+ const shadowColor = type === 'wood' ? '#4e342e' : '#616161';
87
+ const shadowR = Math.floor(r * 0.85);
88
+ commands.push(shapes.drawCircle(cx + Math.floor(r*0.1), cy + Math.floor(r*0.1), shadowR, shadowColor, true));
89
+
90
+ // Texture application
91
+ if (type === 'wood') {
92
+ // Wood grain lines curving around the sphere
93
+ const grainColor = '#3e2723';
94
+ for (let i = -2; i <= 2; i++) {
95
+ const rx = r * 0.8;
96
+ const ry = r * 0.3 * Math.abs(i) + r * 0.2;
97
+ commands.push(shapes.drawEllipse(cx, cy + (i * Math.floor(r*0.3)), Math.floor(rx), Math.floor(ry), grainColor, false));
98
+ }
99
+ } else if (type === 'stone') {
100
+ // Stone cracks and rough patches
101
+ const crackColor = '#212121';
102
+ // Simplified cracks using polylines
103
+ commands.push(shapes.drawPolyline([
104
+ {x: cx - Math.floor(r*0.5), y: cy - Math.floor(r*0.5)},
105
+ {x: cx - Math.floor(r*0.2), y: cy - Math.floor(r*0.1)},
106
+ {x: cx + Math.floor(r*0.3), y: cy - Math.floor(r*0.2)},
107
+ {x: cx + Math.floor(r*0.6), y: cy + Math.floor(r*0.1)}
108
+ ], crackColor));
109
+
110
+ commands.push(shapes.drawPolyline([
111
+ {x: cx - Math.floor(r*0.2), y: cy - Math.floor(r*0.1)},
112
+ {x: cx, y: cy + Math.floor(r*0.4)},
113
+ {x: cx - Math.floor(r*0.4), y: cy + Math.floor(r*0.7)}
114
+ ], crackColor));
115
+ }
116
+
117
+ // Soft Highlight over texture
118
+ const highColor = type === 'wood' ? '#d7ccc8' : '#e0e0e0';
119
+ const highR = Math.floor(r * 0.4);
120
+ commands.push(shapes.drawCircle(cx - Math.floor(r*0.3), cy - Math.floor(r*0.3), highR, highColor, true));
121
+
122
+ return commands;
123
+ }
124
+
125
+ module.exports = {
126
+ drawMetalSphere,
127
+ drawMatteSphere,
128
+ drawGlassSphere,
129
+ drawTexturedSphere
130
+ };
@@ -0,0 +1,49 @@
1
+ const { z } = require('zod');
2
+ const { registerTool, sendCommand } = require('../../core/server');
3
+ const materialLessons = require('./material_lessons');
4
+
5
+ /**
6
+ * Register all material & surface rendering tools on the MCP server.
7
+ * @param {McpServer} server
8
+ */
9
+ function register(server) {
10
+ registerTool(server, 'art_tree_lesson_material_shiny',
11
+ 'Lesson: Materials - Metal vs Matte (High contrast vs diffused highlights)',
12
+ { canvasSize: z.number().int().min(16).max(64).default(32) },
13
+ async (p) => {
14
+ const commands = materialLessons.lessonMaterialShiny(p.canvasSize);
15
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
16
+ return { content: [{ type: 'text', text: `✓ Metal vs Matte material lesson completed` }] };
17
+ });
18
+
19
+ registerTool(server, 'art_tree_lesson_material_glass',
20
+ 'Lesson: Materials - Glass (Transparency, Fresnel effect)',
21
+ { canvasSize: z.number().int().min(16).max(64).default(32) },
22
+ async (p) => {
23
+ const commands = materialLessons.lessonMaterialGlass(p.canvasSize);
24
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
25
+ return { content: [{ type: 'text', text: `✓ Glass material lesson completed` }] };
26
+ });
27
+
28
+ registerTool(server, 'art_tree_lesson_material_texture',
29
+ 'Lesson: Materials - Texture (Wood grain vs Stone cracks)',
30
+ { canvasSize: z.number().int().min(16).max(64).default(32) },
31
+ async (p) => {
32
+ const commands = materialLessons.lessonMaterialTexture(p.canvasSize);
33
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
34
+ return { content: [{ type: 'text', text: `✓ Wood vs Stone texture lesson completed` }] };
35
+ });
36
+
37
+ registerTool(server, 'art_tree_lesson_material_catalog',
38
+ 'Get the catalog of all available Material lessons',
39
+ {},
40
+ () => {
41
+ const catalog = materialLessons.getMaterialLessonCatalog();
42
+ const text = catalog.map(l =>
43
+ ` • ${l.id}: ${l.name} — ${l.description}`
44
+ ).join('\n');
45
+ return { content: [{ type: 'text', text: `📚 Material Lessons:\n\n${text}` }] };
46
+ });
47
+ }
48
+
49
+ module.exports = { register };