Skill: wangjiaocheng/cad-editor (version 1.0.0)

--- name: cad-editor author: 王教成 Wang Jiaocheng (波动几何) description: > CAD制图编辑器 — 用自然语言生成工程图纸（建筑平面图/机械零件/电气布置/管道系统/结构详图）。 支持DXF文件创建、渲染预览、批量导出。触发词：画平面图、CAD、工程图、建筑图、机械图、电气图、 管道图、结构图、画线/圆/矩形/多边形、尺寸标注、DXF、AutoCAD、施工图、配筋图、齿轮、轴承、 阀门、弯头、三通、法兰、门窗、楼梯、墙体、梁板柱基础、螺栓螺母弹簧垫圈销钉键槽、 开关插座灯具配电箱断路器导线组、直管弯头阀门法兰大小头管帽。 --- # CAD Editor Skill 将自然语言指令转换为工程图纸（DXF + PNG/SVG/PDF）。覆盖建筑、机械、电气、管道、结构五大领域， 遵循 GB/T 制图标准。 ## 触发条件 当用户要求绘制 CAD 图纸、工程图、施工图、配筋图，或提及以下任一领域时使用本技能： - 建筑图纸：平面图、立面图、剖面图、门窗、楼梯、墙体、梁板柱基础 - 机械图纸：螺栓、齿轮、轴承、弹簧、垫圈、螺母、销钉、键槽 - 电气图纸：开关、插座、灯具、配电箱、断路器、导线组 - 管道图纸：直管、弯头、三通、法兰、阀门、大小头、管帽 - 结构详图：梁断面、楼板配筋、条形基础、独立基础 - 通用几何：线、圆、弧、矩形、多边形、尺寸标注 ## 工作流 ### 步骤 1：解析自然语言指令 调用 NL 解析器识别意图并提取参数： ```python import sys sys.path.insert(0, 'cad-editor/scripts') from nl_parser.intent_classifier import IntentClassifier from nl_parser.param_extractor import ParamExtractor classifier = IntentClassifier() intent = classifier.classify(user_input) # → intent_dict extractor = ParamExtractor(intent['domain']) params = extractor.extract(user_input) # → param_dict ``` `references/intent_templates.json` 包含 52 条预定义意图映射模板，涵盖 6 大类图纸。 ### 步骤 2：生成绘图脚本 根据意图类型调用脚本生成器： ```python from nl_parser.script_generator import ScriptGenerator gen = ScriptGenerator() script_code = gen.generate(intent, params) # → Python str ``` 生成器内置 18 套脚本模板，按领域分： | 领域 | 模板数 | 覆盖组件 | |---|---|---| | architectural | 5 | 墙/门/窗/柱/楼梯 | | mechanical | 4 | 螺栓/齿轮/轴承/弹簧 | | electrical | 3 | 开关/插座/灯具/导线组 | | piping | 3 | 直管/弯头/三通/阀门 | | structural | 3 | 梁断面/楼板/基础 | ### 步骤 3：执行脚本生成 DXF ```python # 将 script_code 写入临时文件后执行，或直接 exec exec(script_code) # 输出：{output_dir}/{name}.dxf ``` ### 步骤 4：渲染预览图片 ```python from core.renderer import Renderer Renderer.render_quick(doc, output_dir='output', name='drawing_name') # 输出：{output_dir}/drawing_name.png (自动生成) ``` ### 步骤 5：交付结果 向用户展示 PNG 预览图，同时提供 DXF 文件路径。 ## 直接 API 调用方式（跳过 NL 解析） 当需要精确控制或编程调用时，直接使用 Python API： ```python import sys sys.path.insert(0, 'cad-editor/scripts') from core.document import CADDocument from core.renderer import Renderer from layer.manager import LayerManager from layer.linetypes import Linetypes from entities import * from templates.architectural import ArchitecturalTemplates from dimension import LinearDimension from layout.paperspace import PaperSpace doc = CADDocument.new(version='R2010') msp = doc.modelspace() Linetypes.load_standard(doc) LayerManager(doc).setup_template('arch') ArchitecturalTemplates.wall(msp, [(0,0), (5000,0), (5000,3500), (0,3500)], thickness=240) ArchitecturalTemplates.door_single(msp, (2500, 0), width=900) ArchitecturalTemplates.window(msp, (800, 240), (2000, 240)) LinearDimension.chain_horizontal(msp, [(0,-600),(0,0),(5000,0),(5000,-600)], offset=800) PaperSpace.draw_title_block(msp, (0,0), size='A3', title='图纸标题') CADDocument.save(doc, 'output/drawing.dxf') Renderer.render_quick(doc, output_dir='output', name='drawing') ``` ## 支持的图纸类型与指令示例 ### 建筑制图 (GB/T) | 指令示例 | 生成内容 | |---|---| | `画一个4000x3000的建筑平面图` | 外墙+门+窗+标注+图框 | | `3600x4800房间开一扇900宽的门两扇1200的窗` | 指定尺寸的户型 | | `画一个双开门1500mm` | 双扇平开 | | `12步直跑楼梯宽度1200` | 楼梯平面+折断线+方向箭头 | ### 机械制图 (GB/T) | 指令示例 | 生成内容 | |---|---| | `M16六角螺栓头俯视图` | 六角头+内切圆+中心十字线 | | `20齿齿轮端面视图D100` | 齿顶圆+齿根圆+轴孔+径向齿形线 | | `轴承6205侧面图` | 内外圈+滚动体 | | `压缩弹簧线径2外径16有效圈数6` | 锯齿形侧视图 | ### 电气制图 | 指令示例 | 生成内容 | |---|---| | `单极开关符号垂直放置` | 圆触点+倾斜动触杆+引线 | | `三孔电源插座` | 半圆弧+底边+孔位 | | `吸顶灯符号` | 圆形灯具标记 | | `三相导线组间距30mm` | 三条平行导线 | ### 管道/暖通 | 指令示例 | 生成内容 | |---|---| | `DN50直管段带中心线` | 双线管+轴线 | | `90度弯头R100 DN50东北走向` | 双线圆弧弯头 | | `闸阀DN50` | 阀体+手轮 | | `三通DN50/DN30` | 主管+支管接头 | ### 结构详图 | 指令示例 | 生成内容 | |---|---| | `250x500梁断面配3根16底部钢筋` | 截面轮廓+箍筋+纵筋+混凝土填充 | | `楼板120厚双向配筋12@150/10@200` | 板轮廓+钢筋线+弯钩+标注 | | `500x500柱下独立基础2000x2000深600` | 台阶式基础+受力筋+分布筋+标注 | ### 通用几何 | 指令示例 | 生成内容 | |---|---| | `画一个矩形400x300` | 矩形 | | `正六边形外接圆半径100` | 正多边形 | | `圆心(0,0)半径50的圆和直径80的同心圆` | 同心圆组 | ## 输出格式 | 格式 | 用途 | |---|---| | `.dxf` | 主输出，可导入 AutoCAD / FreeCAD / 中望CAD | | `.png` | 快速预览（自动生成） | | `.svg` | 矢量可缩放预览 | | `.pdf` | 打印/交付文档 | ## 依赖库 | 库 | 用途 | 安装 | |---|---|---| | `ezdxf` | DXF 读写引擎 | 必须 | | `matplotlib` | PNG/SVG/PDF 渲染后端 | 必须 | | `numpy` | 数值计算 | 必须 | 安装命令：`pip install ezdxf matplotlib numpy` ## 架构与文件组织 ``` cad-editor/ ├── SKILL.md # 本文件（技能说明书） ├── scripts/ # 可执行代码 │ ├── nl_parser/ # 自然语言解析器 │ │ ├── intent_classifier.py # 意图分类（19 种规则） │ │ ├── param_extractor.py # 参数提取（5 大领域） │ │ └── script_generator.py # 脚本生成（18 套模板） │ ├── core/ # 文档创建 / 单位管理 / 渲染引擎 │ ├── entities/ # 基础实体（线/圆/弧/多段线/文字） │ ├── layer/ # 图层管理 / 线型加载 / 行业图层模板 │ ├── dimension/ # 标注（线性/径向/角度/引线） │ ├── block/ # 图块定义与插入 │ ├── hatch/ # 图案填充（混凝土/砖/金属等） │ ├── tools/ # 编辑工具（偏移/裁剪/阵列/镜像/圆角倒角/测量） │ ├── layout/ # 图纸空间 / 视口 / 图框标题栏 │ ├── export/ # 批量导出 SVG/PDF/PNG │ └── templates/ # 行业图库组件 │ ├── architectural.py # 建筑（墙/门/窗/柱/楼梯/阳台） │ ├── mechanical.py # 机械（螺栓/齿轮/轴承/弹簧/键槽/垫圈/螺母/销） │ ├── electrical.py # 电气（开关/插座/灯具/导线/配电箱/断路器） │ ├── piping.py # 管道（直管/弯头/三通/阀门/法兰/大小头/管帽） │ └── structural.py # 结构（梁断面/楼板配筋/条基/独立基础） ├── references/ # 参考文档（按需加载） │ ├── color_index.md # ACI 颜色表(256色) + 行业配色规范 │ ├── layer_standards.md # GB/T 图层命名规范 │ ├── dxf_entity_codes.md # DXF 组码速查 + ezdxf API 映射 │ └── intent_templates.json # 52 条 NL→参数映射模板 └── assets/ # 输出资源（不加载到上下文） ├── fonts/ # CAD 字体文件 ├── hatch_patterns/ # 自定义填充图案 ├── linetypes/ # 线型定义 └── title_blocks/ # 图框标题栏模板（A0-A4） ``` ## 参考文档索引 | 文档 | 内容 | 加载时机 | |---|---|---| | `references/color_index.md` | ACI 256 色表 + 建筑/机械/电气/管道/结构配色方案 | 需要设置颜色时 | | `references/layer_standards.md` | GB/T 17825 图层命名规范，含 5 大行业预设 | 创建文档/设置图层时 | | `references/dxf_entity_codes.md` | DXF 组码速查表 + ezdxf Python API 对照 | 操作底层实体时 | | `references/intent_templates.json` | 52 条意图模板（NL 正则匹配→参数提取规则）| NL 解析阶段自动读取 |

path: scripts/tools/array.py
content: """阵列工具 - 矩形阵列和环形阵列"""
import math
from typing import List, Tuple


class ArrayTool:
    """阵列操作"""

    @staticmethod
    def rectangular(msp,
                   base_entities_func,
                   base_point: tuple,
                   rows: int = 1,
                   cols: int = 1,
                   row_spacing: float = 100,
                   col_spacing: float = 100):
        """
        矩形阵列
        
        Args:
            base_entities_func: callable(msp, insert_point) -> [实体列表]
                                在给定位置绘制一组基础图元的函数
            base_point: 阵列起始位置（左下角）
            rows: 行数
            cols: 列数
            row_spacing: 行间距（Y方向）
            col_spacing: 列间距（X方向）
        
        Returns:
            所有生成的实体的扁平列表
        """
        all_results = []
        bx, by = base_point
        
        for r in range(rows):
            for c in range(cols):
                px = bx + c * col_spacing
                py = by + r * row_spacing
                entities = base_entities_func(msp, (px, py))
                if entities:
                    all_results.extend(entities)
                    
        return all_results

    @staticmethod
    def polar(msp,
              base_entities_func,
              center: tuple,
              count: int,
              radius: float,
              start_angle: float = 0,
              total_angle: float = 360):
        """
        环形阵列（极坐标阵列）
        
        Args:
            center: 阵列中心点
            count: 实例数量（含原始位置）
            radius: 分布半径
            start_angle: 起始角度（度），0=右侧水平
            total_angle: 总覆盖角度（度），360=整圆
        """
        all_results = []
        cx, cy = center
        
        for i in range(count):
            angle_deg = start_angle + (total_angle * i / max(count-1, 1))
            angle_rad = math.radians(angle_deg)
            
            px = cx + radius * math.cos(angle_rad)
            py = cy + radius * math.sin(angle_rad)
            
            entities = base_entities_func(msp, (px, py))
            if entities:
                all_results.extend(entities)
                
        return all_results

    @staticmethod
    def path_array(msp,
                   base_entities_func,
                   path_points: list,
                   spacing: float = 50):
        """
        沿路径阵列
        
        Args:
            path_points: 路径控制点 [(x,y), ...]
            spacing: 实例之间的距离
        """
        all_results = []
        
        # 计算路径总长度
        total_len = 0
        segments = []
        for i in range(len(path_points)-1):
            dx = path_points[i+1][0] - path_points[i][0]
            dy = path_points[i+1][1] - path_points[i][1]
            seg_len = math.sqrt(dx*dx + dy*dy)
            segments.append((path_points[i], path_points[i+1], seg_len))
            total_len += seg_len
            
        # 沿路径放置实例
        dist = 0
        while dist <= total_len:
            # 找当前距离所在的线段
            accum = 0
            for p1, p2, slen in segments:
                if accum + slen >= dist:
                    t = (dist - accum) / max(slen, 1e-10)
                    px = p1[0] + t*(p2[0]-p1[0])
                    py = p1[1] + t*(p2[1]-p1[1])
                    entities = base_entities_func(msp, (px, py))
                    if entities:
                        all_results.extend(entities)
                    break
                accum += slen
                    
            dist += spacing
            
        return all_results


path: scripts/tools/fillet_chamfer.py
content: """圆角与倒角工具"""
import math
from typing import Tuple


class FilletChamfer:
    """圆角和倒角"""

    @staticmethod
    def fillet_two_lines(msp,
                         p_common: tuple,
                         p1_end: tuple,
                         p2_end: tuple,
                         radius: float,
                         layer='0', color=7):
        """
        两条线的公共顶点处做圆角（简化版：用弧替换尖角）
        
        Args:
            p_common: 两条线的公共交点
            p1_end: 第一条线的另一端
            p2_end: 第二条线的另一端
            radius: 圆角半径
        """
        from entities.curves import CurveEntities
        from entities.basic import BasicEntities
        
        # 计算两边的方向向量
        v1 = (p1_end[0] - p_common[0], p1_end[1] - p_common[1])
        v2 = (p2_end[0] - p_common[0], p2_end[1] - p_common[1])
        
        l1 = math.sqrt(v1[0]**2 + v1[1]**2)
        l2 = math.sqrt(v2[0]**2 + v2[1]**2)
        
        if l1 < 1e-6 or l2 < 1e-6 or radius < 1e-6:
            return None
            
        u1 = (v1[0]/l1, v1[1]/l1)
        u2 = (v2[0]/l2, v2[1]/l2)
        
        # 两向量的夹角
        dot = u1[0]*u2[0] + u1[1]*u2[1]
        angle = math.acos(max(-1, min(1, dot)))
        
        # 圆弧切点距公共点的距离
        tangent_dist = radius / math.tan(angle / 2)
        
        # 切点坐标
        t1 = (p_common[0] + u1[0] * tangent_dist, p_common[1] + u1[1] * tangent_dist)
        t2 = (p_common[0] + u2[0] * tangent_dist, p_common[1] + u2[1] * tangent_dist)
        
        # 圆弧中心（沿角平分线方向）
        bisect_x = u1[0] + u2[0]
        bisect_y = u1[1] + u2[1]
        bl = math.sqrt(bisect_x**2 + bisect_y**2)
        if bl < 1e-6:
            return None
            
        center_dist = radius / math.sin(angle / 2)
        cx = p_common[0] + (bisect_x/bl) * center_dist
        cy = p_common[1] + (bisect_y/bl) * center_dist
        
        # 确定圆弧起止角度
        a1 = math.degrees(math.atan2(t1[1]-cy, t1[0]-cx))
        a2 = math.degrees(math.atan2(t2[1]-cy, t2[0]-cx))
        
        # 绘制圆弧
        arc = CurveEntities.arc(msp, (cx, cy), radius, a1, a2, 
                                layer=layer, color=color)
        
        return {
            'arc': arc,
            'tangent_1': t1,
            'tangent_2': t2,
            'center': (cx, cy),
        }

    @staticmethod
    def chamfer_two_lines(p_common: tuple, p1_end: tuple, p2_end: tuple,
                          distance_a: float, distance_b: float = None):
        """
        两条线的公共顶点处做倒角
        
        Args:
            distance_a: 第一条线上的倒角距离
            distance_b: 第二条线上的倒角距离（None=等于a）
        
        Returns:
            (倒角点1, 倒角点2)
        """
        if distance_b is None:
            distance_b = distance_a
            
        import math
        
        v1 = (p1_end[0] - p_common[0], p1_end[1] - p_common[1])
        v2 = (p2_end[0] - p_common[0], p2_end[1] - p_common[1])
        
        l1 = math.sqrt(v1[0]**2 + v1[1]**2)
        l2 = math.sqrt(v2[0]**2 + v2[1]**2)
        
        if l1 < 1e-6 or l2 < 1e-6:
            return (None, None)
            
        c1 = (p_common[0] + v1[0]/l1 * distance_a,
              p_common[1] + v1[1]/l1 * distance_a)
        c2 = (p_common[0] + v2[0]/l2 * distance_b,
              p_common[1] + v2[1]/l2 * distance_b)
              
        return (c1, c2)

    @staticmethod
    def chamfer_polygon(msp, points: list, distance: float,
                        layer='0', color=7):
        """
        多边形所有顶点做倒角
        
        Returns:
            倒角后的新顶点列表
        """
        from entities.basic import BasicEntities
        
        n = len(points)
        new_points = []
        
        for i in range(n):
            prev_pt = points[i-1]
            curr_pt = points[i]
            next_pt = points[(i+1) % n]
            
            c1, c2 = FilletChamfer.chamfer_two_lines(
                curr_pt, prev_pt, next_pt, distance, distance
            )
            new_points.append(c2 if c2 else curr_pt)
            
        # 画倒角后的轮廓
        from entities.polyline import PolylineEntity
        return PolylineEntity.polygon(msp, new_points, layer=layer, color=color)


path: scripts/tools/measure.py
content: """测量工具 - 长度、面积、距离、角度"""
import math


class MeasureTool:
    """几何测量"""

    @staticmethod
    def distance(p1: tuple, p2: tuple) -> float:
        """两点间距离"""
        return math.sqrt((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2)

    @staticmethod
    def midpoint(p1: tuple, p2: tuple) -> tuple:
        """两点中点"""
        return ((p1[0]+p2[0])/2, (p1[1]+p2[1])/2)

    @staticmethod
    def angle_between(p1: tuple, vertex: tuple, p2: tuple) -> float:
        """三点夹角（度）"""
        v1 = (p1[0]-vertex[0], p1[1]-vertex[1])
        v2 = (p2[0]-vertex[0], p2[1]-vertex[1])
        
        dot = v1[0]*v2[0] + v1[1]*v2[1]
        l1 = math.sqrt(v1[0]**2+v1[1]**2)
        l2 = math.sqrt(v2[0]**2+v2[1]**2)
        
        if l1 < 1e-6 or l2 < 1e-6:
            return 0.0
            
        cos_angle = max(-1, min(1, dot/(l1*l2)))
        return math.degrees(math.acos(cos_angle))

    @staticmethod
    def polygon_area(points: list) -> float:
        """
        多边形面积（Shoelace公式，逆时针为正）
        支持凸多边形和凹多边形
        """
        n = len(points)
        if n < 3:
            return 0.0
            
        area = 0.0
        for i in range(n):
            j = (i+1) % n
            area += points[i][0] * points[j][1]
            area -= points[j][0] * points[i][1]
            
        return abs(area) / 2.0

    @staticmethod
    def polygon_perimeter(points: list) -> float:
        """多边形周长"""
        n = len(points)
        if n < 2:
            return 0.0
        perimeter = 0.0
        for i in range(n):
            perimeter += MeasureTool.distance(points[i], points[(i+1)%n])
        return perimeter

    @staticmethod
    def polygon_center(points: list) -> tuple:
        """多边形质心"""
        n = len(points)
        if n == 0:
            return (0, 0)
        cx = sum(p[0] for p in points) / n
        cy = sum(p[1] for p in points) / n
        return (cx, cy)

    @staticmethod
    def circle_properties(center: tuple, radius: float) -> dict:
        """圆的属性"""
        return {
            'area': math.pi * radius**2,
            'circumference': 2 * math.pi * radius,
            'diameter': radius * 2,
        }

    @staticmethod
    def bounding_box(points: list) -> dict:
        """包围盒 {min_x, min_y, max_x, max_y, width, height, center}"""
        xs = [p[0] for p in points]
        ys = [p[1] for p in points]
        min_x, max_x = min(xs), max(xs)
        min_y, max_y = min(ys), max(ys)
        return {
            'min_x': min_x, 'min_y': min_y,
            'max_x': max_x, 'max_y': max_y,
            'width': max_x - min_x,
            'height': max_y - min_y,
            'center': ((min_x+max_x)/2, (min_y+max_y)/2),
        }

    @staticmethod
    def slope(p1: tuple, p2: tuple) -> float:
        """斜率（度）"""
        dx = p2[0] - p1[0]
        dy = p2[1] - p1[1]
        if abs(dx) < 1e-10:
            return 90.0 if dy > 0 else -90.0
        return math.degrees(math.atan2(dy, dx))


path: scripts/tools/mirror.py
content: """镜像工具"""
import math


class MirrorTool:
    """镜像操作"""

    @staticmethod
    def mirror_point(point: tuple, axis_p1: tuple, axis_p2: tuple) -> tuple:
        """计算一个点关于轴的镜像点"""
        px, py = point
        x1, y1 = axis_p1
        x2, y2 = axis_p2
        
        dx = x2 - x1
        dy = y2 - y1
        d_sq = dx*dx + dy*dy
        
        if d_sq < 1e-10:
            return point  # 退化情况
            
        # 投影
        t = ((px-x1)*dx + (py-y1)*dy) / d_sq
        proj_x = x1 + t * dx
        proj_y = y1 + t * dy
        
        # 镜像点 = 对称位置
        mx = 2*proj_x - px
        my = 2*proj_y - py
        
        return (mx, my)

    @staticmethod
    def mirror_points(points: list, axis_p1: tuple, axis_p2: tuple) -> list:
        """批量镜像点集"""
        return [MirrorTool.mirror_point(p, axis_p1, axis_p2) for p in points]

    @staticmethod
    def mirror_entity(msp, entity_func, original_params: dict,
                      axis_p1: tuple, axis_p2: tuple, **kwargs):
        """
        绘制某个图元的镜像副本
        
        Args:
            entity_func: 图元绘制函数 (如 BasicEntities.line)
            original_params: 原始参数字典（包含坐标参数）
            axis_p1, axis_p2: 镜像轴的两个点
            kwargs: 其他固定参数（layer, color等）
        
        Returns:
            镜像后的实体
        """
        mirrored_params = {}
        
        for key, val in original_params.items():
            if isinstance(val, tuple) and len(val) >= 2:
                # 这是一个坐标点，需要镜像
                mirrored_params[key] = MirrorTool.mirror_point(val, axis_p1, axis_p2)
            elif isinstance(val, list) and len(val) > 0:
                if isinstance(val[0], tuple):
                    mirrored_params[key] = MirrorTool.mirror_points(val, axis_p1, axis_p2)
                else:
                    mirrored_params[key] = val
            else:
                mirrored_params[key] = val
                
        return entity_func(msp, **mirrored_params, **kwargs)


path: scripts/tools/offset.py
content: """偏移工具 - 平行复制曲线"""
import math
from typing import Tuple, List


class OffsetTool:
    """图元偏移操作"""

    @staticmethod
    def offset_line(msp,
                    start: tuple,
                    end: tuple,
                    distance: float,
                    side: str = 'left',
                    layer='0', color=7):
        """
        直线偏移（生成平行线）
        
        Args:
            start, end: 原线段端点
            distance: 偏移距离
            side: 'left' 或 'right'（沿起点到终点的方向看）
        
        Returns:
            新线的 (start, end) 坐标
        """
        dx = end[0] - start[0]
        dy = end[1] - start[1]
        length = math.sqrt(dx*dx + dy*dy)
        if length < 1e-10:
            return None
        
        # 单位法向量
        nx = -dy / length
        ny = dx / length
        
        if side == 'right':
            nx, ny = -nx, -ny
            
        new_start = (start[0] + nx * distance, start[1] + ny * distance)
        new_end = (end[0] + nx * distance, end[1] + ny * distance)

        from entities.basic import BasicEntities
        BasicEntities.line(msp, new_start, new_end, layer=layer, color=color)
        return (new_start, new_end)

    @staticmethod
    def offset_polyline(msp,
                        points: list,
                        distance: float,
                        side: str = 'outside',
                        layer='0', color=7):
        """
        多段线偏移（简化实现：对每条边分别偏移后连接）
        
        注意：这是简化版本，不处理自交和角点修圆
        """
        from entities.basic import BasicEntities
        import math

        n = len(points)
        if n < 3:
            return []
            
        result_pts = []
        
        for i in range(n):
            p1 = points[i]
            p2 = points[(i+1) % n]
            
            dx = p2[0] - p1[0]
            dy = p2[1] - p1[1]
            length = math.sqrt(dx*dx + dy*dy)
            if length < 1e-10:
                continue
                
            # 法向量
            nx = -dy / length
            ny = dx / length
            if side == 'inside':
                nx, ny = -nx, -ny
                
            result_pts.append((p1[0] + nx*distance, p1[1] + ny*distance))

        # 画结果多段线
        from entities.polyline import PolylineEntity
        return PolylineEntity.polyline(msp, result_pts, closed=True,
                                       layer=layer, color=color)


path: scripts/tools/trim_extend.py
content: """裁剪与延伸工具"""
import math
from typing import Optional


class TrimTool:
    """裁剪与延伸"""

    @staticmethod
    def line_intersection(p1, p2, p3, p4) -> Optional[tuple]:
        """求两条直线段的交点"""
        x1, y1 = p1; x2, y2 = p2; x3, y3 = p3; x4, y4 = p4
        
        denom = (x1-x2)*(y3-y4) - (y1-y2)*(x3-x4)
        if abs(denom) < 1e-10:
            return None  # 平行或重合
            
        t = ((x1-x3)*(y3-y4) - (y1-y3)*(x3-x4)) / denom
        
        ix = x1 + t*(x2-x1)
        iy = y1 + t*(y2-y1)
        
        return (ix, iy)

    @staticmethod
    def trim_line_to_boundary(msp, line_start, line_end, 
                               boundary_points: list):
        """
        将直线裁剪到边界多边形（简化：找第一个交点并截断）
        
        Returns:
            裁剪后的新终点，或原终点如果无交点
        """
        for i in range(len(boundary_points)):
            b1 = boundary_points[i]
            b2 = boundary_points[(i+1) % len(boundary_points)]
            
            intersect = TrimTool.line_intersection(line_start, line_end, b1, b2)
            if intersect is not None:
                # 检查交点是否在线段上
                return intersect
                
        return line_end


class ExtendTool:
    """延长工具"""

    @staticmethod
    def extend_line(line_start, line_end, extend_length: float):
        """将直线从末端延长指定长度"""
        dx = line_end[0] - line_start[0]
        dy = line_end[1] - line_start[1]
        length = math.sqrt(dx*dx + dy*dy)
        if length < 1e-10:
            return line_end
            
        ux = dx / length
        uy = dy / length
        
        return (line_end[0] + ux * extend_length, line_end[1] + uy * extend_length)

    @staticmethod
    def extend_line_to_point(line_start, line_end, target_point):
        """延长直线直到经过目标点的垂足或交点"""
        # 计算目标点到直线的投影
        dx = line_end[0] - line_start[0]
        dy = line_end[1] - line_start[1]
        length_sq = dx*dx + dy*dy
        if length_sq < 1e-10:
            return target_point
            
        t = ((target_point[0]-line_start[0])*dx + (target_point[1]-line_start[1])*dy) / length_sq
        if t > 1.0:
            ext_x = line_start[0] + t * dx
            ext_y = line_start[1] + t * dy
            return (ext_x, ext_y)
        else:
            return line_end


path: scripts/tools/__init__.py
content: """编辑工具模块"""
from .offset import OffsetTool
from .trim_extend import TrimTool, ExtendTool
from .array import ArrayTool
from .mirror import MirrorTool
from .fillet_chamfer import FilletChamfer
from .measure import MeasureTool

__all__ = [
    'OffsetTool', 'TrimTool', 'ExtendTool', 'ArrayTool',
    'MirrorTool', 'FilletChamfer', 'MeasureTool'
]


path: scripts/templates/architectural.py
content: """建筑图库 - 墙体、门窗、柱子、楼梯、卫浴"""
import math
from typing import Tuple


class ArchitecturalTemplates:
    """建筑制图标准组件"""

    @staticmethod
    def wall(msp, points: list, thickness: float = 240,
             layer: str = 'wall', color: int = 1):
        """建筑墙体（双线表示墙厚）"""
        from entities.basic import BasicEntities
        half_t = thickness / 2
        results = []
        for i in range(len(points) - 1):
            p1, p2 = points[i], points[i + 1]
            dx, dy = p2[0] - p1[0], p2[1] - p1[1]
            length = math.sqrt(dx*dx + dy*dy)
            if length < 1e-6:
                continue
            nx, ny = -dy/length, dx/length
            results.append(BasicEntities.line(msp,
                (p1[0]+nx*half_t, p1[1]+ny*half_t),
                (p2[0]+nx*half_t, p2[1]+ny*half_t), layer=layer, color=color))
            results.append(BasicEntities.line(msp,
                (p1[0]-nx*half_t, p1[1]-ny*half_t),
                (p2[0]-nx*half_t, p2[1]-ny*half_t), layer=layer, color=color))
        return results

    @staticmethod
    def door_single(msp, position: tuple, width: float = 900,
                    direction: str = 'right', open_angle: float = 90,
                    layer: str = 'door', color: int = 3):
        """单扇平开门"""
        from entities.basic import BasicEntities
        from entities.curves import CurveEntities

        px, py = position; hw = width / 2
        entities = []

        dir_map = {
            'right': ((px, py-hw), (px, py+hw), -90),
            'left':  ((px, py+hw), (px, py-hw), 270),
            'up':    ((px-hw, py), (px+hw, py), 180),
            'down':  ((px+hw, py), (px-hw, py), 0),
        }
        hinge_pt, latch_pt, base_a = dir_map.get(direction, dir_map['right'])

        entities.append(BasicEntities.line(msp, hinge_pt, latch_pt,
                                           layer='door', color=color))
        sign = -1 if direction in ('left','up') else 1
        end_a = base_a + open_angle * sign

        try:
            arc = CurveEntities.arc(msp, hinge_pt, width,
                                    min(base_a, end_a), max(base_a, end_a),
                                    layer=layer, color=color)
            if arc: entities.append(arc)
        except Exception: pass

        rad_e = math.radians(end_a)
        door_end = (hinge_pt[0] + width*math.cos(rad_e),
                    hinge_pt[1] + width*math.sin(rad_e))
        entities.append(BasicEntities.line(msp, hinge_pt, door_end,
                                           layer=layer, color=color))
        return entities

    @staticmethod
    def double_door(msp, center: tuple, total_width: float = 1500,
                    layer: str = 'door'):
        """双开门"""
        cx, cy = center; hw = total_width / 2
        e1 = ArchitecturalTemplates.door_single(msp, (cx,cy), hw, 'right', layer=layer)
        e2 = ArchitecturalTemplates.door_single(msp, (cx,cy), hw, 'left', layer=layer)
        return (e1 or []) + (e2 or [])

    @staticmethod
    def window(msp, corner1: tuple, corner2: tuple,
               frame_width: float = 60, has_sill: bool = True,
               sill_depth: float = 100, layer: str = 'window', color: int = 4):
        """窗户（四线表示窗洞）"""
        from entities.basic import BasicEntities

        x1, y1 = corner1; x2, y2 = corner2
        ents = [
            BasicEntities.line(msp, (x1,y1),(x2,y1), layer=layer, color=color),
            BasicEntities.line(msp, (x1,y2),(x2,y2), layer=layer, color=color),
            BasicEntities.line(msp, (x1,y1),(x1,y2), layer=layer, color=color),
            BasicEntities.line(msp, (x2,y1),(x2,y2), layer=layer, color=color),
        ]

        w, h = x2-x1, y2-y1
        if w > 1200:
            mx = (x1+x2)/2
            ents.append(BasicEntities.line(msp, (mx, y1+20), (mx, y2-20), layer=layer, color=color))
        if h > 900:
            my = (y1+y2)/2
            ents.append(BasicEntities.line(msp, (x1+20, my), (x2-20, my), layer=layer, color=color))

        if has_sill:
            ents.append(BasicEntities.line(msp, (x1-sill_depth, y1), (x2+sill_depth, y1),
                                           layer=layer, color=color, lineweight=35))
        return ents

    @staticmethod
    def column(msp, center: tuple, size=(400,400),
               layer: str = 'column', color: int = 2,
               hatch_pattern: str = 'concrete'):
        """结构柱子"""
        from entities.polyline import PolylineEntity
        from hatch.patterns import HatchFill

        sx, sy = size; hx, hy = sx/2, sy/2
        cx, cy = center
        pts = [(cx-hx,cy-hy),(cx+hx,cy-hy),(cx+hx,cy+hy),(cx-hx,cy+hy)]
        PolylineEntity.polygon(msp, pts, layer=layer, color=color)
        HatchFill.industry_fill(msp, pts, material=hatch_pattern, scale=50, layer=layer)

    @staticmethod
    def stair_straight(msp, bottom_center: tuple, step_count: int = 12,
                       step_width: float = 280, total_width: float = 1200,
                       direction: str = 'up-right',
                       layer: str = 'stair', color: int = 4):
        """直跑楼梯平面图"""
        from entities.basic import BasicEntities
        from entities.text import TextEntity

        bx, by = bottom_center; tw = total_width / 2
        entities = []; is_v = direction == 'up-up'
        go_r = 'right' in direction

        for i in range(step_count):
            if is_v:
                y1, y2 = by+i*step_width, by+(i+1)*step_width
                ents = [BasicEntities.line(msp, (bx-tw,y1),(bx+tw,y1), layer=layer, color=color)]
                if i == step_count-1:
                    ents.append(BasicEntities.line(msp, (bx-tw,y2),(bx+tw,y2), layer=layer, color=color))
            else:
                x1 = (bx-tw+i*step_width) if go_r else (bx+tw-i*step_width)
                x2 = (bx-tw+(i+1)*step_width) if go_r else (bx+tw-(i+1)*step_width)
                ents = [BasicEntities.line(msp, (x1,by-tw),(x1,by+tw), layer=layer, color=color)]
                if i == step_count-1:
                    ents.append(BasicEntities.line(msp, (x2,by-tw),(x2,by+tw), layer=layer, color=color))
            entities.extend(ents)

        # 折断线
        mid_i = step_count // 2
        if is_v:
            my = by + mid_i * step_width
            entities.append(BasicEntities.lines(msp, [(bx-tw-20,my-30),(bx+tw+20,my+30)],
                                                layer=layer, color=color))

        arrow_text = '上 ↑' if is_v else ('上 →' if go_r else '上 ←')
        top_y = by + step_count * step_width + 200
        TextEntity.text(msp, arrow_text, (bx, top_y), height=150,
                       halign='CENTER', layer=layer, color=color)

        return entities

    @staticmethod
    def balcony(msp, start: tuple, end: tuple, depth: float = 1200,
                rail_offset: float = 50, layer: str = 'balcony'):
        """阳台"""
        from entities.basic import BasicEntities
        from entities.polyline import PolylineEntity

        sx, sy = start; ex, ey = end
        dx, dy = ex-sx, ey-sy
        length = math.sqrt(dx*dx+dy*dy)
        nx, ny = -dy/length, dx/length

        # 外轮廓
        outer_pts = [
            (sx, sy),
            (sx + nx*depth, sy + ny*depth),
            (ex + nx*depth, ey + ny*depth),
            (ex, ey)
        ]
        PolylineEntity.polygon(msp, outer_pts, layer=layer, color=5, lineweight=25)

        # 栏杆线
        rail_start = (sx + nx*rail_offset, sy + ny*rail_offset)
        rail_end   = (ex + nx*rail_offset, ey + ny*rail_offset)
        BasicEntities.line(msp, rail_start, rail_end, layer=layer, color=5)

        # 滑门符号
        mx = (sx+ex)/2 + nx*rail_offset
        my = (sy+ey)/2 + ny*rail_offset
        door_w = min(length/3, 800)
        d_half = door_w/2
        BasicEntities.line(msp, (mx-d_half, my), (mx+d_half, my), layer='door', color=3)


path: scripts/templates/electrical.py
content: """电气图库 - 开关、插座、灯具、线路符号"""
import math
from typing import Tuple


class ElectricalTemplates:
    """电气制图标准组件"""

    @staticmethod
    def switch_single_pole(msp, position: tuple,
                           orientation: str = 'vertical',
                           size: float = 200,
                           layer: str = 'symbol', color: int = 1):
        """单极开关（标准符号）"""
        from entities.basic import BasicEntities
        from entities.curves import CurveEntities

        cx, cy = position; s = size; hs = s/2; qs = s*0.15
        ents = []

        is_v = orientation == 'vertical'

        # 圆（触点）
        ents.append(CurveEntities.circle(msp, (cx,cy), qs*2, layer=layer, color=color))

        if is_v:
            # 上引线
            ents.append(BasicEntities.line(msp, (cx,cy+qs*2), (cx,cy+hs), layer='wire', color=7))
            # 下引线
            ents.append(BasicEntities.line(msp, (cx,cy-qs*2), (cx,cy-hs), layer='wire', color=7))
            # 动触点杆（倾斜45°）
            ents.append(BasicEntities.line(msp, (cx,cy-qs*2), (cx+hs*0.3, cy+hs*0.1),
                                           layer=layer, color=color))
        else:
            ents.append(BasicEntities.line(msp, (cx-qs*2,cy), (cx-hs,cy), layer='wire', color=7))
            ents.append(BasicEntities.line(msp, (cx+qs*2,cy), (cx+hs,cy), layer='wire', color=7))
            ents.append(BasicEntities.line(msp, (cx+qs*2,cy), (cx+hs*0.1, cy+hs*0.3),
                                           layer=layer, color=color))

        return ents

    @staticmethod
    def socket_power(msp, position: tuple,
                     type: str = 'three_hole',
                     size: float = 150,
                     layer: str = 'device'):
        """电源插座"""
        from entities.basic import BasicEntities
        from entities.curves import CurveEntities

        cx, cy = position; s = size; hs = s/2
        ents = []

        # 外框半圆或方框
        if type == 'three_hole':
            # 半圆弧 + 底线（两孔+三孔通用外形）
            ents.append(CurveEntities.arc(msp, (cx,cy-s*0.2), hs,
                                          -90, 270, layer=layer, color=3))
            ents.append(BasicEntities.line(msp, (cx-hs,cy-s*0.2), (cx+hs,cy-s*0.2),
                                           layer=layer, color=3))
            # 孔位示意：上二下一（三孔）
            hole_r = s * 0.08
            gap = s * 0.25
            ents.append(CurveEntities.circle(msp, (cx-gap, cy+s*0.05), hole_r, layer=layer, color=7))
            ents.append(CurveEntities.circle(msp, (cx+gap, cy+s*0.05), hole_r, layer=layer, color=7))
            ents.append(CurveEntities.circle(msp, (cx, cy-s*0.12), hole_r, layer=layer, color=7))
        else:
            # 两孔方形插座
            from entities.polyline import PolylineEntity
            PolylineEntity.rect_centered(msp, (cx,cy), s*0.9, s*0.65, layer=layer, color=3)

        return ents

    @staticmethod
    def lamp_ceiling(msp, position: tuple,
                    lamp_type: str = 'downlight',
                    size: float = 120,
                    layer: str = 'device'):
        """灯具符号"""
        from entities.basic import BasicEntities
        from entities.curves import CurveEntities

        cx, cy = position; r = size/2
        ents = []

        types_map = {
            'downlight':   ('circle', 1),
            'ceiling_lamp':('circle_cross', 1),
            'fluorescent': ('rect', 2),     # 长条灯
            'wall_lamp':   ('half_circle', 1),  # 壁灯
            'track':       ('track', 1),      # 轨道射灯
        }

        shape = types_map.get(lamp_type, ('circle', 1))[0]

        if shape == 'circle':
            ents.append(CurveEntities.circle(msp, (cx,cy), r, layer=layer, color=3))
        elif shape == 'circle_cross':
            ents.append(CurveEntities.circle(msp, (cx,cy), r, layer=layer, color=3))
            tick = r * 0.6
            ents.append(BasicEntities.line(msp, (cx-tick,cy),(cx+tick,cy), layer=layer, color=3))
            ents.append(BasicEntities.line(msp, (cx,cy-tick),(cx,cy+tick), layer=layer, color=3))
        elif shape == 'rect':
            from entities.polyline import PolylineEntity
            w = size * 2; h = size * 0.35
            ents.append(PolylineEntity.rect_centered(msp, (cx,cy), w, h, layer=layer, color=3))
        elif shape == 'half_circle':
            ents.append(CurveEntities.arc(msp, (cx,cy), r, 0, 180, layer=layer, color=3))
            ents.append(BasicEntities.line(msp, (cx-r,cy), (cx+r,cy), layer=layer, color=3))

        return ents

    @staticmethod
    def wire_line(msp, start: tuple, end: tuple,
                  phases: int = 3,
                  spacing: float = 30,
                  layer: str = 'wire'):
        """多相导线组（平行线表示）"""
        from entities.basic import BasicEntities

        dx, dy = end[0]-start[0], end[1]-start[1]
        length = math.sqrt(dx*dx + dy*dy)
        ux, uy = dx/length, dy/length
        nx, ny = -uy, ux

        half_gap = spacing * (phases - 1) / 2
        ents = []
        for i in range(phases):
            offset = -half_gap + i * spacing
            ox, oy = offset * nx, offset * ny
            ents.append(BasicEntities.line(msp,
                (start[0]+ox, start[1]+oy),
                (end[0]+ox, end[1]+oy),
                layer=layer, color=7, lineweight=18))
        return ents

    @staticmethod
    def distribution_box(msp, center: tuple,
                         width: float = 400,
                         height: float = 300,
                         label: str = '',
                         layer: str = 'device'):
        """配电箱/控制箱"""
        from entities.polyline import PolylineEntity
        from entities.text import TextEntity

        cx, cy = center; hw, hh = width/2, height/2
        ents = [PolylineEntity.rect_centered(msp, (cx,cy), width, height,
                                             layer=layer, color=3, lineweight=40)]
        if label:
            ents.append(TextEntity.text(msp, label, (cx, cy),
                                       height=min(width,height)*0.18,
                                       halign='CENTER', valign='MIDDLE',
                                       layer=layer, color=7))
        return ents

    @staticmethod
    def circuit_breaker(msp, position: tuple,
                        orientation: str = 'horizontal',
                        poles: int = 1,
                        layer: str = 'device'):
        """断路器符号"""
        from entities.basic import BasicEntities

        cx, cy = position; s = 150
        ents = []

        for p in range(poles):
            offset_y = p * s * 1.5
            px, py = cx, cy + offset_y

            is_h = orientation != 'vertical'
            line_len = s * 0.6

            if is_h:
                ents.append(BasicEntities.line(msp, (px-line_len,py), (px-line_len*0.2,py),
                                               layer='wire', color=7))
                ents.append(BasicEntities.line(msp, (px+line_len*0.2,py), (px+line_len,py),
                                               layer='wire', color=7))
                # 断开触点
                ents.append(BasicEntities.line(msp, (px-line_len*0.2,py),
                                               (px+line_len*0.2, py+s*0.2),
                                               layer='device', color=3))
                # 触点圆
                from entities.curves import CurveEntities
                ents.append(CurveEntities.circle(msp, (px-line_len*0.2, py), s*0.04,
                                                  layer='device', color=3))

        return ents


path: scripts/templates/mechanical.py
content: """机械图库 - 螺钉、齿轮、轴承、键槽、弹簧、标准件"""
import math
from typing import Tuple


class MechanicalTemplates:
    """机械制图标准组件"""

    @staticmethod
    def bolt_top_view(msp, center: tuple, diameter: float,
                      head_type: str = 'hex',
                      layer: str = 'outline', color: int = 7):
        """
        螺栓头俯视图
        Args:
            head_type: 'hex'六角 / 'phillips'十字 / 'slot'一字
        """
        from entities.basic import BasicEntities
        from entities.curves import CurveEntities
        from entities.polyline import PolylineEntity

        cx, cy = center; d = diameter; r = d/2
        ents = []

        if head_type == 'hex':
            # 六角头（正六边形）
            for i in range(6):
                a1 = math.radians(60 * i)
                a2 = math.radians(60 * (i+1))
                p1 = (cx + r*math.cos(a1), cy + r*math.sin(a1))
                p2 = (cx + r*math.cos(a2), cy + r*math.sin(a2))
                ents.append(BasicEntities.line(msp, p1, p2, layer=layer, color=color))

            # 内切圆
            ents.append(CurveEntities.circle(msp, center, r*0.866,
                                              layer='thin', color=5))

            # 中心十字线
            tick = r * 0.15
            ents.append(BasicEntities.line(msp, (cx-tick,cy),(cx+tick,cy),
                                           layer='center', color=1, lineweight=13))
            ents.append(BasicEntities.line(msp, (cx,cy-tick),(cx,cy+tick),
                                           layer='center', color=1, lineweight=13))

        elif head_type == 'phillips':
            # 十字槽圆头
            ents.append(CurveEntities.circle(msp, center, r, layer=layer, color=color))
            slot_len = r * 0.8
            for angle in [0, 45]:
                rad = math.radians(angle)
                sx = slot_len * math.cos(rad); sy = slot_len * math.sin(rad)
                ents.append(BasicEntities.line(msp,
                    (cx-sx, cy-sy), (cx+sx, cy+sy),
                    layer='thin', color=5, lineweight=25))

        elif head_type == 'slot':
            # 一字槽圆头
            ents.append(CurveEntities.circle(msp, center, r, layer=layer, color=color))
            ents.append(BasicEntities.line(msp, (cx-r*0.7, cy), (cx+r*0.7, cy),
                                           layer='thin', color=5, lineweight=25))

        return ents

    @staticmethod
    def gear_profile(msp, center: tuple,
                     teeth: int = 20,
                     outer_diameter: float = 100,
                     root_diameter: float = 80,
                     bore_diameter: float = 20,
                     layer: str = 'outline', color: int = 7):
        """齿轮轮廓（端面视图）"""
        from entities.polyline import PolylineEntity
        from entities.curves import CurveEntities

        cx, cy = center
        ro = outer_diameter / 2; rr = root_diameter / 2; rb = bore_diameter / 2
        tooth_angle = 360.0 / teeth
        ents = []

        # 齿顶圆
        ents.append(CurveEntities.circle(msp, center, ro, layer=layer, color=color, lineweight=50))
        # 齿根圆
        ents.append(CurveEntities.circle(msp, center, rr, layer=layer, color=color, lineweight=35))
        # 轴孔
        ents.append(CurveEntities.circle(msp, center, rb, layer='hidden', color=2, lineweight=25))

        # 简化的齿形表示（径向线）
        for i in range(teeth):
            a = math.radians(i * tooth_angle)
            px = cx + ro * math.cos(a)
            py = cy + ro * math.sin(a)
            from entities.basic import BasicEntities
            ents.append(BasicEntities.line(msp, (cx,cy), (px,py),
                                           layer='thin', color=5, lineweight=18))

        return ents

    @staticmethod
    def bearing(msp, center: tuple,
               outer_dia: float = 60,
               inner_dia: float = 30,
               width: float = 15,
               view: str = 'side',
               layer: str = 'outline'):
        """轴承简化画法"""
        from entities.basic import BasicEntities
        from entities.curves import CurveEntities

        cx, cy = center; ro = outer_dia/2; ri = inner_dia/2; rm = (ro+ri)/2
        ents = []

        if view == 'side':
            # 剖面视图：矩形外圈 + 内圈 + 滚子
            hw = width/2
            # 外圈
            ents.extend([
                BasicEntities.line(msp, (cx-hw, cy-ro), (cx+hw, cy-ro), layer=layer, color=7, linewidth=50),
                BasicEntities.line(msp, (cx-hw, cy-rm), (cx+hw, cy-rm), layer=layer, color=7, linewidth=35),
                BasicEntities.line(msp, (cx-hw, cy-ri), (cx+hw, cy-ri), layer=layer, color=7, linewidth=50),
                BasicEntities.line(msp, (cx-hw, cy-ro), (cx-hw, cy-ri), layer=layer, color=7, linewidth=50),
                BasicEntities.line(msp, (cx+hw, cy-ro), (cx+hw, cy-ri), layer=layer, color=7, linewidth=50),
            ])
            # 滚动体示意（小圆）
            ball_r = (ro - rm) / 2.5
            n_balls = max(int(width / (ball_r*4)), 3)
            for i in range(n_balls):
                bx = cx - hw + width*(i+0.5)/n_balls
                ents.append(CurveEntities.circle(msp, (bx, cy-rm-ball_r), ball_r,
                                                  layer='thin', color=5))
        else:
            # 端面视图：同心圆
            ents.append(CurveEntities.circle(msp, center, ro, layer=layer, color=7, lineweight=50))
            ents.append(CurveEntities.circle(msp, center, rm, layer='thin', color=5))
            ents.append(CurveEntities.circle(msp, center, ri, layer=layer, color=7, lineweight=50))

        return ents

    @staticmethod
    def keyway_side_view(msp, shaft_center: tuple,
                         shaft_dia: float = 30,
                         key_width: float = 8,
                         key_length: float = 20,
                         key_depth: float = 4,
                         layer: str = 'outline'):
        """轴和键槽的侧面视图"""
        from entities.basic import BasicEntities
        from entities.curves import CurveEntities
        from entities.polyline import PolylineEntity

        cx, cy = shaft_center; rs = shaft_dia/2; kl = key_length; kw = key_width
        ents = []

        # 轴的外轮廓
        half_kl = kl / 2
        ents.append(BasicEntities.line(msp, (cx-half_kl, cy-rs), (cx+half_kl, cy-rs),
                                       layer=layer, color=7, linewidth=50))
        ents.append(BasicEntities.line(msp, (cx-half_kl, cy+rs), (cx+half_kl, cy+rs),
                                       layer=layer, color=7, linewidth=50))
        # 键槽（底部开口的矩形）
        kh = key_depth
        ents.extend([
            BasicEntities.line(msp, (cx-half_kl, cy+rs-kh), (cx+half_kl, cy+rs-kh),
                               layer=layer, color=7),
            BasicEntities.line(msp, (cx-half_kl, cy+rs-kh), (cx-half_kl, cy+rs),
                               layer=layer, color=7),
            BasicEntities.line(msp, (cx+half_kl, cy+rs-kh), (cx+half_kl, cy+rs),
                               layer=layer, color=7),
        ])
        return ents

    @staticmethod
    def spring_compression(msp, start: tuple, end: tuple,
                           wire_dia: float = 2,
                           coil_outer_dia: float = 16,
                           active_coils: float = 6,
                           end_coils: float = 2,
                           layer: str = 'outline', color: int = 7):
        """压缩弹簧（侧视，锯齿线表示）"""
        from entities.basic import BasicEntities
        import numpy as np

        sx, sy = start; ex, ey = end
        dx, dy = ex-sx, ey-sy
        length = math.sqrt(dx*dx + dy*dy)

        total_coils = active_coils + end_coils
        pitch = length / total_coils
        amp = coil_outer_dia / 2 - wire_dia / 2

        # 单位方向向量
        ux, uy = dx/length, dy/length
        nx, ny = -uy, ux  # 法向

        pts_per_coil = 12
        n_pts = int(total_coils * pts_per_coil)
        points = []
        for i in range(n_pts + 1):
            t = i / n_pts * length
            phase = (i / pts_per_coil) * 2 * math.pi
            offset = amp * math.sin(phase)
            px = sx + ux*t + nx*offset
            py = sy + uy*t + ny*offset
            points.append((px, py))

        return BasicEntities.lines(msp, points, closed=False,
                                   layer=layer, color=color)

    @staticmethod
    def washer(msp, center: tuple,
              outer_dia: float = 10,
              inner_dia: float = 5.5,
              thickness: float = 1.6,
              layer: str = 'thin'):
        """垫圈"""
        from entities.curves import CurveEntities
        return [
            CurveEntities.circle(msp, center, outer_dia/2, layer=layer, color=7),
            CurveEntities.circle(msp, center, inner_dia/2, layer=layer, color=7),
        ]

    @staticmethod
    def nut_hex(msp, center: tuple,
                size_across_flats: float = 10,
                thickness: float = 5,
                thread_dia: float = 9,
                layer: str = 'outline'):
        """六角螺母"""
        from entities.polyline import PolylineEntity
        s = size_across_flats; t = thickness
        cx, cy = center
        apothem = s * 0.866  # 边心距

        pts = []
        for i in range(6):
            a = math.radians(30 + i * 60)
            pts.append((cx + apothem * math.cos(a), cy + apothem * math.sin(a)))

        return [PolylineEntity.polygon(msp, pts, layer=layer, color=7, lineweight=50)]

    @staticmethod
    def pin_cylindrical(msp, start: tuple, end: tuple,
                        dia: float = 4,
                        layer: str = 'thin'):
        """圆柱销"""
        from entities.basic import BasicEntities
        r = dia/2
        dx, dy = end[0]-start[0], end[1]-start[1]
        length = math.sqrt(dx*dx+dy*dy)
        ux, uy = dx/length, dy/length; nx, ny = -uy, ux

        return [
            BasicEntities.line(msp,
                (start[0]+nx*r, start[1]+ny*r),
                (end[0]+nx*r, end[1]+ny*r), layer=layer, color=7),
            BasicEntities.line(msp,
                (start[0]-nx*r, start[1]-ny*r),
                (end[0]-nx*r, end[1]-ny*r), layer=layer, color=7),
        ]


path: scripts/templates/piping.py
content: """管道图库 - 弯头、三通、阀门、法兰、管件"""
import math
from typing import Tuple


class PipingTemplates:
    """管道/暖通制图标准组件"""

    @staticmethod
    def pipe_straight(msp, start: tuple, end: tuple,
                      diameter: float = 50,
                      layer: str = 'pipe',
                      color: int = 2,
                      show_centerline: bool = True):
        """
        直管段（双线表示）
        
        Args:
            diameter: 管外径
        """
        from entities.basic import BasicEntities
        dx, dy = end[0]-start[0], end[1]-start[1]
        length = math.sqrt(dx*dx+dy*dy)
        r = diameter / 2
        ux, uy = dx/length, dy/length
        nx, ny = -uy, ux

        ents = [
            BasicEntities.line(msp, (start[0]+nx*r, start[1]+ny*r),
                               (end[0]+nx*r, end[1]+ny*r), layer=layer, color=color),
            BasicEntities.line(msp, (start[0]-nx*r, start[1]-ny*r),
                               (end[0]-nx*r, end[1]-ny*r), layer=layer, color=color),
        ]
        if show_centerline:
            cl_offset = diameter * 0.02
            ents.append(BasicEntities.line(msp, start, end,
                                           layer='center', color=1, lineweight=13))
        return ents

    @staticmethod
    def elbow_90(msp, center: tuple, radius: float = 100,
                 diameter: float = 50, angle_dir: str = 'NE',
                 layer: str = 'fitting'):
        """
        90度弯头
        
        Args:
            center: 弯曲中心
            angle_dir: 走向方向 'NE'/'NW'/'SE'/'SW'（从哪个方向来，到哪个方向去）
        Returns:
            双线弯头实体列表
        """
        from entities.curves import CurveEntities
        r = diameter / 2
        R = radius

        dir_angles = {
            'NE': (-180, -90), 'NW': (-90, 0), 'SE': (90, 180), 'SW': (0, 90),
        }
        a_start, a_end = dir_angles.get(angle_dir, (-180,-90))
        ents = []

        # 外弧和内弧
        Ro = R + r; Ri = max(R - r, 1)
        try:
            ents.append(CurveEntities.arc(msp, center, Ro, a_start, a_end, layer=layer, color=5))
            ents.append(CurveEntities.arc(msp, center, Ri, a_start, a_end, layer=layer, color=5))

            # 连接线（端部封口）
            rad_s = math.radians(a_start); rad_e = math.radians(a_end)
            from entities.basic import BasicEntities
            ents.append(BasicEntities.line(msp,
                (center[0]+Ro*math.cos(rad_s), center[1]+Ro*math.sin(rad_s)),
                (center[0]+Ri*math.cos(rad_s), center[1]+Ri*math.sin(rad_s)),
                layer=layer, color=5))
            ents.append(BasicEntities.line(msp,
                (center[0]+Ro*math.cos(rad_e), center[1]+Ro*math.sin(rad_e)),
                (center[0]+Ri*math.cos(rad_e), center[1]+Ri*math.sin(rad_e)),
                layer=layer, color=5))
        except Exception:
            pass
        return ents

    @staticmethod
    def tee(msp, junction: tuple,
            main_dia: float = 50,
            branch_dia: float = 30,
            orientation: str = 'T',    # 'T'(垂直分支) 或 '+'(十字四通)
            main_direction: str = 'H', # 'H'=水平主管 / 'V'=垂直主管
            layer: str = 'valve'):
        """三通/四通接头"""
        from entities.basic import BasicEntities
        from entities.polyline import PolylineEntity

        jx, jy = junction
        rm = main_dia/2; rb = branch_dia/2
        L = max(main_dia, branch_dia) * 2  # 短臂长度
        ents = []

        if main_direction == 'H':
            # 水平主管
            ents.extend([
                BasicEntities.line(msp, (jx-L,jy-rm), (jx+L,jy-rm), layer=layer, color=5),
                BasicEntities.line(msp, (jx-L,jy+rm), (jx+L,jy+rm), layer=layer, color=5),
            ])
            # 垂直支管
            ents.extend([
                BasicEntities.line(msp, (jx-rb,jy), (jx+rb,jy), layer=layer, color=5),
                BasicEntities.line(msp, (jx-rb,jy-L), (jx+rb,jy-L), layer=layer, color=5),
                BasicEntities.line(msp, (jx-rb,jy), (jx-rb,jy-L), layer=layer, color=5),
                BasicEntities.line(msp, (jx+rb,jy), (jx+rb,jy-L), layer=layer, color=5),
            ])
        else:
            # 垂直主管
            ents.extend([
                BasicEntities.line(msp, (jx-rm,jy-L), (jx-rm,jy+L), layer=layer, color=5),
                BasicEntities.line(msp, (jx+rm,jy-L), (jx+rm,jy+L), layer=layer, color=5),
            ])
            ents.extend([
                BasicEntities.line(msp, (jx,jy-rb), (jx,jy+rb), layer=layer, color=5),
                BasicEntities.line(msp, (jx-L,jy-rb), (jx-L,jy+rb), layer=layer, color=5),
                BasicEntities.line(msp, (jx,jy-rb), (jx-L,jy-rb), layer=layer, color=5),
                BasicEntities.line(msp, (jx,jy+rb), (jx-L,jy+rb), layer=layer, color=5),
            ])

        if orientation == '+':
            # 四通：另一侧也有支管
            sign = -1
            opp_L = L
            if main_direction == 'H':
                ents.extend([
                    BasicEntities.line(msp, (jx-rb,jy), (jx+rb,jy), layer=layer, color=5),
                    BasicEntities.line(msp, (jx-rb,jy+opp_L), (jx+rb,jy+opp_L), layer=layer, color=5),
                    BasicEntities.line(msp, (jx-rb,jy), (jx-rb,jy+opp_L), layer=layer, color=5),
                    BasicEntities.line(msp, (jx+rb,jy), (jx+rb,jy+opp_L), layer=layer, color=5),
                ])

        return ents

    @staticmethod
    def valve_gate(msp, position: tuple,
                   pipe_diameter: float = 50,
                   valve_type: str = 'gate',
                   layer: str = 'valve'):
        """
        阀门符号（简化画法）
        
        Args:
            valve_type: 'gate'闸阀 / 'globe'截止阀 / 'check'止回阀 / 'ball'球阀 / 'butterfly'蝶阀
        """
        from entities.basic import BasicEntities
        from entities.polyline import PolylineEntity

        px, py = position; d = pipe_diameter; hw = d * 0.8; hh = d
        ents = []

        if valve_type == 'gate':
            # 闸阀：双线管道 + 内部两个三角形（闸板）
            rm = d/2
            L = d * 0.6
            ents.extend([
                BasicEntities.line(msp, (px-L,py-rm), (px+L,py-rm), layer=layer, color=5),
                BasicEntities.line(msp, (px-L,py+rm), (px+L,py+rm), layer=layer, color=5),
            ])
            # 阀体轮廓
            vhw = d * 0.3; vhh = d * 0.6
            ents.extend([
                PolylineEntity.polygon(msp,
                    [(px-vhw,py-vhh),(px+vhw,py-vhh),(px+vhw,py+vhh),(px-vhw,py+vhh)],
                    layer=layer, color=5),
            ])
            # 手轮/阀杆
            stem_len = d * 0.5
            ents.append(BasicEntities.line(msp, (px,py+vhh), (px,py+vhh+stem_len),
                                           layer=layer, color=5, lineweight=25))
            wheel_r = d * 0.25
            from entities.curves import CurveEntities
            ents.append(CurveEntities.arc(msp, (px,py+vhh+stem_len), wheel_r,
                                          -90, 270, layer=layer, color=5))

        elif valve_type == 'globe':
            # 截止阀：S形阀体 + 波浪线近似
            rm = d/2; L = d * 0.5
            ents.extend([
                BasicEntities.line(msp, (px-L,py-rm), (px+L,py-rm), layer=layer, color=5),
                BasicEntities.line(msp, (px-L,py+rm), (px+L,py+rm), layer=layer, color=5),
            ])
            # S形波纹（用折线近似）
            wave_amp = rm * 0.4
            for y_off in [-rm*0.3, rm*0.3]:
                pts = [(px-wave_amp, py+y_off), (px, py+y_off-wave_amp),
                       (px+wave_amp, py+y_off)]
                ents.extend(BasicEntities.lines(msp, pts, layer=layer, color=5))

        elif valve_type == 'check':
            # 止回阀：箭头 + 弹簧示意
            rm = d/2; L = d * 0.4
            ents.extend([
                BasicEntities.line(msp, (px-L,py-rm), (px+L,py-rm), layer=layer, color=5),
                BasicEntities.line(msp, (px-L,py+rm), (px+L,py+rm), layer=layer, color=5),
            ])
            # 三角形箭头
            arr_size = d * 0.35
            ents.append(PolylineEntity.polygon(msp,
                [(px,arr_size),(px+arr_size,0),(px,-arr_size)],
                layer=layer, color=5))

        elif valve_type == 'ball':
            # 球阀：圆形阀体 + T形手柄
            from entities.curves import CurveEntities
            rm = d/2; L = d * 0.4
            ents.extend([
                BasicEntities.line(msp, (px-L,py-rm), (px+L,py-rm), layer=layer, color=5),
                BasicEntities.line(msp, (px-L,py+rm), (px+L,py+rm), layer=layer, color=5),
                ents.append(CurveEntities.circle(msp, (px,py), rm*1.2, layer=layer, color=5)) if False else None,
            ])
            # 补充球体
            ents.append(CurveEntities.circle(msp, (px,py), rm*1.1, layer=layer, color=5))

        elif valve_type == 'butterfly':
            # 蝶阀：短圆柱 + 圆盘手柄
            rm = d/2; body_len = d * 0.2
            ents.extend([
                BasicEntities.line(msp, (px-body_len,py-rm), (px+body_len,py-rm), layer=layer, color=5),
                BasicEntities.line(msp, (px-body_len,py+rm), (px+body_len,py+rm), layer=layer, color=5),
            ])
            handle_len = d * 0.8
            handle_wid = d * 0.08
            ents.append(PolylineEntity.rect_centered(msp,
                (px+handle_len/2, py+rm+d*0.3), handle_len, handle_wid, layer=layer, color=5))

        return ents

    @staticmethod
    def flange(msp, position: tuple,
               diameter: float = 50,
               thickness: float = 10,
               bolt_count: int = 4,
               layer: str = 'fitting'):
        """法兰盘（端面视图）"""
        from entities.curves import CurveEntities
        from entities.basic import BasicEntities

        cx, cy = position
        ro = diameter/2 + thickness
        ri = diameter/2 - thickness
        rb = diameter/2
        ents = [
            CurveEntities.circle(msp, (cx,cy), ro, layer=layer, color=5, linewidth=40),
            CurveEntities.circle(msp, (cx,cy), ri, layer=layer, color=5, linewidth=25),
            CurveEntities.circle(msp, (cx,cy), rb, layer=layer, color=5),
        ]

        # 螺栓孔
        bolt_r = thickness * 0.6
        bolt_circle_r = (ro + ri) / 2
        for i in range(bolt_count):
            a = 2 * math.pi * i / bolt_count
            bx = cx + bolt_circle_r * math.cos(a)
            by = cy + bolt_circle_r * math.sin(a)
            ents.append(CurveEntities.circle(msp, (bx,by), bolt_r, layer='thin', color=5))

        return ents

    @staticmethod
    def reducer(msp, start: tuple, end: tuple,
                dia_large: float = 80,
                dia_small: float = 50,
                layer: str = 'fitting'):
        """大小头（异径管）渐缩段"""
        from entities.basic import BasicEntities
        dx, dy = end[0]-start[0], end[1]-start[1]
        length = math.sqrt(dx*dx+dy*dy)
        rl = dia_large/2; rs = dia_small/2
        ux, uy = dx/length, dy/length
        nx, ny = -uy, ux

        return [
            BasicEntities.line(msp,
                (start[0]+nx*rl, start[1]+ny*rl),
                (end[0]+nx*rs, end[1]+ny*rs), layer=layer, color=5),
            BasicEntities.line(msp,
                (start[0]-nx*rl, start[1]-ny*rl),
                (end[0]-nx*rs, end[1]-ny*rs), layer=layer, color=5),
        ]

    @staticmethod
    def cap(msp, position: tuple, diameter: float = 50,
            direction: tuple = None,
            layer: str = 'fitting'):
        """管帽/盲板"""
        from entities.curves import CurveEntities
        from entities.basic import BasicEntities
        cx, cy = position; r = diameter/2

        if direction is None:
            # 端面视图：圆
            return [CurveEntities.circle(msp, (cx,cy), r, layer=layer, color=5, linewidth=40)]

        # 侧视：直线封口
        dx, dy = direction
        length = math.sqrt(dx*dx+dy*dy)
        ux, uy = dx/length, dy/length
        nx, ny = -uy, ux
        return [BasicEntities.line(msp,
            (cx+nx*r, cy+ny*r), (cx-nx*r, cy-ny*r),
            layer=layer, color=5, linewidth=40)]


path: scripts/templates/structural.py
content: """结构图库 - 梁、板、柱、基础、钢筋"""
import math
from typing import Tuple


class StructuralTemplates:
    """结构施工图标准组件"""

    @staticmethod
    def beam_section(msp, position: tuple,
                     width: float = 250, height: float = 500,
                     rebar_top: list = None,   # [(dia, count), ...]
                     rebar_bot: list = None,
                     rebar_side: list = None,
                     stirrup_dia: int = 8,
                     stirrup_spacing: int = 150,
                     layer: str = 'outline',
                     hatch: bool = True):
        """
        梁断面（配筋详图）
        
        Args:
            rebar_top: 顶部纵筋列表，如 [(20,2)] = 2根直径20
            rebar_bot: 底部纵筋
            rebar_side: 腰筋/构造筋
        """
        from entities.basic import BasicEntities
        from entities.polyline import PolylineEntity
        from entities.curves import CurveEntities

        cx, cy = position; hw = width/2; hh = height/2
        ents = []

        # 外轮廓
        ents.append(PolylineEntity.rectangle_centered(msp, (cx,cy), width, height,
                                                  layer=layer, color=7, lineweight=50))

        # 混凝土填充
        if hatch:
            from hatch.patterns import HatchFill
            pts = [(cx-hw,cy-hh),(cx+hw,cy-hh),(cx+hw,cy+hh),(cx-hw,cy+hh)]
            HatchFill.industry_fill(msp, pts, material='concrete', scale=30, layer='hatch')

        # 箍筋（矩形）
        c = 25  # 保护层厚度
        sw, sh = width - 2*c, height - 2*c
        ents.append(PolylineEntity.rectangle_centered(msp, (cx,cy), sw, sh,
                                                  layer='rebar', color=1))

        # 纵筋（圆点表示）
        default_rebar = rebar_bot or [(16,3)]
        
        for rebars, y_pos in [
            (rebar_top or [], cy + hh - c),
            (default_rebar, cy - hh + c),
            (rebar_side or [], cy),
        ]:
            for dia, count in rebars:
                rd = dia / 1.5  # 显示放大比例
                if count == 1:
                    ents.append(CurveEntities.circle(msp, (cx,y_pos), rd, layer='rebar', color=1))
                else:
                    spacing_x = sw / (count + 1)
                    for i in range(1, count + 1):
                        rx = cx - sw/2 + i * spacing_x
                        ents.append(CurveEntities.circle(msp, (rx,y_pos), rd, layer='rebar', color=1))

        return ents

    @staticmethod
    def slab_reinforcement(msp, corner1: tuple, corner2: tuple,
                           thickness: float = 120,
                           rebar_bottom_X: str = '12@150',
                           rebar_bottom_Y: str = '10@200',
                           rebar_top_X: str = '',
                           rebar_top_Y: str = '',
                           layer: str = 'outline'):
        """
        楼板配筋平面图
        
        Args:
            rebar_bottom_X: 底部X向钢筋标注 "直径@间距"
        """
        from entities.basic import BasicEntities
        from entities.text import TextEntity
        from entities.curves import CurveEntities

        x1, y1 = corner1; x2, y2 = corner2
        mx, my = (x1+x2)/2, (y1+y2)/2; hw, hh = (x2-x1)/2, (y2-y1)/2
        ents = []

        # 板轮廓
        ents.append(BasicEntities.line(msp, (x1,y1),(x2,y1), layer=layer, color=7, lineweight=50))
        ents.append(BasicEntities.line(msp, (x1,y2),(x2,y2), layer=layer, color=7, lineweight=50))
        ents.append(BasicEntities.line(msp, (x1,y1),(x1,y2), layer=layer, color=7, lineweight=50))
        ents.append(BasicEntities.line(msp, (x2,y1),(x2,y2), layer=layer, color=7, lineweight=50))

        # 钢筋示意线（双向）
        line_offset = min(hw, hh) * 0.15
        # X向底部钢筋
        if rebar_bottom_X:
            ry = y1 + line_offset
            ents.append(BasicEntities.line(msp, (x1+30,ry), (x2-30,ry),
                                           layer='rebar', color=1, lineweight=35))
            # 钢筋弯钩
            hook_len = 40
            ents.append(CurveEntities.arc(msp, (x1+30,ry), hook_len, 90, 270, layer='rebar', color=1))
            ents.append(CurveEntities.arc(msp, (x2-30,ry), hook_len, -90, 90, layer='rebar', color=1))
            # 标注
            TextEntity.text(msp, f'{rebar_bottom_X}', (mx, ry-20),
                           height=min(x2-x1,y2-y1)*0.04,
                           halign='CENTER', layer='text')

        # Y向底部钢筋
        if rebar_bottom_Y:
            rx = x1 + line_offset
            ents.append(BasicEntities.line(msp, (rx,y1+30),(rx,y2-30),
                                           layer='rebar', color=1, lineweight=35))
            hook_len = 40
            ents.append(CurveEntities.arc(msp, (rx,y1+30), hook_len, 0, 180, layer='rebar', color=1))
            ents.append(CurveEntities.arc(msp, (rx,y2-30), hook_len, 180, 360, layer='rebar', color=1))
            TextEntity.text(msp, f'{rebar_bottom_Y}', (rx-30,my),
                           height=min(x2-x1,y2-y1)*0.04,
                           halign='CENTER', valign='MIDDLE', layer='text',
                           rotation=90)

        # 板厚标注
        th_text = f't={thickness}'
        ents.append(TextEntity.text(msp, th_text, (x2-hh*0.5, y2-hh*0.3),
                                    height=min(x2-x1,y2-y1)*0.05, layer='text'))

        return ents

    @staticmethod
    def foundation_strip(msp, start: tuple, end: tuple,
                         top_width: float = 1000,
                         bottom_width: float = 1400,
                         depth: float = 800,
                         rebar_main: str = '20@120',
                         rebar_distribute: str = '12@200',
                         layer: str = 'outline'):
        """
        条形基础剖面图
        
        Returns:
            基础轮廓 + 配筋实体
        """
        from entities.polyline import PolylineEntity
        from entities.basic import BasicEntities
        from entities.text import TextEntity
        from hatch.patterns import HatchFill

        sx, sy = start; ex, ey = end
        dx, dy = ex-sx, ey-sy
        length = math.sqrt(dx*dx+dy*dy)
        ux, uy = dx/length, dy/length
        nx, ny = -uy, ux
        tw = top_width/2; bw = bottom_width/2

        # 梯形截面顶点（从左到右：左上→左下→右下→右上）
        pts = [
            (sx-nx*tw, sy-ny*tw),           # 左上（顶面左侧）
            (sx-nx*bw, sy-ny*(tw+(bw-tw)*depth/(depth))),  # 左下
            (ex-nx*bw, ey-ny*(tw+(bw-tw)*depth/(depth))),  # 右下
            (ex-nx*tw, ey-ny*tw),             # 右上（顶面右侧）
        ]
        # 简化：垂直方向向下延伸深度
        base_y = sy - depth if abs(uy) < 0.9 else sy
        profile_pts = [
            (sx-nx*tw, sy-ny*tw),
            (sx-nx*bw, sy-depth),
            (ex-nx*bw, ey-depth),
            (ex-nx*tw, ey-ny*tw),
        ]

        ents = [PolylineEntity.polygon(msp, profile_pts, layer=layer, color=7)]

        # 混凝土填充
        HatchFill.industry_fill(msp, profile_pts, material='concrete', scale=50, layer='hatch')

        # 底部受力筋（粗横线）
        bar_c = 50  # 保护层
        bar_y = sy - depth + bar_c
        bar_len = bw * 2 - 2 * bar_c
        ents.append(BasicEntities.line(msp,
            (sx-nx*(bw-bar_c), bar_y), (ex-nx*(bw-bar_c), bar_y),
            layer='rebar', color=1, lineweight=45))
        # 弯钩
        from entities.curves import CurveEntities
        hlen = 80
        ents.extend([
            CurveEntities.arc(msp, (sx-nx*(bw-bar_c), bar_y), hlen, 180, 360, layer='rebar', color=1),
            CurveEntities.arc(msp, (ex-nx*(bw-bar_c), bar_y), hlen, 0, 180, layer='rebar', color=1),
        ])

        # 分布筋（竖向短线）
        dist_count = max(int(length / 300), 3)
        for i in range(dist_count + 1):
            t = i / dist_count
            px = sx + t*dx; py = sy + t*dy
            dbar_h = depth * 0.6
            ents.append(BasicEntities.line(msp,
                (px, py-dbar_h), (px, py-depth+bar_c+hlen),
                layer='thin', color=5))

        # 标注
        mid_x = (sx+ex)/2; mid_y = (sy+ey)/2
        ents.append(TextEntity.text(msp, rebar_main, (mid_x, sy-depth+80),
                                   height=footing_depth*0.03, halign='CENTER', layer='text'))
        ents.append(TextEntity.text(msp, f'⌀{bottom_width}',
                                   (mid_x, sy-depth-30),
                                   height=footing_depth*0.02, halign='CENTER', layer='text'))

        return ents

    @staticmethod
    def column_footing(msp, center: tuple,
                       column_size: tuple = (500, 500),
                       footing_size: tuple = (2000, 2000),
                       footing_depth: float = 600,
                       step_count: int = 1,
                       layer: str = 'outline'):
        """
        独立基础（柱下扩展基础）平面图 + 简化剖面
        """
        from entities.polyline import PolylineEntity
        from entities.basic import BasicEntities
        from entities.text import TextEntity
        from entities.curves import CurveEntities
        from hatch.patterns import HatchFill

        cx, cy = center
        cw, ch = column_size; fw, fh = footing_size
        ents = []

        # 基础底板外轮廓（平面）
        ents.append(PolylineEntity.rectangle_centered(msp, (cx,cy), fw, fh,
                                                  layer=layer, color=7, lineweight=50))
        # 柱截面
        ents.append(PolylineEntity.rectangle_centered(msp, (cx,cy), cw, ch,
                                                  layer='column', color=2, lineweight=50))

        # 台阶式（如果有）
        if step_count >= 1:
            s1_w = fw * 0.75; s1_h = fh * 0.75
            ents.append(PolylineEntity.rectangle_centered(msp, (cx,cy), s1_w, s1_h,
                                                      layer=layer, color=7, lineweight=35))
            HatchFill.industry_fill(msp, [
                (-s1_w/2,-s1_h/2),(s1_w/2,-s1_h/2),(s1_w/2,s1_h/2),(-s1_w/2,s1_h/2)
            ], material='concrete', scale=50, layer='hatch')

        # 轴线标记
        tick = fw * 0.08
        ents.append(BasicEntities.line(msp, (cx-fw/2-tick,cy), (cx+cw/2+tick,cy),
                                       layer='axis', color=1, lineweight=13))
        ents.append(BasicEntities.line(msp, (cx,cy-fh/2-tick), (cx,cy+ch/2+tick),
                                       layer='axis', color=1, lineweight=13))

        # 尺寸标注文字
        ents.append(TextEntity.text(msp, f'{fw}×{fh}', (cx, cy+fh/2+50),
                                   height=fw*0.02, halign='CENTER', layer='text'))
        ents.append(TextEntity.text(msp, f'{cw}×{ch}', (cx, cy-ch/2-20),
                                   height=cw*0.025, halign='CENTER', layer='text'))

        return ents


path: scripts/templates/__init__.py
content: """行业图库模块"""
from .architectural import ArchitecturalTemplates
from .mechanical import MechanicalTemplates
from .electrical import ElectricalTemplates
from .piping import PipingTemplates
from .structural import StructuralTemplates

__all__ = [
    'ArchitecturalTemplates', 'MechanicalTemplates', 
    'ElectricalTemplates', 'PipingTemplates', 'StructuralTemplates'
]


path: scripts/nl_parser/intent_classifier.py
content: """
意图分类器 — 识别用户想画什么类型的图
基于关键词匹配 + 规则推理，无需训练模型
"""
from typing import Dict, List, Optional, Tuple
import re


class IntentClassifier:
    """
    自然语言意图 → 标准意图标识

    支持的意图类别：
    - arch_floor_plan     建筑平面图
    - mech_bolt           螺栓
    - mech_gear           齿轮
    - mech_bearing        轴承
    - mech_spring         弹簧
    - elec_switch         开关
    - elec_socket         插座
    - elec_lamp           灯具
    - elec_wires          导线组
    - pipe_straight       直管段
    - pipe_elbow          弯头
    - pipe_valve          阀门
    - pipe_tee            三通/四通
    - struct_beam         梁断面配筋
    - struct_slab         楼板配筋
    - struct_footing      独立基础
    - geo_rectangle       矩形
    - geo_circle          圆
    - geo_polygon         正多边形
    """

    # ============================================================
    # 意图规则库 —— 每条规则：[权重, 意图ID, 关键词列表]
    # 权重越高越优先匹配；同一意图可有多条规则（取最高分）
    # ============================================================

    RULES = [
        # ---------- 建筑 ----------
        (100, "arch_floor_plan", [
            "平面图", "户型图", "建筑平面", "房间", "外墙",
            "开(一|两|三)扇门", "开(一|两|三)扇窗", "画.*房",
            "3600x4800", "4000x3000", "A3.*建筑", "施工图(?!(配筋|结构))",
            "墙体", "墙厚", "柱子(?![配])", "楼梯(?!配)",
        ]),

        # ---------- 机械 ----------
        (95, "mech_bolt", [
            "螺栓", "螺钉", "螺丝头", "六角头", "十字槽",
            "[Mm]\\d+.*(螺栓|螺钉)", "螺栓.*俯视",
        ]),
        (90, "mech_gear", [
            "齿轮", "齿(轮)?端面", "\\d+齿.*齿轮",
            "齿顶圆", "齿根圆", "径向齿形",
        ]),
        (88, "mech_bearing", [
            "轴承", "滚珠轴承", "滚动体", "(620[0-9]|62[0-4][0-9]).*轴承",
            "内外圈.*轴承", "轴承.*(侧面|端面|剖)",
        ]),
        (85, "mech_spring", [
            "弹簧", "压缩弹簧", "拉伸弹簧", "扭转弹簧",
            "线径.*弹簧", "外径.*圈数", "有效圈数",
            "(锯齿|波浪).*(弹|簧)", "弹簧.*侧视",
        ]),

        # ---------- 电气 ----------
        (92, "elec_switch", [
            "开关符号?", "(单极|双极|三极).*开关", "断路器符号?",
            "开关.*(垂直|水平|横放|竖放)", "触点.*开关",
        ]),
        (90, "elec_socket", [
            "插座", "电源插座", "(三孔|五孔|单相|三相).*插",
            "墙壁插座", "地插",
        ]),
        (88, "elec_lamp", [
            "灯具?符号?", "(吸顶灯|长条灯|壁灯|筒灯|射灯|轨道灯|路灯)",
            "灯.*符号", "(荧光灯|日光灯)",
        ]),
        (85, "elec_wires", [
            "导线组", "(\\d+)相导线", "(三相|两相|单相).*线",
            "平行导线", "电线组", "(火线|零线|地线).*间距",
        ]),

        # ---------- 管道 ----------
        (90, "pipe_straight", [
            "直管段", "(DN\\d+).*(直管|管道)", "双线.*管",
            "带中心线.*管", "直管道",
        ]),
        (88, "pipe_elbow", [
            "弯头", "90度.*弯", "45度.*弯",
            "R\\d+.*弯头", "弯头.*(东北|西北|东南|西南)",
        ]),
        (87, "pipe_valve", [
            "阀门", "(闸阀|截止阀|球阀|蝶阀|止回阀)",
            "(阀门|阀).*DN\\d+", "阀体.*手轮",
        ]),
        (85, "pipe_tee", [
            "三通", "四通", "(等径|异径|变径).*三通",
            "DN\\d+/DN\\d+.*三通", "主管.*支管",
        ]),
        (80, "pipe_generic", [
            "法兰", "大小头", "异径管", "管帽",
            "(法兰|大小头|管帽).*DN",
        ]),

        # ---------- 结构 ----------
        (95, "struct_beam", [
            "梁断面", "梁.*配筋", "梁截面", "配筋梁",
            "\\d+x\\d+.*梁.*(配筋|钢筋|断面|截面)",
            "箍筋.*纵筋", "混凝土.*梁",
        ]),
        (90, "struct_slab", [
            "楼板配筋", "板.*配筋", "双向配筋",
            "楼板.*厚度.*钢筋", "(底筋|面筋|负筋).*楼板",
            "\\d+厚.*板.*钢筋",
        ]),
        (88, "struct_footing", [
            "独立基础", "柱下基础", "基础详图(?!(钢))",
            "台阶式基础", "垫层.*受力筋",
            "条形基础", "基础.*配筋",
        ]),

        # ---------- 通用几何 ----------
        (70, "geo_rectangle", [
            "矩形", "画一个.*矩形", "长方形",
            "\\d+x\\d+.*(矩形|长方)", "(宽|高).*矩形",
        ]),
        (70, "geo_circle", [
            "^画.*圆$", "圆形", "同心圆",
            "圆心.*半径", "直径.*圆",
            "circle", "圆.*(半径|R\\d+)",
        ]),
        (68, "geo_polygon", [
            "正多边形", "正(三角|四|五|六|八)边",
            "正多边形.*外接圆", "正(方|六|八)(形|边形)",
        ]),
    ]

    # ============================================================
    # 评分引擎
    # ============================================================

    @classmethod
    def _score(cls, text: str, pattern: str) -> float:
        """计算单个模式对文本的匹配得分"""
        try:
            if re.search(pattern, text):
                # 更长的匹配、更具体的关键词得更高分
                match = re.search(pattern, text)
                return len(match.group()) / max(len(text), 1) * 100 + 50
        except re.error:
            pass

        # 简单包含检查
        if pattern in text:
            return 40.0

        return 0.0

    @classmethod
    def _score_rule(cls, text: str, rule: Tuple) -> float:
        """计算一条规则的总体得分"""
        weight, intent_id, patterns = rule
        scores = [cls._score(text, p) for p in patterns]
        max_score = max(scores) if scores else 0
        return max_score * (weight / 100)

    @staticmethod
    def _is_likely_architecture(text: str) -> bool:
        """快速判断是否偏向建筑工程领域"""
        arch_keywords = [
            '墙', '门', '窗', '柱', '楼梯', '阳台', '卫生间', '厨房',
            '卧室', '客厅', '平面图', '户型', '开间', '进深',
            '砖混', '框架', '剪力墙', '外墙', '内墙',
        ]
        return sum(1 for kw in arch_keywords if kw in text) >= 2

    @staticmethod
    def _is_mechanical(text: str) -> bool:
        """快速判断是否偏向机械领域"""
        mech_keywords = [
            '螺栓', '螺母', '齿轮', '轴承', '弹簧', '键槽', '销钉',
            '垫圈', '螺纹', '轴', '孔', '倒角', '公差',
            'M16', 'M20', '俯视图', '侧视图', '剖面图',
        ]
        return any(kw in text for kw in mech_keywords)

    @staticmethod
    def _is_electrical(text: str) -> bool:
        """快速判断是否偏向电气领域"""
        elec_keywords = [
            '开关', '插座', '灯具', '配电箱', '断路器',
            '导线', '线路', '回路', '相线', '零线', '接地',
            '吸顶灯', '荧光灯', '壁灯',
        ]
        return any(kw in text for kw in elec_keywords)

    @staticmethod
    def _is_piping(text: str) -> bool:
        """快速判断是否偏向管道领域"""
        pipe_keywords = [
            '弯头', '三通', '法兰', '阀门', '闸阀', '球阀',
            '直管', '管道', 'DN', '大小头', '管帽', '蝶阀',
        ]
        return any(kw in text for kw in pipe_keywords)

    @staticmethod
    def _is_structural(text: str) -> bool:
        """快速判断是否偏向结构工程"""
        struct_keywords = [
            '梁', '板', '柱', '基础', '配筋', '钢筋', '箍筋',
            '混凝土', 'C30', 'C25', '楼板', '地基', '承台',
            '独立基础', '条基', '桩', '负筋', '分布筋',
        ]
        return any(kw in text for kw in struct_keywords)

    # ============================================================
    # 主入口
    # ============================================================

    @classmethod
    def classify(cls, text: str) -> str:
        """
        分类用户自然语言输入的绘图意图

        Args:
            text: 用户输入的自然语言

        Returns:
            意图标识字符串，如 "arch_floor_plan"
        """

        # 计算每条规则的得分
        scored_rules = []
        for rule in cls.RULES:
            score = cls._score_rule(text, rule)
            if score > 0:
                scored_rules.append((rule[1], score))

        if not scored_rules:
            # 无明确匹配 — 回退到几何图形
            return cls._fallback_classify(text)

        # 按得分排序
        scored_rules.sort(key=lambda x: x[1], reverse=True)
        best_intent, best_score = scored_rules[0]

        # 如果前两名得分接近，用领域启发式打破平局
        if len(scored_rules) >= 2:
            second_intent, second_score = scored_rules[1]
            if abs(best_score - second_score) < 10:
                best_intent = cls._tiebreak(text, best_intent, second_intent)

        return best_intent

    @classmethod
    def _tiebreak(cls, text: str, intent_a: str, intent_b: str) -> str:
        """当两个意图得分接近时，用领域启发式选择"""

        domain_checkers = {
            "arch": cls._is_likely_architecture,
            "mech": cls._is_mechanical,
            "elec": cls._is_electrical,
            "pipe": cls._is_piping,
            "struct": cls._is_structural,
        }

        for domain_name, checker in domain_checkers.items():
            if checker(text):
                # 检查哪个意图属于该领域
                a_in_domain = intent_a.split('_')[0] in {
                    'arch': ['arch'],
                    'mech': ['mech'],
                    'elec': ['elec'],
                    'pipe': ['pipe'],
                    'struct': ['struct'],
                }.get(domain_name, [])
                b_in_domain = intent_b.split('_')[0] in {
                    'arch': ['arch'],
                    'mech': ['mech'],
                    'elec': ['elec'],
                    'pipe': ['pipe'],
                    'struct': ['struct'],
                }.get(domain_name, [])

                if a_in_domain and not b_in_domain:
                    return intent_a
                elif b_in_domain and not a_in_domain:
                    return intent_b

        # 无法区分，返回得分更高的
        return intent_a

    @classmethod
    def _fallback_classify(cls, text: str) -> str:
        """回退分类：无规则匹配时的兜底策略"""

        # 检查是否有尺寸描述
        has_size = bool(cls.SIZE_PATTERN.search(text))

        if has_size or '画' in text:
            # 有尺寸或"画"字，默认为矩形
            return "geo_rectangle"

        if '圆' in text:
            return "geo_circle"

        if any(w in text for w in ['三角', '四边', '五边', '六边', '八边', '多边形']):
            return "geo_polygon"

        # 完全无法分类
        return "unknown"

    SIZE_PATTERN = re.compile(r'\d+\s*[x×*]\s*\d+', re.IGNORECASE)

    # ============================================================
    # 调试工具
    # ============================================================

    @classmethod
    def debug_classify(cls, text: str) -> str:
        """调试模式：输出所有规则的评分详情"""
        lines = [f"=== 意图分类调试 ===", f"文本: {text}", f"---"]

        scored = []
        for rule in cls.RULES:
            score = cls._score_rule(text, rule)
            if score > 0:
                scored.append((rule[1], score))

        scored.sort(key=lambda x: x[1], reverse=True)

        for intent, score in scored[:5]:
            marker = " ← 最佳" if intent == scored[0][0] else ""
            lines.append(f"  {intent}: {score:.1f}{marker}")

        result = cls.classify(text)
        lines.append(f"---")
        lines.append(f"最终选择: {result}")

        # 领域检测
        lines.append(f"--- 领域检测:")
        lines.append(f"  建筑: {cls._is_likely_architecture(text)}")
        lines.append(f"  机械: {cls._is_mechanical(text)}")
        lines.append(f"  电气: {cls._is_electrical(text)}")
        lines.append(f"  管道: {cls._is_piping(text)}")
        lines.append(f"  结构: {cls._is_structural(text)}")

        return '\n'.join(lines)

    @classmethod
    def list_intents(cls) -> List[str]:
        """列出所有支持的意图类型"""
        intents = list(set(rule[1] for rule in cls.RULES))
        intents.sort()
        return intents


path: scripts/nl_parser/__init__.py
content: """自然语言解析器模块"""
from .intent_classifier import IntentClassifier
from .param_extractor import ParamExtractor
from .script_generator import ScriptGenerator

__all__ = ['IntentClassifier', 'ParamExtractor', 'ScriptGenerator']


path: scripts/layout/paperspace.py
content: """图纸空间/布局管理"""
import ezdxf
from typing import Optional


# 标准图纸尺寸（mm）- GB/T 14689
PAPER_SIZES = {
    'A0':   (841,   1189),
    'A1':   (594,   841),
    'A2':   (420,   594),
    'A3':   (297,   420),
    'A4':   (210,   297),
    'A0L':  (1189,  841),  # 横版
    'A1L':  (841,   594),
    'A2L':  (594,   420),
    'A3L':  (420,   297),
    'A4L':  (297,   210),
}


class PaperSpace:
    """图纸布局管理"""

    @staticmethod
    def create_layout(doc: ezdxf.document.Drawing,
                      name: str = 'Layout1',
                      paper_size: str = 'A3') -> ezdxf.layouts.Layout:
        """创建新图纸空间"""
        if paper_size not in PAPER_SIZES:
            raise ValueError(f"未知纸张大小 '{paper_size}'，可用: {list(PAPER_SIZES.keys())}")
        try:
            layout = doc.layouts.new(name)
        except Exception:
            layout = doc.layouts.get(name)
        w, h = PAPER_SIZES[paper_size]
        try:
            layout.dxf.paper_width = w
            layout.dxf.paper_height = h
        except Exception:
            pass
        return layout

    @staticmethod
    def add_viewport(layout,
                     center: tuple,
                     width: float,
                     height: float,
                     scale: float = 1.0,
                     model_center: tuple = None):
        """创建视口（模型空间到图纸空间的窗口）"""
        vp = layout.add_viewport(
            center=(center[0], center[1]),
            size=(width, height),
            dxfattribs={'status': 1}
        )
        try:
            vp.dxf.view_height = height * scale
            if model_center:
                vp.dxf.center_point = (model_center[0], model_center[1])
        except Exception:
            pass
        return vp

    @staticmethod
    def draw_title_block(msp_or_layout,
                         corner: tuple,
                         size: str = 'A3',
                         title: str = '',
                         drawing_no: str = '',
                         scale_text: str = '1:100',
                         layer: str = 'frame',
                         color: int = 7) -> dict:
        """绘制标准图框和标题栏（GB风格）"""
        from entities.basic import BasicEntities
        from entities.polyline import PolylineEntity
        from entities.text import TextEntity

        if size not in PAPER_SIZES:
            size = 'A3'
        pw, ph = PAPER_SIZES[size]
        x, y = corner

        margin_left = 25
        margin_other = 5
        fw = pw - margin_left - margin_other
        fh = ph - 2 * margin_other

        # 外框线（粗实线）
        PolylineEntity.rectangle(msp_or_layout,
                                  (x+margin_left, y+margin_other),
                                  (x+margin_left+fw, y+margin_other+fh),
                                  layer=layer, color=color, lineweight=53)

        # 内框
        PolylineEntity.rectangle(msp_or_layout,
                                  (x+margin_left, y+margin_other),
                                  (x+margin_left+fw, y+margin_other+fh),
                                  layer=layer, color=color)

        # 标题栏（右下角 180x40mm）
        tb_x = x + margin_left + fw - 180
        tb_y = y + margin_other
        tb_w, tb_h = 180, 40

        PolylineEntity.rectangle(msp_or_layout,
                                  (tb_x, tb_y), (tb_x + tb_w, tb_y + tb_h),
                                  layer=layer, color=color)

        # 分隔线
        BasicEntities.line(msp_or_layout, (tb_x, tb_y+20), (tb_x+tb_w, tb_y+20),
                           layer=layer, color=color)
        BasicEntities.line(msp_or_layout, (tb_x+80, tb_y), (tb_x+80, tb_y+20),
                           layer=layer, color=color)
        BasicEntities.line(msp_or_layout, (tb_x+120, tb_y), (tb_x+120, tb_y+20),
                           layer=layer, color=color)

        # 标题文字
        if title:
            TextEntity.text(msp_or_layout, title, (tb_x + tb_w/2, tb_y + 10),
                           height=5, halign='CENTER', valign='MIDDLE', layer=layer, color=color)

        TextEntity.text(msp_or_layout, '图号', (tb_x+100, tb_y+10),
                       height=3, halign='CENTER', valign='MIDDLE', layer=layer, color=color)
        if drawing_no:
            TextEntity.text(msp_or_layout, drawing_no, (tb_x+150, tb_y+10),
                           height=3, halign='CENTER', valign='MIDDLE', layer=layer, color=color)

        TextEntity.text(msp_or_layout, '比例', (tb_x+40, tb_y+28),
                       height=2.5, halign='CENTER', valign='MIDDLE', layer=layer, color=color)
        TextEntity.text(msp_or_layout, scale_text, (tb_x+60, tb_y+28),
                       height=2.5, halign='CENTER', valign='MIDDLE', layer=layer, color=color)

        return {
            'frame_rect': ((x+margin_left, y+margin_other), (x+margin_left+fw, y+margin_other+fh)),
            'title_block_rect': ((tb_x, tb_y), (tb_x+tb_w, tb_y+tb_h)),
            'inner_area': ((x+margin_left, y+margin_other), (x+margin_left+fw, y+margin_other+fh)),
        }


path: scripts/layout/viewport.py
content: """视口管理"""
import math


class Viewport:
    """视口操作"""

    @staticmethod
    def fit_to_extent(layout, viewport, min_pt: tuple, max_pt: tuple):
        """调整视口以显示指定范围"""
        extent_width = max_pt[0] - min_pt[0]
        extent_height = max_pt[1] - min_pt[1]

        try:
            viewport.dxf.center_point = (
                (min_pt[0] + max_pt[0]) / 2,
                (min_pt[1] + max_pt[1]) / 2,
            )
            viewport.dxf.view_height = max(extent_width, extent_height) * 1.05
        except Exception:
            pass

        return viewport

    @staticmethod
    def freeze_layer_in_vp(viewport, layer_name: str):
        """在视口中冻结指定图层"""
        try:
            viewport.freeze([layer_name])
        except Exception:
            pass


path: scripts/layout/__init__.py
content: """布局模块"""
from .paperspace import PaperSpace, PAPER_SIZES
from .viewport import Viewport

__all__ = ['PaperSpace', 'Viewport', 'PAPER_SIZES']


path: scripts/layer/__init__.py
content: """图层模块"""
from .manager import LayerManager
from .linetypes import Linetypes

__all__ = ['LayerManager', 'Linetypes']


path: scripts/hatch/__init__.py
content: """填充模块"""
from .patterns import HatchPatterns, HatchFill

__all__ = ['HatchPatterns', 'HatchFill']


path: scripts/export/__init__.py
content: """导出模块"""
from .to_svg import dxf_to_svg
from .to_pdf import dxf_to_pdf
from .batch_export import BatchExporter, render_dxf_to_png

__all__ = ['dxf_to_svg', 'dxf_to_pdf', 'BatchExporter', 'render_dxf_to_png']


path: scripts/entities/__init__.py
content: """图元模块"""
from .basic import BasicEntities
from .curves import CurveEntities
from .polyline import PolylineEntity
from .text import TextEntity

__all__ = ['BasicEntities', 'CurveEntities', 'PolylineEntity', 'TextEntity']


path: scripts/dimension/__init__.py
content: """尺寸标注模块"""
from .linear import LinearDimension
from .radial import RadialDimension
from .leader import LeaderAnnotation

__all__ = ['LinearDimension', 'RadialDimension', 'LeaderAnnotation']


path: scripts/core/__init__.py
content: """核心模块"""
from .document import CADDocument
from .units import Units
from .renderer import Renderer

__all__ = ['CADDocument', 'Units', 'Renderer']


path: scripts/block/__init__.py
content: """图块模块"""
from .block_lib import BlockLib
from .attributes import BlockAttributes

__all__ = ['BlockLib', 'BlockAttributes']


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)