Skip to content

Commit 41f04d1

Browse files
committed
Make FCStd exports compatible with FreeCAD
1 parent 25acf9d commit 41f04d1

File tree

1 file changed

+98
-41
lines changed

1 file changed

+98
-41
lines changed
Lines changed: 98 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1+
import re
2+
import tempfile
3+
14
import freecad as fc
25
import OfflineRenderingUtils # type: ignore[import]
3-
import tempfile
4-
import re
56

67
from .loader import _hex_to_rgb
78
from .props.base_prop import BaseProp
89
from . import props as Props
910

11+
1012
def sanitize_object_name(name: str) -> str:
1113
"""Convert object names to FreeCAD-compatible format."""
12-
sanitized = name.replace(" ", "_").replace("-", "_")
13-
return re.sub(r"[^\w_]", "_", sanitized)
14+
s = name.replace(" ", "_").replace("-", "_")
15+
return re.sub(r"[^\w_]", "_", s)
16+
1417

1518
def build_prop_handlers():
1619
return {
17-
Cls.name(): Cls for Cls in Props.__dict__.values()
20+
Cls.name(): Cls
21+
for Cls in Props.__dict__.values()
1822
if isinstance(Cls, type) and issubclass(Cls, BaseProp)
1923
}
2024

25+
2126
def update_references(jcad_dict, name_mapping):
27+
"""Walk all parameters and rewrite any string/list references using name_mapping."""
2228
for obj in jcad_dict.get("objects", []):
2329
params = obj.get("parameters", {})
2430
for key, val in params.items():
@@ -30,30 +36,29 @@ def update_references(jcad_dict, name_mapping):
3036
for item in val
3137
]
3238

39+
3340
def create_body_and_coordinates(doc, body_obj, fc_objects):
41+
"""Create a PartDesign::Body so that FreeCAD auto-creates Origin & axes."""
3442
body_name = body_obj["name"]
3543
body_fc = doc.addObject("PartDesign::Body", body_name)
3644
fc_objects[body_name] = body_fc
3745

46+
# Once the Body is created, FreeCAD auto-adds an Origin with child axes & planes.
3847
if not body_fc.Origin:
3948
return body_fc
4049

4150
fc_objects["Origin"] = body_fc.Origin
42-
role_map = {
43-
'X_Axis': "X_Axis",
44-
'Y_Axis': "Y_Axis",
45-
'Z_Axis': "Z_Axis",
46-
'XY_Plane': "XY_Plane",
47-
'XZ_Plane': "XZ_Plane",
48-
'YZ_Plane': "YZ_Plane"
49-
}
5051
for child in body_fc.Origin.Group:
51-
role = getattr(child, 'Role', '')
52-
if role in role_map:
53-
fc_objects[role_map[role]] = child
52+
role = getattr(child, "Role", "")
53+
# Role is typically "X_Axis", "Y_Axis", "Z_Axis", "XY_Plane", etc.
54+
if role in {"X_Axis", "Y_Axis", "Z_Axis", "XY_Plane", "XZ_Plane", "YZ_Plane"}:
55+
fc_objects[role] = child
56+
5457
return body_fc
5558

59+
5660
def apply_object_properties(fc_obj, jcad_obj, prop_handlers, doc):
61+
"""Run through all non‐color parameters and use the Prop handlers to set them."""
5762
params = jcad_obj.get("parameters", {})
5863
for prop, jval in params.items():
5964
if prop == "Color" or not hasattr(fc_obj, prop):
@@ -75,55 +80,107 @@ def apply_object_properties(fc_obj, jcad_obj, prop_handlers, doc):
7580
except Exception as e:
7681
print(f"Error setting {prop} on {fc_obj.Name}: {e}")
7782

78-
def build_guidata_entry(name, params, opts):
79-
hexcol = params.get("Color") or opts.get("color", "#808080")
80-
return {
81-
"ShapeColor": {"type": "App::PropertyColor", "value": _hex_to_rgb(hexcol)},
82-
"Visibility": {"type": "App::PropertyBool", "value": opts.get("visible", True)},
83-
}
8483

8584
def export_jcad_to_fcstd(jcad_dict: dict) -> "fc.Document":
8685
doc = fc.app.newDocument("__jcad_export__")
8786
doc.Meta = jcad_dict.get("metadata", {})
87+
8888
prop_handlers = build_prop_handlers()
89-
coordinate_names = {"Origin", "X_Axis", "Y_Axis", "Z_Axis", "XY_Plane", "XZ_Plane", "YZ_Plane"}
89+
coordinate_names = {
90+
"Origin", "X_Axis", "Y_Axis", "Z_Axis", "XY_Plane", "XZ_Plane", "YZ_Plane"
91+
}
9092

91-
# Create name mapping and update references
92-
name_mapping = {obj["name"]: sanitize_object_name(obj["name"]) for obj in jcad_dict.get("objects", [])}
93-
for obj in jcad_dict["objects"]:
93+
# 1) Sanitize all JCAD object names and build a mapping
94+
name_mapping = {
95+
obj["name"]: sanitize_object_name(obj["name"])
96+
for obj in jcad_dict.get("objects", [])
97+
}
98+
for obj in jcad_dict.get("objects", []):
9499
obj["name"] = name_mapping[obj["name"]]
95100
update_references(jcad_dict, name_mapping)
96101

97102
fc_objects = {}
98-
guidata = {}
99-
body_objs = [o for o in jcad_dict.get("objects", []) if o["shape"] == "PartDesign::Body"]
100-
other_objs = [o for o in jcad_dict["objects"] if o not in body_objs and o["name"] not in coordinate_names]
103+
guidata = {} # objectName → { "ShapeColor": (r,g,b), "Visibility": bool }
104+
105+
# 2) Separate out any PartDesign::Body objects
106+
body_objs = [
107+
o for o in jcad_dict.get("objects", [])
108+
if o["shape"] == "PartDesign::Body"
109+
]
110+
other_objs = [
111+
o for o in jcad_dict.get("objects", [])
112+
if o not in body_objs and o["name"] not in coordinate_names
113+
]
101114

102-
# Process bodies and coordinates
115+
# 3) Create bodies (so that coordinate system objects appear)
103116
for body_obj in body_objs:
104117
body_fc = create_body_and_coordinates(doc, body_obj, fc_objects)
105118
apply_object_properties(body_fc, body_obj, prop_handlers, doc)
106-
107-
# Get body properties for coordinates
108-
body_params = body_obj.get("parameters", {})
119+
120+
# Record body color
121+
hexcol = body_obj.get("parameters", {}).get("Color") or \
122+
(jcad_dict.get("options", {}).get(body_obj["name"], {}).get("color") or "#808080")
123+
rgb = _hex_to_rgb(hexcol)
124+
125+
# Instead of just building color_map, build a complete guidata dictionary
109126
body_opts = jcad_dict.get("options", {}).get(body_obj["name"], {})
110-
guidata[body_obj["name"]] = build_guidata_entry(body_obj["name"], body_params, body_opts)
127+
# Check visibility: prioritize options, then object-level visible, default to True
128+
visible = body_opts.get("visible")
129+
if visible is None:
130+
visible = body_obj.get("visible", True)
111131

112-
# Add coordinate objects to guidata
132+
guidata[body_obj["name"]] = {
133+
"ShapeColor": {"type": "App::PropertyColor", "value": rgb},
134+
"Visibility": {"type": "App::PropertyBool", "value": visible},
135+
}
136+
137+
# Any coordinate‐system objects that now exist should inherit the same color
113138
for coord_name in coordinate_names:
114139
if coord_name in fc_objects:
115-
guidata[coord_name] = build_guidata_entry(coord_name, body_params, body_opts)
140+
guidata[coord_name] = {
141+
"ShapeColor": {"type": "App::PropertyColor", "value": rgb},
142+
"Visibility": {"type": "App::PropertyBool", "value": False}, # Usually coordinate objects are hidden
143+
}
116144

117-
# Process other objects
145+
# 4) Create all other (non-body, non-coordinate) objects
118146
for obj in other_objs:
119147
fc_obj = doc.addObject(obj["shape"], obj["name"])
120148
fc_objects[obj["name"]] = fc_obj
121149
apply_object_properties(fc_obj, obj, prop_handlers, doc)
150+
151+
# Instead of just building color_map, build a complete guidata dictionary
122152
obj_opts = jcad_dict.get("options", {}).get(obj["name"], {})
123-
guidata[obj["name"]] = build_guidata_entry(obj["name"], obj.get("parameters", {}), obj_opts)
153+
hexcol = obj.get("parameters", {}).get("Color") or obj_opts.get("color", "#808080")
154+
# Check visibility: prioritize options, then object-level visible, default to True
155+
visible = obj_opts.get("visible")
156+
if visible is None:
157+
visible = obj.get("visible", True)
158+
159+
# Build guidata entry with both color and visibility
160+
guidata[obj["name"]] = {
161+
"ShapeColor": {"type": "App::PropertyColor", "value": _hex_to_rgb(hexcol)},
162+
"Visibility": {"type": "App::PropertyBool", "value": visible},
163+
}
124164

165+
# 5) Recompute so that FreeCAD has generated any missing children
125166
doc.recompute()
126-
167+
168+
# 6) Save to a temp FCStd using guidata instead of colors
127169
with tempfile.NamedTemporaryFile(delete=False, suffix=".FCStd") as tmp:
128-
OfflineRenderingUtils.save(doc, filename=tmp.name, guidata=guidata)
129-
return fc.app.openDocument(tmp.name)
170+
tmp_path = tmp.name
171+
172+
# Default camera settings
173+
default_camera = 'OrthographicCamera { viewportMapping ADJUST_CAMERA position 8.5470247 -1.1436439 9.9673195 orientation 0.86492187 0.23175442 0.44519675 1.0835806 nearDistance 0.19726367 farDistance 17.140171 aspectRatio 1 focalDistance 8.6602545 height 17.320509 }'
174+
175+
# Add camera to guidata - try the direct string approach
176+
guidata["GuiCameraSettings"] = default_camera
177+
178+
# Use guidata to include both color, visibility, AND camera
179+
OfflineRenderingUtils.save(
180+
doc,
181+
filename=tmp_path,
182+
guidata=guidata
183+
)
184+
185+
# 7) Finally, open that new FCStd in FreeCAD and return the Document handle
186+
return fc.app.openDocument(tmp_path)

0 commit comments

Comments
 (0)