1+ import re
2+ import tempfile
3+
14import freecad as fc
25import OfflineRenderingUtils # type: ignore[import]
3- import tempfile
4- import re
56
67from .loader import _hex_to_rgb
78from .props .base_prop import BaseProp
89from . import props as Props
910
11+
1012def 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
1518def 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+
2126def 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+
3340def 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+
5660def 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
8584def 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