Compare commits
2 Commits
feat/dag-a
...
feat/runne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da2a360c56 | ||
|
|
3dd0da3964 |
156
freecad/runner.py
Normal file
156
freecad/runner.py
Normal file
@@ -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)
|
||||||
@@ -717,6 +717,28 @@ class Silo_New:
|
|||||||
return _server_mode == "normal"
|
return _server_mode == "normal"
|
||||||
|
|
||||||
|
|
||||||
|
def _push_dag_after_upload(doc, part_number, revision_number):
|
||||||
|
"""Extract and push the feature DAG after a successful upload.
|
||||||
|
|
||||||
|
Failures are logged as warnings -- DAG sync must never block save.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from dag import extract_dag
|
||||||
|
|
||||||
|
nodes, edges = extract_dag(doc)
|
||||||
|
if not nodes:
|
||||||
|
return
|
||||||
|
|
||||||
|
result = _client.push_dag(part_number, revision_number, nodes, edges)
|
||||||
|
node_count = result.get("node_count", len(nodes))
|
||||||
|
edge_count = result.get("edge_count", len(edges))
|
||||||
|
FreeCAD.Console.PrintMessage(
|
||||||
|
f"DAG synced: {node_count} nodes, {edge_count} edges\n"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintWarning(f"DAG sync failed: {e}\n")
|
||||||
|
|
||||||
|
|
||||||
class Silo_Save:
|
class Silo_Save:
|
||||||
"""Save locally and upload to MinIO."""
|
"""Save locally and upload to MinIO."""
|
||||||
|
|
||||||
@@ -787,6 +809,8 @@ class Silo_Save:
|
|||||||
new_rev = result["revision_number"]
|
new_rev = result["revision_number"]
|
||||||
FreeCAD.Console.PrintMessage(f"Uploaded as revision {new_rev}\n")
|
FreeCAD.Console.PrintMessage(f"Uploaded as revision {new_rev}\n")
|
||||||
|
|
||||||
|
_push_dag_after_upload(doc, part_number, new_rev)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
FreeCAD.Console.PrintWarning(f"Upload failed: {e}\n")
|
FreeCAD.Console.PrintWarning(f"Upload failed: {e}\n")
|
||||||
FreeCAD.Console.PrintMessage("File saved locally but not uploaded.\n")
|
FreeCAD.Console.PrintMessage("File saved locally but not uploaded.\n")
|
||||||
@@ -841,6 +865,8 @@ class Silo_Commit:
|
|||||||
new_rev = result["revision_number"]
|
new_rev = result["revision_number"]
|
||||||
FreeCAD.Console.PrintMessage(f"Committed revision {new_rev}: {comment}\n")
|
FreeCAD.Console.PrintMessage(f"Committed revision {new_rev}: {comment}\n")
|
||||||
|
|
||||||
|
_push_dag_after_upload(doc, part_number, new_rev)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
FreeCAD.Console.PrintError(f"Commit failed: {e}\n")
|
FreeCAD.Console.PrintError(f"Commit failed: {e}\n")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user