@pixel-normal-edit/mcp 2.0.3 → 2.0.5
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/README.md +14 -3
- package/core/command-bus.js +49 -0
- package/core/firebase.js +26 -0
- package/core/server.js +31 -0
- package/domains/art-tree/index.js +27 -0
- package/domains/art-tree/lessons.js +193 -0
- package/domains/art-tree/shapes.js +197 -0
- package/domains/art-tree/tools.js +179 -0
- package/index.js +36 -550
- package/package.json +2 -2
- package/tools/anchors.js +27 -0
- package/tools/animation.js +60 -0
- package/tools/canvas.js +38 -0
- package/tools/colors.js +24 -0
- package/tools/drawing.js +80 -0
- package/tools/filters.js +21 -0
- package/tools/health.js +16 -0
- package/tools/history.js +21 -0
- package/tools/index.js +42 -0
- package/tools/modes.js +27 -0
- package/tools/query.js +48 -0
- package/tools/region.js +24 -0
- package/tools/sprite.js +43 -0
- package/tools/workspace.js +47 -0
- package/transport/http.js +86 -0
- package/transport/stdio.js +21 -0
package/README.md
CHANGED
|
@@ -16,6 +16,14 @@ This bridge solves that by using Firebase Firestore as a real-time message bus:
|
|
|
16
16
|
3. **Browser** listens to that Firestore document, executes the command on the canvas, and writes the result back.
|
|
17
17
|
4. **Bridge** reads the result and returns it to the AI Agent.
|
|
18
18
|
|
|
19
|
+
## Security Information
|
|
20
|
+
|
|
21
|
+
You might notice that the Firebase API keys (`VITE_FIREBASE_API_KEY`, etc.) are included directly in the source code of this package. This is completely safe and by design:
|
|
22
|
+
|
|
23
|
+
* **API Keys are Routing Identifiers:** In the context of Firebase Web SDK, these keys are NOT secret passwords. They merely act as a "public address" to tell the bridge which Firebase project to connect to.
|
|
24
|
+
* **Firestore Security Rules:** Real security is enforced on the server-side via `firestore.rules`. The rules ensure that connections can only be established using a valid, unguessable UUID (the Session ID).
|
|
25
|
+
* **Zero User Data Access:** The bridge only has access to its designated `mcp_sessions` sandbox. It is explicitly blocked from reading or writing any user accounts, passwords, or other collections.
|
|
26
|
+
|
|
19
27
|
## Usage (For Users)
|
|
20
28
|
|
|
21
29
|
You do not need to download or clone this repository manually to use it!
|
|
@@ -24,7 +32,7 @@ To connect your AI, simply open Pixel Normal Edit, go to **Settings > Account >
|
|
|
24
32
|
|
|
25
33
|
For example, to run it via `npx`:
|
|
26
34
|
```bash
|
|
27
|
-
npx -y @pixel-normal-edit/mcp YOUR_SESSION_ID
|
|
35
|
+
npx -y @pixel-normal-edit/mcp@latest YOUR_SESSION_ID
|
|
28
36
|
```
|
|
29
37
|
*(Replace `YOUR_SESSION_ID` with the ID provided in your app).*
|
|
30
38
|
|
|
@@ -37,9 +45,12 @@ Add this to your `claude_desktop_config.json`:
|
|
|
37
45
|
"command": "npx",
|
|
38
46
|
"args": [
|
|
39
47
|
"-y",
|
|
40
|
-
"@pixel-normal-edit/mcp",
|
|
48
|
+
"@pixel-normal-edit/mcp@latest",
|
|
41
49
|
"YOUR_SESSION_ID"
|
|
42
|
-
]
|
|
50
|
+
],
|
|
51
|
+
"env": {
|
|
52
|
+
"DOTENV_CONFIG_QUIET": "true"
|
|
53
|
+
}
|
|
43
54
|
}
|
|
44
55
|
}
|
|
45
56
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* command-bus.js — Firebase-backed command bus for MCP
|
|
4
|
+
*
|
|
5
|
+
* Sends a JSON command payload to Firestore under the current session,
|
|
6
|
+
* then waits for the browser editor to respond with success/error.
|
|
7
|
+
*/
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const { doc, setDoc, onSnapshot } = require('firebase/firestore');
|
|
10
|
+
const { db } = require('./firebase');
|
|
11
|
+
|
|
12
|
+
const SESSION = process.argv[2] || process.env.MCP_SESSION || 'default-session';
|
|
13
|
+
const TIMEOUT = parseInt(process.env.MCP_TIMEOUT || '20000');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Send a command payload to the browser via Firestore and wait for response.
|
|
17
|
+
* @param {Object} payload - The command action and parameters
|
|
18
|
+
* @returns {Promise<Object>} MCP-compatible response object
|
|
19
|
+
*/
|
|
20
|
+
async function sendCommand(payload) {
|
|
21
|
+
const id = crypto.randomUUID();
|
|
22
|
+
const ref = doc(db, 'mcp_sessions', SESSION, 'commands', id);
|
|
23
|
+
await setDoc(ref, { ...payload, status: 'pending', timestamp: Date.now() });
|
|
24
|
+
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
unsub();
|
|
28
|
+
resolve({
|
|
29
|
+
isError: true,
|
|
30
|
+
content: [{ type: 'text', text: `⏱ Timeout (${TIMEOUT}ms). Is the browser tab open with session: ${SESSION}?` }]
|
|
31
|
+
});
|
|
32
|
+
}, TIMEOUT);
|
|
33
|
+
|
|
34
|
+
const unsub = onSnapshot(ref, (snap) => {
|
|
35
|
+
const d = snap.data();
|
|
36
|
+
if (!d) return;
|
|
37
|
+
if (d.status === 'success') {
|
|
38
|
+
clearTimeout(timer); unsub();
|
|
39
|
+
const out = d.result !== undefined ? JSON.stringify(d.result, null, 2) : '✓ Done';
|
|
40
|
+
resolve({ content: [{ type: 'text', text: out }] });
|
|
41
|
+
} else if (d.status === 'error') {
|
|
42
|
+
clearTimeout(timer); unsub();
|
|
43
|
+
resolve({ isError: true, content: [{ type: 'text', text: `❌ ${d.error}` }] });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { sendCommand, SESSION };
|
package/core/firebase.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* firebase.js — Firebase initialization for MCP Firebase Bridge
|
|
4
|
+
*
|
|
5
|
+
* Initializes and exports the Firestore database instance.
|
|
6
|
+
* All Firebase config is hardcoded (safe as these are public API keys).
|
|
7
|
+
*/
|
|
8
|
+
const { initializeApp } = require('firebase/app');
|
|
9
|
+
const { getFirestore } = require('firebase/firestore');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const dotenv = require('dotenv');
|
|
12
|
+
|
|
13
|
+
dotenv.config({ path: path.join(__dirname, '..', '..', '.env') });
|
|
14
|
+
|
|
15
|
+
const app = initializeApp({
|
|
16
|
+
apiKey: 'AIzaSyBSrvCt58Jhsh14wbC2bD2KLFUUVbAVim0',
|
|
17
|
+
authDomain: 'pixel-normal-edit.firebaseapp.com',
|
|
18
|
+
projectId: 'pixel-normal-edit',
|
|
19
|
+
storageBucket: 'pixel-normal-edit.firebasestorage.app',
|
|
20
|
+
messagingSenderId: '397075334229',
|
|
21
|
+
appId: '1:397075334229:web:b02eede3fc7b41d02f80dc',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const db = getFirestore(app);
|
|
25
|
+
|
|
26
|
+
module.exports = { db };
|
package/core/server.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* server.js — MCP Server instance factory
|
|
4
|
+
*
|
|
5
|
+
* Creates and configures the McpServer instance.
|
|
6
|
+
* Provides a tool registration factory for consistent tool definitions.
|
|
7
|
+
*/
|
|
8
|
+
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
9
|
+
const { sendCommand } = require('./command-bus');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a new MCP server instance
|
|
13
|
+
* @returns {McpServer}
|
|
14
|
+
*/
|
|
15
|
+
function createServer() {
|
|
16
|
+
return new McpServer({ name: 'PixelNormalEdit', version: '2.0.0' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register a single-command tool on the server
|
|
21
|
+
* @param {McpServer} server - The MCP server instance
|
|
22
|
+
* @param {string} name - Tool name
|
|
23
|
+
* @param {string} desc - Tool description
|
|
24
|
+
* @param {Object} schema - Zod schema for parameters
|
|
25
|
+
* @param {Function} mapToCmd - Function mapping params to command payload
|
|
26
|
+
*/
|
|
27
|
+
function registerTool(server, name, desc, schema, mapToCmd) {
|
|
28
|
+
server.tool(name, desc, schema, async (params) => sendCommand(mapToCmd(params)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { createServer, registerTool, sendCommand };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* domains/art-tree/index.js — Art Tree Module Aggregator
|
|
4
|
+
*
|
|
5
|
+
* Aggregates and exports all Art Tree module components.
|
|
6
|
+
* To add new features to this module, create a new file and add it here.
|
|
7
|
+
*
|
|
8
|
+
* Module namespace: art_tree_* (all tools prefixed with art_tree_)
|
|
9
|
+
*/
|
|
10
|
+
const shapes = require('./shapes');
|
|
11
|
+
const lessons = require('./lessons');
|
|
12
|
+
const tools = require('./tools');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register all Art Tree MCP tools on the server
|
|
16
|
+
* @param {McpServer} server
|
|
17
|
+
*/
|
|
18
|
+
function registerAll(server) {
|
|
19
|
+
tools.register(server);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
shapes,
|
|
24
|
+
lessons,
|
|
25
|
+
tools,
|
|
26
|
+
registerAll,
|
|
27
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lessons.js — Art Tree: Basic Shapes Lessons
|
|
4
|
+
*
|
|
5
|
+
* Layer: Lesson API (built on top of Shape API)
|
|
6
|
+
*
|
|
7
|
+
* Provides structured lessons that guide learners through
|
|
8
|
+
* drawing basic geometric shapes step by step.
|
|
9
|
+
* Each lesson returns a sequence of command payloads.
|
|
10
|
+
*/
|
|
11
|
+
const shapes = require('./shapes');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lesson 1: Lines — Introduction to drawing straight lines.
|
|
15
|
+
* Teaches horizontal, vertical, and diagonal lines.
|
|
16
|
+
* @param {number} canvasSize - Canvas dimension (assumes square)
|
|
17
|
+
* @param {string} color - Primary color for the lesson
|
|
18
|
+
* @returns {Array<Object>} Array of command payloads
|
|
19
|
+
*/
|
|
20
|
+
function lessonLines(canvasSize = 32, color = '#ff0000') {
|
|
21
|
+
const mid = Math.floor(canvasSize / 2);
|
|
22
|
+
const margin = Math.floor(canvasSize * 0.15);
|
|
23
|
+
return [
|
|
24
|
+
// Horizontal line
|
|
25
|
+
shapes.drawLine(margin, mid, canvasSize - margin, mid, color),
|
|
26
|
+
// Vertical line
|
|
27
|
+
shapes.drawLine(mid, margin, mid, canvasSize - margin, '#0000ff'),
|
|
28
|
+
// Diagonal (top-left to bottom-right)
|
|
29
|
+
shapes.drawLine(margin, margin, canvasSize - margin, canvasSize - margin, '#00aa00'),
|
|
30
|
+
// Diagonal (top-right to bottom-left)
|
|
31
|
+
shapes.drawLine(canvasSize - margin, margin, margin, canvasSize - margin, '#ff8800'),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Lesson 2: Squares & Rectangles — Understanding right angles and proportions.
|
|
37
|
+
* @param {number} canvasSize - Canvas dimension
|
|
38
|
+
* @param {string} color - Primary color
|
|
39
|
+
* @returns {Array<Object>} Array of command payloads
|
|
40
|
+
*/
|
|
41
|
+
function lessonSquares(canvasSize = 32, color = '#1565c0') {
|
|
42
|
+
const margin = Math.floor(canvasSize * 0.1);
|
|
43
|
+
const small = Math.floor(canvasSize * 0.2);
|
|
44
|
+
const large = Math.floor(canvasSize * 0.35);
|
|
45
|
+
return [
|
|
46
|
+
// Small square (outline)
|
|
47
|
+
shapes.drawSquare(margin, margin, small, color, false),
|
|
48
|
+
// Large square (filled)
|
|
49
|
+
shapes.drawSquare(canvasSize - margin - large, margin, large, '#ff7043', true),
|
|
50
|
+
// Rectangle (horizontal)
|
|
51
|
+
shapes.drawRectangle(margin, canvasSize - margin - small, large + small, small, '#4caf50', false),
|
|
52
|
+
// Rectangle (vertical, filled)
|
|
53
|
+
shapes.drawRectangle(canvasSize - margin - small, canvasSize - margin - large, small, large, '#9c27b0', true),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Lesson 3: Circles — Learning curves and symmetry.
|
|
59
|
+
* @param {number} canvasSize - Canvas dimension
|
|
60
|
+
* @param {string} color - Primary color
|
|
61
|
+
* @returns {Array<Object>} Array of command payloads
|
|
62
|
+
*/
|
|
63
|
+
function lessonCircles(canvasSize = 32, color = '#e91e63') {
|
|
64
|
+
const mid = Math.floor(canvasSize / 2);
|
|
65
|
+
const r1 = Math.floor(canvasSize * 0.15);
|
|
66
|
+
const r2 = Math.floor(canvasSize * 0.3);
|
|
67
|
+
return [
|
|
68
|
+
// Small circle (outline)
|
|
69
|
+
shapes.drawCircle(mid, mid, r1, color, false),
|
|
70
|
+
// Large circle (filled)
|
|
71
|
+
shapes.drawCircle(mid, mid, r2, '#2196f3', true),
|
|
72
|
+
// Small filled circle offset
|
|
73
|
+
shapes.drawCircle(mid + r1, mid - r1, Math.floor(r1 / 2), '#ff9800', true),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Lesson 4: Ellipses — Understanding oval shapes and proportions.
|
|
79
|
+
* @param {number} canvasSize - Canvas dimension
|
|
80
|
+
* @param {string} color - Primary color
|
|
81
|
+
* @returns {Array<Object>} Array of command payloads
|
|
82
|
+
*/
|
|
83
|
+
function lessonEllipses(canvasSize = 32, color = '#9c27b0') {
|
|
84
|
+
const mid = Math.floor(canvasSize / 2);
|
|
85
|
+
const rx = Math.floor(canvasSize * 0.3);
|
|
86
|
+
const ry = Math.floor(canvasSize * 0.15);
|
|
87
|
+
return [
|
|
88
|
+
// Horizontal ellipse (outline)
|
|
89
|
+
shapes.drawEllipse(mid, mid, rx, ry, color, false),
|
|
90
|
+
// Vertical ellipse (filled)
|
|
91
|
+
shapes.drawEllipse(mid, mid, ry, rx, '#4caf50', true),
|
|
92
|
+
// Small circle-like ellipse
|
|
93
|
+
shapes.drawEllipse(mid + rx, mid - ry, Math.floor(rx / 3), Math.floor(ry / 2), '#ff5722', true),
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Lesson 5: Triangles — Three-point shapes and stability.
|
|
99
|
+
* @param {number} canvasSize - Canvas dimension
|
|
100
|
+
* @param {string} color - Primary color
|
|
101
|
+
* @returns {Array<Object>} Array of command payloads
|
|
102
|
+
*/
|
|
103
|
+
function lessonTriangles(canvasSize = 32, color = '#ff5722') {
|
|
104
|
+
const mid = Math.floor(canvasSize / 2);
|
|
105
|
+
const size = Math.floor(canvasSize * 0.4);
|
|
106
|
+
const margin = Math.floor(canvasSize * 0.1);
|
|
107
|
+
return [
|
|
108
|
+
// Equilateral triangle (outline)
|
|
109
|
+
shapes.drawEquilateralTriangle(mid, mid, size, color, false),
|
|
110
|
+
// Right triangle (filled)
|
|
111
|
+
shapes.drawTriangle([
|
|
112
|
+
{ x: margin, y: canvasSize - margin },
|
|
113
|
+
{ x: margin, y: margin },
|
|
114
|
+
{ x: Math.floor(canvasSize * 0.35), y: canvasSize - margin },
|
|
115
|
+
], '#3f51b5', true),
|
|
116
|
+
// Inverted triangle
|
|
117
|
+
shapes.drawEquilateralTriangle(mid, mid, Math.floor(size * 0.6), '#009688', true),
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Lesson 6: Polygons — Multi-sided shapes.
|
|
123
|
+
* @param {number} canvasSize - Canvas dimension
|
|
124
|
+
* @param {string} color - Primary color
|
|
125
|
+
* @returns {Array<Object>} Array of command payloads
|
|
126
|
+
*/
|
|
127
|
+
function lessonPolygons(canvasSize = 32, color = '#607d8b') {
|
|
128
|
+
const mid = Math.floor(canvasSize / 2);
|
|
129
|
+
const r = Math.floor(canvasSize * 0.35);
|
|
130
|
+
return [
|
|
131
|
+
// Pentagon
|
|
132
|
+
shapes.drawRegularPolygon(mid, mid, 5, r, '#4caf50', false),
|
|
133
|
+
// Hexagon (filled)
|
|
134
|
+
shapes.drawRegularPolygon(mid, mid, 6, Math.floor(r * 0.7), '#ff9800', true),
|
|
135
|
+
// Octagon
|
|
136
|
+
shapes.drawRegularPolygon(mid, mid, 8, Math.floor(r * 0.5), '#9c27b0', false),
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Lesson 7: Composition — Combining basic shapes to create simple objects.
|
|
142
|
+
* @param {number} canvasSize - Canvas dimension
|
|
143
|
+
* @returns {Array<Object>} Array of command payloads
|
|
144
|
+
*/
|
|
145
|
+
function lessonComposition(canvasSize = 32) {
|
|
146
|
+
const mid = Math.floor(canvasSize / 2);
|
|
147
|
+
const q1 = Math.floor(canvasSize * 0.25);
|
|
148
|
+
const q3 = Math.floor(canvasSize * 0.75);
|
|
149
|
+
const small = Math.floor(canvasSize * 0.1);
|
|
150
|
+
return [
|
|
151
|
+
// House: square body
|
|
152
|
+
shapes.drawSquare(q1, mid, q3 - q1, '#8d6e63', true),
|
|
153
|
+
// House: triangle roof
|
|
154
|
+
shapes.drawTriangle([
|
|
155
|
+
{ x: q1 - 1, y: mid },
|
|
156
|
+
{ x: mid, y: q1 },
|
|
157
|
+
{ x: q3 + 1, y: mid },
|
|
158
|
+
], '#d32f2f', true),
|
|
159
|
+
// Sun: circle
|
|
160
|
+
shapes.drawCircle(canvasSize - small - 2, small + 2, small, '#ffc107', true),
|
|
161
|
+
// Tree: rectangle trunk
|
|
162
|
+
shapes.drawRectangle(q1 + small, canvasSize - small - 2, Math.floor(small / 2), small, '#5d4037', true),
|
|
163
|
+
// Tree: circle top
|
|
164
|
+
shapes.drawCircle(q1 + Math.floor(small / 4), canvasSize - small - 4, Math.floor(small / 2), '#388e3c', true),
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get all available lessons with metadata.
|
|
170
|
+
* @returns {Array<{id: string, name: string, description: string, fn: Function}>}
|
|
171
|
+
*/
|
|
172
|
+
function getLessonCatalog() {
|
|
173
|
+
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 cơ bản tạo vật thể đơn giản', fn: lessonComposition },
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
lessonLines,
|
|
186
|
+
lessonSquares,
|
|
187
|
+
lessonCircles,
|
|
188
|
+
lessonEllipses,
|
|
189
|
+
lessonTriangles,
|
|
190
|
+
lessonPolygons,
|
|
191
|
+
lessonComposition,
|
|
192
|
+
getLessonCatalog,
|
|
193
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* shapes.js — Art Tree: Basic Shapes API
|
|
4
|
+
*
|
|
5
|
+
* Layer: Shape API (built on top of Core Drawing Primitives)
|
|
6
|
+
*
|
|
7
|
+
* Provides higher-level shape drawing functions that compose
|
|
8
|
+
* the primitive drawLine, drawRect, drawCircle, drawEllipse,
|
|
9
|
+
* drawPolygon calls into more meaningful geometric shapes
|
|
10
|
+
* suitable for art lessons.
|
|
11
|
+
*
|
|
12
|
+
* All functions return command payloads compatible with sendCommand.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const PRIMITIVE_ACTIONS = {
|
|
16
|
+
LINE: 'drawLine',
|
|
17
|
+
RECT: 'drawRect',
|
|
18
|
+
CIRCLE: 'drawCircle',
|
|
19
|
+
ELLIPSE: 'drawEllipse',
|
|
20
|
+
POLYGON: 'drawPolygon',
|
|
21
|
+
PIXEL: 'drawPixel',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Draw a straight line between two points.
|
|
26
|
+
* @param {number} x0 - Start X
|
|
27
|
+
* @param {number} y0 - Start Y
|
|
28
|
+
* @param {number} x1 - End X
|
|
29
|
+
* @param {number} y1 - End Y
|
|
30
|
+
* @param {string} color - Hex color
|
|
31
|
+
* @returns {Object} Command payload
|
|
32
|
+
*/
|
|
33
|
+
function drawLine(x0, y0, x1, y1, color) {
|
|
34
|
+
return { action: PRIMITIVE_ACTIONS.LINE, x0, y0, x1, y1, color };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Draw a square (aligned to axes).
|
|
39
|
+
* @param {number} x - Top-left X
|
|
40
|
+
* @param {number} y - Top-left Y
|
|
41
|
+
* @param {number} size - Side length
|
|
42
|
+
* @param {string} color - Hex color
|
|
43
|
+
* @param {boolean} [filled=false] - Fill interior
|
|
44
|
+
* @returns {Object} Command payload
|
|
45
|
+
*/
|
|
46
|
+
function drawSquare(x, y, size, color, filled = false) {
|
|
47
|
+
return { action: PRIMITIVE_ACTIONS.RECT, x, y, w: size, h: size, color, filled };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Draw a rectangle (aligned to axes).
|
|
52
|
+
* @param {number} x - Top-left X
|
|
53
|
+
* @param {number} y - Top-left Y
|
|
54
|
+
* @param {number} w - Width
|
|
55
|
+
* @param {number} h - Height
|
|
56
|
+
* @param {string} color - Hex color
|
|
57
|
+
* @param {boolean} [filled=false] - Fill interior
|
|
58
|
+
* @returns {Object} Command payload
|
|
59
|
+
*/
|
|
60
|
+
function drawRectangle(x, y, w, h, color, filled = false) {
|
|
61
|
+
return { action: PRIMITIVE_ACTIONS.RECT, x, y, w, h, color, filled };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Draw a circle.
|
|
66
|
+
* @param {number} cx - Center X
|
|
67
|
+
* @param {number} cy - Center Y
|
|
68
|
+
* @param {number} r - Radius
|
|
69
|
+
* @param {string} color - Hex color
|
|
70
|
+
* @param {boolean} [filled=false] - Fill interior
|
|
71
|
+
* @returns {Object} Command payload
|
|
72
|
+
*/
|
|
73
|
+
function drawCircle(cx, cy, r, color, filled = false) {
|
|
74
|
+
return { action: PRIMITIVE_ACTIONS.CIRCLE, cx, cy, r, color, filled };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Draw an ellipse.
|
|
79
|
+
* @param {number} cx - Center X
|
|
80
|
+
* @param {number} cy - Center Y
|
|
81
|
+
* @param {number} rx - Horizontal radius
|
|
82
|
+
* @param {number} ry - Vertical radius
|
|
83
|
+
* @param {string} color - Hex color
|
|
84
|
+
* @param {boolean} [filled=false] - Fill interior
|
|
85
|
+
* @returns {Object} Command payload
|
|
86
|
+
*/
|
|
87
|
+
function drawEllipse(cx, cy, rx, ry, color, filled = false) {
|
|
88
|
+
return { action: PRIMITIVE_ACTIONS.ELLIPSE, cx, cy, rx, ry, color, filled };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Draw a triangle from three vertices.
|
|
93
|
+
* @param {Array<{x: number, y: number}>} vertices - Three vertices
|
|
94
|
+
* @param {string} color - Hex color
|
|
95
|
+
* @param {boolean} [filled=false] - Fill interior
|
|
96
|
+
* @returns {Object} Command payload
|
|
97
|
+
*/
|
|
98
|
+
function drawTriangle(vertices, color, filled = false) {
|
|
99
|
+
if (vertices.length !== 3) {
|
|
100
|
+
throw new Error('Triangle requires exactly 3 vertices');
|
|
101
|
+
}
|
|
102
|
+
return { action: PRIMITIVE_ACTIONS.POLYGON, points: vertices, color, filled };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Draw a regular triangle (equilateral) centered at (cx, cy).
|
|
107
|
+
* @param {number} cx - Center X
|
|
108
|
+
* @param {number} cy - Center Y
|
|
109
|
+
* @param {number} sideLength - Length of each side
|
|
110
|
+
* @param {string} color - Hex color
|
|
111
|
+
* @param {boolean} [filled=false] - Fill interior
|
|
112
|
+
* @returns {Object} Command payload
|
|
113
|
+
*/
|
|
114
|
+
function drawEquilateralTriangle(cx, cy, sideLength, color, filled = false) {
|
|
115
|
+
const h = (Math.sqrt(3) / 2) * sideLength;
|
|
116
|
+
const vertices = [
|
|
117
|
+
{ x: Math.round(cx), y: Math.round(cy - h * 2 / 3) },
|
|
118
|
+
{ x: Math.round(cx - sideLength / 2), y: Math.round(cy + h / 3) },
|
|
119
|
+
{ x: Math.round(cx + sideLength / 2), y: Math.round(cy + h / 3) },
|
|
120
|
+
];
|
|
121
|
+
return drawTriangle(vertices, color, filled);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Draw a polygon from a list of points.
|
|
126
|
+
* @param {Array<{x: number, y: number}>} points - Array of vertices (min 3)
|
|
127
|
+
* @param {string} color - Hex color
|
|
128
|
+
* @param {boolean} [filled=false] - Fill interior
|
|
129
|
+
* @returns {Object} Command payload
|
|
130
|
+
*/
|
|
131
|
+
function drawPolygon(points, color, filled = false) {
|
|
132
|
+
return { action: PRIMITIVE_ACTIONS.POLYGON, points, color, filled };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Draw a regular polygon centered at (cx, cy).
|
|
137
|
+
* @param {number} cx - Center X
|
|
138
|
+
* @param {number} cy - Center Y
|
|
139
|
+
* @param {number} sides - Number of sides (3+)
|
|
140
|
+
* @param {number} radius - Distance from center to vertices
|
|
141
|
+
* @param {string} color - Hex color
|
|
142
|
+
* @param {boolean} [filled=false] - Fill interior
|
|
143
|
+
* @returns {Object} Command payload
|
|
144
|
+
*/
|
|
145
|
+
function drawRegularPolygon(cx, cy, sides, radius, color, filled = false) {
|
|
146
|
+
const angleStep = (2 * Math.PI) / sides;
|
|
147
|
+
const points = [];
|
|
148
|
+
// Start from top (-PI/2) for natural orientation
|
|
149
|
+
const startAngle = -Math.PI / 2;
|
|
150
|
+
for (let i = 0; i < sides; i++) {
|
|
151
|
+
const angle = startAngle + i * angleStep;
|
|
152
|
+
points.push({
|
|
153
|
+
x: Math.round(cx + radius * Math.cos(angle)),
|
|
154
|
+
y: Math.round(cy + radius * Math.sin(angle)),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return drawPolygon(points, color, filled);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Draw a grid of squares (useful for pixel art foundation lessons).
|
|
162
|
+
* @param {number} startX - Top-left X
|
|
163
|
+
* @param {number} startY - Top-left Y
|
|
164
|
+
* @param {number} cols - Number of columns
|
|
165
|
+
* @param {number} rows - Number of rows
|
|
166
|
+
* @param {number} cellSize - Size of each cell
|
|
167
|
+
* @param {string} color - Grid line color
|
|
168
|
+
* @returns {Array<Object>} Array of command payloads
|
|
169
|
+
*/
|
|
170
|
+
function drawGrid(startX, startY, cols, rows, cellSize, color) {
|
|
171
|
+
const commands = [];
|
|
172
|
+
// Horizontal lines
|
|
173
|
+
for (let r = 0; r <= rows; r++) {
|
|
174
|
+
const y = startY + r * cellSize;
|
|
175
|
+
commands.push(drawLine(startX, y, startX + cols * cellSize, y, color));
|
|
176
|
+
}
|
|
177
|
+
// Vertical lines
|
|
178
|
+
for (let c = 0; c <= cols; c++) {
|
|
179
|
+
const x = startX + c * cellSize;
|
|
180
|
+
commands.push(drawLine(x, startY, x, startY + rows * cellSize, color));
|
|
181
|
+
}
|
|
182
|
+
return commands;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
PRIMITIVE_ACTIONS,
|
|
187
|
+
drawLine,
|
|
188
|
+
drawSquare,
|
|
189
|
+
drawRectangle,
|
|
190
|
+
drawCircle,
|
|
191
|
+
drawEllipse,
|
|
192
|
+
drawTriangle,
|
|
193
|
+
drawEquilateralTriangle,
|
|
194
|
+
drawPolygon,
|
|
195
|
+
drawRegularPolygon,
|
|
196
|
+
drawGrid,
|
|
197
|
+
};
|