diff --git a/freecad/runner.py b/freecad/runner.py new file mode 100644 index 0000000..5a7514b --- /dev/null +++ b/freecad/runner.py @@ -0,0 +1,156 @@ +"""Headless runner entry points for silorunner compute jobs. + +These functions are invoked via ``create --console -e`` by the +silorunner binary. They must work without a display server. + +Entry Points +------------ +dag_extract(input_path, output_path) + Extract feature DAG and write JSON. +validate(input_path, output_path) + Rebuild all features and report pass/fail per node. +export(input_path, output_path, format='step') + Export geometry to STEP, IGES, STL, or OBJ. +""" + +import json + +import FreeCAD + + +def dag_extract(input_path, output_path): + """Extract the feature DAG from a Create file. + + Parameters + ---------- + input_path : str + Path to the ``.kc`` or ``.FCStd`` file. + output_path : str + Path to write the JSON output. + + Output JSON:: + + {"nodes": [...], "edges": [...]} + """ + from dag import extract_dag + + doc = FreeCAD.openDocument(input_path) + try: + nodes, edges = extract_dag(doc) + with open(output_path, "w") as f: + json.dump({"nodes": nodes, "edges": edges}, f) + FreeCAD.Console.PrintMessage( + f"DAG extracted: {len(nodes)} nodes, {len(edges)} edges -> {output_path}\n" + ) + finally: + FreeCAD.closeDocument(doc.Name) + + +def validate(input_path, output_path): + """Validate a Create file by rebuilding all features. + + Parameters + ---------- + input_path : str + Path to the ``.kc`` or ``.FCStd`` file. + output_path : str + Path to write the JSON output. + + Output JSON:: + + { + "valid": true/false, + "nodes": [ + {"node_key": "Pad001", "state": "clean", "message": null, "properties_hash": "..."}, + ... + ] + } + """ + from dag import classify_type, compute_properties_hash + + doc = FreeCAD.openDocument(input_path) + try: + doc.recompute() + + results = [] + all_valid = True + + for obj in doc.Objects: + if not hasattr(obj, "TypeId"): + continue + node_type = classify_type(obj.TypeId) + if node_type is None: + continue + + state = "clean" + message = None + if hasattr(obj, "isValid") and not obj.isValid(): + state = "failed" + message = f"Feature {obj.Label} failed to recompute" + all_valid = False + + results.append( + { + "node_key": obj.Name, + "state": state, + "message": message, + "properties_hash": compute_properties_hash(obj), + } + ) + + with open(output_path, "w") as f: + json.dump({"valid": all_valid, "nodes": results}, f) + + status = "PASS" if all_valid else "FAIL" + FreeCAD.Console.PrintMessage( + f"Validation {status}: {len(results)} nodes -> {output_path}\n" + ) + finally: + FreeCAD.closeDocument(doc.Name) + + +def export(input_path, output_path, format="step"): + """Export a Create file to an external geometry format. + + Parameters + ---------- + input_path : str + Path to the ``.kc`` or ``.FCStd`` file. + output_path : str + Path to write the exported file. + format : str + One of ``step``, ``iges``, ``stl``, ``obj``. + """ + import Part + + doc = FreeCAD.openDocument(input_path) + try: + shapes = [ + obj.Shape for obj in doc.Objects if hasattr(obj, "Shape") and obj.Shape + ] + if not shapes: + raise ValueError("No geometry found in document") + + compound = Part.makeCompound(shapes) + + format_lower = format.lower() + if format_lower == "step": + compound.exportStep(output_path) + elif format_lower == "iges": + compound.exportIges(output_path) + elif format_lower == "stl": + import Mesh + + Mesh.export([compound], output_path) + elif format_lower == "obj": + import Mesh + + Mesh.export([compound], output_path) + else: + raise ValueError(f"Unsupported format: {format}") + + FreeCAD.Console.PrintMessage( + f"Exported {format_lower.upper()} -> {output_path}\n" + ) + finally: + FreeCAD.closeDocument(doc.Name)