11import freecad as fc
22import OfflineRenderingUtils # type: ignore[import]
3- import base64
43import tempfile
5- import os
64import re
75
86from .loader import _hex_to_rgb
97from .props .base_prop import BaseProp
108from . import props as Props
119
12-
1310def sanitize_object_name (name : str ) -> str :
1411 """Convert object names to FreeCAD-compatible format."""
15- # Replace spaces or hyphens with underscores, then drop any other non-word
1612 sanitized = name .replace (" " , "_" ).replace ("-" , "_" )
1713 return re .sub (r"[^\w_]" , "_" , sanitized )
1814
19-
20- def export_jcad_to_fcstd (jcad_dict : dict ) -> "fc.Document" :
21- doc = fc .app .newDocument ("__jcad_export__" )
22- doc .Meta = jcad_dict .get ("metadata" , {})
23-
24- # Build Prop handler lookup
25- prop_handlers = {
26- Cls .name (): Cls
27- for Cls in Props .__dict__ .values ()
15+ def build_prop_handlers ():
16+ return {
17+ Cls .name (): Cls for Cls in Props .__dict__ .values ()
2818 if isinstance (Cls , type ) and issubclass (Cls , BaseProp )
2919 }
3020
31- # 1) Build a simple original→sanitized name map
32- name_mapping = {
33- obj ["name" ]: sanitize_object_name (obj ["name" ])
34- for obj in jcad_dict .get ("objects" , [])
35- }
36-
37- # 2) Rename each object in place, and fix any string or list references in parameters
21+ def update_references (jcad_dict , name_mapping ):
3822 for obj in jcad_dict .get ("objects" , []):
39- orig = obj ["name" ]
40- obj ["name" ] = name_mapping [orig ]
41-
4223 params = obj .get ("parameters" , {})
4324 for key , val in params .items ():
44- # If it’s a string reference to another object, rewrite it:
4525 if isinstance (val , str ) and val in name_mapping :
4626 params [key ] = name_mapping [val ]
47-
48- # If it’s a list of references, rewrite every entry that matches a key:
4927 elif isinstance (val , list ):
5028 params [key ] = [
51- name_mapping [ item ] if ( isinstance (item , str ) and item in name_mapping ) else item
29+ name_mapping . get ( item , item ) if isinstance (item , str ) else item
5230 for item in val
5331 ]
5432
55- # 3) Replay sanitized objects into FreeCAD
56- guidata = {}
57- for jcad_obj in jcad_dict .get ("objects" , []):
58- shape_type = jcad_obj ["shape" ]
59- name = jcad_obj ["name" ]
60- params = jcad_obj .get ("parameters" , {})
61- opts = jcad_dict .get ("options" , {}).get (name , {})
33+ def create_body_and_coordinates (doc , body_obj , fc_objects ):
34+ body_name = body_obj ["name" ]
35+ body_fc = doc .addObject ("PartDesign::Body" , body_name )
36+ fc_objects [body_name ] = body_fc
37+
38+ if not body_fc .Origin :
39+ return body_fc
40+
41+ 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+ }
50+ 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
54+ return body_fc
55+
56+ def apply_object_properties (fc_obj , jcad_obj , prop_handlers , doc ):
57+ params = jcad_obj .get ("parameters" , {})
58+ for prop , jval in params .items ():
59+ if prop == "Color" or not hasattr (fc_obj , prop ):
60+ continue
61+ prop_type = fc_obj .getTypeIdOfProperty (prop )
62+ Handler = prop_handlers .get (prop_type )
63+ if not Handler :
64+ continue
65+ try :
66+ new_val = Handler .jcad_to_fc (
67+ jval ,
68+ jcad_object = jcad_obj ,
69+ fc_prop = getattr (fc_obj , prop ),
70+ fc_object = fc_obj ,
71+ fc_file = doc ,
72+ )
73+ if new_val is not None :
74+ setattr (fc_obj , prop , new_val )
75+ except Exception as e :
76+ print (f"Error setting { prop } on { fc_obj .Name } : { e } " )
77+
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+ }
6284
63- # Add object to FreeCAD
64- fc_obj = doc .addObject (shape_type , name )
85+ def export_jcad_to_fcstd (jcad_dict : dict ) -> "fc.Document" :
86+ doc = fc .app .newDocument ("__jcad_export__" )
87+ doc .Meta = jcad_dict .get ("metadata" , {})
88+ prop_handlers = build_prop_handlers ()
89+ coordinate_names = {"Origin" , "X_Axis" , "Y_Axis" , "Z_Axis" , "XY_Plane" , "XZ_Plane" , "YZ_Plane" }
6590
66- # Apply all non-color props via handlers
67- for prop , jval in params .items ():
68- if prop == "Color" :
69- continue
70- if hasattr (fc_obj , prop ):
71- t = fc_obj .getTypeIdOfProperty (prop )
72- Handler = prop_handlers .get (t )
73- if Handler :
74- try :
75- new_val = Handler .jcad_to_fc (
76- jval ,
77- jcad_object = jcad_obj ,
78- fc_prop = getattr (fc_obj , prop ),
79- fc_object = fc_obj ,
80- fc_file = doc ,
81- )
82- if new_val is not None :
83- setattr (fc_obj , prop , new_val )
84- except Exception as e :
85- print (f"Error setting { prop } on { name } : { e } " )
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" ]:
94+ obj ["name" ] = name_mapping [obj ["name" ]]
95+ update_references (jcad_dict , name_mapping )
8696
87- # Build guidata entry
88- hexcol = params .get ("Color" ) or opts .get ("color" ) or "#808080"
89- rgb = _hex_to_rgb (hexcol )
90- guidata [name ] = {
91- "ShapeColor" : {"type" : "App::PropertyColor" , "value" : rgb },
92- "Visibility" : {"type" : "App::PropertyBool" , "value" : opts .get ("visible" , True )},
93- }
97+ 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 ]
101+
102+ # Process bodies and coordinates
103+ for body_obj in body_objs :
104+ body_fc = create_body_and_coordinates (doc , body_obj , fc_objects )
105+ apply_object_properties (body_fc , body_obj , prop_handlers , doc )
106+
107+ # Get body properties for coordinates
108+ body_params = body_obj .get ("parameters" , {})
109+ 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 )
111+
112+ # Add coordinate objects to guidata
113+ for coord_name in coordinate_names :
114+ if coord_name in fc_objects :
115+ guidata [coord_name ] = build_guidata_entry (coord_name , body_params , body_opts )
116+
117+ # Process other objects
118+ for obj in other_objs :
119+ fc_obj = doc .addObject (obj ["shape" ], obj ["name" ])
120+ fc_objects [obj ["name" ]] = fc_obj
121+ apply_object_properties (fc_obj , obj , prop_handlers , doc )
122+ obj_opts = jcad_dict .get ("options" , {}).get (obj ["name" ], {})
123+ guidata [obj ["name" ]] = build_guidata_entry (obj ["name" ], obj .get ("parameters" , {}), obj_opts )
94124
95125 doc .recompute ()
96-
97- # 4) Write out a temp FCStd, then apply colors/visibility
126+
98127 with tempfile .NamedTemporaryFile (delete = False , suffix = ".FCStd" ) as tmp :
99- tmp_path = tmp .name
100- doc .saveAs (tmp_path )
101-
102- OfflineRenderingUtils .save (
103- doc ,
104- filename = tmp_path ,
105- guidata = guidata
106- )
107-
108- return fc .app .openDocument (tmp_path )
128+ OfflineRenderingUtils .save (doc , filename = tmp .name , guidata = guidata )
129+ return fc .app .openDocument (tmp .name )
0 commit comments