CAD-MCP Server
by daobataotie
- CAD-MCP
- src
import logging
import re
import math
from typing import Any, Dict, List, Optional, Tuple
import json
import os
import os.path
# 直接读取config.json文件
config_path = os.path.join(os.path.dirname(__file__), 'config.json')
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
logger = logging.getLogger('nlp_processor')
class NLPProcessor:
"""自然语言处理器类,负责解析用户指令并转换为CAD操作"""
def __init__(self):
"""
初始化NLP处理器
"""
self.logger = logging.getLogger(__name__)
self.logger.info("自然语言处理器已初始化")
# 详细颜色名称到RGB值的映射
self.color_rgb_map = {
# 基本颜色
"红色": 1,
"黄色": 2,
"绿色": 3,
"青色": 4,
"蓝色": 5,
"洋红色": 6,
"白色": 7,
"灰色": 8,
"浅灰色": 9,
"黑色": 250,
"棕色": 251,
"橙色": 30,
"紫色": 200,
# 基本颜色 - 英文键
"Red": 1,
"Yellow": 2,
"Green": 3,
"Cyan": 4,
"Blue": 5,
"Magenta": 6,
"White": 7,
"Gray": 8,
"Light Gray": 9,
"Black": 250,
"Brown": 251,
"Orange": 30,
"Purple": 200
}
# 扩展关键词映射
self.shape_keywords = {
# 基本形状
"直线": "line", "线": "line",
"圆": "circle", "圆形": "circle",
"弧": "arc", "圆弧": "arc",
"矩形": "rectangle", "方形": "rectangle", "正方形": "square",
"多段线": "polyline", "折线": "polyline",
"文本": "text", "文字": "text",
"标注": "dimension", "尺寸标注": "dimension",
# 建筑元素
"墙": "wall", "墙体": "wall",
"门": "door", "窗": "window",
"楼梯": "stair", "柱子": "column",
# 电气元素
"插座": "outlet",
"开关": "switch",
"灯": "light", "灯具": "light",
"配电箱": "distribution_box",
# 机械元素
"轴": "shaft",
"齿轮": "gear",
"轴承": "bearing",
"法兰": "flange"
}
# 动作关键词映射
self.action_keywords = {
# 基本动作
"画": "draw", "绘制": "draw", "创建": "draw", "添加": "draw",
"修改": "modify", "调整": "modify", "改变": "modify",
"移动": "move", "旋转": "rotate", "缩放": "scale",
"放大": "scale_up", "缩小": "scale_down",
"删除": "erase", "擦除": "erase", "移除": "erase",
"保存": "save",
# 专业操作
"标注": "dimension",
"填充": "hatch",
"创建图层": "create_layer",
"切换图层": "change_layer"
}
# # 专业领域映射
# self.domain_keywords = {
# "建筑": "architecture",
# "电气": "electrical",
# "机械": "mechanical",
# "土木": "civil",
# "管道": "piping",
# "结构": "structural"
# }
def extract_color_from_command(self, command: str) -> Optional[int]:
if command is None:
return 7
try:
num = int(command)
if num >= 1 and num <= 255:
return num
except:
pass
# 将命令转换为小写
command = command.lower()
# 尝试匹配颜色名称
for color_name in self.color_rgb_map.keys():
if color_name.lower() in command:
return self.color_rgb_map[color_name]
# 尝试匹配颜色描述(如"淡蓝色")
color_pattern = r'([深浅淡]?[a-zA-Z\u4e00-\u9fa5]+色)'
color_matches = re.findall(color_pattern, command)
for color_match in color_matches:
# 检查是否是已知的颜色名称
if color_match in self.color_rgb_map:
return self.color_rgb_map[color_match]
# 如果找不到颜色信息,返回7 默认白色
return 7
def process_command(self, command: str) -> Dict[str, Any]:
"""处理自然语言命令并返回结果"""
self.logger.info(f"处理命令: {command}")
# 解析命令
parsed_command = self.parse_command(command)
# 返回解析结果
return parsed_command
def parse_command(self, command: str) -> Dict[str, Any]:
"""解析自然语言命令并转换为CAD操作参数"""
self.logger.info(f"解析命令: {command}")
# 将命令转换为小写并去除多余空格
command = command.lower().strip()
# 尝试识别命令类型
command_type = self._identify_command_type(command)
self.logger.debug(f"识别到的命令类型: {command_type}")
# 根据命令类型分发到不同的处理函数
if command_type == "draw_line":
return self._parse_draw_line(command)
elif command_type == "draw_circle":
return self._parse_draw_circle(command)
elif command_type == "draw_arc":
return self._parse_draw_arc(command)
elif command_type == "draw_rectangle":
return self._parse_draw_rectangle(command)
elif command_type == "draw_polyline":
return self._parse_draw_polyline(command)
elif command_type == "draw_text":
return self._parse_draw_text(command)
elif command_type == "draw_hatch":
return self._parse_draw_hatch(command)
# elif command_type == "erase":
# return self._parse_erase(command)
# elif command_type == "move":
# return self._parse_move(command)
# elif command_type == "rotate":
# return self._parse_rotate(command)
# elif command_type == "scale":
# return self._parse_scale(command)
elif command_type == "save":
return self._parse_save(command)
elif command_type == "draw_wall":
return self._parse_draw_wall(command)
elif command_type == "create_layer":
return self._parse_create_layer(command)
# elif command_type == "draw_electrical_symbol":
# return self._parse_draw_electrical_symbol(command)
else:
# 默认返回一个错误结果
return {
"type": "unknown",
"error": "无法识别的命令类型",
"original_command": command
}
def _identify_command_type(self, command: str) -> str:
"""识别命令类型"""
# 首先检查专业领域
domain = None
for domain_keyword, domain_type in self.domain_keywords.items():
if domain_keyword in command:
domain = domain_type
break
# 检查操作类型
for action, action_type in self.action_keywords.items():
if action in command:
# 建筑领域特殊处理
# if domain == "architecture":
# for shape, shape_type in self.shape_keywords.items():
# if shape in command:
# if shape_type == "wall":
# return "draw_wall"
# elif shape_type == "door":
# return "draw_door"
# elif shape_type == "window":
# return "draw_window"
# # 电气领域特殊处理
# elif domain == "electrical":
# for shape, shape_type in self.shape_keywords.items():
# if shape in command:
# if shape_type in ["outlet", "switch", "light"]:
# return f"draw_electrical_{shape_type}"
# 基本形状处理
for shape, shape_type in self.shape_keywords.items():
if shape in command:
if action_type == "draw":
if shape_type == "line":
return "draw_line"
elif shape_type == "circle":
return "draw_circle"
elif shape_type == "arc":
return "draw_arc"
elif shape_type in ["rectangle", "square"]:
return "draw_rectangle"
elif shape_type == "polyline":
return "draw_polyline"
elif shape_type == "text":
return "draw_text"
elif shape_type == "dimension":
return "add_dimension"
# 检查是否是创建图层命令
if "图层" in command and any(action in command for action in ["创建", "新建", "添加"]):
return "create_layer"
# 检查是否是标注命令
if "标注" in command:
return "add_dimension"
# # 检查其他通用命令
# if any(word in command for word in ["删除", "擦除", "移除"]):
# return "erase"
# if any(word in command for word in ["移动", "挪动"]):
# return "move"
# if "旋转" in command:
# return "rotate"
# if any(word in command for word in ["缩放", "放大", "缩小"]):
# return "scale"
if "保存" in command:
return "save"
# 如果无法识别,返回未知类型
return "unknown"
def _extract_coordinates(self, text: str) -> List[Tuple[float, float, float]]:
"""从文本中提取坐标点"""
# 匹配坐标格式: (x,y,z) 或 (x,y) 或 x,y,z 或 x,y
pattern = r'\(?\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)(?:\s*,\s*(-?\d+\.?\d*))?\s*\)?'
matches = re.finditer(pattern, text)
coordinates = []
for match in matches:
x = float(match.group(1))
y = float(match.group(2))
z = float(match.group(3)) if match.group(3) else 0.0
coordinates.append((x, y, z))
return coordinates
def _extract_numbers(self, text: str) -> List[float]:
"""从文本中提取数字"""
pattern = r'(-?\d+\.?\d*)'
matches = re.findall(pattern, text)
return [float(match) for match in matches]
def _parse_draw_line(self, command: str) -> Dict[str, Any]:
"""解析绘制直线命令"""
# 尝试提取坐标
coordinates = self._extract_coordinates(command)
if len(coordinates) >= 2:
# 如果找到至少两个坐标点,使用前两个作为起点和终点
return {
"type": "draw_line",
"start_point": coordinates[0],
"end_point": coordinates[1]
}
else:
# 如果没有找到足够的坐标点,尝试使用默认值
# 这里可以根据需要设置默认的起点和终点
return {
"type": "draw_line",
"start_point": (0, 0, 0),
"end_point": (100, 100, 0),
"note": "使用默认坐标,因为命令中未提供足够的坐标信息"
}
def _parse_draw_circle(self, command: str) -> Dict[str, Any]:
"""解析绘制圆命令"""
# 尝试提取坐标和半径
coordinates = self._extract_coordinates(command)
numbers = self._extract_numbers(command)
# 提取半径
radius = None
radius_pattern = r'(?:半径|r|radius)[^\d]*?(-?\d+\.?\d*)'
radius_match = re.search(radius_pattern, command, re.IGNORECASE)
if radius_match:
radius = float(radius_match.group(1))
elif len(numbers) > 0:
# 如果没有明确指定半径,使用找到的第一个数字作为半径
radius = numbers[0]
else:
# 默认半径
radius = 50.0
# 提取中心点
center = None
if len(coordinates) > 0:
center = coordinates[0]
else:
# 默认中心点
center = (0, 0, 0)
return {
"type": "draw_circle",
"center": center,
"radius": radius
}
def _parse_draw_arc(self, command: str) -> Dict[str, Any]:
"""解析绘制圆弧命令"""
# 尝试提取坐标、半径和角度
coordinates = self._extract_coordinates(command)
numbers = self._extract_numbers(command)
# 提取中心点
center = None
if len(coordinates) > 0:
center = coordinates[0]
else:
# 默认中心点
center = (0, 0, 0)
# 提取半径
radius = None
radius_pattern = r'(?:半径|r|radius)[^\d]*?(-?\d+\.?\d*)'
radius_match = re.search(radius_pattern, command, re.IGNORECASE)
if radius_match:
radius = float(radius_match.group(1))
elif len(numbers) > 0:
# 如果没有明确指定半径,使用找到的第一个数字作为半径
radius = numbers[0]
else:
# 默认半径
radius = 50.0
# 提取起始角度和结束角度
start_angle = 0.0
end_angle = 90.0
start_angle_pattern = r'(?:起始角度|start angle)[^\d]*?(-?\d+\.?\d*)'
start_angle_match = re.search(start_angle_pattern, command, re.IGNORECASE)
if start_angle_match:
start_angle = float(start_angle_match.group(1))
end_angle_pattern = r'(?:结束角度|end angle)[^\d]*?(-?\d+\.?\d*)'
end_angle_match = re.search(end_angle_pattern, command, re.IGNORECASE)
if end_angle_match:
end_angle = float(end_angle_match.group(1))
# 确保所有必要参数都有值
if center is None:
center = (0, 0, 0)
if radius is None:
radius = 50.0
if start_angle is None:
start_angle = 0.0
if end_angle is None:
end_angle = 90.0
self.logger.debug(f"解析圆弧命令结果: 中心点={center}, 半径={radius}, 起始角度={start_angle}, 结束角度={end_angle}")
return {
"type": "draw_arc",
"center": center,
"radius": radius,
"start_angle": start_angle,
"end_angle": end_angle
}
def _parse_draw_rectangle(self, command: str) -> Dict[str, Any]:
"""解析绘制矩形命令"""
# 尝试提取坐标
coordinates = self._extract_coordinates(command)
if len(coordinates) >= 2:
# 如果找到至少两个坐标点,使用前两个作为对角点
return {
"type": "draw_rectangle",
"corner1": coordinates[0],
"corner2": coordinates[1]
}
else:
# 如果没有找到足够的坐标点,尝试提取宽度和高度
width = 100.0
height = 100.0
width_pattern = r'(?:宽度|width)[^\d]*?(-?\d+\.?\d*)'
width_match = re.search(width_pattern, command, re.IGNORECASE)
if width_match:
width = float(width_match.group(1))
height_pattern = r'(?:高度|height)[^\d]*?(-?\d+\.?\d*)'
height_match = re.search(height_pattern, command, re.IGNORECASE)
if height_match:
height = float(height_match.group(1))
# 如果找到一个坐标点,使用它作为起点
if len(coordinates) == 1:
corner1 = coordinates[0]
corner2 = (corner1[0] + width, corner1[1] + height, corner1[2])
else:
# 默认起点和终点
corner1 = (0, 0, 0)
corner2 = (width, height, 0)
return {
"type": "draw_rectangle",
"corner1": corner1,
"corner2": corner2
}
def _parse_draw_polyline(self, command: str) -> Dict[str, Any]:
"""解析绘制多段线命令"""
# 尝试提取坐标
coordinates = self._extract_coordinates(command)
# 检查是否需要闭合
closed = "闭合" in command or "封闭" in command
if len(coordinates) >= 2:
# 如果找到至少两个坐标点,使用它们作为多段线的点
return {
"type": "draw_polyline",
"points": coordinates,
"closed": closed
}
else:
# 如果没有找到足够的坐标点,返回错误
return {
"type": "error",
"message": "绘制多段线需要至少两个坐标点"
}
def _parse_draw_text(self, command: str) -> Dict[str, Any]:
"""解析绘制文本命令"""
# 尝试提取坐标
coordinates = self._extract_coordinates(command)
# 提取文本内容
text_pattern = r'[文本内容|text|内容][::]\s*[\"\'](.*?)[\"\']'
text_match = re.search(text_pattern, command)
text = ""
if text_match:
text = text_match.group(1)
else:
# 尝试提取引号中的内容作为文本
quote_pattern = r'[\"\'](.*?)[\"\']'
quote_match = re.search(quote_pattern, command)
if quote_match:
text = quote_match.group(1)
else:
# 如果没有找到引号中的内容,使用默认文本
text = "示例文本"
# 提取文本高度
height = 2.5 # 默认高度
height_pattern = r'(?:高度|height)[^\d]*?(-?\d+\.?\d*)'
height_match = re.search(height_pattern, command, re.IGNORECASE)
if height_match:
height = float(height_match.group(1))
# 提取旋转角度
rotation = 0.0 # 默认角度
rotation_pattern = r'(?:旋转|角度|rotation)[^\d]*?(-?\d+\.?\d*)'
rotation_match = re.search(rotation_pattern, command, re.IGNORECASE)
if rotation_match:
rotation = float(rotation_match.group(1))
# 提取插入点
position = None
if len(coordinates) > 0:
position = coordinates[0]
else:
# 默认插入点
position = (0, 0, 0)
return {
"type": "draw_text",
"position": position,
"text": text,
"height": height,
"rotation": rotation
}
def _parse_draw_hatch(self, command: str) -> Dict[str, Any]:
"""解析绘制填充命令"""
# 尝试提取坐标点集
coordinates = self._extract_coordinates(command)
# 提取填充图案名称
pattern_name = "SOLID" # 默认为实体填充
pattern_patterns = [
r'(?:图案|pattern)[^\w]*?["\'](.*?)["\']\'',
r'(?:图案|pattern)[^\w]*?(\w+)'
]
for pattern in pattern_patterns:
pattern_match = re.search(pattern, command, re.IGNORECASE)
if pattern_match:
pattern_name = pattern_match.group(1).upper()
break
# 提取填充比例
scale = 1.0 # 默认比例
scale_pattern = r'(?:比例|缩放|scale)[^\d]*?(\d+\.?\d*)'
scale_match = re.search(scale_pattern, command, re.IGNORECASE)
if scale_match:
scale = float(scale_match.group(1))
# 检查是否有足够的点来定义填充边界
if len(coordinates) >= 3:
self.logger.debug(f"解析填充命令结果: 点集={coordinates}, 图案={pattern_name}, 比例={scale}")
return {
"type": "draw_hatch",
"points": coordinates,
"pattern_name": pattern_name,
"scale": scale
}
else:
# 如果没有足够的点,返回错误信息
self.logger.warning("填充命令解析失败: 需要至少3个点来定义填充边界")
return {
"type": "error",
"message": "绘制填充需要至少3个点来定义边界"
}
# def _parse_erase(self, command: str) -> Dict[str, Any]:
# """解析删除命令"""
# # 尝试提取实体ID
# entity_id_pattern = r'(?:ID|编号|实体)[^\w]*?([a-fA-F0-9]+)'
# entity_id_match = re.search(entity_id_pattern, command, re.IGNORECASE)
# if entity_id_match:
# entity_id = entity_id_match.group(1)
# return {
# "type": "erase",
# "entity_id": entity_id
# }
# else:
# # 如果没有找到实体ID,返回错误
# return {
# "type": "error",
# "message": "删除命令需要指定实体ID"
# }
# def _parse_move(self, command: str) -> Dict[str, Any]:
# """解析移动命令"""
# # 尝试提取实体ID
# entity_id_pattern = r'(?:ID|编号|实体)[^\w]*?([a-fA-F0-9]+)'
# entity_id_match = re.search(entity_id_pattern, command, re.IGNORECASE)
# # 尝试提取坐标
# coordinates = self._extract_coordinates(command)
# if entity_id_match and len(coordinates) >= 2:
# entity_id = entity_id_match.group(1)
# from_point = coordinates[0]
# to_point = coordinates[1]
# return {
# "type": "move",
# "entity_id": entity_id,
# "from_point": from_point,
# "to_point": to_point
# }
# else:
# # 如果没有找到足够的信息,返回错误
# return {
# "type": "error",
# "message": "移动命令需要指定实体ID和两个坐标点"
# }
# def _parse_rotate(self, command: str) -> Dict[str, Any]:
# """解析旋转命令"""
# # 尝试提取实体ID
# entity_id_pattern = r'(?:ID|编号|实体)[^\w]*?([a-fA-F0-9]+)'
# entity_id_match = re.search(entity_id_pattern, command, re.IGNORECASE)
# # 尝试提取坐标
# coordinates = self._extract_coordinates(command)
# # 提取旋转角度
# rotation = 0.0 # 默认角度
# rotation_pattern = r'(?:旋转|角度|rotation)[^\d]*?(-?\d+\.?\d*)'
# rotation_match = re.search(rotation_pattern, command, re.IGNORECASE)
# if rotation_match:
# rotation = float(rotation_match.group(1))
# if entity_id_match and len(coordinates) >= 1:
# entity_id = entity_id_match.group(1)
# base_point = coordinates[0]
# return {
# "type": "rotate",
# "entity_id": entity_id,
# "base_point": base_point,
# "rotation_angle": rotation
# }
# else:
# # 如果没有找到足够的信息,返回错误
# return {
# "type": "error",
# "message": "旋转命令需要指定实体ID、基点和旋转角度"
# }
# def _parse_scale(self, command: str) -> Dict[str, Any]:
"""解析缩放命令"""
# 尝试提取实体ID
entity_id_pattern = r'(?:ID|编号|实体)[^\w]*?([a-fA-F0-9]+)'
entity_id_match = re.search(entity_id_pattern, command, re.IGNORECASE)
# 尝试提取坐标
coordinates = self._extract_coordinates(command)
# 提取缩放比例
scale_factor = 1.0 # 默认比例
scale_pattern = r'(?:比例|缩放|scale)[^\d]*?(-?\d+\.?\d*)'
scale_match = re.search(scale_pattern, command, re.IGNORECASE)
if scale_match:
scale_factor = float(scale_match.group(1))
elif "放大" in command:
scale_factor = 2.0
elif "缩小" in command:
scale_factor = 0.5
if entity_id_match and len(coordinates) >= 1:
entity_id = entity_id_match.group(1)
base_point = coordinates[0]
return {
"type": "scale",
"entity_id": entity_id,
"base_point": base_point,
"scale_factor": scale_factor
}
else:
# 如果没有找到足够的信息,返回错误
return {
"type": "error",
"message": "缩放命令需要指定实体ID、基点和缩放比例"
}
def _parse_save(self, command: str) -> Dict[str, Any]:
"""解析绘制墙体命令"""
# 尝试提取坐标
coordinates = self._extract_coordinates(command)
# 提取墙体宽度
width = 10.0 # 默认宽度
width_pattern = r'(?:宽度|宽|厚|厚度)[^\d]*?(\d+\.?\d*)'
width_match = re.search(width_pattern, command)
if width_match:
width = float(width_match.group(1))
if len(coordinates) >= 2:
start_point = coordinates[0]
end_point = coordinates[1]
return {
"type": "draw_wall",
"start_point": start_point,
"end_point": end_point,
"width": width
}
else:
# 没有足够的坐标信息,返回错误
return {
"type": "error",
"message": "绘制墙体需要提供起点和终点坐标"
}
def _parse_create_layer(self, command: str) -> Dict[str, Any]:
"""解析保存命令"""
# 尝试提取文件路径
path_pattern = r'(?:路径|保存到|path)[^\w]*?[\"\'](.*?)[\"\']'
path_match = re.search(path_pattern, command, re.IGNORECASE)
if path_match:
file_path = path_match.group(1)
else:
# 默认文件路径
file_path = os.path.join(config["output"]["directory"], config["output"]["default_filename"])
return {
"type": "save",
"file_path": file_path
}
# def _parse_draw_wall(self, command: str) -> Dict[str, Any]:
# """解析创建图层命令"""
# # 尝试提取图层名称
# layer_name_pattern = r'(?:名称|名字|叫)[^\w]*?[\"\'](.*?)[\"\']'
# layer_name_match = re.search(layer_name_pattern, command)
# if not layer_name_match:
# # 尝试另一种模式
# layer_name_pattern = r'图层[^\w]*?[\"\'](.*?)[\"\']'
# layer_name_match = re.search(layer_name_pattern, command)
# # 提取颜色
# color = 7 # 默认白色
# # 基本颜色映射(CAD颜色索引)
# color_map = {
# "红": 1, "红色": 1,
# "黄": 2, "黄色": 2,
# "绿": 3, "绿色": 3,
# "青": 4, "青色": 4,
# "蓝": 5, "蓝色": 5,
# "洋红": 6, "洋红色": 6, "紫": 6, "紫色": 6,
# "白": 7, "白色": 7,
# "灰": 8, "灰色": 8
# }
# for color_name, color_code in color_map.items():
# if color_name in command:
# color = color_code
# break
# # 尝试提取RGB颜色值
# rgb_color = self.extract_color_from_command(command)
# if rgb_color is not None:
# # 如果找到了RGB颜色值,优先使用它
# color = rgb_color
# if layer_name_match:
# layer_name = layer_name_match.group(1)
# return {
# "type": "create_layer",
# "layer_name": layer_name,
# "color": color
# }
# else:
# # 如果没有找到图层名称,尝试使用默认名称
# return {
# "type": "create_layer",
# "layer_name": "新图层",
# "color": color,
# "note": "使用默认图层名称,因为命令中未提供图层名称"
# }
# def _parse_draw_electrical_symbol(self, command: str) -> Dict[str, Any]:
# """解析绘制电气符号命令"""
# # 尝试提取坐标
# coordinates = self._extract_coordinates(command)
# # 提取缩放比例
# scale = 1.0 # 默认比例
# scale_pattern = r'(?:比例|缩放|scale)[^\d]*?(\d+\.?\d*)'
# scale_match = re.search(scale_pattern, command, re.IGNORECASE)
# if scale_match:
# scale = float(scale_match.group(1))
# # 提取旋转角度
# rotation = 0.0 # 默认角度
# rotation_pattern = r'(?:旋转|角度|rotation)[^\d]*?(-?\d+\.?\d*)'
# rotation_match = re.search(rotation_pattern, command, re.IGNORECASE)
# if rotation_match:
# rotation = float(rotation_match.group(1))
# if len(coordinates) >= 1:
# insertion_point = coordinates[0]
# return {
# "type": "draw_electrical_symbol",
# "insertion_point": insertion_point,
# "scale": scale,
# "rotation": rotation
# }
# else:
# # 没有坐标信息,返回错误
# return {
# "type": "error",
# }
ID: lnxlujxe3f