Files
cdsl-cad/backend/cdsl_importer/distill_output3.py
T
2026-08-25 17:41:24 +08:00

350 lines
14 KiB
Python

"""将 output3 的程序化圆柱样本蒸馏为自足的短 CDSL。
这些样本的 SolidWorks 历史把每个切口存成独立草图/切除特征。本脚本按
文件名中的设计族选择一个可复用的 motif + layout 语义描述;不会复制
草图 entities、逐切口坐标、compiler_context 或任何编码后的几何。
"""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
from typing import Any
SPECS: dict[str, dict[str, Any]] = {
"cylinder_001": {
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 16.0, "outer_radius_mm": 25.0, "half_angle_deg": 4.583662},
"layout": {"type": "angular", "count": 12, "start_angle_deg": 15.0, "orientation": "radial"},
},
"cylinder_002": {
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 8.0, "outer_radius_mm": 12.5, "half_angle_deg": 4.583662},
"layout": {"type": "angular", "count": 12, "start_angle_deg": 15.0, "orientation": "radial"},
},
"cylinder_003": {
"motif": {"type": "circle", "radius_mm": 2.4},
"layout": {"type": "ring", "radius_mm": 34.0, "count": 32},
},
"cylinder_004": {
"motif": {"type": "square", "width_mm": 3.3},
"layout": {"type": "ring", "radius_mm": 32.0, "count": 24},
},
"cylinder_005": {
"motif": {"type": "square", "width_mm": 4.242641},
"layout": {"type": "ring", "radius_mm": 32.0, "count": 24},
},
"cylinder_006": {
"motif": {"type": "circle", "radius_mm": 2.2},
"layout": {
"type": "disc_grid", "count_x": 7, "count_y": 7,
"spacing_x_mm": 10.0, "spacing_y_mm": 10.0,
"center_mm": [0.0, 0.0], "max_center_radius_mm": 36.1,
},
},
"cylinder_007": {
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 12.0, "outer_radius_mm": 20.0, "half_angle_deg": 4.583662},
"layout": {
"type": "concentric_rings", "orientation": "radial",
"rings": [
{"type": "angular", "count": 12, "start_angle_deg": 15.0},
{"type": "angular", "count": 24, "start_angle_deg": 7.5},
],
},
"ring_motifs": [
{"inner_radius_mm": 12.0, "outer_radius_mm": 20.0, "half_angle_deg": 4.583662, "count": 12, "start_angle_deg": 15.0},
{"inner_radius_mm": 28.0, "outer_radius_mm": 40.0, "half_angle_deg": 2.864789, "count": 24, "start_angle_deg": 7.5},
],
},
"cylinder_008": {
"motif": {"type": "obround", "length_mm": 7.0, "width_mm": 2.8},
"layout": {"type": "ring", "radius_mm": 34.0, "count": 24, "orientation": "radial"},
},
"cylinder_009": {
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 10.0, "outer_radius_mm": 42.0, "half_angle_deg": 3.437747},
"layout": {"type": "angular", "count": 12, "start_angle_deg": 15.0, "orientation": "radial"},
},
"cylinder_010": {
"motif": {"type": "circle", "radius_mm": 2.3},
"layout": {
"type": "concentric_rings",
"rings": [
{"radius_mm": 18.0, "count": 12, "start_angle_deg": 0.0},
{"radius_mm": 36.0, "count": 24, "start_angle_deg": 0.0},
],
},
},
"cylinder_011": {
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 10.0, "outer_radius_mm": 20.0, "half_angle_deg": 4.010705},
"layout": {"type": "angular", "count": 8, "start_angle_deg": 22.5, "orientation": "radial"},
},
"cylinder_012": {
"motif": {"type": "circle", "radius_mm": 1.9},
"layout": {"type": "ring", "radius_mm": 26.0, "count": 24},
},
"cylinder_013": {
"motif": {"type": "square", "width_mm": 2.7},
"layout": {"type": "ring", "radius_mm": 25.0, "count": 16},
},
"cylinder_014": {
"motif": {"type": "square", "width_mm": 3.535534},
"layout": {"type": "ring", "radius_mm": 25.0, "count": 16},
},
"cylinder_015": {
"motif": {"type": "circle", "radius_mm": 1.8},
"layout": {
"type": "disc_grid", "count_x": 6, "count_y": 5,
"spacing_x_mm": 9.0, "spacing_y_mm": 9.0,
"center_mm": [0.0, 0.0],
},
},
"cylinder_016": {
"motif": {"type": "d_shape_polygon", "stem_length_mm": 1.9, "nose_depth_mm": 1.9, "half_height_mm": 1.3775},
"layout": {
"type": "open_arc", "radius_mm": 35.0, "count": 18,
"start_angle_deg": 18.0, "end_angle_deg": 262.8,
},
},
"cylinder_017": {
"motif": {"type": "cross", "size_mm": 4.62, "arm_width_mm": 2.9568},
"layout": {
"type": "open_arc", "radius_mm": 31.0, "count": 14,
"start_angle_deg": 189.0, "end_angle_deg": 387.0,
},
},
"cylinder_018": {
"motif": {"type": "obround", "length_mm": 5.75, "width_mm": 2.3},
"layout": {"type": "ring", "radius_mm": 27.0, "count": 16, "orientation": "radial"},
},
"cylinder_019": {
"motif": {"type": "circle", "radius_mm": 1.8},
"layout": {
"type": "concentric_rings",
"rings": [
{"radius_mm": 14.0, "count": 8, "start_angle_deg": 0.0},
{"radius_mm": 30.0, "count": 16, "start_angle_deg": 0.0},
],
},
},
"cylinder_020": {
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 9.0, "outer_radius_mm": 36.0, "half_angle_deg": 3.151268},
"layout": {"type": "angular", "count": 10, "start_angle_deg": 18.0, "orientation": "radial"},
},
"cylinder_021": {
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 12.0, "outer_radius_mm": 20.0, "half_angle_deg": 4.010705},
"layout": {"type": "angular", "count": 8, "start_angle_deg": 22.5, "orientation": "radial"},
"ring_motifs": [
{"inner_radius_mm": 12.0, "outer_radius_mm": 20.0, "half_angle_deg": 4.010705, "count": 8, "start_angle_deg": 22.5},
{"inner_radius_mm": 28.0, "outer_radius_mm": 39.0, "half_angle_deg": 2.57831, "count": 16, "start_angle_deg": 11.25},
],
},
"cylinder_022": {
"motif": {"type": "skew_hexagon", "nominal_radius_mm": 1.875},
"layout": {
"type": "spiral", "count": 28, "start_radius_mm": 8.054,
"radius_step_mm": 0.851, "start_angle_deg": -0.105, "angle_step_deg": 33.2394,
"orientation": "snapped_radial", "orientation_snap_deg": 45.0,
"orientation_offset_deg": 0.0,
},
},
"cylinder_023": {
"motif": {"type": "skew_hexagon", "nominal_radius_mm": 1.9},
"layout": {
"type": "cross_lines", "count_per_axis": 6, "spacing_mm": 13.6,
"orientation_offset_deg": 0.0,
},
},
"cylinder_024": {
"motif": {"type": "triangle", "radius_mm": 2.3},
"layout": {
"type": "x_field", "levels": 5, "spacing_mm": 11.0,
"orientation": "diagonal_axes",
},
},
"cylinder_025": {
"motif": {
"type": "teardrop_polygon",
"left_width_mm": 1.116, "right_width_mm": 1.548,
"tip_height_mm": 2.79, "bottom_depth_mm": 1.476,
"shoulder_height_mm": 1.242,
},
"layout": {
"type": "twin_strips", "x_offset_mm": 15.0, "count_y": 12,
"y_start_mm": -34.0, "y_end_mm": 34.0,
},
},
"cylinder_026": {
"motif": {"type": "trapezoid", "bottom_width_mm": 4.32, "top_width_mm": 2.376, "height_mm": 3.24},
"layout": {
"type": "concentric_rings", "orientation": "radial",
"rings": [
{"radius_mm": 12.0, "count": 8, "start_angle_deg": 0.0},
{"radius_mm": 31.0, "count": 16, "start_angle_deg": 0.0},
],
},
},
"cylinder_027": {
"motif": {"type": "d_shape_polygon", "stem_length_mm": 1.8, "nose_depth_mm": 1.8, "half_height_mm": 1.305},
"layout": {"type": "corner_clusters", "levels_mm": [12.0, 18.5, 25.0]},
},
"cylinder_028": {
"motif": {"type": "square", "width_mm": 2.969848},
"layout": {"type": "diamond_field", "manhattan_radius": 3, "spacing_mm": 8.0},
},
"cylinder_029": {
"motif": {"type": "annular_sector_polygon", "inner_radius_mm": 11.0, "outer_radius_mm": 22.0, "half_angle_deg": 4.010705},
"layout": {"type": "angular", "count": 10, "start_angle_deg": 18.0, "orientation": "radial"},
},
}
def _pattern_sketches(spec: dict[str, Any]) -> list[dict[str, Any]]:
base = {
"id": "sketch_001",
"name": "草图1",
"workplane": {
"origin_mm": [0.0, 0.0, 0.0],
"x_dir": [1.0, 0.0, 0.0],
"y_dir": [0.0, 1.0, 0.0],
"normal": [0.0, 0.0, 1.0],
},
"profile": {"type": "circle", "center": [0.0, 0.0], "radius_mm": 50.0},
}
cut_workplane = {
"origin_mm": [0.0, 0.0, 20.0],
"x_dir": [1.0, 0.0, 0.0],
"y_dir": [0.0, 1.0, 0.0],
"normal": [0.0, 0.0, 1.0],
}
ring_motifs = spec.get("ring_motifs")
if not ring_motifs:
cut = {
"id": "sketch_002",
"name": "图案草图",
"workplane": cut_workplane,
"profile": {
"type": "patterned_cutouts",
"motif": spec["motif"],
"layout": spec["layout"],
},
}
return [base, cut]
# 两组扇区的内外半径不同,仍合并成一个切除特征;组合只包含
# 两个有名字的程序化子图案,不保存任何逐实例坐标。
cut = {
"id": "sketch_002",
"name": "双环图案草图",
"workplane": cut_workplane,
"profile": {
"type": "compound_patterned_cutouts",
"patterns": [
{
"motif": {
"type": "annular_sector_polygon",
"inner_radius_mm": item["inner_radius_mm"],
"outer_radius_mm": item["outer_radius_mm"],
"half_angle_deg": item["half_angle_deg"],
},
"layout": {
"type": "angular",
"count": item["count"],
"start_angle_deg": item["start_angle_deg"],
"orientation": "radial",
},
}
for item in ring_motifs
],
},
}
return [base, cut]
def make_cdsl(source: Path, spec: dict[str, Any]) -> dict[str, Any]:
part_id = source.name.removesuffix(".solidworks_evidence_v2.json")
return {
"schema": "cad.cdsl.llm.v1",
"schema_version": "1.0.0",
"kind": "part",
"part_id": part_id,
"features": [
{
"id": "f01",
"atomic_id": "extrude_add_blind",
"depends_on": [],
"name": "凸台-拉伸1",
"params": {"distance_mm": 20.0},
"sketch_id": "sketch_001",
},
{
"id": "f02",
"atomic_id": "extrude_cut_blind",
"depends_on": ["f01"],
"name": "程序化图案切除",
"params": {"distance_mm": 8.0, "reverse": True},
"sketch_id": "sketch_002",
},
],
"geometry": {"sketches": _pattern_sketches(spec)},
"meta": {
"source": source.name,
"parameterized_sketches": 2,
"profile_from_sketches": 0,
"notes": [
"CDSL-only drawable: repeated source sketches are represented by one procedural motif and layout",
"No compiler_context, source entities, per-instance coordinates, or encoded geometry",
],
},
}
def distill(input_dir: Path, output_dir: Path) -> list[Path]:
sources = sorted(input_dir.glob("cylinder_*.solidworks_evidence_v2.json"))
written: list[Path] = []
missing: list[str] = []
for source in sources:
key = source.name[:12]
spec = SPECS.get(key)
if spec is None:
missing.append(source.name)
continue
part_id = source.name.removesuffix(".solidworks_evidence_v2.json")
part_dir = output_dir / part_id
part_dir.mkdir(parents=True, exist_ok=True)
out = part_dir / f"{part_id}.cdsl.json"
text = json.dumps(make_cdsl(source, spec), ensure_ascii=False, indent=2)
# 与 b005/b006 一致:短标量数组保持单行,结构数组仍按层级展开。
scalar_array = re.compile(
r"\[\n(?P<body>(?:[ \t]+(?:-?\d+(?:\.\d+)?|true|false|null|\"[^\"\\n]*\"),?\n)+)[ \t]*\]"
)
def compact(match: re.Match[str]) -> str:
values = [line.strip().rstrip(",") for line in match.group("body").splitlines()]
inline = "[" + ", ".join(values) + "]"
return inline if len(inline) <= 100 else match.group(0)
text = scalar_array.sub(compact, text)
out.write_text(
text + "\n",
encoding="utf-8",
)
written.append(out)
if missing:
raise RuntimeError("No semantic specification for: " + ", ".join(missing))
return written
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input_dir", type=Path)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
paths = distill(args.input_dir, args.out)
print(f"wrote {len(paths)} CDSL files to {args.out}")
if __name__ == "__main__":
main()