"""将 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(?:[ \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()