@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.
Files changed (40) 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/cross_section_lessons.js +65 -0
  8. package/domains/art-tree/cross_section_shapes.js +84 -0
  9. package/domains/art-tree/cross_section_tools.js +51 -0
  10. package/domains/art-tree/curve_lessons.js +138 -0
  11. package/domains/art-tree/curve_shapes.js +90 -0
  12. package/domains/art-tree/curve_tools.js +51 -0
  13. package/domains/art-tree/ellipse_lessons.js +105 -0
  14. package/domains/art-tree/ellipse_shapes.js +63 -0
  15. package/domains/art-tree/ellipse_tools.js +60 -0
  16. package/domains/art-tree/hidden_lessons.js +55 -0
  17. package/domains/art-tree/hidden_shapes.js +97 -0
  18. package/domains/art-tree/hidden_tools.js +51 -0
  19. package/domains/art-tree/index.js +22 -0
  20. package/domains/art-tree/lessons.js +112 -0
  21. package/domains/art-tree/light_lessons.js +92 -0
  22. package/domains/art-tree/light_shapes.js +136 -0
  23. package/domains/art-tree/light_tools.js +49 -0
  24. package/domains/art-tree/perspective_lessons.js +138 -0
  25. package/domains/art-tree/perspective_shapes.js +96 -0
  26. package/domains/art-tree/perspective_tools.js +60 -0
  27. package/domains/art-tree/shapes.js +15 -0
  28. package/domains/art-tree/structure_lessons.js +102 -0
  29. package/domains/art-tree/structure_shapes.js +74 -0
  30. package/domains/art-tree/structure_tools.js +51 -0
  31. package/domains/art-tree/surface_lessons.js +112 -0
  32. package/domains/art-tree/surface_shapes.js +87 -0
  33. package/domains/art-tree/surface_tools.js +60 -0
  34. package/domains/art-tree/tools.js +54 -0
  35. package/domains/art-tree/transform_lessons_1.js +108 -0
  36. package/domains/art-tree/transform_lessons_2.js +102 -0
  37. package/domains/art-tree/transform_shapes.js +109 -0
  38. package/domains/art-tree/transform_tools.js +104 -0
  39. package/package.json +1 -1
  40. package/tools/workspace.js +14 -4
@@ -0,0 +1,87 @@
1
+ const shapes = require('./shapes');
2
+ const shapes3d = require('./3d_shapes');
3
+
4
+ function drawCupAnalysis(cx, cy, r, h) {
5
+ const commands = [];
6
+ const topY = cy - Math.floor(h/2);
7
+ const botY = cy + Math.floor(h/2);
8
+ const topRy = Math.floor(r * 0.3);
9
+ const botRy = Math.floor(r * 0.4);
10
+
11
+ // Outer Wall (Convex) - Blue
12
+ commands.push(shapes.drawLine(cx - r, topY, cx - Math.floor(r*0.8), botY, '#2196f3'));
13
+ commands.push(shapes.drawLine(cx + r, topY, cx + Math.floor(r*0.8), botY, '#2196f3'));
14
+ // Base (Flat/Curved)
15
+ commands.push(shapes.drawEllipse(cx, botY, Math.floor(r*0.8), botRy, '#4caf50', false));
16
+
17
+ // Inner Wall (Concave) - Purple
18
+ // Draw the back half of the inner ellipse (the visible inside of the cup)
19
+ const innerR = Math.floor(r * 0.9);
20
+ const innerRy = Math.floor(topRy * 0.9);
21
+ // Just draw the full inner ellipse but highlighted differently
22
+ commands.push(shapes.drawEllipse(cx, topY, innerR, innerRy, '#9c27b0', false));
23
+
24
+ // Lip / Rim (Transitional edge) - Orange
25
+ // We represent the lip as the gap between the inner and outer ellipses at the top
26
+ commands.push(shapes.drawEllipse(cx, topY, r, topRy, '#ff9800', false));
27
+
28
+ // Handle (Tube surface) - Red
29
+ // Draw a curved handle on the right side
30
+ const hx = cx + r;
31
+ const hy = cy;
32
+ const hr = Math.floor(h * 0.3);
33
+ // Two nested semi-circles (arcs) for the handle thickness
34
+ const handleOuter = [];
35
+ const handleInner = [];
36
+ for(let i=0; i<=18; i++) {
37
+ const angle = (i/18) * Math.PI - Math.PI/2; // -90 to 90 degrees
38
+ handleOuter.push({ x: Math.round(hx + hr * Math.cos(angle)), y: Math.round(hy + hr * Math.sin(angle)) });
39
+ handleInner.push({ x: Math.round(hx + (hr-5) * Math.cos(angle)), y: Math.round(hy + (hr-5) * Math.sin(angle)) });
40
+ }
41
+ commands.push(shapes.drawPolyline(handleOuter, '#f44336'));
42
+ commands.push(shapes.drawPolyline(handleInner, '#f44336'));
43
+
44
+ return commands;
45
+ }
46
+
47
+ function drawChairAnalysis(cx, cy, w) {
48
+ const commands = [];
49
+ const h = Math.floor(w * 1.5);
50
+ const d = Math.floor(w * 0.8);
51
+ const seatY = cy;
52
+ const dx = Math.floor(d * 0.7);
53
+ const dy = Math.floor(d * 0.5);
54
+
55
+ // Seat (Flat plane) - Green
56
+ const fX = cx - Math.floor(w/2);
57
+ const bX = fX + dx;
58
+ const bY = seatY - dy;
59
+ // Seat wireframe
60
+ commands.push(shapes.drawRectangle(fX, seatY, w, 10, '#4caf50', false)); // Front thickness
61
+ commands.push(shapes.drawLine(fX, seatY, bX, bY, '#4caf50'));
62
+ commands.push(shapes.drawLine(fX+w, seatY, bX+w, bY, '#4caf50'));
63
+ commands.push(shapes.drawLine(bX, bY, bX+w, bY, '#4caf50'));
64
+
65
+ // Backrest (Flat plane + Hard edges) - Blue
66
+ commands.push(shapes.drawRectangle(bX, bY - Math.floor(h*0.5), w, Math.floor(h*0.5), '#2196f3', false));
67
+ // Inner rungs (Hard edges)
68
+ commands.push(shapes.drawRectangle(bX + 10, bY - Math.floor(h*0.4), 5, Math.floor(h*0.4), '#ff9800', false));
69
+ commands.push(shapes.drawRectangle(bX + w - 15, bY - Math.floor(h*0.4), 5, Math.floor(h*0.4), '#ff9800', false));
70
+
71
+ // Legs (Cylindrical surfaces) - Red
72
+ const legH = Math.floor(h*0.4);
73
+ const legR = 3;
74
+ // Front-left
75
+ commands.push(shapes.drawLine(fX+legR, seatY+10, fX+legR, seatY+10+legH, '#f44336'));
76
+ commands.push(shapes.drawLine(fX+10-legR, seatY+10, fX+10-legR, seatY+10+legH, '#f44336'));
77
+ // Front-right
78
+ commands.push(shapes.drawLine(fX+w-10+legR, seatY+10, fX+w-10+legR, seatY+10+legH, '#f44336'));
79
+ commands.push(shapes.drawLine(fX+w-legR, seatY+10, fX+w-legR, seatY+10+legH, '#f44336'));
80
+
81
+ return commands;
82
+ }
83
+
84
+ module.exports = {
85
+ drawCupAnalysis,
86
+ drawChairAnalysis
87
+ };
@@ -0,0 +1,60 @@
1
+ const { z } = require('zod');
2
+ const { registerTool, sendCommand } = require('../../core/server');
3
+ const surfaceLessons = require('./surface_lessons');
4
+
5
+ const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/).describe('Hex color e.g. #ff0000');
6
+
7
+ /**
8
+ * Register all surface analysis tools on the MCP server.
9
+ * @param {McpServer} server
10
+ */
11
+ function register(server) {
12
+ registerTool(server, 'art_tree_lesson_surface_types',
13
+ 'Lesson: Surfaces - Analyze Flat, Convex, and Concave planes',
14
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#2196f3') },
15
+ async (p) => {
16
+ const commands = surfaceLessons.lessonSurfaceTypes(p.canvasSize, p.color);
17
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
18
+ return { content: [{ type: 'text', text: `✓ Surface Types lesson completed` }] };
19
+ });
20
+
21
+ registerTool(server, 'art_tree_lesson_edge_types',
22
+ 'Lesson: Surfaces - Analyze Hard vs Soft (Transitional) edges',
23
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#ff5722') },
24
+ async (p) => {
25
+ const commands = surfaceLessons.lessonEdgeTypes(p.canvasSize, p.color);
26
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
27
+ return { content: [{ type: 'text', text: `✓ Edge Types lesson completed` }] };
28
+ });
29
+
30
+ registerTool(server, 'art_tree_lesson_surface_cup',
31
+ 'Lesson: Surfaces - Cup Analysis (Real object breakdown)',
32
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#607d8b') },
33
+ async (p) => {
34
+ const commands = surfaceLessons.lessonCupAnalysis(p.canvasSize, p.color);
35
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
36
+ return { content: [{ type: 'text', text: `✓ Cup Surface Analysis lesson completed` }] };
37
+ });
38
+
39
+ registerTool(server, 'art_tree_lesson_surface_chair',
40
+ 'Lesson: Surfaces - Chair Analysis (Real object breakdown)',
41
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#795548') },
42
+ async (p) => {
43
+ const commands = surfaceLessons.lessonChairAnalysis(p.canvasSize, p.color);
44
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
45
+ return { content: [{ type: 'text', text: `✓ Chair Surface Analysis lesson completed` }] };
46
+ });
47
+
48
+ registerTool(server, 'art_tree_lesson_surface_catalog',
49
+ 'Get the catalog of all available Surface lessons',
50
+ {},
51
+ () => {
52
+ const catalog = surfaceLessons.getSurfaceLessonCatalog();
53
+ const text = catalog.map(l =>
54
+ ` • ${l.id}: ${l.name} — ${l.description}`
55
+ ).join('\n');
56
+ return { content: [{ type: 'text', text: `📚 Surface Lessons:\n\n${text}` }] };
57
+ });
58
+ }
59
+
60
+ module.exports = { register };
@@ -164,6 +164,60 @@ function register(server) {
164
164
  return { content: [{ type: 'text', text: `✓ Bài học tổng hợp hoàn thành (${commands.length} nét vẽ)` }] };
165
165
  });
166
166
 
167
+ registerTool(server, 'art_tree_lesson_freehand',
168
+ 'Lesson: Vẽ bằng tay — Luyện vẽ bằng tay, hạn chế dùng thước',
169
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#795548') },
170
+ async (p) => {
171
+ const commands = lessons.lessonFreehand(p.canvasSize, p.color);
172
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
173
+ return { content: [{ type: 'text', text: `✓ Bài học vẽ tay hoàn thành (${commands.length} đoạn thẳng nhỏ)` }] };
174
+ });
175
+
176
+ registerTool(server, 'art_tree_lesson_proportions',
177
+ 'Lesson: Tỉ lệ và góc — Tập giữ tỉ lệ và góc (Proportions and Angles)',
178
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#3f51b5') },
179
+ async (p) => {
180
+ const commands = lessons.lessonProportionsAndAngles(p.canvasSize, p.color);
181
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
182
+ return { content: [{ type: 'text', text: `✓ Bài học tỉ lệ và góc hoàn thành` }] };
183
+ });
184
+
185
+ registerTool(server, 'art_tree_lesson_curves',
186
+ 'Lesson: Đường cong — Đường cong C, S',
187
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#9c27b0') },
188
+ async (p) => {
189
+ const commands = lessons.lessonCurves(p.canvasSize, p.color);
190
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
191
+ return { content: [{ type: 'text', text: `✓ Bài học đường cong C, S hoàn thành (${commands.length} nét)` }] };
192
+ });
193
+
194
+ registerTool(server, 'art_tree_lesson_spiral',
195
+ 'Lesson: Xoắn ốc — Vẽ đường xoắn ốc',
196
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#ff9800') },
197
+ async (p) => {
198
+ const commands = lessons.lessonSpiral(p.canvasSize, p.color);
199
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
200
+ return { content: [{ type: 'text', text: `✓ Bài học xoắn ốc hoàn thành (${commands.length} nét)` }] };
201
+ });
202
+
203
+ registerTool(server, 'art_tree_lesson_strokes',
204
+ 'Lesson: Các loại nét — Nét dài, nét ngắn, nét đậm–nhạt',
205
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#2196f3') },
206
+ async (p) => {
207
+ const commands = lessons.lessonStrokes(p.canvasSize, p.color);
208
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
209
+ return { content: [{ type: 'text', text: `✓ Bài học nét vẽ hoàn thành` }] };
210
+ });
211
+
212
+ registerTool(server, 'art_tree_lesson_parallel_intersecting',
213
+ 'Lesson: Song song & giao nhau — Vẽ các đường song song và đường giao nhau',
214
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#00bcd4') },
215
+ async (p) => {
216
+ const commands = lessons.lessonParallelAndIntersecting(p.canvasSize, p.color);
217
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
218
+ return { content: [{ type: 'text', text: `✓ Bài học song song và giao nhau hoàn thành` }] };
219
+ });
220
+
167
221
  registerTool(server, 'art_tree_lesson_catalog',
168
222
  'Get the catalog of all available Art Tree lessons with descriptions',
169
223
  {},
@@ -0,0 +1,108 @@
1
+ const shapes = require('./shapes');
2
+ const transformShapes = require('./transform_shapes');
3
+ const csShapes = require('./cross_section_shapes');
4
+
5
+ /**
6
+ * Lesson: Stretch (Elongate)
7
+ * Sphere -> Egg, Box -> Bar
8
+ */
9
+ function lessonStretch(canvasSize = 32, color = '#2196f3') {
10
+ const commands = [];
11
+ const qX1 = Math.floor(canvasSize * 0.25);
12
+ const qX2 = Math.floor(canvasSize * 0.75);
13
+ const cy = Math.floor(canvasSize / 2);
14
+ const r = Math.floor(canvasSize * 0.15);
15
+
16
+ // 1. Stretch: Sphere -> Egg
17
+ commands.push(shapes.drawCircle(qX1, cy, r, '#bdbdbd', false)); // Original sphere (ghost)
18
+ commands.push(...transformShapes.drawEgg(qX1, cy, r, color)); // Stretched into egg
19
+
20
+ // 2. Stretch: Box -> Bar (Tall)
21
+ const w = Math.floor(canvasSize * 0.15);
22
+ // Original box (ghost)
23
+ commands.push(shapes.drawRectangle(qX2 - w/2, cy - w/2, w, w, '#bdbdbd', false));
24
+ // Stretched bar
25
+ commands.push(shapes.drawRectangle(qX2 - w/2, cy - w * 1.5, w, w * 3, '#9c27b0', false));
26
+
27
+ return commands;
28
+ }
29
+
30
+ /**
31
+ * Lesson: Squash (Compress)
32
+ * Sphere -> Disk, Box -> Plate
33
+ */
34
+ function lessonSquash(canvasSize = 32, color = '#ff9800') {
35
+ const commands = [];
36
+ const qX1 = Math.floor(canvasSize * 0.25);
37
+ const qX2 = Math.floor(canvasSize * 0.75);
38
+ const cy = Math.floor(canvasSize / 2);
39
+ const r = Math.floor(canvasSize * 0.15);
40
+
41
+ // 1. Squash: Sphere -> Disk
42
+ commands.push(shapes.drawCircle(qX1, cy, r, '#bdbdbd', false)); // Original
43
+ // Squashed into a flat ellipse/disk
44
+ commands.push(shapes.drawEllipse(qX1, cy, r * 1.2, Math.floor(r * 0.3), color, false));
45
+
46
+ // 2. Squash: Box -> Plate
47
+ const w = Math.floor(canvasSize * 0.2);
48
+ commands.push(shapes.drawRectangle(qX2 - w/2, cy - w/2, w, w, '#bdbdbd', false)); // Original
49
+ // Squashed into plate
50
+ commands.push(shapes.drawRectangle(qX2 - w, cy - Math.floor(w*0.2), w * 2, Math.floor(w*0.4), '#4caf50', false));
51
+
52
+ return commands;
53
+ }
54
+
55
+ /**
56
+ * Lesson: Taper & Swell (Thu nhỏ hoặc phình to)
57
+ * Cylinder -> Bottle
58
+ */
59
+ function lessonTaperSwell(canvasSize = 32, color = '#00bcd4') {
60
+ const commands = [];
61
+ const cx = Math.floor(canvasSize / 2);
62
+ const totalH = Math.floor(canvasSize * 0.8);
63
+ const topY = Math.floor(canvasSize * 0.1);
64
+ const bottomY = topY + totalH;
65
+ const r = Math.floor(canvasSize * 0.25);
66
+
67
+ // Original Cylinder (ghost)
68
+ commands.push(shapes.drawLine(cx - r, topY, cx - r, bottomY, '#bdbdbd'));
69
+ commands.push(shapes.drawLine(cx + r, topY, cx + r, bottomY, '#bdbdbd'));
70
+
71
+ // Tapered and Swelled form (Bottle)
72
+ const sections = [
73
+ { y: topY, r: Math.floor(r * 0.3) }, // Neck (Tapered)
74
+ { y: topY + Math.floor(totalH * 0.4), r: r },// Body (Swelled back to original radius)
75
+ { y: bottomY, r: Math.floor(r * 0.8) } // Base (Slightly tapered)
76
+ ];
77
+ commands.push(...csShapes.drawLoftedForm(cx, topY, bottomY, sections, color, true));
78
+
79
+ return commands;
80
+ }
81
+
82
+ /**
83
+ * Lesson: Bend (Uốn cong)
84
+ * Cylinder -> Bent Tube
85
+ */
86
+ function lessonBend(canvasSize = 32, color = '#e91e63') {
87
+ const commands = [];
88
+ const cx = Math.floor(canvasSize / 2);
89
+ const cy = Math.floor(canvasSize / 2);
90
+ const r = Math.floor(canvasSize * 0.1);
91
+ const length = Math.floor(canvasSize * 0.35);
92
+
93
+ // Original straight tube (ghost, pointing right)
94
+ commands.push(shapes.drawLine(cx, cy - r, cx + length * 2, cy - r, '#bdbdbd'));
95
+ commands.push(shapes.drawLine(cx, cy + r, cx + length * 2, cy + r, '#bdbdbd'));
96
+
97
+ // Bent tube
98
+ commands.push(...transformShapes.drawBentTube(cx, cy + length, r, length, color));
99
+
100
+ return commands;
101
+ }
102
+
103
+ module.exports = {
104
+ lessonStretch,
105
+ lessonSquash,
106
+ lessonTaperSwell,
107
+ lessonBend
108
+ };
@@ -0,0 +1,102 @@
1
+ const shapes = require('./shapes');
2
+ const transformShapes = require('./transform_shapes');
3
+ const structureLessons = require('./structure_lessons');
4
+
5
+ /**
6
+ * Lesson: Rotate (Xoay)
7
+ * Analyzing how objects change when rotated around X, Y, Z axes.
8
+ * We can reuse the Axis Orientation lesson as it perfectly demonstrates rotation.
9
+ */
10
+ function lessonRotate(canvasSize = 32, color = '#ff5722') {
11
+ // Reusing the axis orientation lesson which draws cylinders rotated along X, Y, Z
12
+ return structureLessons.lessonAxisOrientation(canvasSize, color);
13
+ }
14
+
15
+ /**
16
+ * Lesson: Cut (Cắt khối)
17
+ * Sphere -> Truncated Sphere (exposing internal cross-section)
18
+ */
19
+ function lessonCut(canvasSize = 32, color = '#8bc34a') {
20
+ const commands = [];
21
+ const cx = Math.floor(canvasSize / 2);
22
+ const cy = Math.floor(canvasSize / 2);
23
+ const r = Math.floor(canvasSize * 0.35);
24
+
25
+ // Original sphere (ghost)
26
+ commands.push(shapes.drawCircle(cx, cy, r, '#bdbdbd', false));
27
+
28
+ // Cut sphere
29
+ commands.push(...transformShapes.drawTruncatedSphere(cx, cy, r, color));
30
+
31
+ return commands;
32
+ }
33
+
34
+ /**
35
+ * Lesson: Hollow (Khoét rỗng)
36
+ * Cylinder -> Pipe/Hole
37
+ */
38
+ function lessonHollow(canvasSize = 32, color = '#3f51b5') {
39
+ const commands = [];
40
+ const cx = Math.floor(canvasSize / 2);
41
+ const cy = Math.floor(canvasSize / 2);
42
+ const outerR = Math.floor(canvasSize * 0.35);
43
+ const innerR = Math.floor(canvasSize * 0.25);
44
+ const h = Math.floor(canvasSize * 0.6);
45
+
46
+ // Hollow cylinder
47
+ commands.push(...transformShapes.drawHollowCylinder(cx, cy, outerR, innerR, h, color));
48
+
49
+ return commands;
50
+ }
51
+
52
+ /**
53
+ * Lesson: Combine (Ghép khối)
54
+ * Box + Cylinder (e.g., a wheel on an axle, or a tower on a base)
55
+ */
56
+ function lessonCombine(canvasSize = 32, color = '#607d8b') {
57
+ const commands = [];
58
+ const cx = Math.floor(canvasSize / 2);
59
+ const cy = Math.floor(canvasSize / 2);
60
+
61
+ // 1. Base Box
62
+ const w = Math.floor(canvasSize * 0.5);
63
+ const h = Math.floor(canvasSize * 0.2);
64
+ const d = Math.floor(canvasSize * 0.3);
65
+ const dx = Math.floor(d * 0.7);
66
+ const dy = Math.floor(d * 0.5);
67
+
68
+ const fX = cx - Math.floor(w/2);
69
+ const fY = cy + Math.floor(canvasSize * 0.2);
70
+
71
+ // Front face
72
+ commands.push(shapes.drawRectangle(fX, fY, w, h, color, false));
73
+ // Top face (oblique)
74
+ commands.push(shapes.drawLine(fX, fY, fX + dx, fY - dy, color));
75
+ commands.push(shapes.drawLine(fX + w, fY, fX + w + dx, fY - dy, color));
76
+ commands.push(shapes.drawLine(fX + dx, fY - dy, fX + w + dx, fY - dy, color));
77
+
78
+ // 2. Intersecting Cylinder (Tower on top of box)
79
+ const cylR = Math.floor(canvasSize * 0.15);
80
+ const cylH = Math.floor(canvasSize * 0.4);
81
+ const cylX = cx + Math.floor(dx/2);
82
+ const cylY = fY - Math.floor(dy/2); // Center of the top face
83
+
84
+ // Draw cylinder standing on the box
85
+ const topY = cylY - cylH;
86
+ // Top ellipse
87
+ commands.push(shapes.drawEllipse(cylX, topY, cylR, Math.floor(cylR * 0.3), '#ff9800', false));
88
+ // Base ellipse (intersecting the box top face)
89
+ commands.push(shapes.drawEllipse(cylX, cylY, cylR, Math.floor(cylR * 0.3), '#ff9800', false));
90
+ // Vertical edges
91
+ commands.push(shapes.drawLine(cylX - cylR, topY, cylX - cylR, cylY, '#ff9800'));
92
+ commands.push(shapes.drawLine(cylX + cylR, topY, cylX + cylR, cylY, '#ff9800'));
93
+
94
+ return commands;
95
+ }
96
+
97
+ module.exports = {
98
+ lessonRotate,
99
+ lessonCut,
100
+ lessonHollow,
101
+ lessonCombine
102
+ };
@@ -0,0 +1,109 @@
1
+ const shapes = require('./shapes');
2
+ const ellipseShapes = require('./ellipse_shapes');
3
+
4
+ function drawEgg(cx, cy, r, color) {
5
+ const commands = [];
6
+ // Approximating an egg by drawing two halves (top stretched, bottom semi-circle)
7
+ // We'll use multiple ellipses overlapping, or a polygon approximation
8
+ // Let's use a simpler polygon approximation for the egg contour
9
+ const points = [];
10
+ for (let i = 0; i <= 36; i++) {
11
+ const t = (i / 36) * 2 * Math.PI;
12
+ const px = r * Math.cos(t);
13
+ // Stretch the top half
14
+ const stretch = Math.sin(t) < 0 ? 1.5 : 1.0;
15
+ const py = r * Math.sin(t) * stretch;
16
+ points.push({ x: Math.round(cx + px), y: Math.round(cy + py) });
17
+ }
18
+ commands.push(shapes.drawPolyline(points, color));
19
+
20
+ // Add cross contour to show 3D volume
21
+ commands.push(shapes.drawEllipse(cx, cy + Math.floor(r * 0.2), r, Math.floor(r * 0.3), '#03a9f4', false));
22
+ return commands;
23
+ }
24
+
25
+ function drawBentTube(cx, cy, r, length, color) {
26
+ const commands = [];
27
+ // Draw a curved tube using a semicircular path
28
+ const pathR = length; // radius of the bend path
29
+ // We place ellipses along the arc
30
+ for (let i = 0; i <= 6; i++) {
31
+ const angle = (i / 6) * Math.PI; // 0 to 180 degrees
32
+ const ex = Math.round(cx + pathR * Math.cos(angle));
33
+ const ey = Math.round(cy - pathR * Math.sin(angle));
34
+ // Rotate the ellipse to be perpendicular to the path
35
+ commands.push(...ellipseShapes.drawRotatedEllipse(ex, ey, Math.floor(r * 0.3), r, angle, color));
36
+ }
37
+ // Connect inner and outer edges (approximate arcs)
38
+ const innerPts = [], outerPts = [];
39
+ for (let i = 0; i <= 18; i++) {
40
+ const a = (i / 18) * Math.PI;
41
+ innerPts.push({ x: Math.round(cx + (pathR - r) * Math.cos(a)), y: Math.round(cy - (pathR - r) * Math.sin(a)) });
42
+ outerPts.push({ x: Math.round(cx + (pathR + r) * Math.cos(a)), y: Math.round(cy - (pathR + r) * Math.sin(a)) });
43
+ }
44
+ commands.push(shapes.drawPolyline(innerPts, color));
45
+ commands.push(shapes.drawPolyline(outerPts, color));
46
+
47
+ return commands;
48
+ }
49
+
50
+ function drawTruncatedSphere(cx, cy, r, color) {
51
+ const commands = [];
52
+ // Sphere outline (partial)
53
+ const points = [];
54
+ const cutAngle = Math.PI / 4; // Top section cut off
55
+ for (let i = 0; i <= 36; i++) {
56
+ const t = (i / 36) * 2 * Math.PI;
57
+ // Skip the top part
58
+ if (t > Math.PI + cutAngle && t < 2 * Math.PI - cutAngle) continue;
59
+ points.push({ x: Math.round(cx + r * Math.cos(t)), y: Math.round(cy + r * Math.sin(t)) });
60
+ }
61
+ commands.push(shapes.drawPolyline(points, color));
62
+
63
+ // Cut surface (ellipse)
64
+ const cutY = Math.round(cy + r * Math.sin(Math.PI + cutAngle));
65
+ const cutRx = Math.round(r * Math.cos(cutAngle));
66
+ const cutRy = Math.floor(cutRx * 0.3);
67
+ commands.push(shapes.drawEllipse(cx, cutY, cutRx, cutRy, '#ff5722', false)); // Highlight cut surface
68
+
69
+ // Cross contour to reinforce sphere shape
70
+ commands.push(shapes.drawEllipse(cx, cy + Math.floor(r*0.2), r, Math.floor(r*0.3), '#03a9f4', false));
71
+
72
+ return commands;
73
+ }
74
+
75
+ function drawHollowCylinder(cx, cy, outerR, innerR, h, color) {
76
+ const commands = [];
77
+ const topY = cy - Math.floor(h/2);
78
+ const botY = cy + Math.floor(h/2);
79
+
80
+ // Outer bottom
81
+ commands.push(shapes.drawEllipse(cx, botY, outerR, Math.floor(outerR * 0.3), color, false));
82
+
83
+ // Outer top
84
+ commands.push(shapes.drawEllipse(cx, topY, outerR, Math.floor(outerR * 0.3), color, false));
85
+ // Inner top (the hole)
86
+ commands.push(shapes.drawEllipse(cx, topY, innerR, Math.floor(innerR * 0.3), '#e91e63', false));
87
+
88
+ // Inner bottom (visible through hole - rough approximation)
89
+ // We just draw the back curve of the inner bottom ellipse
90
+ // For simplicity, we draw the full ellipse but lighter
91
+ commands.push(shapes.drawEllipse(cx, botY, innerR, Math.floor(innerR * 0.3), '#bdbdbd', false));
92
+
93
+ // Vertical edges
94
+ commands.push(shapes.drawLine(cx - outerR, topY, cx - outerR, botY, color));
95
+ commands.push(shapes.drawLine(cx + outerR, topY, cx + outerR, botY, color));
96
+
97
+ // Inner edges (depth)
98
+ commands.push(shapes.drawLine(cx - innerR, topY, cx - innerR, botY, '#9e9e9e'));
99
+ commands.push(shapes.drawLine(cx + innerR, topY, cx + innerR, botY, '#9e9e9e'));
100
+
101
+ return commands;
102
+ }
103
+
104
+ module.exports = {
105
+ drawEgg,
106
+ drawBentTube,
107
+ drawTruncatedSphere,
108
+ drawHollowCylinder
109
+ };
@@ -0,0 +1,104 @@
1
+ const { z } = require('zod');
2
+ const { registerTool, sendCommand } = require('../../core/server');
3
+ const lessons1 = require('./transform_lessons_1');
4
+ const lessons2 = require('./transform_lessons_2');
5
+
6
+ const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/).describe('Hex color e.g. #ff0000');
7
+
8
+ /**
9
+ * Register all shape transformation tools on the MCP server.
10
+ * @param {McpServer} server
11
+ */
12
+ function register(server) {
13
+ // --- Scale & Deform ---
14
+ registerTool(server, 'art_tree_lesson_transform_stretch',
15
+ 'Lesson: Transform - Stretch (Elongate a sphere or box)',
16
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#2196f3') },
17
+ async (p) => {
18
+ const commands = lessons1.lessonStretch(p.canvasSize, p.color);
19
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
20
+ return { content: [{ type: 'text', text: `✓ Stretch transformation lesson completed` }] };
21
+ });
22
+
23
+ registerTool(server, 'art_tree_lesson_transform_squash',
24
+ 'Lesson: Transform - Squash (Compress a sphere or box)',
25
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#ff9800') },
26
+ async (p) => {
27
+ const commands = lessons1.lessonSquash(p.canvasSize, p.color);
28
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
29
+ return { content: [{ type: 'text', text: `✓ Squash transformation lesson completed` }] };
30
+ });
31
+
32
+ registerTool(server, 'art_tree_lesson_transform_taper',
33
+ 'Lesson: Transform - Taper/Swell (Cylinder to Bottle)',
34
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#00bcd4') },
35
+ async (p) => {
36
+ const commands = lessons1.lessonTaperSwell(p.canvasSize, p.color);
37
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
38
+ return { content: [{ type: 'text', text: `✓ Taper/Swell transformation lesson completed` }] };
39
+ });
40
+
41
+ registerTool(server, 'art_tree_lesson_transform_bend',
42
+ 'Lesson: Transform - Bend (Cylinder to curved tube)',
43
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#e91e63') },
44
+ async (p) => {
45
+ const commands = lessons1.lessonBend(p.canvasSize, p.color);
46
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
47
+ return { content: [{ type: 'text', text: `✓ Bend transformation lesson completed` }] };
48
+ });
49
+
50
+ // --- Boolean & Orientation ---
51
+ registerTool(server, 'art_tree_lesson_transform_rotate',
52
+ 'Lesson: Transform - Rotate (Analyze rotation around X, Y, Z)',
53
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#ff5722') },
54
+ async (p) => {
55
+ const commands = lessons2.lessonRotate(p.canvasSize, p.color);
56
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
57
+ return { content: [{ type: 'text', text: `✓ Rotate transformation lesson completed` }] };
58
+ });
59
+
60
+ registerTool(server, 'art_tree_lesson_transform_cut',
61
+ 'Lesson: Transform - Cut (Truncate a sphere)',
62
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#8bc34a') },
63
+ async (p) => {
64
+ const commands = lessons2.lessonCut(p.canvasSize, p.color);
65
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
66
+ return { content: [{ type: 'text', text: `✓ Cut transformation lesson completed` }] };
67
+ });
68
+
69
+ registerTool(server, 'art_tree_lesson_transform_hollow',
70
+ 'Lesson: Transform - Hollow (Hollow out a cylinder)',
71
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#3f51b5') },
72
+ async (p) => {
73
+ const commands = lessons2.lessonHollow(p.canvasSize, p.color);
74
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
75
+ return { content: [{ type: 'text', text: `✓ Hollow transformation lesson completed` }] };
76
+ });
77
+
78
+ registerTool(server, 'art_tree_lesson_transform_combine',
79
+ 'Lesson: Transform - Combine (Intersect a box and a cylinder)',
80
+ { canvasSize: z.number().int().min(16).max(64).default(32), color: hexColor.default('#607d8b') },
81
+ async (p) => {
82
+ const commands = lessons2.lessonCombine(p.canvasSize, p.color);
83
+ await Promise.all(commands.map(cmd => sendCommand(cmd)));
84
+ return { content: [{ type: 'text', text: `✓ Combine transformation lesson completed` }] };
85
+ });
86
+
87
+ registerTool(server, 'art_tree_lesson_transform_catalog',
88
+ 'Get the catalog of all available Transformation lessons',
89
+ {},
90
+ () => {
91
+ const text = `📚 Transformation Lessons:
92
+ • transform_stretch: Stretch/Elongate (Cầu -> Trứng)
93
+ • transform_squash: Squash/Compress (Cầu -> Đĩa dẹt)
94
+ • transform_taper: Taper/Swell (Trụ -> Chai)
95
+ • transform_bend: Bend (Trụ -> Ống cong)
96
+ • transform_rotate: Rotate (Xoay 3D)
97
+ • transform_cut: Cut/Truncate (Cắt khối)
98
+ • transform_hollow: Hollow out (Khoét rỗng)
99
+ • transform_combine: Combine (Ghép khối)`;
100
+ return { content: [{ type: 'text', text }] };
101
+ });
102
+ }
103
+
104
+ module.exports = { register };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixel-normal-edit/mcp",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "MCP server for Pixel Normal Edit canvas - enables AI agents to draw pixel art",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -19,14 +19,24 @@ function register(server) {
19
19
  {},
20
20
  () => ({ action: 'getActiveTabId' }));
21
21
 
22
- registerTool(server, 'workspace_create_tab',
22
+ const { sendCommand } = require('../core/server');
23
+ server.tool('workspace_create_tab',
23
24
  'Create a new blank canvas tab',
24
25
  {
25
26
  name: z.string().optional().describe('Tab name'),
26
- width: z.number().int().default(32).describe('Canvas width'),
27
- height: z.number().int().default(32).describe('Canvas height')
27
+ width: z.number().int().optional().describe('Canvas width'),
28
+ height: z.number().int().optional().describe('Canvas height')
28
29
  },
29
- (p) => ({ action: 'createTab', ...p }));
30
+ async (p) => {
31
+ if (p.width === undefined || p.height === undefined) {
32
+ return {
33
+ isError: true,
34
+ content: [{ type: 'text', text: 'Please provide the width and height dimensions (resize) when creating a new canvas.' }]
35
+ };
36
+ }
37
+ return sendCommand({ action: 'createTab', ...p });
38
+ }
39
+ );
30
40
 
31
41
  registerTool(server, 'workspace_switch_tab',
32
42
  'Switch to a different tab by ID (get IDs from workspace_list_tabs)',