freeform-modeling-mcp 1.0.26
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 +61 -0
- package/index.cjs +21 -0
- package/package.json +48 -0
- package/src/cli.ts +47 -0
- package/src/commands/start.ts +88 -0
- package/src/commands/status.ts +81 -0
- package/src/dev.ts +24 -0
- package/src/lib/fileproxy/index.ts +505 -0
- package/src/lib/fileproxy/proxy-server.js +347 -0
- package/src/lib/gltf2obj/compress.js +33 -0
- package/src/lib/gltf2obj/engine/core/core.js +34 -0
- package/src/lib/gltf2obj/engine/core/eventnotifier.js +39 -0
- package/src/lib/gltf2obj/engine/core/taskrunner.js +88 -0
- package/src/lib/gltf2obj/engine/export/exporter.js +38 -0
- package/src/lib/gltf2obj/engine/export/exporterbase.js +86 -0
- package/src/lib/gltf2obj/engine/export/exportermodel.js +117 -0
- package/src/lib/gltf2obj/engine/export/exporterobj.js +147 -0
- package/src/lib/gltf2obj/engine/geometry/box3d.js +60 -0
- package/src/lib/gltf2obj/engine/geometry/coord2d.js +35 -0
- package/src/lib/gltf2obj/engine/geometry/coord3d.js +126 -0
- package/src/lib/gltf2obj/engine/geometry/coord4d.js +15 -0
- package/src/lib/gltf2obj/engine/geometry/geometry.js +56 -0
- package/src/lib/gltf2obj/engine/geometry/matrix.js +440 -0
- package/src/lib/gltf2obj/engine/geometry/octree.js +160 -0
- package/src/lib/gltf2obj/engine/geometry/quaternion.js +83 -0
- package/src/lib/gltf2obj/engine/geometry/transformation.js +63 -0
- package/src/lib/gltf2obj/engine/geometry/tween.js +31 -0
- package/src/lib/gltf2obj/engine/import/importer.js +270 -0
- package/src/lib/gltf2obj/engine/import/importerbase.js +115 -0
- package/src/lib/gltf2obj/engine/import/importerfiles.js +139 -0
- package/src/lib/gltf2obj/engine/import/importergltf.js +1045 -0
- package/src/lib/gltf2obj/engine/import/importerutils.js +102 -0
- package/src/lib/gltf2obj/engine/io/binaryreader.js +93 -0
- package/src/lib/gltf2obj/engine/io/binarywriter.js +92 -0
- package/src/lib/gltf2obj/engine/io/bufferutils.js +85 -0
- package/src/lib/gltf2obj/engine/io/externallibs.js +42 -0
- package/src/lib/gltf2obj/engine/io/fileutils.js +120 -0
- package/src/lib/gltf2obj/engine/io/textwriter.js +41 -0
- package/src/lib/gltf2obj/engine/main.js +19 -0
- package/src/lib/gltf2obj/engine/model/color.js +130 -0
- package/src/lib/gltf2obj/engine/model/generator.js +433 -0
- package/src/lib/gltf2obj/engine/model/material.js +243 -0
- package/src/lib/gltf2obj/engine/model/mesh.js +173 -0
- package/src/lib/gltf2obj/engine/model/meshbuffer.js +233 -0
- package/src/lib/gltf2obj/engine/model/meshinstance.js +128 -0
- package/src/lib/gltf2obj/engine/model/meshutils.js +64 -0
- package/src/lib/gltf2obj/engine/model/model.js +195 -0
- package/src/lib/gltf2obj/engine/model/modelfinalization.js +377 -0
- package/src/lib/gltf2obj/engine/model/modelutils.js +117 -0
- package/src/lib/gltf2obj/engine/model/node.js +178 -0
- package/src/lib/gltf2obj/engine/model/object.js +90 -0
- package/src/lib/gltf2obj/engine/model/property.js +86 -0
- package/src/lib/gltf2obj/engine/model/quantities.js +46 -0
- package/src/lib/gltf2obj/engine/model/topology.js +139 -0
- package/src/lib/gltf2obj/engine/model/triangle.js +99 -0
- package/src/lib/gltf2obj/engine/threejs/threemodelloader.js +85 -0
- package/src/lib/gltf2obj/handler.js +63 -0
- package/src/lib/gltf2obj/index.js +47 -0
- package/src/lib/logger.ts +304 -0
- package/src/lib/print.ts +37 -0
- package/src/lib/utils.ts +34 -0
- package/src/lib/wsbridge/bridge.ts +639 -0
- package/src/lib/wsbridge/lock.ts +258 -0
- package/src/llmClient.ts +245 -0
- package/src/prompt.ts +187 -0
- package/src/server.ts +125 -0
- package/src/tools/assets-tool.ts +314 -0
- package/src/tools/basic-tools.ts +531 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/material-tools.ts +88 -0
- package/src/tools/modeling/auxiliary-curve-tool.ts +44 -0
- package/src/tools/modeling/find-face.ts +38 -0
- package/src/tools/modeling/sweep-tool.ts +60 -0
- package/src/tools/modeling/utils.ts +181 -0
- package/src/tools/modeling-tools.ts +135 -0
- package/src/tools/screenshot-tools.ts +107 -0
- package/src/tools/tools-info.ts +107 -0
- package/src/tools/tripo3d-tools.ts +496 -0
- package/src/types/index.ts +25 -0
- package/src/types/tripo3d.ts +38 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context } from "../../types";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import logger from "../../lib/logger";
|
|
4
|
+
import { llmClient } from "../../llmClient";
|
|
5
|
+
|
|
6
|
+
export const findFaceByMultiplePointsTool = {
|
|
7
|
+
name: "find_face_by_points",
|
|
8
|
+
description:
|
|
9
|
+
"Finds a face by specifying multiple points that lie on the face. By providing 1-5 points that lie on the target face, you can identify faces reliably even when topology changes occur. Returns the face's information including its ID, normal vector, area, and other properties.",
|
|
10
|
+
parameters: z.object({
|
|
11
|
+
points: z.array(
|
|
12
|
+
z.object({
|
|
13
|
+
x: z.number().describe("X coordinate of a point on the face"),
|
|
14
|
+
y: z.number().describe("Y coordinate of a point on the face"),
|
|
15
|
+
z: z.number().describe("Z coordinate of a point on the face"),
|
|
16
|
+
})
|
|
17
|
+
).min(1).max(5)
|
|
18
|
+
.describe("Array of points that lie on the face you want to find. More points increase finding accuracy."),
|
|
19
|
+
tolerance: z.number().positive().optional().default(0.001)
|
|
20
|
+
.describe("Maximum distance between points and face for matching to occur (default: 0.001 model units)")
|
|
21
|
+
}),
|
|
22
|
+
execute: async (args, context: Context): Promise<string> => {
|
|
23
|
+
try {
|
|
24
|
+
if (!llmClient.hasConnectedKoomasterClients()) {
|
|
25
|
+
logger.warn(`No clients connected when attempting to find face by points`);
|
|
26
|
+
return "No modeling software clients connected. Please start the software and connect to this server.";
|
|
27
|
+
}
|
|
28
|
+
const result = await llmClient.sendCommand("find_face_by_points", args);
|
|
29
|
+
return result;
|
|
30
|
+
} catch (e) {
|
|
31
|
+
const errorMessage = `Error during face finding by points: ${e instanceof Error ? e.message : String(e)
|
|
32
|
+
}`;
|
|
33
|
+
logger.error(errorMessage);
|
|
34
|
+
context.log.error(errorMessage);
|
|
35
|
+
return errorMessage;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Context } from "../../types";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import logger from "../../lib/logger";
|
|
4
|
+
import { llmClient } from "../../llmClient";
|
|
5
|
+
|
|
6
|
+
// Tool: Sweep Face Along Auxiliary Curves
|
|
7
|
+
export const sweepTool = {
|
|
8
|
+
name: 'sweep',
|
|
9
|
+
description: 'Creates a 3D solid by sweeping a FACE along auxiliary curves. NOTE: This tool operates ONLY on faces, not on entire objects or groups. The face can be either selected by the user or created programmatically. IMPORTANT: The face being swept (profile) must NOT be parallel to the auxiliary curves (path) - the sweep direction must have some component perpendicular to the face normal for the operation to succeed. After the sweep operation completes, the original face (profile) will be automatically deleted from the model. If you need to perform multiple sweep operations with the same profile, you will need to recreate the face after each sweep operation using add_faces or other face creation methods.',
|
|
10
|
+
parameters: z.object({
|
|
11
|
+
face_id: z.string()
|
|
12
|
+
.describe('ID of the face to sweep. You can obtain this ID by: 1) Using get_selection with type="face" to get a user-selected face, 2) Creating a new face with add_faces and using the returned face ID, or 3) Using find_face_by_points tool to locate a face when model geometry has been modified or split. IMPORTANT: The face being swept (profile) must NOT be parallel to the auxiliary curves (path) - there must be some angle between the face normal and the path direction, otherwise the sweep operation will fail.'),
|
|
13
|
+
auxiliary_curve_ids: z.array(z.string())
|
|
14
|
+
.describe('IDs of auxiliary curves to use as the sweep path. Obtain these either from user-selected curves (get_selection) or by creating new curves (create_auxiliary_curve). IMPORTANT: The curves must connect end-to-end in a continuous path (each curve must start where the previous one ends), though they do not need to form a closed loop. Disconnected curves will cause the sweep operation to fail.'),
|
|
15
|
+
}),
|
|
16
|
+
execute: async (args, context: Context): Promise<string> => {
|
|
17
|
+
logger.info(`Tool executed: sweep with face_id "${args.face_id}" and auxiliary curve IDs ${JSON.stringify(args.auxiliary_curve_ids)}`);
|
|
18
|
+
try {
|
|
19
|
+
// Check if any modeling software clients are connected
|
|
20
|
+
if (!llmClient.hasConnectedKoomasterClients()) {
|
|
21
|
+
return "No modeling software clients connected. Please start the software and connect to this server.";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate parameters
|
|
25
|
+
if (!args.face_id) {
|
|
26
|
+
return `Error: You must provide a face_id to sweep. You can:
|
|
27
|
+
|
|
28
|
+
1. Ask the user to select a face in the scene, then use get_selection tool with type="face" to obtain the ID of the selected face, OR
|
|
29
|
+
2. Create a new face using the add_faces tool and use the returned face ID.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!args.auxiliary_curve_ids || args.auxiliary_curve_ids.length === 0) {
|
|
33
|
+
return `Error: You must provide at least one auxiliary curve ID for the sweep path. Follow these steps:
|
|
34
|
+
|
|
35
|
+
1. First use get_selection to check if the user has already selected any auxiliary curves.
|
|
36
|
+
2. If no auxiliary curves are selected, create them using the create_auxiliary_curve tool with a 3D path.
|
|
37
|
+
3. Once you have the auxiliary curve IDs (either from get_selection or create_auxiliary_curve), use them in this tool.
|
|
38
|
+
|
|
39
|
+
Example workflow:
|
|
40
|
+
- Get face ID: either get_selection with type="face" or create with add_faces
|
|
41
|
+
- Get curve IDs: either get_selection or create_auxiliary_curve
|
|
42
|
+
- Sweep: use this tool with the obtained face_id and auxiliary_curve_ids`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Send the sweep command
|
|
46
|
+
const result = await llmClient.sendCommand('sweep', args);
|
|
47
|
+
|
|
48
|
+
// Return success message with information from the result
|
|
49
|
+
return `${result}
|
|
50
|
+
The sweep operation used face ${args.face_id} and swept it along the path defined by auxiliary curve(s): ${args.auxiliary_curve_ids.join(', ')}.
|
|
51
|
+
`;
|
|
52
|
+
// Note: To see the result, use the get_scene_screenshot tool to capture an image of the scene.
|
|
53
|
+
} catch (e) {
|
|
54
|
+
const errorMessage = `Error creating swept object: ${e instanceof Error ? e.message : String(e)}`;
|
|
55
|
+
logger.error(errorMessage);
|
|
56
|
+
context.log.error(errorMessage);
|
|
57
|
+
return errorMessage;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
|
|
2
|
+
export function parseSvgPath3D(pathString: string) {
|
|
3
|
+
// 移除多余空格并标准化
|
|
4
|
+
const normalizedPath = pathString.trim().replace(/\s+/g, ' ');
|
|
5
|
+
// 用于存储解析后的路径段
|
|
6
|
+
const pathSegments = [];
|
|
7
|
+
// 当前点的位置
|
|
8
|
+
let currentX = 0, currentY = 0, currentZ = 0;
|
|
9
|
+
// 记录路径起始点,用于闭合路径
|
|
10
|
+
let startX = 0, startY = 0, startZ = 0;
|
|
11
|
+
// 辅助函数:解析坐标字符串为数字数组
|
|
12
|
+
const parseCoords = (coordStr: string): number[] => {
|
|
13
|
+
// Replace commas with spaces, then split by spaces and filter out empty strings
|
|
14
|
+
return coordStr.replace(/,/g, ' ')
|
|
15
|
+
.split(/\s+/)
|
|
16
|
+
.filter(s => s.length > 0)
|
|
17
|
+
.map(coord => parseFloat(coord.trim()));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// 将路径字符串分解为命令和参数 - 只关注M, L, C, A, Z命令
|
|
21
|
+
const tokens = normalizedPath.match(/([MLCAZmlcaz])\s*([^MLCAZmlcaz]*)/g) || [];
|
|
22
|
+
|
|
23
|
+
for (const token of tokens) {
|
|
24
|
+
const command = token[0].toUpperCase();
|
|
25
|
+
const paramsStr = token.substring(1).trim();
|
|
26
|
+
|
|
27
|
+
switch (command) {
|
|
28
|
+
case 'M': // 移动到
|
|
29
|
+
{
|
|
30
|
+
const coords = parseCoords(paramsStr);
|
|
31
|
+
if (coords.length !== 3) {
|
|
32
|
+
throw new Error(`Invalid M command. Expected 3 coordinates, got ${coords.length}.`);
|
|
33
|
+
}
|
|
34
|
+
currentX = coords[0];
|
|
35
|
+
currentY = coords[1];
|
|
36
|
+
currentZ = coords[2];
|
|
37
|
+
// 记录起始点,用于Z命令
|
|
38
|
+
startX = currentX;
|
|
39
|
+
startY = currentY;
|
|
40
|
+
startZ = currentZ;
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
|
|
44
|
+
case 'L': // 线段
|
|
45
|
+
{
|
|
46
|
+
const coords = parseCoords(paramsStr);
|
|
47
|
+
if (coords.length !== 3) {
|
|
48
|
+
throw new Error(`Invalid L command. Expected 3 coordinates, got ${coords.length}.`);
|
|
49
|
+
}
|
|
50
|
+
// 创建线段
|
|
51
|
+
pathSegments.push({
|
|
52
|
+
type: 'line',
|
|
53
|
+
startPoint: [currentX, currentY, currentZ],
|
|
54
|
+
endPoint: [coords[0], coords[1], coords[2]]
|
|
55
|
+
});
|
|
56
|
+
// 更新当前点
|
|
57
|
+
currentX = coords[0];
|
|
58
|
+
currentY = coords[1];
|
|
59
|
+
currentZ = coords[2];
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
|
|
63
|
+
case 'C': // 三次贝塞尔曲线
|
|
64
|
+
{
|
|
65
|
+
const coords = parseCoords(paramsStr);
|
|
66
|
+
if (coords.length !== 9) {
|
|
67
|
+
throw new Error(`Invalid C command. Expected 9 coordinates (3 points), got ${coords.length}.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 创建3D贝塞尔曲线
|
|
71
|
+
pathSegments.push({
|
|
72
|
+
type: 'bezier',
|
|
73
|
+
startPoint: [currentX, currentY, currentZ],
|
|
74
|
+
controlPoint1: [coords[0], coords[1], coords[2]],
|
|
75
|
+
controlPoint2: [coords[3], coords[4], coords[5]],
|
|
76
|
+
endPoint: [coords[6], coords[7], coords[8]]
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 更新当前点为终点
|
|
80
|
+
currentX = coords[6];
|
|
81
|
+
currentY = coords[7];
|
|
82
|
+
currentZ = coords[8];
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case 'A': // 通过三点定义的圆弧
|
|
87
|
+
{
|
|
88
|
+
// A midX,midY,midZ endX,endY,endZ
|
|
89
|
+
const coords = parseCoords(paramsStr);
|
|
90
|
+
if (coords.length !== 6) {
|
|
91
|
+
throw new Error(`Invalid A command. Expected 6 coordinates, got ${coords.length}.`);
|
|
92
|
+
}
|
|
93
|
+
const midX = coords[0];
|
|
94
|
+
const midY = coords[1];
|
|
95
|
+
const midZ = coords[2];
|
|
96
|
+
const endX = coords[3];
|
|
97
|
+
const endY = coords[4];
|
|
98
|
+
const endZ = coords[5];
|
|
99
|
+
// 起点(当前位置)
|
|
100
|
+
const startPoint = [currentX, currentY, currentZ];
|
|
101
|
+
// 弧上的中间点
|
|
102
|
+
const midPoint = [midX, midY, midZ];
|
|
103
|
+
// 终点
|
|
104
|
+
const endPoint = [endX, endY, endZ];
|
|
105
|
+
// 检查三点是否共线
|
|
106
|
+
const isCollinear = checkCollinear(startPoint, midPoint, endPoint);
|
|
107
|
+
if (isCollinear) {
|
|
108
|
+
// 如果共线,改为创建线段
|
|
109
|
+
pathSegments.push({
|
|
110
|
+
type: 'line',
|
|
111
|
+
startPoint: startPoint,
|
|
112
|
+
endPoint: endPoint
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
// 创建圆弧
|
|
116
|
+
pathSegments.push({
|
|
117
|
+
type: 'arc',
|
|
118
|
+
startPoint: startPoint,
|
|
119
|
+
centerPoint: midPoint, // 这是弧上的中点
|
|
120
|
+
endPoint: endPoint
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// 更新当前点
|
|
124
|
+
currentX = endX;
|
|
125
|
+
currentY = endY;
|
|
126
|
+
currentZ = endZ;
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
case 'Z': // 闭合路径
|
|
131
|
+
// 添加一个线段闭合路径
|
|
132
|
+
if (
|
|
133
|
+
Math.abs(currentX - startX) > 0.0001 ||
|
|
134
|
+
Math.abs(currentY - startY) > 0.0001 ||
|
|
135
|
+
Math.abs(currentZ - startZ) > 0.0001
|
|
136
|
+
) {
|
|
137
|
+
pathSegments.push({
|
|
138
|
+
type: 'line',
|
|
139
|
+
startPoint: [currentX, currentY, currentZ],
|
|
140
|
+
endPoint: [startX, startY, startZ]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// 更新当前点为起始点
|
|
144
|
+
currentX = startX;
|
|
145
|
+
currentY = startY;
|
|
146
|
+
currentZ = startZ;
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
default:
|
|
150
|
+
throw new Error(`Unsupported path command: ${command}. Only M, L, C, A, and Z commands are supported.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return pathSegments;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 检查三点是否共线
|
|
158
|
+
function checkCollinear(p1, p2, p3, tolerance = 0.0001) {
|
|
159
|
+
// 计算向量p1->p2和p1->p3
|
|
160
|
+
const v1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
|
|
161
|
+
const v2 = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
|
|
162
|
+
|
|
163
|
+
// 计算向量的叉积
|
|
164
|
+
const cross = [
|
|
165
|
+
v1[1] * v2[2] - v1[2] * v2[1],
|
|
166
|
+
v1[2] * v2[0] - v1[0] * v2[2],
|
|
167
|
+
v1[0] * v2[1] - v1[1] * v2[0]
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
// 计算叉积的大小
|
|
171
|
+
const crossLength = Math.sqrt(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]);
|
|
172
|
+
|
|
173
|
+
// 计算向量v1和v2的长度
|
|
174
|
+
const v1Length = Math.sqrt(v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2]);
|
|
175
|
+
const v2Length = Math.sqrt(v2[0] * v2[0] + v2[1] * v2[1] + v2[2] * v2[2]);
|
|
176
|
+
|
|
177
|
+
// 计算sin值,如果接近0则三点共线
|
|
178
|
+
const sinValue = crossLength / (v1Length * v2Length);
|
|
179
|
+
|
|
180
|
+
return sinValue < tolerance;
|
|
181
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Context } from "../types";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import logger from "../lib/logger";
|
|
4
|
+
import { llmClient } from "../llmClient";
|
|
5
|
+
import { parseSvgPath3D } from "./modeling/utils";
|
|
6
|
+
import { UserError } from "fastmcp";
|
|
7
|
+
|
|
8
|
+
export const addFacesTool = {
|
|
9
|
+
name: "add_faces",
|
|
10
|
+
description:
|
|
11
|
+
"Creates new faces in the 3D model using SVG-style path notation. Each face must form a closed planar boundary - all points defining a single face MUST be coplanar (lie exactly on the same plane). IMPORTANT: When a new face intersects or overlaps with existing faces, Koomaster will automatically split those faces, which may cause the original face IDs to change or become invalid. After adding faces that might cause splits, use the 'find_face_by_points' tool to locate the resulting faces by specifying points that lie on them. ",
|
|
12
|
+
parameters: z.object({
|
|
13
|
+
faces: z
|
|
14
|
+
.array(
|
|
15
|
+
z.object({
|
|
16
|
+
path: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe('3D curve path defined using SVG path commands:\n- M x,y,z: Move to initial point\n- L x,y,z: Line to point\n- C cp1x,cp1y,cp1z cp2x,cp2y,cp2z endX,endY,endZ: Cubic Bezier curve with two control points\n- A midX,midY,midZ endX,endY,endZ: Arc defined by three points\n - Current position is the start point\n - midX,midY,midZ: A point ON THE ARC between start and end (NOT the center)\n - endX,endY,endZ: End point of the arc\n- Z: Close path\n\nIMPORTANT NOTES:\n1. The path MUST form a closed shape (use Z command at the end)\n2. All points MUST be coplanar (on the same plane)\n3. The arc command uses a custom 3-point format (not standard SVG arc)\n4. For arcs, the three points must not be collinear\n5. For a circle, use 3-4 arcs connecting points along the circumference\n\nEXAMPLES:\n- Square: "M 0,0,0 L 100,0,0 L 100,100,0 L 0,100,0 Z"\n- Rectangle with rounded corner: "M 0,0,0 L 100,0,0 L 100,80,0 A 90,90,0 80,100,0 L 0,100,0 Z"\n- Circle approximation: "M 100,0,0 A 70.7,70.7,0 0,100,0 A -70.7,70.7,0 -100,0,0 A -70.7,-70.7,0 0,-100,0 A 70.7,-70.7,0 100,0,0 Z"\n- Curved path: "M 0,0,0 C 30,10,0 60,30,0 100,0,0 L 100,100,0 L 0,100,0 Z"'),
|
|
19
|
+
})
|
|
20
|
+
)
|
|
21
|
+
.min(1)
|
|
22
|
+
.describe(
|
|
23
|
+
"An array of face definitions, each with an SVG-style path string that defines a closed, planar boundary. Multiple faces can be created in a single operation. Each created face will receive a unique ID that can be used in other operations."
|
|
24
|
+
),
|
|
25
|
+
}),
|
|
26
|
+
execute: async (args, context: Context): Promise<string> => {
|
|
27
|
+
if (!llmClient.hasConnectedKoomasterClients()) {
|
|
28
|
+
logger.warn("No clients connected when attempting to add faces");
|
|
29
|
+
return "No modeling software clients connected. Please start the software and connect to this server.";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const processedArgs = {
|
|
34
|
+
faces: args.faces.map(face => {
|
|
35
|
+
try {
|
|
36
|
+
// 解析SVG路径字符串
|
|
37
|
+
const pathCurves = parseSvgPath3D(face.path);
|
|
38
|
+
return {
|
|
39
|
+
// faceName: face.faceName,
|
|
40
|
+
path: pathCurves
|
|
41
|
+
};
|
|
42
|
+
} catch (parseError) {
|
|
43
|
+
throw new Error(`Error parsing SVG path for face "${face.path}": ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const result = await llmClient.sendCommand("add_faces", processedArgs);
|
|
49
|
+
|
|
50
|
+
return result;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
const errorMessage = `Error adding faces: ${e instanceof Error ? e.message : String(e)}`;
|
|
53
|
+
logger.error(errorMessage);
|
|
54
|
+
context.log.error(errorMessage);
|
|
55
|
+
return `Failed to create faces: ${e instanceof Error ? e.message : String(e)}. Common issues include:
|
|
56
|
+
- Non-coplanar points: All points defining a face MUST lie exactly on the same plane
|
|
57
|
+
- Path does not form a closed loop (ensure it ends with Z command)
|
|
58
|
+
- Invalid arc definition (check A command parameters)
|
|
59
|
+
- Self-intersecting boundaries
|
|
60
|
+
- Invalid SVG path syntax
|
|
61
|
+
- Missing or incorrect coordinates`;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
export const getFacePathTool = {
|
|
66
|
+
name: "get_face_path",
|
|
67
|
+
description:
|
|
68
|
+
"Retrieves the SVG-style path representation of a face's boundary. This tool extracts the geometric outline of a face and returns it as a path string that can be used for visualization or further operations. The path includes all curves that form the outer boundary of the face.",
|
|
69
|
+
parameters: z.object({
|
|
70
|
+
name: z
|
|
71
|
+
.string()
|
|
72
|
+
.describe(
|
|
73
|
+
'ID of the face to extract the path from. You can obtain this ID by: 1) Using get_selection with type="face" to get a user-selected face, 2) Creating a new face with add_faces and using the returned face ID, or 3) Using find_face_by_points tool to locate a face when model geometry has been modified or split.'
|
|
74
|
+
),
|
|
75
|
+
}),
|
|
76
|
+
execute: async (args, context: Context): Promise<string> => {
|
|
77
|
+
try {
|
|
78
|
+
if (!llmClient.hasConnectedKoomasterClients()) {
|
|
79
|
+
logger.warn(`No clients connected when attempting to get face path`);
|
|
80
|
+
throw new UserError("No modeling software clients connected. Please start the software and connect to this server.");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = await llmClient.sendCommand("get_face_path", args);
|
|
84
|
+
return result;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
const errorMessage = `Error retrieving face path: ${e instanceof Error ? e.message : String(e)
|
|
87
|
+
}`;
|
|
88
|
+
logger.error(errorMessage);
|
|
89
|
+
context.log.error(errorMessage);
|
|
90
|
+
return errorMessage;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const pullFacesTool = {
|
|
96
|
+
name: "pull_faces",
|
|
97
|
+
description:
|
|
98
|
+
"Performs a push/pull operation on one or more faces to create or modify 3D shapes. This tool allows you to extrude faces along their normal direction to add or remove volume from a model.",
|
|
99
|
+
parameters: z.object({
|
|
100
|
+
faceNames: z
|
|
101
|
+
.array(z.string())
|
|
102
|
+
.min(1)
|
|
103
|
+
.describe(
|
|
104
|
+
'Array of face names to push/pull. These should be the face identifiers that you want to extrude. You can get face names from previously created faces, by using tools that provide model information, or specifically by using the "find_face_by_points" tool to locate faces when model geometry has been modified or split.'
|
|
105
|
+
),
|
|
106
|
+
distance: z
|
|
107
|
+
.number()
|
|
108
|
+
.describe(
|
|
109
|
+
"The distance to push/pull the face(s), measured in model units. Use positive values to extrude outward (adding volume) or negative values to extrude inward (removing volume if possible)."
|
|
110
|
+
),
|
|
111
|
+
separator: z
|
|
112
|
+
.boolean()
|
|
113
|
+
.describe(
|
|
114
|
+
"Whether to keep the original face(s) unchanged. If true, a new face will be created at the end of the extrusion while preserving the original face. If false, the original face will be moved to the new position."
|
|
115
|
+
),
|
|
116
|
+
}),
|
|
117
|
+
execute: async (args, context: Context): Promise<string> => {
|
|
118
|
+
try {
|
|
119
|
+
if (!llmClient.hasConnectedKoomasterClients()) {
|
|
120
|
+
logger.warn(`No clients connected when attempting to pull faces`);
|
|
121
|
+
return "No modeling software clients connected. Please start the software and connect to this server.";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const result = await llmClient.sendCommand("pull_faces", args);
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
} catch (e) {
|
|
128
|
+
const errorMessage = `Error during push/pull operation: ${e instanceof Error ? e.message : String(e)
|
|
129
|
+
}`;
|
|
130
|
+
logger.error(errorMessage);
|
|
131
|
+
context.log.error(errorMessage);
|
|
132
|
+
return errorMessage;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Context } from "../types";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import logger from "../lib/logger";
|
|
4
|
+
import { llmClient } from "../llmClient";
|
|
5
|
+
|
|
6
|
+
export const getObjectsScreenshotTool = {
|
|
7
|
+
name: 'get_objects_screenshot',
|
|
8
|
+
description: 'Captures a screenshot of the specific objects from different view angles. Note: Only supports KJL models, Tripo3D models, and basic geometric shapes - face screenshots are not supported.',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
view_angle: z.enum(['front', 'back', 'left', 'right', 'top', 'bottom']).default('top')
|
|
11
|
+
.describe('Camera angle for the screenshot. Options: standard orthographic views (front, back, left, right, top, bottom) .'),
|
|
12
|
+
groupNames: z.array(z.string())
|
|
13
|
+
.describe('array of group names to capture.')
|
|
14
|
+
}),
|
|
15
|
+
execute: async (args, context: Context): Promise<any> => {
|
|
16
|
+
logger.info(`Tool executed: get_scene_screenshot with view_angle "${args.view_angle}"`);
|
|
17
|
+
try {
|
|
18
|
+
// Check if any modeling software clients are connected
|
|
19
|
+
if (!llmClient.hasConnectedKoomasterClients()) {
|
|
20
|
+
return "No modeling software clients connected. Please start the software and connect to this server.";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Capture the screenshot using getModelSnapshot
|
|
24
|
+
const result = await llmClient.sendCommand('get_objects_screenshot', args);
|
|
25
|
+
|
|
26
|
+
if (!result) {
|
|
27
|
+
return "Failed to capture screenshot.";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!result.startsWith("data:")) {
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let base64Data = result;
|
|
35
|
+
// Extract base64 data if it includes data URL format (data:image/png;base64,)
|
|
36
|
+
if (base64Data.includes(',')) {
|
|
37
|
+
base64Data = base64Data.split(',')[1];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "image",
|
|
44
|
+
data: base64Data,
|
|
45
|
+
mimeType: "image/png",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
} catch (e) {
|
|
50
|
+
const errorMessage = `Error capturing screenshot: ${e instanceof Error ? e.message : String(e)}`;
|
|
51
|
+
logger.error(errorMessage);
|
|
52
|
+
context.log.error(errorMessage);
|
|
53
|
+
return errorMessage;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const getSceneScreenshotTool = {
|
|
59
|
+
name: 'get_scene_screenshot',
|
|
60
|
+
description: 'Captures a screenshot of the entire 3D scene from the specified view angle. Note: the screenshot result may appear upside-down compared to the actual scene orientation.',
|
|
61
|
+
parameters: z.object({
|
|
62
|
+
view_angle: z.enum(['front', 'back', 'left', 'right', 'top', 'bottom', 'auto']).default('auto')
|
|
63
|
+
.describe('Camera angle for the screenshot. Standard orthographic views available, or select "auto" to automatically calculate an optimal angle that includes all objects in the scene. The "auto" view positions the camera at a diagonal angle (from the top-right-front quadrant) to provide a comprehensive overview of the scene.'),
|
|
64
|
+
}),
|
|
65
|
+
execute: async (args, context: Context): Promise<any> => {
|
|
66
|
+
logger.info(`Tool executed: get_scene_screenshot with view_angle "${args.view_angle}"`);
|
|
67
|
+
try {
|
|
68
|
+
// Check if any modeling software clients are connected
|
|
69
|
+
if (!llmClient.hasConnectedKoomasterClients()) {
|
|
70
|
+
return "No modeling software clients connected. Please start the software and connect to this server.";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Capture the screenshot using getModelSnapshot
|
|
74
|
+
const result = await llmClient.sendCommand('get_scene_screenshot', args);
|
|
75
|
+
|
|
76
|
+
if (!result) {
|
|
77
|
+
return "Failed to capture screenshot.";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!result.startsWith("data:")) {
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let base64Data = result;
|
|
85
|
+
// Extract base64 data if it includes data URL format (data:image/png;base64,)
|
|
86
|
+
if (base64Data.includes(',')) {
|
|
87
|
+
base64Data = base64Data.split(',')[1];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: "image",
|
|
94
|
+
data: base64Data,
|
|
95
|
+
mimeType: "image/png",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
} catch (e) {
|
|
100
|
+
const errorMessage = `Error capturing screenshot: ${e instanceof Error ? e.message : String(e)}`;
|
|
101
|
+
logger.error(errorMessage);
|
|
102
|
+
context.log.error(errorMessage);
|
|
103
|
+
return errorMessage;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import logger from "../lib/logger";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Context } from "../types";
|
|
4
|
+
import { llmClient } from "../llmClient";
|
|
5
|
+
import { MainStrategy } from "../prompt";
|
|
6
|
+
|
|
7
|
+
// 更高级的帮助工具 - 获取可用的工具列表和使用信息
|
|
8
|
+
export const getToolsInfoTool = {
|
|
9
|
+
name: "get_tools_info",
|
|
10
|
+
description:
|
|
11
|
+
"CRITICAL FIRST STEP: Returns comprehensive documentation about all available KooMaster(酷大师) tools, coordinate system specifications, measurement units, and best practices. Must be called before using any other functions to ensure proper model placement and prevent clipping issues. Provides essential information about the Z-up right-handed coordinate system and proper object positioning.",
|
|
12
|
+
parameters: z.object({}),
|
|
13
|
+
execute: async (_args: {}, _context: Context): Promise<string> => {
|
|
14
|
+
logger.info(`Tool executed: get_tools_info`);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// 组织工具信息
|
|
18
|
+
const toolsInfo = {
|
|
19
|
+
basic: [
|
|
20
|
+
"get_scene_info - Get detailed information about the current Koomaster scene",
|
|
21
|
+
"get_objects_info - Get detailed information about specific objects in the Koomaster scene",
|
|
22
|
+
"create_object - Create primitive objects (cube, sphere, etc.)",
|
|
23
|
+
"modify_object - Change object properties (location, rotation, scale)",
|
|
24
|
+
"delete_objects - Delete objects from the Koomaster scene by name",
|
|
25
|
+
"copy_object - Create a copy of an existing object with a new name and transform properties. Currently only supports copying \"group\" type objects (KJL models, Tripo3D models, and basic geometry)",
|
|
26
|
+
"get_selection - Get detailed information about the currently selected objects in the scene. Supports analyzing selected faces (with vertex coordinates and bounding boxes) or selected groups (with transform and hierarchy information).",
|
|
27
|
+
"get_objects_screenshot - Captures a screenshot of the specific objects from different view angles. Note: Only supports KJL models, Tripo3D models, and basic geometric shapes - face screenshots are not supported.",
|
|
28
|
+
"get_scene_screenshot - Captures a screenshot of the entire 3D scene from the specified view angle. Note: the screenshot result may appear upside-down compared to the actual scene orientation."
|
|
29
|
+
],
|
|
30
|
+
material: [
|
|
31
|
+
"get_material_names - Retrieves a list of all available material names from the connected koomaster client. This function queries the active connection to obtain the names of all materials that can be applied to 3D models. Returns the complete list of material names as a string, with materials typically separated by a delimiter. Useful for discovering what materials are available before applying them to specific models.",
|
|
32
|
+
"set_face_material - Applies a specific material to an existing face in the 3D model. This tool allows you to change the appearance of a face by assigning a different material to it.",
|
|
33
|
+
"set_group_material - Applies a specific material to an entire group of objects in the 3D model. This tool changes the appearance of all elements within the named group by assigning a different material to them.",
|
|
34
|
+
],
|
|
35
|
+
modeling: [
|
|
36
|
+
"add_faces - Creates new faces in the 3D model using SVG-style path notation. Each face must form a closed planar boundary - all points defining a single face MUST be coplanar (lie exactly on the same plane). IMPORTANT: When a new face intersects or overlaps with existing faces, Koomaster will automatically split those faces, which may cause the original face IDs to change or become invalid. After adding faces that might cause splits, use the 'find_face_by_points' tool to locate the resulting faces by specifying points that lie on them. TIP: If creating faces for sweep operations, consider creating them in empty space away from your main model to avoid unintended face splitting. During sweep operations, the source face will be automatically removed after the sweep completes.",
|
|
37
|
+
"find_face_by_points - Finds a face by specifying multiple points that lie on the face. By providing 1-5 points that lie on the target face, you can identify faces reliably even when topology changes occur. Returns the face's information including its ID, normal vector, area, and other properties.",
|
|
38
|
+
"get_face_path - Retrieves the SVG-style path representation of a face's boundary. This tool extracts the geometric outline of a face and returns it as a path string that can be used for visualization or further operations. The path includes all curves that form the outer boundary of the face.",
|
|
39
|
+
"pull_faces - Performs a push/pull operation on one or more faces to create or modify 3D shapes. This tool allows you to extrude faces along their normal direction to add or remove volume from a model.",
|
|
40
|
+
"create_auxiliary_curve - Creates an auxiliary 3D curve using SVG-like path commands with explicit 3D coordinates.",
|
|
41
|
+
"sweep - Creates a 3D solid by sweeping a FACE along auxiliary curves. NOTE: This tool operates ONLY on faces, not on entire objects or groups. The face can be either selected by the user or created programmatically. IMPORTANT: The face being swept (profile) must NOT be parallel to the auxiliary curves (path) - the sweep direction must have some component perpendicular to the face normal for the operation to succeed. After the sweep operation completes, the original face (profile) will be automatically deleted from the model.",
|
|
42
|
+
],
|
|
43
|
+
// tripo3d: [
|
|
44
|
+
// "get_tripo3d_status - Check if Tripo3D is configured and available for use by verifying the presence of required API keys and configuration",
|
|
45
|
+
// "create_tripo3d_job - Generate a 3D model using Fal.ai Tripo3D service from text or images",
|
|
46
|
+
// "poll_tripo3d_job_status - Check the status of a Tripo3D 3D model generation job",
|
|
47
|
+
// "import_generated_asset - Import a 3D model generated by Tripo3D into the scene",
|
|
48
|
+
// "poll_tripo3d_import_status - Query the progress status of importing a Tripo3D-generated 3D model into the Koomaster scene"
|
|
49
|
+
// ],
|
|
50
|
+
assets: [
|
|
51
|
+
"batch_search_kjl_assets - Performs a concurrent batch search for multiple 3D models and assets in the KJL(酷家乐) database. Returns a combined list of models that match each search term, including their names, IDs, categories, and dimensions. Use this when you need to search for multiple different types of models at once.",
|
|
52
|
+
"batch_get_kjl_asset_snapshot - Retrieves preview images for multiple KJL(酷家乐) asset models concurrently based on their model IDs. This helps visualize multiple assets before placing them in the scene. Use this tool to efficiently examine the appearance and style of several models at once to ensure they match your design requirements.",
|
|
53
|
+
// "search_kjl_assets - Searches for 3D models and assets in the KJL database based on a text description. Returns a list of models that match the search criteria, including their names, IDs, categories, and dimensions.",
|
|
54
|
+
// "get_kjl_asset_snapshot - Retrieves a preview image for a specific KJL(酷家乐) asset model based on its model ID. This helps visualize the asset before placing it in the scene. Use this tool to examine the appearance and style of a model to ensure it matches your design requirements.",
|
|
55
|
+
"place_kjl_asset - Places a 3D model from the KJL(酷家乐) asset library into the current scene with specified position, rotation, and scale. Before placing, analyze the model\'s bounding box dimensions to determine proper orientation - elongated objects like wall panels may require 90° rotation based on intended function. Ensure bottoms align with the ground plane (z=0) by setting z-value to half the model\'s height. After placement, verify no clipping with existing objects and confirm dimensions match real-world proportions.",
|
|
56
|
+
"replace_kjl_asset - Replaces an existing object in the scene with a new 3D model from the KJL(酷家乐) asset library. The replacement maintains the position, rotation, aligning the centers of both models. After replacement, review the scene to ensure no intersections or clipping with other objects."
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 连接状态信息
|
|
61
|
+
const connectionStatus = llmClient.hasConnectedKoomasterClients()
|
|
62
|
+
? `✅ Connected to Koomaster client`
|
|
63
|
+
: "❌ Not connected to Koomaster";
|
|
64
|
+
|
|
65
|
+
// 格式化工具信息
|
|
66
|
+
let result = `# KoomasterMCP Tools Overview\n\n`;
|
|
67
|
+
result += `${connectionStatus}\n\n`;
|
|
68
|
+
result += `## Basic Tools\n\n`;
|
|
69
|
+
toolsInfo.basic.forEach((tool) => {
|
|
70
|
+
result += `- ${tool}\n`;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
result += `\n## Assets Tools\n\n`;
|
|
74
|
+
toolsInfo.assets.forEach((tool) => {
|
|
75
|
+
result += `- ${tool}\n`;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
result += `\n## Tripo3D Integration\n\n`;
|
|
79
|
+
toolsInfo.tripo3d.forEach((tool) => {
|
|
80
|
+
result += `- ${tool}\n`;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
result += `\n## Modeling Tools\n\n`;
|
|
84
|
+
toolsInfo.modeling.forEach((tool) => {
|
|
85
|
+
result += `- ${tool}\n`;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
result += `\n## Material Tools\n\n`;
|
|
89
|
+
toolsInfo.material.forEach((tool) => {
|
|
90
|
+
result += `- ${tool}\n`;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
result += MainStrategy;
|
|
94
|
+
|
|
95
|
+
logger.debug(
|
|
96
|
+
`Generated tools info summary (${result.length} characters)`
|
|
97
|
+
);
|
|
98
|
+
return result;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
const errorMessage = `Error getting tools info: ${e instanceof Error ? e.message : String(e)
|
|
101
|
+
}`;
|
|
102
|
+
logger.error(errorMessage);
|
|
103
|
+
return `Error getting tools info: ${e instanceof Error ? e.message : String(e)
|
|
104
|
+
}`;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|