Files
cdsl-cad/backend/engine/cdsl_engine/sketch_solver.py
T
2026-08-19 19:34:30 +08:00

1750 lines
71 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""轮廓求解器:参数化草图描述 → 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