"""轮廓求解器:参数化草图描述 → entities + contour_edges_mm。 LLM 只需输出离散决策(type, radius, width …), 求解器负责生成精确的实体和轮廓边坐标。 架构:注册表模式 —— 每个轮廓类型对应一个生成器函数, 按 "type" 字符串索引。新增形状只需 3 步: 1. 写 def solver_xxx(profile, meta) -> (entities, contour) 2. 注册: SHAPE_GENERATORS["xxx"] = solver_xxx 3. 在 convert 脚本中输出对应的 profile 支持的 profile 类型: - circle: 单个圆 - annulus: 同心圆环 - circles: 多圆(引擎自动判断加/切除) - circle_grid: 矩形圆孔阵列(行列+间距) - rectangle: 矩形 - rectangle_with_circles: 矩形 + 内圆孔/岛 - rectangle_with_fillets: 带圆角的矩形(4角倒圆),可选内圆 - rectangle_with_symmetric_notches: 对称槽板(矩形+4个U形缺口) - obround: 槽形 / 键槽(2平行线 + 2半圆) - polygon: N边多边形(顶点列表) - ibone: 工字形凸耳(12线+4弧+4孔) - circle_with_arc_notches: 圆+均匀圆弧凹口 - circular_sector_slot: 圆弧扇区+中心矩形槽 - circle_with_radial_tabs: 圆+径向矩形凸耳(带圆角) - filleted_rect_side_slots: 圆角矩形+两侧中心U形槽 - d_shape: D形(半圆+弦线) - partial_ring: 部分圆环(同心弧+径向线) - partial_ring_with_arc_island: 扇区环 + 弦上偏移弧岛(保留材料岛) - concentric_arc_profile: 同心圆弧轮廓(多段弧+圆心标记) - patterned_cutouts: 母形 + 规则布局的多区域切口 - compound_patterned_cutouts: 多组母形/布局合并为一个切除草图 """ from __future__ import annotations import math from copy import deepcopy from typing import Any # ═══════════════════════════════════════════════════════════════ # 基础几何原语 # ═══════════════════════════════════════════════════════════════ def _circle(center: list[float], radius_mm: float, construction: bool = False) -> dict[str, Any]: return { "type": "circle", "center": [float(center[0]), float(center[1])], "radius_mm": float(radius_mm), "construction": construction, } def _line(start: list[float], end: list[float], construction: bool = False) -> dict[str, Any]: return { "type": "line", "start": [float(start[0]), float(start[1])], "end": [float(end[0]), float(end[1])], "construction": construction, } def _contour_line(start_mm: list[float], end_mm: list[float]) -> dict[str, Any]: return { "type": "line", "start_mm": [ float(start_mm[0]), float(start_mm[1]), float(start_mm[2]) if len(start_mm) > 2 else 0.0, ], "end_mm": [ float(end_mm[0]), float(end_mm[1]), float(end_mm[2]) if len(end_mm) > 2 else 0.0, ], } def _contour_arc( start_mm: list[float], end_mm: list[float], center_mm: list[float], radius_mm: float | None, ) -> dict[str, Any]: return { "type": "arc", "start_mm": [ float(start_mm[0]), float(start_mm[1]), float(start_mm[2]) if len(start_mm) > 2 else 0.0, ], "end_mm": [ float(end_mm[0]), float(end_mm[1]), float(end_mm[2]) if len(end_mm) > 2 else 0.0, ], "center_mm": [ float(center_mm[0]), float(center_mm[1]), float(center_mm[2]) if len(center_mm) > 2 else 0.0, ], "radius_mm": float(radius_mm) if radius_mm is not None else None, } # ═══════════════════════════════════════════════════════════════ # 3D 坐标转换 # ═══════════════════════════════════════════════════════════════ def _to_3d(workplane: dict[str, Any], u: float, v: float) -> list[float]: """将2D局部坐标 (u,v) 映射到3D世界坐标。""" origin = workplane.get("origin_mm") or [0, 0, 0] x_dir = workplane.get("x_dir") or [1, 0, 0] normal = workplane.get("normal") or [0, 0, 1] y_dir = [ normal[1] * x_dir[2] - normal[2] * x_dir[1], normal[2] * x_dir[0] - normal[0] * x_dir[2], normal[0] * x_dir[1] - normal[1] * x_dir[0], ] return [ origin[0] + u * x_dir[0] + v * y_dir[0], origin[1] + u * x_dir[1] + v * y_dir[1], origin[2] + u * x_dir[2] + v * y_dir[2], ] def _transform_contours(contour: list[dict[str, Any]], wp: dict[str, Any]) -> list[dict[str, Any]]: """将轮廓边的2D坐标映射为3D世界坐标。""" result: list[dict[str, Any]] = [] x_dir = wp.get("x_dir") or [1, 0, 0] normal = wp.get("normal") or [0, 0, 1] for e in contour: e2 = deepcopy(e) if e["type"] == "line": e2["start_mm"] = _to_3d(wp, e["start_mm"][0], e["start_mm"][1]) e2["end_mm"] = _to_3d(wp, e["end_mm"][0], e["end_mm"][1]) elif e["type"] == "arc": e2["start_mm"] = _to_3d(wp, e["start_mm"][0], e["start_mm"][1]) e2["end_mm"] = _to_3d(wp, e["end_mm"][0], e["end_mm"][1]) e2["center_mm"] = _to_3d(wp, e["center_mm"][0], e["center_mm"][1]) e2["normal"] = list(normal) result.append(e2) return result # ═══════════════════════════════════════════════════════════════ # 矩形 / 圆辅助 # ═══════════════════════════════════════════════════════════════ def _build_rect_bounds(profile: dict[str, Any]) -> tuple[float, float, float, float]: """从 profile 中提取矩形的 (x0, y0, x1, y1) 边界。""" center = profile.get("center") w = float(profile.get("width_mm") or 0) h = float(profile.get("height_mm") or 0) if center and w > 0 and h > 0: cx, cy = float(center[0]), float(center[1]) return cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2 mn = profile.get("min_mm") mx = profile.get("max_mm") if mn and mx: return float(mn[0]), float(mn[1]), float(mx[0]), float(mx[1]) raise ValueError("rectangle profile needs (center+width+height) or (min+max)") def _rect_lines_and_contour( x0: float, y0: float, x1: float, y1: float, ) -> tuple[list[dict], list[dict]]: p00, p10, p11, p01 = [x0, y0, 0.0], [x1, y0, 0.0], [x1, y1, 0.0], [x0, y1, 0.0] entities = [ _line([x0, y0], [x1, y0]), _line([x1, y0], [x1, y1]), _line([x1, y1], [x0, y1]), _line([x0, y1], [x0, y0]), ] contour = [ _contour_line(p00, p10), _contour_line(p10, p11), _contour_line(p11, p01), _contour_line(p01, p00), ] return entities, contour def _build_circle_entities(items: list[dict]) -> list[dict]: entities: list[dict] = [] for item in items: center = item.get("center") or [0.0, 0.0] r = float(item.get("radius_mm") or 0) if r <= 0: raise ValueError("circle radius must be > 0") entities.append(_circle(center, r, construction=False)) return entities def _filleted_rect_contour( x0: float, y0: float, x1: float, y1: float, r: float, ) -> tuple[list[dict], list[dict]]: """生成带圆角矩形的实体线和轮廓边(4直线 + 4圆弧)。""" if r <= 0: return _rect_lines_and_contour(x0, y0, x1, y1) cx0, cx1 = x0 + r, x1 - r cy0, cy1 = y0 + r, y1 - r entities = [ _line([cx0, y0], [cx1, y0]), _line([x0, cy0], [x0, cy1]), _line([cx0, y1], [cx1, y1]), _line([x1, cy0], [x1, cy1]), ] contour = [ _contour_line([x0, cy0, 0.0], [x0, cy1, 0.0]), _contour_arc([x0, cy1, 0.0], [cx0, y1, 0.0], [cx0, cy1, 0.0], r), _contour_line([cx0, y1, 0.0], [cx1, y1, 0.0]), _contour_arc([cx1, y1, 0.0], [x1, cy1, 0.0], [cx1, cy1, 0.0], r), _contour_line([x1, cy1, 0.0], [x1, cy0, 0.0]), _contour_arc([x1, cy0, 0.0], [cx1, y0, 0.0], [cx1, cy0, 0.0], r), _contour_line([cx1, y0, 0.0], [cx0, y0, 0.0]), _contour_arc([cx0, y0, 0.0], [x0, cy0, 0.0], [cx0, cy0, 0.0], r), ] return entities, contour # ═══════════════════════════════════════════════════════════════ # 形状生成器(每个是一个独立函数,按 type 注册) # ═══════════════════════════════════════════════════════════════ _Ctx = dict[str, Any] def _gen_circle(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: center = profile.get("center") or [0.0, 0.0] r = float(profile.get("radius_mm") or 0) if r <= 0: raise ValueError("circle radius must be > 0") entities = [_circle(center, r)] cx, cy, c3d = float(center[0]), float(center[1]), [float(center[0]), float(center[1]), 0.0] contour = [ _contour_arc([cx + r, cy, 0.0], [cx, cy + r, 0.0], c3d, r), _contour_arc([cx, cy + r, 0.0], [cx - r, cy, 0.0], c3d, r), _contour_arc([cx - r, cy, 0.0], [cx, cy - r, 0.0], c3d, r), _contour_arc([cx, cy - r, 0.0], [cx + r, cy, 0.0], c3d, r), ] return entities, contour def _gen_annulus(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: center = profile.get("center") or [0.0, 0.0] inner_r = float(profile.get("inner_radius_mm") or 0) outer_r = float(profile.get("outer_radius_mm") or 0) if inner_r <= 0 or outer_r <= 0: raise ValueError("annulus radii must be > 0") if inner_r >= outer_r: raise ValueError("inner_radius >= outer_radius") return [_circle(center, inner_r), _circle(center, outer_r)], [] def _gen_circles(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: items = profile.get("items") or [] if not items: raise ValueError("circles items must be non-empty") return _build_circle_entities(items), [] def _gen_circle_grid(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """矩形圆孔阵列:由起点圆心 + 间距 + 行列数生成。 参数: radius_mm: 孔半径 count_x / count_y: 列数、行数 spacing_x_mm / spacing_y_mm: 圆心间距 origin_mm: 第一孔圆心 [u,v](默认沿 +u/+v 铺开) 或 center_mm: 阵列几何中心(与 origin_mm 二选一) 覆盖: b006(4×5 通孔阵列) """ r = float(profile["radius_mm"]) nx = int(profile["count_x"]) ny = int(profile["count_y"]) sx = float(profile["spacing_x_mm"]) sy = float(profile["spacing_y_mm"]) if r <= 0 or nx < 1 or ny < 1: raise ValueError("circle_grid: invalid radius/counts") if profile.get("center_mm") is not None: cc = profile["center_mm"] u0 = float(cc[0]) - (nx - 1) * sx / 2.0 v0 = float(cc[1]) - (ny - 1) * sy / 2.0 else: origin = profile.get("origin_mm") or [0.0, 0.0] u0, v0 = float(origin[0]), float(origin[1]) items = [ {"center": [u0 + i * sx, v0 + j * sy], "radius_mm": r} for j in range(ny) for i in range(nx) ] return _build_circle_entities(items), [] def _gen_rectangle(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: x0, y0, x1, y1 = _build_rect_bounds(profile) return _rect_lines_and_contour(x0, y0, x1, y1) def _gen_rect_with_circles(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: boundary = profile.get("boundary") or {} circle_items = profile.get("circles") or [] btype = boundary.get("type") or "rectangle" if btype in ("rectangle", "rectangle_with_fillets"): if btype == "rectangle": x0, y0, x1, y1 = _build_rect_bounds(boundary) ent, con = _rect_lines_and_contour(x0, y0, x1, y1) else: fr = float(boundary.get("fillet_radius_mm") or 0) x0, y0, x1, y1 = _build_rect_bounds(boundary) ent, con = _filleted_rect_contour(x0, y0, x1, y1, fr) ent.extend(_build_circle_entities(circle_items)) return ent, con raise ValueError(f"rectangle_with_circles: unsupported boundary type {btype!r}") def _gen_rect_with_fillets(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: fr = float(profile.get("fillet_radius_mm") or 0) x0, y0, x1, y1 = _build_rect_bounds(profile) ent, con = _filleted_rect_contour(x0, y0, x1, y1, fr) ent.extend(_build_circle_entities(profile.get("circles") or [])) return ent, con def _gen_obround(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: center = profile.get("center") length = float(profile.get("length_mm") or 0) width = float(profile.get("width_mm") or 0) if length <= 0 or width <= 0: raise ValueError("obround needs positive length/width") r = width / 2 cx, cy = (float(center[0]), float(center[1])) if center else (0.0, 0.0) offset = max(0, (length - width) / 2) left_cx, right_cx = cx - offset, cx + offset if offset < 0.001: c3d = [cx, cy, 0.0] contour = [ _contour_arc([cx + r, cy, 0.0], [cx, cy + r, 0.0], c3d, r), _contour_arc([cx, cy + r, 0.0], [cx - r, cy, 0.0], c3d, r), _contour_arc([cx - r, cy, 0.0], [cx, cy - r, 0.0], c3d, r), _contour_arc([cx, cy - r, 0.0], [cx + r, cy, 0.0], c3d, r), ] return [_circle([cx, cy], r)], contour top_y, bot_y = cy + r, cy - r left_c3d, right_c3d = [left_cx, cy, 0.0], [right_cx, cy, 0.0] contour = [ _contour_arc([right_cx, top_y, 0.0], [right_cx + r, cy, 0.0], right_c3d, r), _contour_arc([right_cx + r, cy, 0.0], [right_cx, bot_y, 0.0], right_c3d, r), _contour_line([right_cx, bot_y, 0.0], [left_cx, bot_y, 0.0]), _contour_arc([left_cx, bot_y, 0.0], [left_cx - r, cy, 0.0], left_c3d, r), _contour_arc([left_cx - r, cy, 0.0], [left_cx, top_y, 0.0], left_c3d, r), _contour_line([left_cx, top_y, 0.0], [right_cx, top_y, 0.0]), ] entities = [ _line([left_cx, bot_y], [right_cx, bot_y]), _line([left_cx, top_y], [right_cx, top_y]), _circle([left_cx, cy], r), _circle([right_cx, cy], r), ] return entities, contour def _gen_polygon(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: vertices = profile.get("vertices") or [] if len(vertices) >= 3: pts_2d = [(float(v[0]), float(v[1])) for v in vertices] entities, contour = [], [] for i in range(len(pts_2d)): s, e = pts_2d[i], pts_2d[(i + 1) % len(pts_2d)] entities.append(_line(list(s), list(e))) contour.append(_contour_line([s[0], s[1], 0.0], [e[0], e[1], 0.0])) return entities, contour # 015133: no vertices in profile -> use entities from compiler_context ents = meta.get("_entities") or [] if not ents: raise ValueError("polygon needs at least 3 vertices or existing entities in sketch") contour = [] for e in ents: t = e.get("type", "") if t == "line": s = e.get("start", [0, 0]) ed = e.get("end", [0, 0]) contour.append(_contour_line([float(s[0]), float(s[1]), 0.0], [float(ed[0]), float(ed[1]), 0.0])) elif t == "arc": contour.append(_contour_line( [float(e["start"][0]), float(e["start"][1]), 0.0], [float(e["end"][0]), float(e["end"][1]), 0.0])) return list(ents), contour def _gen_ibone(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """工字形凸耳:12线 + 4弧 + 4孔""" bw, bh = float(profile["body_width_mm"]), float(profile["body_height_mm"]) fw, fh = float(profile["flange_width_mm"]), float(profile["flange_height_mm"]) cr = float(profile["corner_radius_mm"]) hr = float(profile.get("hole_radius_mm") or 0) hw, hfw, ar = bw / 2, fw / 2, bw / 2 - cr av_bot, av_top = fh - cr, bh - fh + cr segs = [ ("L", hfw, 0, -hfw, 0), ("L", -hfw, 0, -hfw, fh - cr), ("A", -hfw, fh - cr, -ar, fh, -ar, fh - cr, cr), ("L", -ar, fh, -hw, fh), ("L", -hw, fh, -hw, bh - fh), ("L", -hw, bh - fh, -ar, bh - fh), ("A", -ar, bh - fh, -hfw, bh - fh + cr, -ar, bh - fh + cr, cr), ("L", -hfw, bh - fh + cr, -hfw, bh), ("L", -hfw, bh, hfw, bh), ("L", hfw, bh, hfw, bh - fh + cr), ("A", hfw, bh - fh + cr, ar, bh - fh, ar, bh - fh + cr, cr), ("L", ar, bh - fh, hw, bh - fh), ("L", hw, bh - fh, hw, fh), ("L", hw, fh, ar, fh), ("A", ar, fh, hfw, fh - cr, ar, fh - cr, cr), ("L", hfw, fh - cr, hfw, 0), ] entities, contour = [], [] for s in segs: if s[0] == "L": _, u1, v1, u2, v2 = s entities.append(_line([u1, v1], [u2, v2])) contour.append(_contour_line([u1, v1, 0.0], [u2, v2, 0.0])) else: _, u1, v1, u2, v2, cu, cv, r = s contour.append(_contour_arc([u1, v1, 0.0], [u2, v2, 0.0], [cu, cv, 0.0], r)) if hr > 0: for cu, cv in [(-ar, av_bot), (ar, av_bot), (-ar, av_top), (ar, av_top)]: entities.append(_circle([cu, cv], hr)) return entities, contour def _gen_rect_symmetric_notches(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """对称槽板:矩形+4个U形缺口(简化多边形 或 精确弧边)""" w, h = float(profile["width_mm"]), float(profile["height_mm"]) n = profile.get("notch") or {} n_ys, n_ye = float(n["y_start"]), float(n["y_end"]) n_depth = float(n["depth_mm"]) n_ir = float(n.get("inner_radius_mm") or 0) n_cr = float(n.get("corner_radius_mm") or 0) hw = w / 2 inner_u = hw - n_depth if n_ir > 0 and n_cr > 0: icu, icv = hw - n_depth / 2, (n_ys + n_ye) / 2 entities, contour = [], [] for u1, v1, u2, v2 in [ (hw, 0, hw, n_ys), (hw, n_ye, hw, h - n_ye), (hw, h - n_ys, hw, h), (-hw, h, -hw, h - n_ys), (-hw, h - n_ye, -hw, n_ye), (-hw, n_ys, -hw, 0), (-hw, 0, hw, 0), (hw, h, -hw, h), ]: entities.append(_line([u1, v1], [u2, v2])) contour.append(_contour_line([u1, v1, 0.0], [u2, v2, 0.0])) def _notch(sign_u, y_bot, y_top): u = sign_u * hw ec = sign_u * (hw - n_cr) icu2 = sign_u * icu contour.append(_contour_arc( [u, y_bot, 0.0], [ec, y_bot + n_cr, 0.0], [ec, y_bot, 0.0], n_cr)) av = y_bot + n_cr au = icu2 - sign_u * math.sqrt(max(0.0, n_ir ** 2 - (av - icv) ** 2)) contour.append(_contour_line([ec, av, 0.0], [au, av, 0.0])) bv = y_top - n_cr bu = icu2 - sign_u * math.sqrt(max(0.0, n_ir ** 2 - (bv - icv) ** 2)) contour.append(_contour_arc( [au, av, 0.0], [bu, bv, 0.0], [icu2, icv, 0.0], n_ir)) contour.append(_contour_line([bu, bv, 0.0], [ec, bv, 0.0])) contour.append(_contour_arc( [ec, bv, 0.0], [u, y_top, 0.0], [ec, y_top, 0.0], n_cr)) _notch(+1, n_ys, n_ye) _notch(+1, h - n_ye, h - n_ys) _notch(-1, n_ys, n_ye) _notch(-1, h - n_ye, h - n_ys) return entities, contour # 简化多边形(5 参数) verts = [ (hw, 0), (hw, n_ys), (inner_u, n_ys), (inner_u, n_ye), (hw, n_ye), (hw, h - n_ye), (inner_u, h - n_ye), (inner_u, h - n_ys), (hw, h - n_ys), (hw, h), (-hw, h), (-hw, h - n_ys), (-inner_u, h - n_ys), (-inner_u, h - n_ye), (-hw, h - n_ye), (-hw, n_ye), (-inner_u, n_ye), (-inner_u, n_ys), (-hw, n_ys), (-hw, 0), ] entities, contour = [], [] pts = [(float(v[0]), float(v[1])) for v in verts] for i in range(len(pts)): s, e = pts[i], pts[(i + 1) % len(pts)] entities.append(_line(list(s), list(e))) contour.append(_contour_line([s[0], s[1], 0.0], [e[0], e[1], 0.0])) return entities, contour def _gen_revolve_chamfer(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """旋转切除的梯形截面(5顶点,相对轴顶点定义)。""" ah = float(profile["axis_height_mm"]) tw = float(profile["top_width_mm"]) bw = float(profile["bottom_width_mm"]) wi = float(profile.get("wall_inset_mm") or 0) si = float(profile.get("step_inset_mm") or 0) side = profile.get("on_axis_side", "left") sign = -1 if side == "left" else 1 v0 = (sign * tw, -wi); v1 = (0.0, 0.0); v2 = (0.0, -ah) v3 = (sign * bw, -ah); v4 = (sign * tw, -si) entities, contour = [], [] for s, e in [(v0, v1), (v1, v2), (v2, v3), (v3, v4), (v4, v0)]: entities.append(_line(list(s), list(e))) contour.append(_contour_line([s[0], s[1], 0.0], [e[0], e[1], 0.0])) return entities, contour def _gen_revolve_chamfer_slanted(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """旋转切除的斜底梯形截面(5顶点)。 与 revolve_chamfer 的区别:底部为斜边(轴底→壁底不是水平线)。 参数: axis_height_mm: 轴侧总高度(V1→V2) top_width_mm: 顶部宽度(轴→外壁) wall_inset_mm: 顶部台阶深度(V0→V4 的 V 偏移) wall_height_mm: 壁段高度(V4→V3) wall_width_mm: 壁距轴的距离 on_axis_side: "left"(U负) 或 "right"(U正) """ ah = float(profile["axis_height_mm"]) tw = float(profile["top_width_mm"]) wi = float(profile.get("wall_inset_mm") or 0) wh = float(profile["wall_height_mm"]) ww = float(profile["wall_width_mm"]) side = profile.get("on_axis_side", "left") sign = -1 if side == "left" else 1 v0 = (sign * tw, 0.0) # 顶部外侧 v1 = (0.0, 0.0) # 轴顶点 v2 = (0.0, -ah) # 轴底部 v3 = (sign * ww, -wi - wh) # 壁底部(斜边连接到 V2) v4 = (sign * ww, -wi) # 壁顶部(台阶) entities, contour = [], [] for s, e in [(v0, v1), (v1, v2), (v2, v3), (v3, v4), (v4, v0)]: entities.append(_line(list(s), list(e))) contour.append(_contour_line([s[0], s[1], 0.0], [e[0], e[1], 0.0])) return entities, contour # ═══════════════════════════════════════════════════════════════ # 弧边复合轮廓生成器(按"015133 手册"方法注册) # ═══════════════════════════════════════════════════════════════ def _gen_circle_with_arc_notches(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """圆+圆弧凹口:大圆上均匀分布的弧形缺口。 参数: outer_radius_mm: 大圆半径 notch_radius_mm: 每个凹口的圆弧半径 notch_angles_deg: 凹口所在的角度列表(度,从+u顺时针) 默认 [0, 90, 180, 270](十字槽) 例: [0,90,180,270] → 十字形,[45,135,225,315] → 斜十字 覆盖文件: 48, 49, 50, 82 """ import math R = float(profile["outer_radius_mm"]) r = float(profile["notch_radius_mm"]) angles_deg = profile.get("notch_angles_deg", [0, 90, 180, 270]) # 每个凹口在大圆上占据的半角宽度 delta = math.acos(max(-1.0, min(1.0, 1.0 - r * r / (2.0 * R * R)))) angles_rad = [math.radians(a) for a in sorted(angles_deg)] entities, contour = [], [] n = len(angles_rad) for i in range(n): prev_end = angles_rad[i - 1] + delta # 上一个凹口离开点 curr_enter = angles_rad[i] - delta # 当前凹口入口 # 大弧:从上一个凹口离开点到当前凹口入口(顺时针) ps_u, ps_v = R * math.cos(prev_end), R * math.sin(prev_end) pe_u, pe_v = R * math.cos(curr_enter), R * math.sin(curr_enter) contour.append(_contour_arc( [ps_u, ps_v, 0.0], [pe_u, pe_v, 0.0], [0.0, 0.0, 0.0], R, )) # 凹口弧:从入口→出口,中心在外圆上 curr_exit = angles_rad[i] + delta nc_u = R * math.cos(angles_rad[i]) nc_v = R * math.sin(angles_rad[i]) pn_enter_u = R * math.cos(curr_enter) pn_enter_v = R * math.sin(curr_enter) pn_exit_u = R * math.cos(curr_exit) pn_exit_v = R * math.sin(curr_exit) # 凹口弧:从出口回到入口(与大弧方向相反) contour.append(_contour_arc( [pn_exit_u, pn_exit_v, 0.0], [pn_enter_u, pn_enter_v, 0.0], [nc_u, nc_v, 0.0], r, )) entities.extend(_build_circle_entities(profile.get("circles") or [])) return entities, contour def _gen_circular_sector_slot(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """圆弧扇区槽:一段大圆弧 + 两条径向线 + 一个矩形槽口。 形状:一个扇形(大圆弧 + 两侧径向线),中心开矩形槽。 由两条大弧(上/下)、两条径向线、一个中央矩形槽组成。 参数: arc_radius_mm: 大弧半径(圆心在原点) slot_half_width_mm: 槽口半宽(从圆心起的径向距离) chord_half_mm: 弧弦线半长(弧的跨距,决定弧幅度) 覆盖文件: 87, 88, 89, 90 """ import math R = float(profile["arc_radius_mm"]) hw = float(profile["slot_half_width_mm"]) ch = float(profile["chord_half_mm"]) # 弧端点:在圆上,到中心轴的垂直距离为 ch # 弧端点在圆 R 上,距离中心轴 ch,其角度为 asin(ch/R) half_angle = math.asin(max(-1.0, min(1.0, ch / R))) # 弧端点坐标(圆上,2 个象限) arc_x = R * math.cos(half_angle) arc_y = R * math.sin(half_angle) if ch >= 0 else -R * math.sin(-half_angle) # 4 个关键点(顺时针) # 下弧: x 从 -arc_x 到 +arc_x, y = -ch (在圆上 y = ±arc_y ≈ ±ch) # 右上角弧段 arc_x_neg_angle = R * math.cos(-half_angle) arc_y_neg = R * math.sin(-half_angle) p_bot_right = (arc_x_neg_angle, arc_y_neg) # 右下(圆上,负半角) p_bot_left = (arc_x, arc_y) # 右下(圆上,正半角)... 等等 # 直接按 87 的几何定义:下弧从 (+xs, -ch) 到 (-xs, -ch),上弧从 (-xs, +ch) 到 (+xs, +ch) # xs 由圆 R 和 ch 确定 xs = math.sqrt(max(0, R * R - ch * ch)) contour = [ # 下弧:从 (xs, -ch) 到 (-xs, -ch),圆心原点,半径 R(顺时针) _contour_arc([xs, -ch, 0.0], [-xs, -ch, 0.0], [0.0, 0.0, 0.0], R), # 左侧线:(-xs, -ch) → (-xs, +ch) ... # 不对,夹着槽口 ] # 重新按 87 的实际边序列构建 # [0] line (-27.5, -13)→(-27.5, +13) → 槽口左竖线 # [1] line (-27.5, +13)→(-37.83, +13) → 径向连接 # [2] arc r=40 c=(0,0) s=(+37.83, +13)→(-37.83, +13) → 上弧 # [3] line (+27.5, +13)→(+37.83, +13) → 径向连接(右侧) # [4] line (+27.5, -13)→(+27.5, +13) → 槽口右竖线 # [5] line (+27.5, -13)→(+37.83, -13) → 径向连接 # [6] arc r=40 c=(0,0) s=(-37.83, -13)→(+37.83, -13) → 下弧 # [7] line (-27.5, -13)→(-37.83, -13) → 径向连接 # 参数化: # slot_half = 27.5 (槽口半宽) # chord_half = 13 (弧端点的 w 坐标,确定弧的跨度) # arc_radius = 40 # arc_x_end = sqrt(R² - ch²) = sqrt(1600 - 169) ≈ 37.83 sh = hw # slot half axe = math.sqrt(max(0.0, R * R - ch * ch)) # arc x-endpoint contour = [ # 槽口竖线(从左下到左上) _contour_line([-sh, -ch, 0.0], [-sh, ch, 0.0]), # 连接到弧(从槽口左上到弧左下) _contour_line([-sh, ch, 0.0], [-axe, ch, 0.0]), # 上弧(从弧左下到弧右下,经过原点顶) _contour_arc([axe, ch, 0.0], [-axe, ch, 0.0], [0.0, 0.0, 0.0], R), # 连接到槽口(从弧右下到槽口右上) _contour_line([sh, ch, 0.0], [axe, ch, 0.0]), # 槽口竖线(从右上到右下) _contour_line([sh, ch, 0.0], [sh, -ch, 0.0]), # 连接到弧(从槽口右下到弧右上) _contour_line([sh, -ch, 0.0], [axe, -ch, 0.0]), # 下弧(从弧右上到弧左上,经过原点底) _contour_arc([-axe, -ch, 0.0], [axe, -ch, 0.0], [0.0, 0.0, 0.0], R), # 连接到槽口(从弧左上到槽口左下) _contour_line([-sh, -ch, 0.0], [-axe, -ch, 0.0]), ] entities = [ _line([-sh, -ch], [-sh, ch]), _line([-sh, ch], [-axe, ch]), _line([sh, ch], [axe, ch]), _line([sh, ch], [sh, -ch]), _line([sh, -ch], [axe, -ch]), _line([-sh, -ch], [-axe, -ch]), ] entities.extend(_build_circle_entities(profile.get("circles") or [])) return entities, contour def _gen_circle_with_radial_tabs(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """圆+径向凸耳:大圆弧上有矩形凸起。 形状:一个大圆被两侧的矩形凸耳取代部分弧段。 简化表示为直边多边形(忽略 r=1 的圆角,体积误差 <1%)。 参数: outer_radius_mm: 大圆半径 tab_u_half_mm: 凸耳半宽(弧线方向,从根部到内缘) tab_v_offset_mm: 凸耳离弧线的垂直距离(即凸耳顶部距弧线的v偏移) 覆盖文件: 91, 92, 93, 94, 95 """ import math R = float(profile["outer_radius_mm"]) tu = float(profile.get("tab_u_half_mm") or 0) tv = float(profile.get("tab_v_offset_mm") or 0) # 凸耳根部在圆上的角度 angle = math.asin(min(1.0, max(0.0, tv / R))) # 弧上根部点(右侧) root_u = R * math.cos(angle) root_v = R * math.sin(angle) # 凸耳内缘 inner_u = root_u - tu inner_v = root_v * 0.9 # 略浅于弧线 # 构建轮廓:大弧(上) → 右凸耳 → 大弧(下) → 左凸耳 → 闭合 contour = [] # 上弧:从左侧根部到右侧根部(经过顶点) contour.append(_contour_arc( [root_u, root_v, 0.0], [-root_u, root_v, 0.0], [0.0, 0.0, 0.0], R)) # 右侧凸耳(多边形:根部→内顶→内底→根部) contour.append(_contour_line([root_u, root_v, 0.0], [inner_u, inner_v, 0.0])) contour.append(_contour_line([inner_u, inner_v, 0.0], [inner_u, -inner_v, 0.0])) contour.append(_contour_line([inner_u, -inner_v, 0.0], [root_u, -root_v, 0.0])) # 下弧:从右侧底部到左侧底部(经过底点) contour.append(_contour_arc( [-root_u, -root_v, 0.0], [root_u, -root_v, 0.0], [0.0, 0.0, 0.0], R)) # 左侧凸耳(镜像) contour.append(_contour_line([-root_u, -root_v, 0.0], [-inner_u, -inner_v, 0.0])) contour.append(_contour_line([-inner_u, -inner_v, 0.0], [-inner_u, inner_v, 0.0])) contour.append(_contour_line([-inner_u, inner_v, 0.0], [-root_u, root_v, 0.0])) entities = [ _line([root_u, root_v], [inner_u, inner_v]), _line([inner_u, inner_v], [inner_u, -inner_v]), _line([inner_u, -inner_v], [root_u, -root_v]), _line([-root_u, -root_v], [-inner_u, -inner_v]), _line([-inner_u, -inner_v], [-inner_u, inner_v]), _line([-inner_u, inner_v], [-root_u, root_v]), ] entities.extend(_build_circle_entities(profile.get("circles") or [])) return entities, contour def _gen_filleted_rect_side_slots(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """圆角矩形+两侧中心凹槽。 形状:圆角矩形,左右两侧中心各有一个 U 形凹槽(半圆槽)。 参数: half_width_mm: 矩形半宽(不含圆角) half_height_mm: 矩形半高(不含圆角) corner_radius_mm: 四角圆角半径 slot_radius_mm: 两侧中心凹槽半径(默认 5.0) circles: 可选内部圆(孔洞),[{center:[x,y], radius_mm:r}, …] 覆盖文件: 58, 63, 64, 65, 66 """ hw = float(profile["half_width_mm"]) hh = float(profile["half_height_mm"]) cr = float(profile["corner_radius_mm"]) sr = float(profile.get("slot_radius_mm") or cr * 0.5) # 注意: v=-Z, 所以 v 正方向朝 Z 负 # 矩形范围: u=[-hw,hw], v=[-hh,+hh] 对应 z=[+hh,-hh] # 上边 (z=+hh): v=-hh, 下边 (z=-hh): v=+hh entities, contour = [], [] top_v, bot_v = -hh, hh # 上边 v=-hh, 下边 v=+hh # 上边(直线,从左上角到右上角) contour.append(_contour_line( [-(hw - cr), top_v, 0.0], [(hw - cr), top_v, 0.0])) entities.append(_line([-(hw - cr), top_v], [(hw - cr), top_v])) # 右上圆角(逆时针绕 center: 从顶点到右侧) contour.append(_contour_arc( [(hw - cr), top_v, 0.0], [hw, top_v + cr, 0.0], [(hw - cr), top_v + cr, 0.0], cr)) # 右边上半(从圆角到凹槽上方) contour.append(_contour_line( [hw, top_v + cr, 0.0], [hw, -sr, 0.0])) entities.append(_line([hw, top_v + cr], [hw, -sr])) # 右侧中心凹槽(半圆向内的 U 形凹口) contour.append(_contour_arc( [hw, sr, 0.0], [hw, -sr, 0.0], [hw, 0.0, 0.0], sr)) # 右边下半(从凹槽下方到右下角) contour.append(_contour_line( [hw, sr, 0.0], [hw, bot_v - cr, 0.0])) entities.append(_line([hw, sr], [hw, bot_v - cr])) # 右下圆角 contour.append(_contour_arc( [hw, bot_v - cr, 0.0], [(hw - cr), bot_v, 0.0], [(hw - cr), bot_v - cr, 0.0], cr)) # 下边 contour.append(_contour_line( [(hw - cr), bot_v, 0.0], [-(hw - cr), bot_v, 0.0])) entities.append(_line([(hw - cr), bot_v], [-(hw - cr), bot_v])) # 左下圆角 contour.append(_contour_arc( [-(hw - cr), bot_v, 0.0], [-hw, bot_v - cr, 0.0], [-(hw - cr), bot_v - cr, 0.0], cr)) # 左边下半 contour.append(_contour_line( [-hw, bot_v - cr, 0.0], [-hw, sr, 0.0])) entities.append(_line([-hw, bot_v - cr], [-hw, sr])) # 左侧中心凹槽 contour.append(_contour_arc( [-hw, -sr, 0.0], [-hw, sr, 0.0], [-hw, 0.0, 0.0], sr)) # 左边上半 contour.append(_contour_line( [-hw, -sr, 0.0], [-hw, top_v + cr, 0.0])) entities.append(_line([-hw, -sr], [-hw, top_v + cr])) # 左上圆角 contour.append(_contour_arc( [-hw, top_v + cr, 0.0], [-(hw - cr), top_v, 0.0], [-(hw - cr), top_v + cr, 0.0], cr)) entities.extend(_build_circle_entities(profile.get("circles") or [])) return entities, contour # ═══════════════════════════════════════════════════════════════ # 更多弧边复合轮廓生成器 # ═══════════════════════════════════════════════════════════════ def _gen_d_shape(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """D形(半圆+弦线):一条直线 + 一条大圆弧,形如字母 D。 参数: radius_mm: 大弧半径(圆心在原点) chord_sign: 弦线方向,"left"=弦在 x>0 侧,"right"=弦在 x<0 侧 默认 "left"(弦线在 +x 侧,弧形开口朝 -x) 覆盖文件: 144358 """ import math R = float(profile["radius_mm"]) side = profile.get("chord_sign", "left") sign = 1 if side == "left" else -1 # chord at x=cx (cx^2 + y^2 = R^2) # For side="left": chord at x = sqrt(R^2 - y_len^2) ... # Actually, from 144358 data: arc r=41 c=(0,0) from (34,22.9) to (34,-22.9) # So the chord is at x=34, v ranges from -22.9 to 22.9 # v_max = sqrt(R^2 - x^2) = sqrt(41^2 - 34^2) = sqrt(1681-1156) = sqrt(525) ≈ 22.91 ✓ v_max = math.sqrt(max(0.0, R * R - (R - 7) * (R - 7))) # 实际上,chord x 可以根据 radius 推导 # 使用 chord_x 参数如果存在,否则用近似 chord_x = float(profile.get("chord_x_mm") or R * 0.83) # 默认在半径 83% 处 v_half = math.sqrt(max(0.0, R * R - chord_x * chord_x)) cx = sign * chord_x # 弦线 x 坐标 contour = [ # 弦线(从下到上) _contour_line([cx, -v_half, 0.0], [cx, v_half, 0.0]), # 大弧(从右上到左下,即从左到右沿弧线) _contour_arc([cx, v_half, 0.0], [cx, -v_half, 0.0], [0.0, 0.0, 0.0], R), ] entities = [ _line([cx, -v_half], [cx, v_half]), ] entities.extend(_build_circle_entities(profile.get("circles") or [])) return entities, contour def _gen_partial_ring(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """部分圆环(同心圆弧+径向直线):两段同心弧 + 两条径向线。 形状像一个扇区环 (sector annulus),由内外两段同心弧和两侧径向线组成。 参数: inner_radius_mm: 内弧半径 outer_radius_mm: 外弧半径 half_angle_deg: 弧的半角度(两侧各 half_angle 度,总张角 2*half_angle) 覆盖文件: 177126 """ import math ir = float(profile["inner_radius_mm"]) oR = float(profile["outer_radius_mm"]) h_deg = float(profile.get("half_angle_deg") or 45.0) h_rad = math.radians(h_deg) # 内弧端点 iu_pos = ir * math.cos(h_rad) iv_pos = ir * math.sin(h_rad) iu_neg = ir * math.cos(-h_rad) iv_neg = ir * math.sin(-h_rad) # 外弧端点 ou_pos = oR * math.cos(h_rad) ov_pos = oR * math.sin(h_rad) ou_neg = oR * math.cos(-h_rad) ov_neg = oR * math.sin(-h_rad) contour = [ # 右侧径向线(从内弧到外弧,+h角度) _contour_line([iu_pos, iv_pos, 0.0], [ou_pos, ov_pos, 0.0]), # 外弧(从 +h 到 -h) _contour_arc([ou_neg, ov_neg, 0.0], [ou_pos, ov_pos, 0.0], [0.0, 0.0, 0.0], oR), # 左侧径向线(从外弧到内弧,-h角度) _contour_line([ou_neg, ov_neg, 0.0], [iu_neg, iv_neg, 0.0]), # 内弧(从 -h 到 +h) _contour_arc([iu_pos, iv_pos, 0.0], [iu_neg, iv_neg, 0.0], [0.0, 0.0, 0.0], ir), ] entities = [ _line([iu_pos, iv_pos], [ou_pos, ov_pos]), _line([ou_neg, ov_neg], [iu_neg, iv_neg]), ] entities.extend(_build_circle_entities(profile.get("circles") or [])) return entities, contour def _gen_partial_ring_with_arc_island(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """扇区环 + 外弦上的等宽弧岛(岛不切除,作为区域内孔)。 每个 replica 生成一块「外轮廓=扇区环、内孔=偏移弧岛」的区域。 岛外弧端点落在扇区外弧弦上,横坐标取 ±inner·cos(half_angle)(相对角平分线)。 参数: inner_radius_mm / outer_radius_mm / half_angle_deg: 扇区环 island_radius_mm: 岛外弧半径 island_gap_mm: 岛内外弧径向间距(等宽) center_angles_deg 或 replicas[{center_angle_deg}]: 各扇区角平分线方向(度) 覆盖: b005 """ ir = float(profile["inner_radius_mm"]) oR = float(profile["outer_radius_mm"]) h_deg = float(profile.get("half_angle_deg") or 45.0) island_r = float(profile["island_radius_mm"]) gap = float(profile.get("island_gap_mm") or 1.0) if ir <= 0 or oR <= ir or island_r <= gap: raise ValueError("partial_ring_with_arc_island: invalid radii") replicas = profile.get("replicas") if replicas: angles = [float(r["center_angle_deg"]) for r in replicas] else: angles = [float(a) for a in (profile.get("center_angles_deg") or [0.0])] h = math.radians(h_deg) entities: list[_Ctx] = [] regions: list[dict[str, Any]] = [] for ca_deg in angles: ca = math.radians(ca_deg) a0, a1 = ca - h, ca + h def polar(r: float, ang: float) -> list[float]: return [r * math.cos(ang), r * math.sin(ang), 0.0] # 扇区环外轮廓(逆时针:外弧 a0→a1,径向,内弧 a1→a0,径向) ou0, ou1 = polar(oR, a0), polar(oR, a1) iu0, iu1 = polar(ir, a0), polar(ir, a1) outer = [ _contour_arc(ou0, ou1, [0.0, 0.0, 0.0], oR), _contour_line(ou1, iu1), _contour_arc(iu1, iu0, [0.0, 0.0, 0.0], ir), _contour_line(iu0, ou0), ] entities.extend([ _line(ou0[:2], ou1[:2]), _line(ou1[:2], iu1[:2]), _line(iu1[:2], iu0[:2]), _line(iu0[:2], ou0[:2]), ]) # 外弦中点与弦向单位向量;岛端点 = M ± inner·cos(h)·chord_dir ux, uy = math.cos(ca), math.sin(ca) mx = oR * ux * math.cos(h) my = oR * uy * math.cos(h) cdx, cdy = -uy, ux span = ir * math.cos(h) e1 = [mx + span * cdx, my + span * cdy, 0.0] e2 = [mx - span * cdx, my - span * cdy, 0.0] # 岛心在角平分线上:|E - t·u| = island_r,取距原点较近根 dot = e1[0] * ux + e1[1] * uy e2n = e1[0] * e1[0] + e1[1] * e1[1] disc = max(0.0, dot * dot - (e2n - island_r * island_r)) t1, t2 = dot - math.sqrt(disc), dot + math.sqrt(disc) t = t1 if abs(t1) <= abs(t2) else t2 cx, cy = t * ux, t * uy c3 = [cx, cy, 0.0] def inward(pt: list[float]) -> list[float]: vx, vy = cx - pt[0], cy - pt[1] L = math.hypot(vx, vy) or 1.0 return [pt[0] + vx / L * gap, pt[1] + vy / L * gap, 0.0] i1, i2 = inward(e1), inward(e2) ri = island_r - gap # 岛孔:外弧 e1→e2(经外侧鼓包)再经内弧返回;与扇区同向时作孔需反向 # 外弧走短弧中指向外侧(远离原点)的那条 hole = [ _contour_arc(e1, e2, c3, island_r), _contour_line(e2, i2), _contour_arc(i2, i1, c3, ri), _contour_line(i1, e1), ] entities.extend([ _line(e1[:2], e2[:2]), _line(e2[:2], i2[:2]), _line(i2[:2], i1[:2]), _line(i1[:2], e1[:2]), ]) regions.append({"outer": outer, "holes": [hole]}) meta["_regions"] = regions return entities, [] def _gen_arc_chain(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """弧链轮廓:多段首尾相连的弧形成闭合轮廓(各弧可有不同圆心)。 用于 revolve 特征的截面草图,由多段圆弧端到端连接组成。 参数: arcs: 弧描述列表 [{radius_mm, center:[u,v], start_angle_deg, end_angle_deg}, ...] (每个弧从 start_angle 到 end_angle,起点与上一条弧终点重合) 覆盖文件: 020543 """ import math arc_list = profile.get("arcs") or [] if not arc_list or len(arc_list) < 2: raise ValueError("arc_chain needs at least 2 arcs") entities, contour = [], [] for arc_desc in arc_list: r = float(arc_desc["radius_mm"]) center = arc_desc.get("center") or [0.0, 0.0] cu, cv = float(center[0]), float(center[1]) sa = math.radians(float(arc_desc["start_angle_deg"])) ea = math.radians(float(arc_desc["end_angle_deg"])) su = cu + r * math.cos(sa) sv = cv + r * math.sin(sa) eu = cu + r * math.cos(ea) ev = cv + r * math.sin(ea) contour.append(_contour_arc( [su, sv, 0.0], [eu, ev, 0.0], [cu, cv, 0.0], r, )) entities.extend(_build_circle_entities(profile.get("circles") or [])) return entities, contour def _gen_radial_slot(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """径向槽:两段同心圆弧 + 两端圆角,形如弧形环段。 用于 extrude_cut 在圆柱壁面上开弧形槽口。 参数: inner_radius_mm: 内弧半径 outer_radius_mm: 外弧半径 start_angle_deg: 槽起始角度(度,从工作平面 x_dir 方向逆时针测量) end_angle_deg: 槽终止角度 覆盖文件: 020543 (sk_02, sk_03, sk_04) """ import math ir = float(profile["inner_radius_mm"]) oR = float(profile["outer_radius_mm"]) sa_deg = float(profile["start_angle_deg"]) ea_deg = float(profile["end_angle_deg"]) fr = (oR - ir) / 2.0 # 端盖圆角半径 sa = math.radians(sa_deg) ea = math.radians(ea_deg) entities, contour = [], [] # 角度从工作平面 x_dir 方向测量 → u = r·cos(θ), v = r·sin(θ) # 1. 内弧(从 start→end) isu = ir * math.cos(sa); isv = ir * math.sin(sa) ieu = ir * math.cos(ea); iev = ir * math.sin(ea) contour.append(_contour_arc( [isu, isv, 0.0], [ieu, iev, 0.0], [0.0, 0.0, 0.0], ir, )) # 2. 终端圆角(半圆,从内弧终点到外弧终点) fc_u = (ir + oR) / 2.0 fcu_s = fc_u * math.cos(ea); fcv_s = fc_u * math.sin(ea) osu = oR * math.cos(sa); osv = oR * math.sin(sa) oeu = oR * math.cos(ea); oev = oR * math.sin(ea) contour.append(_contour_arc( [ieu, iev, 0.0], [oeu, oev, 0.0], [fcu_s, fcv_s, 0.0], fr, )) # 3. 外弧(从 end→start,反向) contour.append(_contour_arc( [oeu, oev, 0.0], [osu, osv, 0.0], [0.0, 0.0, 0.0], oR, )) # 4. 起始端圆角(从外弧起点到内弧起点) fcu_e = fc_u * math.cos(sa); fcv_e = fc_u * math.sin(sa) contour.append(_contour_arc( [osu, osv, 0.0], [isu, isv, 0.0], [fcu_e, fcv_e, 0.0], fr, )) entities.extend(_build_circle_entities(profile.get("circles") or [])) return entities, contour # ═══════════════════════════════════════════════════════════════ # 程序化重复切口 # ═══════════════════════════════════════════════════════════════ def _poly_contour(vertices: list[tuple[float, float]]) -> list[_Ctx]: """把按顺序给出的二维顶点变成闭合直线轮廓。""" return [ _contour_line( [vertices[i][0], vertices[i][1], 0.0], [vertices[(i + 1) % len(vertices)][0], vertices[(i + 1) % len(vertices)][1], 0.0], ) for i in range(len(vertices)) ] def _transform_pattern_contour( contour: list[_Ctx], x_mm: float, y_mm: float, angle_deg: float, scale: float = 1.0, ) -> list[_Ctx]: """旋转、缩放并平移一个二维轮廓。""" a = math.radians(angle_deg) ca, sa = math.cos(a), math.sin(a) def point(p: list[float]) -> list[float]: x, y = float(p[0]) * scale, float(p[1]) * scale return [x_mm + x * ca - y * sa, y_mm + x * sa + y * ca, 0.0] result: list[_Ctx] = [] for edge in contour: item = deepcopy(edge) item["start_mm"] = point(edge["start_mm"]) item["end_mm"] = point(edge["end_mm"]) if edge.get("center_mm") is not None: item["center_mm"] = point(edge["center_mm"]) if edge.get("radius_mm") is not None: item["radius_mm"] = float(edge["radius_mm"]) * scale result.append(item) return result def _pattern_motif_contour(motif: _Ctx) -> list[_Ctx]: """从少量命名尺寸生成一个切口母形。""" kind = str(motif.get("type") or "") if kind == "circle": radius = float(motif["radius_mm"]) return _gen_circle({"type": "circle", "radius_mm": radius}, {})[1] if kind in ("square", "rectangle"): width = float(motif["width_mm"]) height = float(motif.get("height_mm") or width) return _rect_lines_and_contour(-width / 2.0, -height / 2.0, width / 2.0, height / 2.0)[1] if kind == "obround": length = float(motif["length_mm"]) width = float(motif["width_mm"]) return _gen_obround( {"type": "obround", "center": [0.0, 0.0], "length_mm": length, "width_mm": width}, {}, )[1] if kind == "cross": size = float(motif["size_mm"]) arm = float(motif["arm_width_mm"]) half, arm_half = size / 2.0, arm / 2.0 vertices = [ (-arm_half, -half), (arm_half, -half), (arm_half, -arm_half), (half, -arm_half), (half, arm_half), (arm_half, arm_half), (arm_half, half), (-arm_half, half), (-arm_half, arm_half), (-half, arm_half), (-half, -arm_half), (-arm_half, -arm_half), ] return _poly_contour(vertices) if kind == "d_shape_polygon": stem = float(motif["stem_length_mm"]) nose = float(motif["nose_depth_mm"]) half_height = float(motif["half_height_mm"]) segments = int(motif.get("arc_segments") or 14) vertices = [(-stem, -half_height), (-stem, half_height), (0.0, half_height)] # 右半椭圆;首尾端点已由直线给出,内部取样由引擎固化。 for i in range(1, segments): angle = math.pi / 2.0 - math.pi * i / segments vertices.append((nose * math.cos(angle), half_height * math.sin(angle))) vertices.append((0.0, -half_height)) return _poly_contour(vertices) if kind == "regular_hexagon": radius = float(motif["radius_mm"]) return _poly_contour([ ( radius * math.cos(math.radians(60.0 * i)), radius * math.sin(math.radians(60.0 * i)), ) for i in range(6) ]) if kind == "skew_hexagon": # 该族来自六边形母形的非对称离散模板;只保留一个名义半径, # 其余稳定比例由引擎固化,不把六个顶点写进 CDSL。 radius = float(motif["nominal_radius_mm"]) return _poly_contour([ (radius, 0.0), (radius * 0.317014, radius * 0.682, ), (-radius * 0.5, radius * 0.682), (-radius * 1.183014, 0.0), (-radius * 0.408494, -radius * 0.774519), (radius * 0.317014, -radius * 0.774519), ]) if kind == "triangle": radius = float(motif["radius_mm"]) return _poly_contour([ ( radius * math.cos(math.radians(120.0 * i)), radius * math.sin(math.radians(120.0 * i)), ) for i in range(3) ]) if kind == "teardrop_polygon": if motif.get("left_width_mm") is not None: left = float(motif["left_width_mm"]) right = float(motif["right_width_mm"]) tip = float(motif["tip_height_mm"]) bottom = -float(motif["bottom_depth_mm"]) shoulder = float(motif["shoulder_height_mm"]) return _poly_contour([ (0.0, tip), (right, shoulder), (right, bottom), (-left, bottom), (-left, shoulder), ]) width = float(motif["width_mm"]) height = float(motif["height_mm"]) shoulder = float(motif.get("shoulder_fraction") or 0.58) half = width / 2.0 top = height / 2.0 bottom = -height / 2.0 shoulder_y = bottom + height * shoulder return _poly_contour([ (0.0, top), (half, shoulder_y), (half, bottom), (-half, bottom), (-half, shoulder_y), ]) if kind == "trapezoid": bottom = float(motif["bottom_width_mm"]) top = float(motif["top_width_mm"]) height = float(motif["height_mm"]) hh = height / 2.0 return _poly_contour([ (-bottom / 2.0, -hh), (bottom / 2.0, -hh), (top / 2.0, hh), (-top / 2.0, hh), ]) if kind == "annular_sector_polygon": inner = float(motif["inner_radius_mm"]) outer = float(motif["outer_radius_mm"]) half_angle = float(motif["half_angle_deg"]) segments = int(motif.get("arc_segments") or 8) outer_pts = [ ( outer * math.cos(math.radians(-half_angle + 2.0 * half_angle * i / segments)), outer * math.sin(math.radians(-half_angle + 2.0 * half_angle * i / segments)), ) for i in range(segments + 1) ] inner_pts = [ ( inner * math.cos(math.radians(half_angle - 2.0 * half_angle * i / segments)), inner * math.sin(math.radians(half_angle - 2.0 * half_angle * i / segments)), ) for i in range(segments + 1) ] return _poly_contour(outer_pts + inner_pts) raise ValueError(f"patterned_cutouts: unsupported motif type {kind!r}") def _pattern_placements(layout: _Ctx) -> list[tuple[float, float, float, float]]: """展开语义布局,返回 (x, y, rotation_deg, scale)。""" kind = str(layout.get("type") or "") orientation = str(layout.get("orientation") or "fixed") orientation_offset = float(layout.get("orientation_offset_deg") or 0.0) def orient(angle: float) -> float: if orientation == "radial": return angle + orientation_offset if orientation == "tangential": return angle + 90.0 + orientation_offset if orientation == "snapped_radial": snap = float(layout.get("orientation_snap_deg") or 45.0) return round(angle / snap) * snap + orientation_offset return orientation_offset if kind in ("ring", "angular"): radius = float(layout.get("radius_mm") or 0.0) count = int(layout["count"]) start = float(layout.get("start_angle_deg") or 0.0) step = float(layout.get("angle_step_deg") or (360.0 / count)) angular_only = kind == "angular" return [ ( 0.0 if angular_only else radius * math.cos(math.radians(start + i * step)), 0.0 if angular_only else radius * math.sin(math.radians(start + i * step)), orient(start + i * step), 1.0, ) for i in range(count) ] if kind == "concentric_rings": result: list[tuple[float, float, float, float]] = [] for ring in layout.get("rings") or []: merged = dict(layout) merged.update(ring) merged["type"] = "ring" result.extend(_pattern_placements(merged)) return result if kind == "disc_grid": nx, ny = int(layout["count_x"]), int(layout["count_y"]) sx, sy = float(layout["spacing_x_mm"]), float(layout["spacing_y_mm"]) center = layout.get("center_mm") or [0.0, 0.0] x0 = float(center[0]) - (nx - 1) * sx / 2.0 y0 = float(center[1]) - (ny - 1) * sy / 2.0 limit = layout.get("max_center_radius_mm") points = [ (x0 + i * sx, y0 + j * sy) for j in range(ny) for i in range(nx) ] if limit is not None: points = [(x, y) for x, y in points if math.hypot(x, y) <= float(limit) + 1e-9] return [(x, y, orientation_offset, 1.0) for x, y in points] if kind == "open_arc": radius = float(layout["radius_mm"]) count = int(layout["count"]) start, end = float(layout["start_angle_deg"]), float(layout["end_angle_deg"]) step = 0.0 if count == 1 else (end - start) / (count - 1) return [ ( radius * math.cos(math.radians(start + i * step)), radius * math.sin(math.radians(start + i * step)), orient(start + i * step), 1.0, ) for i in range(count) ] if kind == "spiral": count = int(layout["count"]) start_radius = float(layout["start_radius_mm"]) radius_step = float(layout["radius_step_mm"]) start_angle = float(layout.get("start_angle_deg") or 0.0) angle_step = float(layout["angle_step_deg"]) result = [] for i in range(count): radius = start_radius + i * radius_step angle = start_angle + i * angle_step result.append(( radius * math.cos(math.radians(angle)), radius * math.sin(math.radians(angle)), orient(angle), 1.0, )) return result if kind == "cross_lines": count = int(layout["count_per_axis"]) spacing = float(layout["spacing_mm"]) start = -(count - 1) * spacing / 2.0 result = [] for i in range(count): value = start + i * spacing result.append((value, 0.0, orientation_offset, 1.0)) result.append((0.0, value, orientation_offset + 90.0, 1.0)) return result if kind == "x_field": levels = int(layout["levels"]) spacing = float(layout["spacing_mm"]) start = -(levels - 1) * spacing / 2.0 result = [] for i in range(levels): value = start + i * spacing if abs(value) < 1e-9: rotation = 135.0 + orientation_offset if orientation == "diagonal_axes" else orientation_offset result.append((0.0, 0.0, rotation, 1.0)) else: for y in (value, -value): if orientation == "diagonal_axes": rotation = (135.0 if value * y > 0 else 45.0) + orientation_offset else: angle = math.degrees(math.atan2(y, value)) rotation = orient(angle) result.append((value, y, rotation, 1.0)) return result if kind == "twin_strips": x_offset = float(layout["x_offset_mm"]) count = int(layout["count_y"]) y_start = float(layout["y_start_mm"]) y_end = float(layout["y_end_mm"]) step = 0.0 if count == 1 else (y_end - y_start) / (count - 1) return [ (x, y_start + j * step, orientation_offset, 1.0) for j in range(count) for x in (-x_offset, x_offset) ] if kind == "corner_clusters": levels = [float(v) for v in (layout.get("levels_mm") or [])] return [ (sx * x, sy * y, orientation_offset, 1.0) for sx in (-1.0, 1.0) for sy in (-1.0, 1.0) for y in levels for x in levels ] if kind == "diamond_field": radius = int(layout["manhattan_radius"]) spacing = float(layout["spacing_mm"]) return [ (i * spacing, j * spacing, orientation_offset, 1.0) for distance in range(radius + 1) for j in range(-radius, radius + 1) for i in range(-radius, radius + 1) if abs(i) + abs(j) == distance ] raise ValueError(f"patterned_cutouts: unsupported layout type {kind!r}") def _gen_patterned_cutouts(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """一个母形 + 一个语义布局,运行时展开成多个独立切除区域。""" motif = profile.get("motif") or {} layout = profile.get("layout") or {} base = _pattern_motif_contour(motif) regions = [] for x, y, angle, scale in _pattern_placements(layout): regions.append({ "outer": _transform_pattern_contour(base, x, y, angle, scale), "holes": [], }) if not regions: raise ValueError("patterned_cutouts: layout produced no regions") meta["_regions"] = regions return [], [] def _gen_compound_patterned_cutouts(profile: _Ctx, meta: _Ctx) -> tuple[list[_Ctx], list[_Ctx]]: """把少量不同母形/布局的程序化图案合并到同一草图。""" regions: list[_Ctx] = [] for pattern in profile.get("patterns") or []: motif = pattern.get("motif") or {} layout = pattern.get("layout") or {} base = _pattern_motif_contour(motif) for x, y, angle, scale in _pattern_placements(layout): regions.append({ "outer": _transform_pattern_contour(base, x, y, angle, scale), "holes": [], }) if not regions: raise ValueError("compound_patterned_cutouts: patterns produced no regions") meta["_regions"] = regions return [], [] # ═══════════════════════════════════════════════════════════════ # 生成器注册表 —— 唯一索引点 # ═══════════════════════════════════════════════════════════════ SHAPE_GENERATORS: dict[str, Any] = { "circle": _gen_circle, "annulus": _gen_annulus, "circles": _gen_circles, "circle_grid": _gen_circle_grid, "rectangle": _gen_rectangle, "rectangle_with_circles": _gen_rect_with_circles, "rectangle_with_fillets": _gen_rect_with_fillets, "obround": _gen_obround, "polygon": _gen_polygon, "ibone": _gen_ibone, "rectangle_with_symmetric_notches": _gen_rect_symmetric_notches, "revolve_chamfer": _gen_revolve_chamfer, "revolve_chamfer_slanted": _gen_revolve_chamfer_slanted, # 弧边复合轮廓(按 015133 手册方法注册,同形异构通过参数复用) "circle_with_arc_notches": _gen_circle_with_arc_notches, "circular_sector_slot": _gen_circular_sector_slot, "circle_with_radial_tabs": _gen_circle_with_radial_tabs, "filleted_rect_side_slots": _gen_filleted_rect_side_slots, # 弧边形状 "d_shape": _gen_d_shape, "partial_ring": _gen_partial_ring, "partial_ring_with_arc_island": _gen_partial_ring_with_arc_island, "radial_slot": _gen_radial_slot, "patterned_cutouts": _gen_patterned_cutouts, "compound_patterned_cutouts": _gen_compound_patterned_cutouts, "arc_chain": _gen_arc_chain, "complex_arc_shape": _gen_polygon, # 从 compiler_context entities 重建 "unknown_shape": _gen_polygon, # 未分类形状也走 compiler_context 回退 } # ═══════════════════════════════════════════════════════════════ # 注册表功能:扩展、查询 # ═══════════════════════════════════════════════════════════════ def register_shape(ptype: str, generator: Any) -> None: """注册一个新的轮廓生成器。扩展用途。""" SHAPE_GENERATORS[ptype] = generator def list_registered_shapes() -> list[str]: """返回所有已注册的形状生成器名称。""" return sorted(SHAPE_GENERATORS.keys()) # ═══════════════════════════════════════════════════════════════ # 形状能力矩阵(供外部查询:哪些形状可自动检测,哪些需手动指定) # ═══════════════════════════════════════════════════════════════ _ShapeInfo = dict[str, Any] SHAPE_CAPABILITIES: dict[str, _ShapeInfo] = { "circle": {"detectable": True, "arity": "circle", "description": "单圆"}, "annulus": {"detectable": True, "arity": "circles", "description": "同心圆环"}, "circles": {"detectable": True, "arity": "circles", "description": "多圆(非同心)"}, "circle_grid": {"detectable": False, "arity": "circles", "description": "矩形圆孔阵列"}, "rectangle": {"detectable": True, "arity": "polygon", "description": "4线矩形"}, "rectangle_with_circles": {"detectable": True, "arity": "mixed", "description": "矩形+内圆孔"}, "rectangle_with_fillets": {"detectable": False, "arity": "mixed", "description": "圆角矩形(4弧+4线)"}, "obround": {"detectable": True, "arity": "mixed", "description": "槽形/键槽(2线+2半圆弧)"}, "polygon": {"detectable": True, "arity": "polygon", "description": "N边多边形"}, "ibone": {"detectable": False, "arity": "mixed", "description": "工字形凸耳(12线+4弧+4孔)"}, "rectangle_with_symmetric_notches": {"detectable": False,"arity": "mixed", "description": "对称槽板(矩形+4U形缺口)"}, "revolve_chamfer": {"detectable": True, "arity": "polygon", "description": "旋转梯形截面"}, "revolve_chamfer_slanted": {"detectable": True, "arity": "polygon", "description": "旋转斜底梯形截面"}, "circle_with_arc_notches": {"detectable": False, "arity": "mixed", "description": "圆+均匀弧形凹口"}, "circular_sector_slot": {"detectable": False, "arity": "mixed", "description": "圆弧扇区+中心矩形槽"}, "circle_with_radial_tabs": {"detectable": False, "arity": "mixed", "description": "圆+径向矩形凸耳"}, "filleted_rect_side_slots": {"detectable": False, "arity": "mixed", "description": "圆角矩形+两侧中心U形槽"}, "d_shape": {"detectable": True, "arity": "mixed", "description": "D形(半圆+弦线)"}, "partial_ring": {"detectable": True, "arity": "mixed", "description": "部分圆环(扇区环)"}, "partial_ring_with_arc_island": {"detectable": False, "arity": "mixed", "description": "扇区环+弦上偏移弧岛"}, "radial_slot": {"detectable": False, "arity": "mixed", "description": "径向弧形槽"}, "arc_chain": {"detectable": False, "arity": "arcs", "description": "多段弧链轮廓"}, "patterned_cutouts": {"detectable": False, "arity": "regions", "description": "程序化重复切口"}, "compound_patterned_cutouts": {"detectable": False, "arity": "regions", "description": "复合程序化重复切口"}, } # ═══════════════════════════════════════════════════════════════ # 主入口 # ═══════════════════════════════════════════════════════════════ def resolve_profile(sketch: dict[str, Any]) -> dict[str, Any]: """按 type 查找生成器,生成 entities + contour_edges_mm。 对于返回非空 contour 的生成器,会额外保留原始 sketch.entities 中的非 construction circle 实体(孔洞/圆岛),确保不丢失内部特征。 """ profile = sketch.get("profile") if not profile: return sketch ptype = profile.get("type") generator = SHAPE_GENERATORS.get(ptype) if generator is None: raise ValueError(f"sketch {sketch.get('id')}: unsupported profile type {ptype!r}") meta = {"id": sketch.get("id"), "name": sketch.get("name"), "_entities": sketch.get("entities"), "_contour": sketch.get("contour_edges_mm")} entities, contour = generator(profile, meta) out = deepcopy(sketch) # 保留原始草图中的非 construction circle 实体(这些是内部孔洞/圆岛) orig_ents = sketch.get("entities") or [] keep_circles = [ e for e in orig_ents if e.get("type") == "circle" and not e.get("construction") ] if keep_circles and contour: # 只对生成器产出 contour 的场合保留 circles(轮廓生成器 + 内部圆孔) entities = list(entities) + keep_circles out["entities"] = entities wp = sketch.get("workplane") if contour: out["contour_edges_mm"] = _transform_contours(contour, wp) if wp else contour regions = meta.get("_regions") or [] if regions: out["contour_regions_mm"] = [ { "outer": _transform_contours(reg["outer"], wp) if wp else reg["outer"], "holes": [ _transform_contours(hole, wp) if wp else hole for hole in (reg.get("holes") or []) ], } for reg in regions ] return out def resolve_all_sketches(cdsl: dict[str, Any]) -> dict[str, Any]: """对 CDSL 中所有带 profile 字段的草图进行解析。 支持 profile_from 字段:引用另一个草图的 profile,避免重复。 例:sk_05: {"profile_from": "sk_03"} → 使用 sk_03 的 profile。 """ geom = cdsl.get("geometry") or {} sketches = geom.get("sketches") or [] # 第一遍: 解析所有有自己 profile 的草图 resolved: dict[str, dict] = {} for sk in sketches: sid = sk.get("id") if sid is None: continue if "profile" in sk: resolved[sid] = resolve_profile(sk) # 第二遍: 解析 profile_from 引用(支持 profile_shift 偏移) for sk in sketches: sid = sk.get("id") pf = sk.get("profile_from") if pf and sid: src = resolved.get(pf) if src is None: raise ValueError( f"sketch {sid}: profile_from={pf!r} not found or not yet resolved" ) sk2 = deepcopy(sk) sk2["profile"] = deepcopy(src.get("profile")) sk2.pop("profile_from", None) # profile_shift: 对 polygon 顶点做 2D 偏移(同形异构共享) shift = sk.get("profile_shift") if shift and len(shift) == 2 and sk2["profile"].get("type") == "polygon": du, dv = float(shift[0]), float(shift[1]) for v in sk2["profile"]["vertices"]: v[0] = round(v[0] + du, 6) v[1] = round(v[1] + dv, 6) sk2.pop("profile_shift", None) resolved[sid] = resolve_profile(sk2) # 按原顺序输出 result = [] for sk in sketches: sid = sk.get("id") if sid and sid in resolved: result.append(resolved[sid]) else: result.append(deepcopy(sk)) out = deepcopy(cdsl) out.setdefault("geometry", {})["sketches"] = result return out