1751 lines
71 KiB
Python
1751 lines
71 KiB
Python
"""轮廓求解器:参数化草图描述 → entities + contour_edges_mm。
|
||
|
||
LLM 只需输出离散决策(type, radius, width …),
|
||
求解器负责生成精确的实体和轮廓边坐标。
|
||
|
||
架构:注册表模式 —— 每个轮廓类型对应一个生成器函数,
|
||
按 "type" 字符串索引。新增形状或调整既有 profile 参数契约时:
|
||
1. 写 def solver_xxx(profile, meta) -> (entities, contour)
|
||
2. 注册: SHAPE_GENERATORS["xxx"] = solver_xxx
|
||
3. 在 convert 脚本中输出对应的 profile
|
||
4. 同步更新 profile_schema.json(Agent 与后端校验器的公开契约)
|
||
|
||
支持的 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
|