22import OfflineRenderingUtils # type: ignore[import]
33import base64
44import tempfile
5- # from .loader import FCStd
65import os
6+ import re
77
88from .loader import _hex_to_rgb
99from .props .base_prop import BaseProp
1010from . import props as Props
1111
1212
13- def export_jcad_to_fcstd (jcad_dict : dict ) -> 'fc.Document' :
13+ def sanitize_object_name (name : str ) -> str :
14+ """Convert object names to FreeCAD-compatible format."""
15+ # Replace spaces or hyphens with underscores, then drop any other non-word
16+ sanitized = name .replace (" " , "_" ).replace ("-" , "_" )
17+ return re .sub (r"[^\w_]" , "_" , sanitized )
18+
19+
20+ def export_jcad_to_fcstd (jcad_dict : dict ) -> "fc.Document" :
1421 doc = fc .app .newDocument ("__jcad_export__" )
1522 doc .Meta = jcad_dict .get ("metadata" , {})
16-
23+
24+ # Build Prop handler lookup
1725 prop_handlers = {
1826 Cls .name (): Cls
1927 for Cls in Props .__dict__ .values ()
2028 if isinstance (Cls , type ) and issubclass (Cls , BaseProp )
2129 }
2230
23- # LOCAL variable for guidata (no function attribute)
24- guidata = {}
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+ }
2536
37+ # 2) Rename each object in place, and fix any string or list references in parameters
38+ for obj in jcad_dict .get ("objects" , []):
39+ orig = obj ["name" ]
40+ obj ["name" ] = name_mapping [orig ]
41+
42+ params = obj .get ("parameters" , {})
43+ for key , val in params .items ():
44+ # If it’s a string reference to another object, rewrite it:
45+ if isinstance (val , str ) and val in name_mapping :
46+ params [key ] = name_mapping [val ]
47+
48+ # If it’s a list of references, rewrite every entry that matches a key:
49+ elif isinstance (val , list ):
50+ params [key ] = [
51+ name_mapping [item ] if (isinstance (item , str ) and item in name_mapping ) else item
52+ for item in val
53+ ]
54+
55+ # 3) Replay sanitized objects into FreeCAD
56+ guidata = {}
2657 for jcad_obj in jcad_dict .get ("objects" , []):
2758 shape_type = jcad_obj ["shape" ]
28- name = jcad_obj ["name" ]
29- params = jcad_obj .get ("parameters" , {})
30- opts = jcad_dict .get ("options" , {}).get (name , {})
59+ name = jcad_obj ["name" ]
60+ params = jcad_obj .get ("parameters" , {})
61+ opts = jcad_dict .get ("options" , {}).get (name , {})
3162
63+ # Add object to FreeCAD
3264 fc_obj = doc .addObject (shape_type , name )
3365
66+ # Apply all non-color props via handlers
3467 for prop , jval in params .items ():
3568 if prop == "Color" :
3669 continue
@@ -44,7 +77,7 @@ def export_jcad_to_fcstd(jcad_dict: dict) -> 'fc.Document':
4477 jcad_object = jcad_obj ,
4578 fc_prop = getattr (fc_obj , prop ),
4679 fc_object = fc_obj ,
47- fc_file = doc
80+ fc_file = doc ,
4881 )
4982 if new_val is not None :
5083 setattr (fc_obj , prop , new_val )
@@ -56,13 +89,15 @@ def export_jcad_to_fcstd(jcad_dict: dict) -> 'fc.Document':
5689 rgb = _hex_to_rgb (hexcol )
5790 guidata [name ] = {
5891 "ShapeColor" : {"type" : "App::PropertyColor" , "value" : rgb },
59- "Visibility" : {"type" : "App::PropertyBool" , "value" : opts .get ("visible" , True )}
92+ "Visibility" : {"type" : "App::PropertyBool" , "value" : opts .get ("visible" , True )},
6093 }
6194
6295 doc .recompute ()
6396
97+ # 4) Write out a temp FCStd, then apply colors/visibility
6498 with tempfile .NamedTemporaryFile (delete = False , suffix = ".FCStd" ) as tmp :
6599 tmp_path = tmp .name
100+ doc .saveAs (tmp_path )
66101
67102 OfflineRenderingUtils .save (
68103 doc ,
0 commit comments