1132 lines
43 KiB
Python
1132 lines
43 KiB
Python
"""
|
||
SW 建模历史 JSON → 参数化 CDSL 转换器 (v4)
|
||
============================================
|
||
严格按照 015133 手册要求生成 LLM 训练友好的 CDSL:
|
||
|
||
Phase 1 — 草图参数化 (注册表模式):
|
||
- @sketch_classifier 装饰器注册形状分类器
|
||
- 通用性: 新增分类器无需修改主逻辑
|
||
- 与 sketch_solver.py 的 SHAPE_GENERATORS 对齐
|
||
|
||
Phase 2 — 同形复用:
|
||
- 检测内容相同的草图 → profile_from: "sk_xx"
|
||
- profile_shift 处理镜像偏移
|
||
|
||
Phase 3 — 轴原点推导:
|
||
- 所有 revolve 特征 axis.origin_mm → from_contour_vertex
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import hashlib
|
||
import json
|
||
import math
|
||
from copy import deepcopy
|
||
from pathlib import Path
|
||
from typing import Any, Callable
|
||
|
||
try:
|
||
from engine.cdsl_engine.translator import normalize_to_ir
|
||
except ModuleNotFoundError: # Engine root added to sys.path by product runtime.
|
||
from cdsl_engine.translator import normalize_to_ir
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 草图分类器注册表 —— 通用性核心
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
# 分类器签名: 接收 (lines, arcs, circles, const_lines, all_non_const) -> profile_dict | None
|
||
ClassifierFunc = Callable[[list[dict], list[dict], list[dict], list[dict], list[dict]], dict[str, Any] | None]
|
||
|
||
SKETCH_CLASSIFIERS: list[tuple[str, int, ClassifierFunc]] = []
|
||
"""形状分类器注册表 (name, priority, function)。priority 越小优先级越高。"""
|
||
|
||
|
||
def register_classifier(name: str, priority: int = 100) -> Callable[[ClassifierFunc], ClassifierFunc]:
|
||
"""注册装饰器:将分类函数注册到 SKETCH_CLASSIFIERS。
|
||
|
||
用法:
|
||
@register_classifier("obround", priority=10)
|
||
def _classify_obround(lines, arcs, circles, const_lines, all_ents):
|
||
...
|
||
return profile_dict or None
|
||
"""
|
||
def decorator(fn: ClassifierFunc) -> ClassifierFunc:
|
||
SKETCH_CLASSIFIERS.append((name, priority, fn))
|
||
return fn
|
||
return decorator
|
||
|
||
|
||
def _classify_sketch_shape(sketch: dict[str, Any]) -> dict[str, Any]:
|
||
"""按优先级依次尝试所有注册的分类器,返回第一个非 None 结果。
|
||
|
||
架构设计: 新增形状只需 @register_classifier 装饰一个函数,
|
||
无需修改此函数或 if/elif 链。—— 015133 手册"注册表模式"
|
||
"""
|
||
entities = sketch.get("entities") or []
|
||
if not entities:
|
||
return None
|
||
|
||
non_const = [e for e in entities if not e.get("construction")]
|
||
const_lines = [e for e in entities if e.get("type") == "line" and e.get("construction")]
|
||
|
||
lines = [e for e in non_const if e.get("type") == "line"]
|
||
circles = [e for e in non_const if e.get("type") == "circle"]
|
||
arcs = [e for e in non_const if e.get("type") == "arc"]
|
||
|
||
if len(lines) == 0 and len(circles) == 0 and len(arcs) == 0:
|
||
return None
|
||
|
||
# 预计算常用辅助数据(避免每个分类器重复计算)
|
||
# 按优先级执行分类器
|
||
sorted_classifiers = sorted(SKETCH_CLASSIFIERS, key=lambda x: x[1])
|
||
for name, priority, fn in sorted_classifiers:
|
||
result = fn(lines, arcs, circles, const_lines, non_const)
|
||
if result is not None:
|
||
return result
|
||
|
||
# 无法识别的形状: 015133 坐标仅存 compiler_context
|
||
return {
|
||
"type": "unknown_shape",
|
||
"signature": "L{}_A{}_C{}".format(len(lines), len(arcs), len(circles)),
|
||
"_note": "exact rebuild will use compiler_context",
|
||
}
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 基础几何形状分类器
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
@register_classifier("circle", priority=1)
|
||
def _classify_circle(lines, arcs, circles, const_lines, all_ents):
|
||
"""单圆:1 个 circle 实体,无线段无弧"""
|
||
if len(lines) == 0 and len(arcs) == 0 and len(circles) == 1:
|
||
c = circles[0]
|
||
return {
|
||
"type": "circle",
|
||
"radius_mm": round(float(c["radius_mm"]), 6),
|
||
"center": [round(float(c["center"][0]), 6), round(float(c["center"][1]), 6)],
|
||
}
|
||
return None
|
||
|
||
|
||
@register_classifier("annulus", priority=2)
|
||
def _classify_annulus(lines, arcs, circles, const_lines, all_ents):
|
||
"""同心圆环:2 个同心圆"""
|
||
if len(lines) != 0 or len(arcs) != 0 or len(circles) != 2:
|
||
return None
|
||
c0, c1 = circles[0], circles[1]
|
||
if (abs(float(c0["center"][0]) - float(c1["center"][0])) < 1e-6
|
||
and abs(float(c0["center"][1]) - float(c1["center"][1])) < 1e-6):
|
||
radii = sorted([float(c0["radius_mm"]), float(c1["radius_mm"])])
|
||
return {
|
||
"type": "annulus",
|
||
"center": [round(float(c0["center"][0]), 6), round(float(c0["center"][1]), 6)],
|
||
"outer_radius_mm": round(radii[1], 6),
|
||
"inner_radius_mm": round(radii[0], 6),
|
||
}
|
||
return None
|
||
|
||
|
||
@register_classifier("circles", priority=3)
|
||
def _classify_circles(lines, arcs, circles, const_lines, all_ents):
|
||
"""多圆(非同心):N 个圆,无线段"""
|
||
if len(lines) == 0 and len(arcs) == 0 and len(circles) >= 2:
|
||
return {
|
||
"type": "circles",
|
||
"items": [{
|
||
"center": [round(float(c["center"][0]), 6), round(float(c["center"][1]), 6)],
|
||
"radius_mm": round(float(c["radius_mm"]), 6),
|
||
} for c in circles],
|
||
}
|
||
return None
|
||
|
||
|
||
@register_classifier("rectangle", priority=4)
|
||
def _classify_rect(lines, arcs, circles, const_lines, all_ents):
|
||
"""纯矩形:4 条线,无弧"""
|
||
if len(lines) == 4 and len(arcs) == 0 and len(circles) == 0:
|
||
return _classify_rectangle(lines)
|
||
return None
|
||
|
||
|
||
@register_classifier("obround", priority=5)
|
||
def _classify_obround(lines, arcs, circles, const_lines, all_ents):
|
||
"""槽形/键槽:2 条平行直线 + 2 段半圆弧 = 运动场的形状。
|
||
|
||
检测几何签名:
|
||
- 2 条直线(长度相等且平行)
|
||
- 2 条圆弧(半径相等,两弧圆心在直线端点处)
|
||
- 2 条直线端点分别连接到 2 条弧的端点
|
||
"""
|
||
if len(lines) != 2 or len(arcs) != 2 or len(circles) > 0:
|
||
return None
|
||
|
||
# 获取线条方向
|
||
s1 = [float(lines[0]["start"][0]), float(lines[0]["start"][1])]
|
||
e1 = [float(lines[0]["end"][0]), float(lines[0]["end"][1])]
|
||
s2 = [float(lines[1]["start"][0]), float(lines[1]["start"][1])]
|
||
e2 = [float(lines[1]["end"][0]), float(lines[1]["end"][1])]
|
||
|
||
# 线 1 方向
|
||
d1_u, d1_v = e1[0] - s1[0], e1[1] - s1[1]
|
||
d2_u, d2_v = e2[0] - s2[0], e2[1] - s2[1]
|
||
|
||
len1 = math.hypot(d1_u, d1_v)
|
||
len2 = math.hypot(d2_u, d2_v)
|
||
|
||
if len1 < 1e-6 or len2 < 1e-6:
|
||
return None
|
||
|
||
# 平行性检查 (cos 接近 ±1)
|
||
dot = (d1_u * d2_u + d1_v * d2_v) / (len1 * len2)
|
||
if abs(abs(dot) - 1.0) > 0.01:
|
||
return None
|
||
|
||
# 长度相等
|
||
if abs(len1 - len2) > 0.1:
|
||
return None
|
||
|
||
# 弧半径相等
|
||
r1 = float(arcs[0].get("radius_mm", 0))
|
||
r2 = float(arcs[1].get("radius_mm", 0))
|
||
if abs(r1 - r2) > 0.01 or r1 <= 0:
|
||
return None
|
||
|
||
# 直线平行 → X_dir 与线条方向平行
|
||
# obround 方向: length 沿线条方向, width = 2*r
|
||
dir_u, dir_v = d1_u / len1, d1_v / len1
|
||
width = 2 * r1
|
||
length = len1 + width # 总长 = 直线长度 + 2*半径
|
||
|
||
mid_x = (s1[0] + e1[0] + s2[0] + e2[0]) / 4
|
||
mid_y = (s1[1] + e1[1] + s2[1] + e2[1]) / 4
|
||
|
||
return {
|
||
"type": "obround",
|
||
"center": [round(mid_x, 6), round(mid_y, 6)],
|
||
"length_mm": round(length, 6),
|
||
"width_mm": round(width, 6),
|
||
}
|
||
|
||
|
||
@register_classifier("d_shape", priority=6)
|
||
def _classify_d_shape(lines, arcs, circles, const_lines, all_ents):
|
||
"""D 形:1 条直线 + 1 段圆弧(半圆+弦线)。
|
||
|
||
几何签名:
|
||
- 1 条线 + 1 条弧
|
||
- 弧的端点 = 线的端点
|
||
- 弧接近半圆 (sweep ≈ 180°)
|
||
"""
|
||
if len(lines) != 1 or len(arcs) != 1 or len(circles) > 0:
|
||
return None
|
||
|
||
ln_start = [float(lines[0]["start"][0]), float(lines[0]["start"][1])]
|
||
ln_end = [float(lines[0]["end"][0]), float(lines[0]["end"][1])]
|
||
arc_center = [float(arcs[0].get("center", [0, 0])[0]), float(arcs[0].get("center", [0, 0])[1])]
|
||
arc_radius = float(arcs[0].get("radius_mm", 0))
|
||
arc_sweep = arcs[0].get("arc_sweep_deg", 0)
|
||
|
||
if arc_radius <= 0:
|
||
return None
|
||
|
||
# 弧端点应与线端点重合
|
||
arc_start = [float(arcs[0].get("start", [0, 0])[0]), float(arcs[0].get("start", [0, 0])[1])]
|
||
arc_end = [float(arcs[0].get("end", [0, 0])[0]), float(arcs[0].get("end", [0, 0])[1])]
|
||
|
||
# 线/弧端点匹配(任意组合)
|
||
eps = 0.01
|
||
dists = [
|
||
(math.hypot(ln_start[0] - arc_start[0], ln_start[1] - arc_start[1]),
|
||
math.hypot(ln_end[0] - arc_end[0], ln_end[1] - arc_end[1])),
|
||
(math.hypot(ln_start[0] - arc_end[0], ln_start[1] - arc_end[1]),
|
||
math.hypot(ln_end[0] - arc_start[0], ln_end[1] - arc_start[1])),
|
||
]
|
||
ok = any(d[0] < eps and d[1] < eps for d in dists)
|
||
if not ok:
|
||
return None
|
||
|
||
# 弦线方向: 圆心到弦中点的方向 = 负 chord_sign
|
||
chord_mid = [(ln_start[0] + ln_end[0]) / 2, (ln_start[1] + ln_end[1]) / 2]
|
||
to_chord = [chord_mid[0] - arc_center[0], chord_mid[1] - arc_center[1]]
|
||
|
||
cdx = arc_center[0] - chord_mid[0]
|
||
if cdx > 0:
|
||
chord_sign = "left" # 圆心在弦右侧,弧开口朝左(D 的圆弧在左侧)
|
||
else:
|
||
chord_sign = "right"
|
||
|
||
chord_len = math.hypot(ln_end[0] - ln_start[0], ln_end[1] - ln_start[1])
|
||
chord_x = math.sqrt(max(0, arc_radius * arc_radius - (chord_len / 2) ** 2))
|
||
|
||
return {
|
||
"type": "d_shape",
|
||
"radius_mm": round(arc_radius, 6),
|
||
"chord_sign": chord_sign,
|
||
"chord_x_mm": round(chord_x, 6),
|
||
}
|
||
|
||
|
||
@register_classifier("partial_ring", priority=7)
|
||
def _classify_partial_ring(lines, arcs, circles, const_lines, all_ents):
|
||
"""部分圆环(扇区环):2 段同心弧 + 2 条径向直线。
|
||
|
||
几何签名:
|
||
- 2 线 + 2 弧
|
||
- 2 弧同心
|
||
- 每条线连接一对外弧端点和内弧端点
|
||
"""
|
||
if len(lines) != 2 or len(arcs) != 2 or len(circles) > 0:
|
||
return None
|
||
|
||
c0 = [float(arcs[0].get("center", [0, 0])[0]), float(arcs[0].get("center", [0, 0])[1])]
|
||
c1 = [float(arcs[1].get("center", [0, 0])[0]), float(arcs[1].get("center", [0, 0])[1])]
|
||
|
||
if abs(c0[0] - c1[0]) > 0.01 or abs(c0[1] - c1[1]) > 0.01:
|
||
return None
|
||
|
||
r0 = float(arcs[0].get("radius_mm", 0))
|
||
r1 = float(arcs[1].get("radius_mm", 0))
|
||
if r0 <= 0 or r1 <= 0:
|
||
return None
|
||
|
||
ir = min(r0, r1)
|
||
oR = max(r0, r1)
|
||
|
||
# 线的端点应连接到弧端点
|
||
# 径向线应从内弧端点到外弧端点
|
||
# 计算大约的角度范围
|
||
a0_s = [float(arcs[0].get("start", [0, 0])[0]), float(arcs[0].get("start", [0, 0])[1])]
|
||
a0_e = [float(arcs[0].get("end", [0, 0])[0]), float(arcs[0].get("end", [0, 0])[1])]
|
||
|
||
# 相对于圆心的角度
|
||
def angle_from(u, v):
|
||
return math.degrees(math.atan2(v - c0[1], u - c0[0]))
|
||
|
||
angles = [angle_from(p[0], p[1]) for p in [a0_s, a0_e]]
|
||
half_angle = abs(angles[0] - angles[1]) / 2
|
||
if half_angle > 180:
|
||
half_angle = 360 - half_angle
|
||
|
||
return {
|
||
"type": "partial_ring",
|
||
"inner_radius_mm": round(ir, 6),
|
||
"outer_radius_mm": round(oR, 6),
|
||
"half_angle_deg": round(half_angle, 6),
|
||
}
|
||
|
||
|
||
@register_classifier("rectangle_with_circles", priority=8)
|
||
def _classify_rect_with_circles(lines, arcs, circles, const_lines, all_ents):
|
||
"""矩形内嵌圆孔:矩形 + 内部圆形孔洞。
|
||
|
||
几何签名:
|
||
- 4 条线构成矩形
|
||
- N 个圆全部在矩形内部
|
||
"""
|
||
if len(lines) != 4 or len(arcs) != 0 or len(circles) == 0:
|
||
return None
|
||
|
||
rect = _classify_rectangle(lines)
|
||
if rect is None:
|
||
return None
|
||
|
||
# 检查所有圆是否在矩形内
|
||
mn = rect.get("min_mm", [0, 0])
|
||
mx = rect.get("max_mm", [0, 0])
|
||
x0, y0 = mn[0], mn[1]
|
||
x1, y1 = mx[0], mx[1]
|
||
|
||
inner_circles = []
|
||
for c in circles:
|
||
cx = float(c["center"][0])
|
||
cy = float(c["center"][1])
|
||
cr = float(c["radius_mm"])
|
||
if x0 - cr < cx < x1 + cr and y0 - cr < cy < y1 + cr:
|
||
inner_circles.append({
|
||
"center": [round(cx, 6), round(cy, 6)],
|
||
"radius_mm": round(cr, 6),
|
||
})
|
||
|
||
if not inner_circles:
|
||
return None
|
||
|
||
return {
|
||
"type": "rectangle_with_circles",
|
||
"boundary": {
|
||
"type": "rectangle",
|
||
"width_mm": rect.get("width_mm"),
|
||
"height_mm": rect.get("height_mm"),
|
||
"min_mm": mn,
|
||
"max_mm": mx,
|
||
},
|
||
"circles": inner_circles,
|
||
}
|
||
|
||
|
||
@register_classifier("polygon", priority=50)
|
||
def _classify_polygon(lines, arcs, circles, const_lines, all_ents):
|
||
"""普通多边形:N 条线段,无弧无圆。顶点坐标仅存 compiler_context。"""
|
||
if len(lines) < 3 or len(arcs) > 0 or len(circles) > 0:
|
||
return None
|
||
|
||
# 4 线优先返回 rectangle
|
||
if len(lines) == 4:
|
||
rect = _classify_rectangle(lines)
|
||
if rect:
|
||
rect["type"] = "rectangle"
|
||
return rect
|
||
|
||
# 015133: polygon 不携带顶点坐标,坐标属于 Execution IR
|
||
has_axis = len(const_lines) >= 1
|
||
return {
|
||
"type": "polygon",
|
||
"n_sides": len(lines),
|
||
"_has_construction_axis": has_axis,
|
||
}
|
||
|
||
|
||
@register_classifier("revolve_straight_profile", priority=55)
|
||
def _classify_revolve_polygon(lines, arcs, circles, const_lines, all_ents):
|
||
"""旋转特征的直边截面(有 construction line 做轴,5 边梯形等)。
|
||
|
||
> polygon 分类器(priority=50) 如果没匹配到,说明有弧/圆。
|
||
> 这里处理有构造线但非纯多边形的情况。
|
||
"""
|
||
if len(const_lines) < 1:
|
||
return None
|
||
if len(lines) < 3:
|
||
return None
|
||
if len(arcs) > 0 or len(circles) > 0:
|
||
return None
|
||
|
||
# 尝试 revolve_chamfer / revolve_chamfer_slanted 识别
|
||
if len(lines) == 5:
|
||
vertices = _extract_polygon_vertices(lines)
|
||
if len(vertices) == 5:
|
||
# 5 边梯形截面: revolve_chamfer 或 slanted 变体
|
||
# 分析形状特征
|
||
v = vertices
|
||
# 找到到原点最近的点 (轴侧)
|
||
dists = [math.hypot(vx[0], vx[1]) for vx in v]
|
||
min_idx = dists.index(min(dists))
|
||
axis_v = v[min_idx]
|
||
max_idx = dists.index(max(dists))
|
||
wall_v = v[max_idx]
|
||
|
||
# 最高点和最低点
|
||
ys = [vx[1] for vx in v]
|
||
top_y, bot_y = max(ys), min(ys)
|
||
|
||
# 找到轴侧高度
|
||
axis_height = abs(top_y - bot_y)
|
||
|
||
# 找到壁侧参数
|
||
wall_width = abs(wall_v[0])
|
||
wall_y = wall_v[1]
|
||
wall_inset = abs(top_y - wall_y)
|
||
|
||
if wall_inset > 0:
|
||
return {
|
||
"type": "revolve_chamfer_slanted",
|
||
"axis_height_mm": round(axis_height, 6),
|
||
"top_width_mm": round(abs(axis_v[0]) if abs(axis_v[0]) > 0.01 else abs(wall_width), 6),
|
||
"wall_inset_mm": round(wall_inset, 6),
|
||
"wall_height_mm": round(abs(bot_y - wall_y), 6),
|
||
"wall_width_mm": round(wall_width, 6),
|
||
"on_axis_side": "right" if wall_v[0] > 0 else "left",
|
||
}
|
||
|
||
return {
|
||
"type": "revolve_chamfer",
|
||
"axis_height_mm": round(axis_height, 6),
|
||
"top_width_mm": round(abs(float(v[max_idx][0])), 6),
|
||
"bottom_width_mm": round(abs(float(v[(min_idx + 2) % 5][0])), 6),
|
||
"wall_inset_mm": round(wall_inset, 6),
|
||
"on_axis_side": "right" if wall_v[0] > 0 else "left",
|
||
}
|
||
|
||
# 普通 revolve 多边形截面
|
||
vertices = _extract_polygon_vertices(lines)
|
||
if len(vertices) >= 3:
|
||
return {
|
||
"type": "polygon",
|
||
"vertices": vertices,
|
||
"_has_construction_axis": True,
|
||
}
|
||
return None
|
||
|
||
|
||
@register_classifier("complex_arc_shape", priority=90)
|
||
def _classify_complex_arc_shape(lines, arcs, circles, const_lines, all_ents):
|
||
"""含弧复杂形状:任何无法被更高优先级分类器识别的含弧草图。
|
||
|
||
015133: 坐标仅存 compiler_context,CDSL 只保留类型签名。
|
||
"""
|
||
if len(arcs) == 0:
|
||
return None
|
||
|
||
return {
|
||
"type": "complex_arc_shape",
|
||
"n_lines": len(lines),
|
||
"n_arcs": len(arcs),
|
||
"n_circles": len(circles),
|
||
"_signature": "L{}_A{}_C{}".format(len(lines), len(arcs), len(circles)),
|
||
}
|
||
|
||
|
||
|
||
def _clean_cdsl_floats(obj: Any, decimals: int = 6) -> Any:
|
||
"""递归清理CDSL中所有浮点数到指定精度。移除IEEE 754噪音 (如31.500000000000007→31.5)。
|
||
|
||
015133标准: LLM不应学习的浮点精度噪音。
|
||
"""
|
||
if isinstance(obj, dict):
|
||
return {k: _clean_cdsl_floats(v, decimals) for k, v in obj.items()}
|
||
elif isinstance(obj, list):
|
||
return [_clean_cdsl_floats(item, decimals) for item in obj]
|
||
elif isinstance(obj, float):
|
||
# 跳过方向向量 (归一化后的单位向量分量本身就是无理数,如0.707...)
|
||
# 跳过编译器上下文中的精确值
|
||
return round(obj, decimals)
|
||
return obj
|
||
|
||
def _content_hash(sketch: dict[str, Any]) -> str:
|
||
"""对草图实体内容计算 hash,用于检测同形复用"""
|
||
entities = sketch.get("entities") or []
|
||
keys_data = []
|
||
for ent in entities:
|
||
if ent.get("construction"):
|
||
continue
|
||
t = ent.get("type", "?")
|
||
if t == "line":
|
||
s = ent.get("start", [0, 0])
|
||
e = ent.get("end", [0, 0])
|
||
keys_data.append(f"line:{s[0]:.4f},{s[1]:.4f}:{e[0]:.4f},{e[1]:.4f}")
|
||
elif t == "circle":
|
||
c = ent.get("center", [0, 0])
|
||
r = ent.get("radius_mm", 0)
|
||
keys_data.append(f"circle:{c[0]:.4f},{c[1]:.4f}:r{r:.4f}")
|
||
elif t == "arc":
|
||
c = ent.get("center", [0, 0])
|
||
r = ent.get("radius_mm", 0)
|
||
s = ent.get("start", [0, 0])
|
||
e = ent.get("end", [0, 0])
|
||
keys_data.append(f"arc:{c[0]:.4f},{c[1]:.4f}:r{r:.4f}:{s[0]:.4f},{s[1]:.4f}:{e[0]:.4f},{e[1]:.4f}")
|
||
elif t == "point":
|
||
p = ent.get("point_mm") or ent.get("point") or [0, 0]
|
||
keys_data.append(f"point:{p[0]:.4f},{p[1]:.4f}")
|
||
else:
|
||
keys_data.append(f"unknown:{json.dumps(ent, sort_keys=True)}")
|
||
keys_data.sort()
|
||
return hashlib.md5("|".join(keys_data).encode()).hexdigest()[:12]
|
||
|
||
|
||
def _classify_rectangle(lines: list[dict[str, Any]]) -> dict[str, Any] | None:
|
||
"""从 4 条线段识别矩形"""
|
||
pts = []
|
||
for l in lines:
|
||
s = l.get("start", [0, 0])
|
||
e = l.get("end", [0, 0])
|
||
pts.append((round(float(s[0]), 6), round(float(s[1]), 6)))
|
||
pts.append((round(float(e[0]), 6), round(float(e[1]), 6)))
|
||
|
||
xs = [p[0] for p in pts]
|
||
ys = [p[1] for p in pts]
|
||
min_x, max_x = min(xs), max(xs)
|
||
min_y, max_y = min(ys), max(ys)
|
||
w = round(max_x - min_x, 6)
|
||
h = round(max_y - min_y, 6)
|
||
|
||
if w < 1e-6 or h < 1e-6:
|
||
return None
|
||
|
||
# rectangle 只保留 width/height + center,min_mm/max_mm 是坐标污染
|
||
cx = round((min_x + max_x) / 2, 6)
|
||
cy = round((min_y + max_y) / 2, 6)
|
||
result = {
|
||
"type": "rectangle",
|
||
"width_mm": w,
|
||
"height_mm": h,
|
||
"center": [cx, cy],
|
||
}
|
||
return result
|
||
|
||
|
||
def _extract_polygon_vertices(lines: list[dict[str, Any]]) -> list[list[float]]:
|
||
"""从连续线段提取有序顶点"""
|
||
if len(lines) < 2:
|
||
return []
|
||
adj = {}
|
||
for l in lines:
|
||
s = (round(float(l["start"][0]), 6), round(float(l["start"][1]), 6))
|
||
e = (round(float(l["end"][0]), 6), round(float(l["end"][1]), 6))
|
||
adj[s] = e
|
||
|
||
if not adj:
|
||
return []
|
||
|
||
first = next(iter(adj.keys()))
|
||
order = [first]
|
||
cur = first
|
||
seen = {first}
|
||
for _ in range(len(lines) + 1):
|
||
nxt = adj.get(cur)
|
||
if nxt is None or nxt in seen:
|
||
break
|
||
order.append(nxt)
|
||
seen.add(nxt)
|
||
cur = nxt
|
||
return [[float(v[0]), float(v[1])] for v in order]
|
||
|
||
|
||
def _generate_contour_from_loops(sketch: dict[str, Any]) -> list[dict[str, Any]]:
|
||
"""从 IR 草图 loops + entities 生成 contour_edges_mm"""
|
||
entities = sketch.get("entities") or []
|
||
loops = sketch.get("loops") or []
|
||
|
||
contours = []
|
||
# 如果没有 loops 数据,从 entities 直接生成
|
||
if not loops:
|
||
non_const = [e for e in entities if not e.get("construction")]
|
||
for ent in non_const:
|
||
c = _entity_to_contour(ent)
|
||
if c:
|
||
contours.append(c)
|
||
return contours
|
||
|
||
# 有 loops 数据:从 loop 的 entity_indices 提取
|
||
for loop in loops:
|
||
indices = loop.get("entity_indices") or []
|
||
for idx in indices:
|
||
if idx < len(entities) and not entities[idx].get("construction", False):
|
||
c = _entity_to_contour(entities[idx])
|
||
if c:
|
||
contours.append(c)
|
||
|
||
return contours
|
||
|
||
|
||
def _entity_to_contour(ent: dict[str, Any]) -> dict[str, Any] | None:
|
||
"""将单个草图实体转换为 contour_edge"""
|
||
t = ent.get("type", "")
|
||
if t == "line":
|
||
s = ent.get("start", [0, 0])
|
||
e = ent.get("end", [0, 0])
|
||
return {
|
||
"type": "line",
|
||
"start_mm": [float(s[0]), float(s[1]), 0.0],
|
||
"end_mm": [float(e[0]), float(e[1]), 0.0],
|
||
}
|
||
elif t == "arc":
|
||
s = ent.get("start", [0, 0])
|
||
e = ent.get("end", [0, 0])
|
||
c = ent.get("center", [0, 0])
|
||
return {
|
||
"type": "arc",
|
||
"start_mm": [float(s[0]), float(s[1]), 0.0],
|
||
"end_mm": [float(e[0]), float(e[1]), 0.0],
|
||
"center_mm": [float(c[0]), float(c[1]), 0.0],
|
||
"radius_mm": float(ent.get("radius_mm", 0)),
|
||
}
|
||
elif t == "circle":
|
||
c = ent.get("center", [0, 0])
|
||
r = ent.get("radius_mm", 0)
|
||
# 圆分解为 4 段弧
|
||
cx, cy = float(c[0]), float(c[1])
|
||
return {
|
||
"type": "arc",
|
||
"start_mm": [cx + r, cy, 0.0],
|
||
"end_mm": [cx, cy + r, 0.0],
|
||
"center_mm": [cx, cy, 0.0],
|
||
"radius_mm": r,
|
||
}
|
||
return None
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# 主轴: SW JSON → 参数化 CDSL
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
def convert_sw_json_to_cdsl(
|
||
sw_json_path: str | Path,
|
||
part_id: str | None = None,
|
||
) -> dict[str, Any]:
|
||
sw_json_path = Path(sw_json_path)
|
||
with open(sw_json_path, "r", encoding="utf-8") as f:
|
||
sw_data = json.load(f)
|
||
|
||
if part_id is None:
|
||
part_id = sw_json_path.stem.replace(".solidworks_rebuild_extract", "")
|
||
|
||
ir = normalize_to_ir(sw_data)
|
||
|
||
# ── Phase 1: 参数化每个草图 ──
|
||
ir_sketches = {s["id"]: s for s in ir.get("sketches", [])}
|
||
cdsl_sketches: list[dict[str, Any]] = []
|
||
id_to_cdsl_sketch: dict[str, dict[str, Any]] = {}
|
||
|
||
for sk_id, sk in sorted(ir_sketches.items()):
|
||
name = sk.get("name", "")
|
||
wp = sk.get("workplane") or {}
|
||
|
||
# 跳过放样轮廓草图(由 loft 操作直接处理)
|
||
if sk.get("loft_profile"):
|
||
continue
|
||
|
||
# 跳过纯参考草图 (如 body reference)
|
||
entities = sk.get("entities") or []
|
||
non_const = [e for e in entities if not e.get("construction")]
|
||
if len(non_const) == 0:
|
||
cdsl_sk = {
|
||
"id": sk_id,
|
||
"name": name,
|
||
"workplane": {
|
||
"origin_mm": wp.get("origin_mm", [0, 0, 0]),
|
||
"x_dir": wp.get("x_dir", [1, 0, 0]),
|
||
"y_dir": wp.get("y_dir", [0, 1, 0]),
|
||
"normal": wp.get("normal", [0, 0, 1]),
|
||
},
|
||
}
|
||
cdsl_sketches.append(cdsl_sk)
|
||
id_to_cdsl_sketch[sk_id] = cdsl_sk
|
||
continue
|
||
|
||
profile = _classify_sketch_shape(sk)
|
||
content_h = _content_hash(sk)
|
||
|
||
cdsl_sk = {
|
||
"id": sk_id,
|
||
"name": name,
|
||
"workplane": {
|
||
"origin_mm": wp.get("origin_mm", [0, 0, 0]),
|
||
"x_dir": wp.get("x_dir", [1, 0, 0]),
|
||
"y_dir": wp.get("y_dir", [0, 1, 0]),
|
||
"normal": wp.get("normal", [0, 0, 1]),
|
||
},
|
||
"_content_hash": content_h,
|
||
}
|
||
if profile:
|
||
# 复杂形状回退:保留原始实体和 contour_edges_mm 以保证 exact 路径重建精度
|
||
# 已注册的形状(circle/obround/d_shape 等)直接用 profile
|
||
# 015133: 所有形状都用 profile,坐标仅存 compiler_context
|
||
cdsl_sk["profile"] = profile
|
||
|
||
cdsl_sketches.append(cdsl_sk)
|
||
id_to_cdsl_sketch[sk_id] = cdsl_sk
|
||
|
||
# ── Phase 2: profile_from for ALL duplicates (015133 standard) ──
|
||
hash_to_first: dict[str, str] = {}
|
||
dedup_count = 0
|
||
|
||
for cdsl_sk in cdsl_sketches:
|
||
ch = cdsl_sk.pop("_content_hash", None)
|
||
if not ch:
|
||
continue
|
||
sk_id = cdsl_sk["id"]
|
||
|
||
if ch in hash_to_first:
|
||
src_id = hash_to_first[ch]
|
||
src_sk = id_to_cdsl_sketch.get(src_id)
|
||
if not src_sk:
|
||
continue
|
||
|
||
src_has_profile = bool(src_sk.get("profile"))
|
||
this_has_profile = bool(cdsl_sk.get("profile"))
|
||
|
||
src_origin = src_sk.get("workplane", {}).get("origin_mm", [0, 0, 0])
|
||
this_origin = cdsl_sk.get("workplane", {}).get("origin_mm", [0, 0, 0])
|
||
dx = round(this_origin[0] - src_origin[0], 6)
|
||
dy = round(this_origin[1] - src_origin[1], 6)
|
||
|
||
if src_has_profile and this_has_profile:
|
||
# case 1: both have registered profiles -> standard profile_from
|
||
cdsl_sk["profile_from"] = src_id
|
||
cdsl_sk.pop("profile", None)
|
||
cdsl_sk.pop("entities", None)
|
||
cdsl_sk.pop("contour_edges_mm", None)
|
||
cdsl_sk.pop("workplane", None) # 仅编译器上下文需要,LLM不学
|
||
cdsl_sk.pop("_fallback_reason", None)
|
||
if abs(dx) > 1e-6 or abs(dy) > 1e-6:
|
||
cdsl_sk["profile_shift"] = [dx, dy]
|
||
dedup_count += 1
|
||
|
||
elif not src_has_profile and not this_has_profile:
|
||
# case 2: complex shapes - create polygon profile on source
|
||
src_ents = src_sk.get("entities", [])
|
||
src_contours = src_sk.get("contour_edges_mm", [])
|
||
if src_ents or src_contours:
|
||
if not src_sk.get("profile"):
|
||
vertices = []
|
||
for ce in src_contours:
|
||
s = ce.get("start_mm", [0, 0, 0])
|
||
if len(s) >= 2:
|
||
vertices.append([float(s[0]), float(s[1])])
|
||
if not vertices:
|
||
for e in src_ents:
|
||
s = e.get("start", [0, 0])
|
||
if len(s) >= 2:
|
||
vertices.append([float(s[0]), float(s[1])])
|
||
if vertices:
|
||
src_sk["profile"] = {
|
||
"type": "polygon",
|
||
"vertices": vertices,
|
||
"_from_entities": True,
|
||
}
|
||
cdsl_sk["profile_from"] = src_id
|
||
cdsl_sk.pop("profile", None)
|
||
cdsl_sk.pop("entities", None)
|
||
cdsl_sk.pop("contour_edges_mm", None)
|
||
cdsl_sk.pop("workplane", None) # 仅编译器上下文需要,LLM不学
|
||
cdsl_sk.pop("_fallback_reason", None)
|
||
if abs(dx) > 1e-6 or abs(dy) > 1e-6:
|
||
cdsl_sk["profile_shift"] = [dx, dy]
|
||
dedup_count += 1
|
||
|
||
elif src_has_profile:
|
||
# case 3: source has profile, duplicate has only entities
|
||
cdsl_sk["profile_from"] = src_id
|
||
cdsl_sk.pop("profile", None)
|
||
cdsl_sk.pop("entities", None)
|
||
cdsl_sk.pop("contour_edges_mm", None)
|
||
cdsl_sk.pop("_fallback_reason", None)
|
||
if abs(dx) > 1e-6 or abs(dy) > 1e-6:
|
||
cdsl_sk["profile_shift"] = [dx, dy]
|
||
dedup_count += 1
|
||
else:
|
||
hash_to_first[ch] = sk_id
|
||
|
||
# ── Phase 3: 构建 CDSL features ──
|
||
ops = [op for op in ir.get("operations", [])
|
||
if op.get("type") not in ("assembly_compose",)]
|
||
|
||
cdsl_features: list[dict[str, Any]] = []
|
||
feature_ids: list[str] = []
|
||
name_to_id: dict[str, str] = {}
|
||
|
||
for i, op in enumerate(ops):
|
||
fid = f"f{i + 1:02d}"
|
||
op_type = op.get("type", "")
|
||
sketch_id = op.get("sketch")
|
||
params = op.get("parameters", {})
|
||
|
||
# 确定 atomic_id
|
||
atomic_id = _map_atomic_id(op_type, params)
|
||
|
||
# 构建 CDSL 参数
|
||
cdsl_params = _build_cdsl_params(op, params, op_type, atomic_id, cdsl_features)
|
||
|
||
feat: dict[str, Any] = {
|
||
"id": fid,
|
||
"atomic_id": atomic_id,
|
||
"depends_on": list(feature_ids),
|
||
"name": op.get("name", fid),
|
||
"params": cdsl_params,
|
||
}
|
||
|
||
if sketch_id:
|
||
feat["sketch_id"] = sketch_id
|
||
|
||
cdsl_features.append(feat)
|
||
feature_ids.append(fid)
|
||
name_to_id[op.get("name", "")] = fid
|
||
|
||
# 修复 pattern source_features 引用
|
||
for feat in cdsl_features:
|
||
if feat["atomic_id"] == "pattern_linear":
|
||
raw = op_to_raw(op, feat, ops)
|
||
if raw:
|
||
src_names = raw.get("source_feature_names", [])
|
||
src_ids = [name_to_id.get(n, n) for n in src_names]
|
||
feat["params"]["source_feature_ids"] = src_ids
|
||
|
||
# ── 清理空草图 (015133: 空参考草图移入compiler_context,不污染Learning IR) ──
|
||
empty_ids = set()
|
||
for cdsl_sk in cdsl_sketches:
|
||
if not cdsl_sk.get("profile") and not cdsl_sk.get("profile_from"):
|
||
has_geo = bool(cdsl_sk.get("entities") or cdsl_sk.get("contour_edges_mm"))
|
||
if not has_geo:
|
||
empty_ids.add(cdsl_sk["id"])
|
||
# 仅移除未被feature引用的空草图 (防止破坏特征引用)
|
||
if empty_ids:
|
||
ref_ids = set()
|
||
for feat in cdsl_features:
|
||
sid = feat.get("sketch_id")
|
||
if sid and sid in empty_ids:
|
||
ref_ids.add(sid)
|
||
safe_remove = empty_ids - ref_ids
|
||
cdsl_sketches = [s for s in cdsl_sketches if s["id"] not in safe_remove]
|
||
|
||
|
||
# 构建 CDSL (不含 compiler_context, LLM 训练可直接加载)
|
||
cdsl: dict[str, Any] = {
|
||
"schema": "cad.cdsl.llm.v1",
|
||
"schema_version": "1.0.0",
|
||
"kind": "part",
|
||
"part_id": part_id,
|
||
"features": cdsl_features,
|
||
"geometry": {"sketches": cdsl_sketches},
|
||
"meta": {
|
||
"unit": "mm",
|
||
"source": "sw_history_distill_llm_v1",
|
||
"optimization": "015133-compliant",
|
||
"parameterized_sketches": sum(1 for s in cdsl_sketches if s.get("profile")),
|
||
"profile_from_sketches": sum(1 for s in cdsl_sketches if s.get("profile_from")),
|
||
"from_contour_vertex_count": sum(
|
||
1 for f in cdsl_features
|
||
if "revolve" in f["atomic_id"]
|
||
and f["params"].get("axis", {}).get("from_contour_vertex") is not None
|
||
),
|
||
"raw_sketch_count": sum(1 for s in cdsl_sketches if not s.get("profile") and not s.get("profile_from")),
|
||
},
|
||
}
|
||
|
||
# The import classifier may use historical shape labels while recognizing
|
||
# a SolidWorks sketch. Lower them before exposing the result as CDSL so
|
||
# the runtime contract remains line/arc/circle/contour based.
|
||
try:
|
||
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
|
||
except ImportError: # pragma: no cover - direct converter invocation
|
||
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
|
||
|
||
cdsl = _clean_cdsl_floats(cdsl)
|
||
return lower_legacy_profiles(cdsl)
|
||
|
||
|
||
def op_to_raw(feat: dict, op: dict, all_ops: list) -> dict | None:
|
||
"""从 pattern 操作的 raw_parameters 提取数据"""
|
||
raw = op.get("raw_parameters", {})
|
||
if not raw:
|
||
# 从更早的 feature 中查找
|
||
for o in all_ops:
|
||
if o.get("type") == "linear_pattern":
|
||
raw = o.get("raw_parameters", {})
|
||
if raw:
|
||
break
|
||
if not raw:
|
||
return None
|
||
srcs = raw.get("source_features") or []
|
||
return {
|
||
"source_feature_names": [s.get("name", "") for s in srcs if isinstance(s, dict)],
|
||
}
|
||
|
||
|
||
def _map_atomic_id(op_type: str, params: dict) -> str:
|
||
if op_type in ("extrude_add", "extrude_cut"):
|
||
rev_dist = params.get("reverse_distance_mm") or 0
|
||
both = params.get("both_directions", False)
|
||
if op_type == "extrude_add" and (both or rev_dist > 1e-6):
|
||
return "extrude_add_two_sided"
|
||
return "extrude_cut_blind" if op_type == "extrude_cut" else "extrude_add_blind"
|
||
if op_type in ("revolve_add", "revolve_cut"):
|
||
return op_type
|
||
if op_type in ("linear_pattern", "pattern_linear"):
|
||
return "pattern_linear"
|
||
if op_type == "pattern_mirror":
|
||
return "pattern_mirror"
|
||
if op_type == "shell":
|
||
return "shell"
|
||
if op_type == "draft":
|
||
return "draft"
|
||
if op_type == "loft":
|
||
return "loft"
|
||
if op_type == "dome":
|
||
return "dome"
|
||
if op_type == "thicken":
|
||
return "thicken"
|
||
if op_type == "noop":
|
||
return "noop"
|
||
if op_type == "hole":
|
||
if (params.get("countersink_diameter_mm") or 0) > 0:
|
||
return "hole_countersink"
|
||
if (params.get("counterbore_diameter_mm") or 0) > 0:
|
||
return "hole_counterbore"
|
||
return "hole_blind"
|
||
return op_type
|
||
|
||
|
||
def _build_cdsl_params(op: dict, params: dict, op_type: str, atomic_id: str, prev_features: list = None) -> dict:
|
||
result = {}
|
||
|
||
if "extrude" in op_type:
|
||
dist = params.get("distance_mm") or 0
|
||
result["distance_mm"] = float(dist)
|
||
rev_dist = params.get("reverse_distance_mm") or 0
|
||
rev = params.get("reverse", False)
|
||
both = params.get("both_directions", False)
|
||
if both and rev_dist > 1e-6:
|
||
result["reverse_distance_mm"] = float(rev_dist)
|
||
elif rev:
|
||
result["reverse"] = True
|
||
|
||
elif "revolve" in op_type:
|
||
result["angle_deg"] = float(params.get("angle_deg") or 360)
|
||
ax = params.get("axis") or params.get("axis_reference") or {}
|
||
direction = ax.get("direction") or [0, 0, 1]
|
||
# Phase 3: from_contour_vertex 替代 origin_mm
|
||
result["axis"] = {"direction": list(direction)}
|
||
origin = ax.get("origin_mm")
|
||
if origin:
|
||
# 用 from_contour_vertex: 0 替代浮点坐标
|
||
result["axis"]["from_contour_vertex"] = 0
|
||
elif ax.get("from_contour_vertex") is not None:
|
||
result["axis"]["from_contour_vertex"] = int(ax["from_contour_vertex"])
|
||
elif ax.get("from_workplane_origin"):
|
||
result["axis"]["from_workplane_origin"] = True
|
||
else:
|
||
result["axis"]["from_contour_vertex"] = 0
|
||
|
||
elif atomic_id == "pattern_linear":
|
||
raw = op.get("raw_parameters", {})
|
||
params2 = params
|
||
d1_raw = raw.get("direction1") or [1, 0, 0]
|
||
d2_raw = raw.get("direction2") or [0, 1, 0]
|
||
d1 = d1_raw.get("vector") if isinstance(d1_raw, dict) else d1_raw
|
||
d2 = d2_raw.get("vector") if isinstance(d2_raw, dict) else d2_raw
|
||
result["pattern_count_1"] = int(raw.get("d1_total_instances") or params2.get("total_instances") or 2)
|
||
result["spacing_1_mm"] = float(raw.get("d1_spacing_mm") or params2.get("spacing_mm") or 0)
|
||
result["direction_1"] = d1
|
||
result["pattern_count_2"] = int(raw.get("d2_total_instances") or 1)
|
||
result["spacing_2_mm"] = float(raw.get("d2_spacing_mm") or 0)
|
||
result["direction_2"] = d2
|
||
|
||
elif atomic_id == "pattern_mirror":
|
||
raw = op.get("raw_parameters", {})
|
||
src_features = params.get("source_features") or []
|
||
result["source_feature_ids"] = [f.get("name") if isinstance(f, dict) else str(f) for f in src_features]
|
||
# 镜像面数据从 C# 插件 raw_parameters 直接获取
|
||
mirror_origin = raw.get("mirror_plane_origin")
|
||
mirror_normal = raw.get("mirror_plane_normal")
|
||
if mirror_origin and isinstance(mirror_origin, (list, tuple)) and len(mirror_origin) >= 3:
|
||
result["mirror_plane_origin_mm"] = [float(v) for v in mirror_origin]
|
||
if mirror_normal and isinstance(mirror_normal, (list, tuple)) and len(mirror_normal) >= 3:
|
||
result["mirror_plane_normal"] = [float(v) for v in mirror_normal]
|
||
|
||
elif atomic_id == "shell":
|
||
result["thickness_mm"] = float(params.get("thickness_mm") or 0)
|
||
result["faces_to_remove"] = params.get("faces_to_remove") or []
|
||
|
||
elif atomic_id == "draft":
|
||
result["angle_deg"] = float(params.get("angle_deg") or 0)
|
||
result["draft_type"] = params.get("draft_type", "")
|
||
pull_dir = params.get("pull_direction", [0, 0, 1])
|
||
result["pull_direction"] = [float(v) for v in pull_dir] if pull_dir else [0, 0, 1]
|
||
|
||
elif atomic_id == "loft":
|
||
result["profile_sketches"] = params.get("profile_sketches") or []
|
||
result["is_closed"] = params.get("is_closed", False)
|
||
|
||
elif atomic_id == "dome":
|
||
result["height_mm"] = float(params.get("height_mm") or 0)
|
||
|
||
elif atomic_id == "thicken":
|
||
result["thickness_mm"] = float(params.get("thickness_mm") or 0)
|
||
|
||
elif atomic_id == "noop":
|
||
result["sw_type"] = params.get("sw_type", "")
|
||
|
||
elif atomic_id.startswith("hole"):
|
||
result["diameter_mm"] = float(params.get("diameter_mm") or 0)
|
||
result["depth_mm"] = float(params.get("depth_mm") or 0)
|
||
result["positions"] = params.get("positions") or []
|
||
|
||
elif atomic_id == "fillet":
|
||
result["radius_mm"] = float(params.get("radius_mm") or 0)
|
||
|
||
elif atomic_id == "chamfer":
|
||
result["distance_mm"] = float(params.get("distance_mm") or 0)
|
||
|
||
return result
|
||
|
||
|
||
def _simplified_entities(entities: list[dict]) -> list[dict]:
|
||
"""简化的实体数据(保留足够的几何信息供 sketch_solver 重建)"""
|
||
result = []
|
||
for ent in entities:
|
||
s = {"type": ent.get("type", "?")}
|
||
if ent.get("type") == "line":
|
||
s["start"] = (ent.get("start") or [0, 0])[:2]
|
||
s["end"] = (ent.get("end") or [0, 0])[:2]
|
||
elif ent.get("type") == "circle":
|
||
s["center"] = (ent.get("center") or [0, 0])[:2]
|
||
s["radius_mm"] = ent.get("radius_mm")
|
||
elif ent.get("type") == "arc":
|
||
s["center"] = (ent.get("center") or [0, 0])[:2]
|
||
s["start"] = (ent.get("start") or [0, 0])[:2]
|
||
s["end"] = (ent.get("end") or [0, 0])[:2]
|
||
s["radius_mm"] = ent.get("radius_mm")
|
||
s["is_circle"] = ent.get("is_circle", False)
|
||
if ent.get("construction"):
|
||
s["construction"] = True
|
||
result.append(s)
|
||
return result
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# CLI
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
def write_cdsl_outputs(
|
||
cdsl: dict[str, Any],
|
||
out_dir: Path,
|
||
*,
|
||
sw_data: dict[str, Any] | None = None,
|
||
write_compiler_context: bool = False,
|
||
) -> dict[str, Path]:
|
||
"""写入 CDSL;可选写入 compiler_context(仅调试,非 cdsl_only 验收依赖)。"""
|
||
out_dir = Path(out_dir)
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
part_id = str(cdsl.get("part_id") or "part")
|
||
cdsl_clean = _clean_cdsl_floats(cdsl)
|
||
cdsl_path = out_dir / f"{part_id}.cdsl.json"
|
||
cdsl_path.write_text(json.dumps(cdsl_clean, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
|
||
paths: dict[str, Path] = {"cdsl": cdsl_path}
|
||
if write_compiler_context and sw_data is not None:
|
||
ir = normalize_to_ir(sw_data)
|
||
ctx_data = {
|
||
"version": ir.get("version", "ir-0.1"),
|
||
"metadata": ir.get("metadata", {}),
|
||
"sketches": ir.get("sketches", []),
|
||
"operations": ir.get("operations", []),
|
||
"references": ir.get("references", []),
|
||
"validation_hints": ir.get("validation_hints", {}),
|
||
"rebuild_contract": ir.get("rebuild_contract", {}),
|
||
}
|
||
ctx_path = out_dir / f"{part_id}.compiler_context.json"
|
||
ctx_path.write_text(json.dumps(ctx_data, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
|
||
paths["compiler_context"] = ctx_path
|
||
return paths
|
||
|
||
|
||
def main():
|
||
import argparse
|
||
from collections import Counter
|
||
ap = argparse.ArgumentParser(description="SW JSON → Parameterized CDSL")
|
||
ap.add_argument("sw_json", type=Path)
|
||
ap.add_argument("--out", "-o", type=Path, required=True, help="输出目录")
|
||
ap.add_argument("--part-id", type=str, default=None)
|
||
ap.add_argument("--write-context", action="store_true", help="额外写出 compiler_context(调试用)")
|
||
args = ap.parse_args()
|
||
|
||
sw_data = json.loads(args.sw_json.read_text(encoding="utf-8"))
|
||
cdsl = convert_sw_json_to_cdsl(args.sw_json, args.part_id)
|
||
paths = write_cdsl_outputs(
|
||
cdsl, args.out, sw_data=sw_data, write_compiler_context=args.write_context
|
||
)
|
||
|
||
meta = cdsl["meta"]
|
||
print(f"Parameterized CDSL → {paths['cdsl']}")
|
||
print(f" features: {len(cdsl['features'])}, sketches: {len(cdsl['geometry']['sketches'])}")
|
||
print(f" parameterized: {meta['parameterized_sketches']}, profile_from: {meta['profile_from_sketches']}")
|
||
print(f" from_contour_vertex: {meta['from_contour_vertex_count']}")
|
||
ac = Counter(f["atomic_id"] for f in cdsl["features"])
|
||
for aid, cnt in sorted(ac.items()):
|
||
print(f" {aid}: {cnt}")
|
||
if "compiler_context" in paths:
|
||
print(f" compiler_context → {paths['compiler_context']} (debug only)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|