Skip to content

Commit a9e5170

Browse files
committed
Fix color export
1 parent 86c5529 commit a9e5170

File tree

7 files changed

+108
-38
lines changed

7 files changed

+108
-38
lines changed
Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,73 @@
11
import freecad as fc
2+
import OfflineRenderingUtils # type: ignore[import]
23
import base64
34
import tempfile
4-
from .loader import FCStd
5+
# from .loader import FCStd
56
import os
67

8+
from .loader import _hex_to_rgb
9+
from .props.base_prop import BaseProp
10+
from . import props as Props
711

8-
def convert_jcad_to_fcstd(jcad_dict: dict) -> 'fc.Document':
9-
# 1) Spin up a brand‑new FreeCAD doc and write it out to a temp .FCStd
10-
blank = fc.app.newDocument("__jcad_blank__")
11-
blank.recompute() # ensure it's fully initialized
12-
with tempfile.NamedTemporaryFile(delete=False, suffix=".FCStd") as tmp_blank:
13-
blank.saveAs(tmp_blank.name)
14-
tmp_blank.flush()
15-
fc.app.closeDocument(blank.Name)
1612

17-
# 2) Read its bytes, encode as base64, give to FCStd loader
18-
with open(tmp_blank.name, "rb") as f:
19-
b64_blank = base64.b64encode(f.read()).decode("utf-8")
20-
os.remove(tmp_blank.name)
13+
def export_jcad_to_fcstd(jcad_dict: dict) -> 'fc.Document':
14+
doc = fc.app.newDocument("__jcad_export__")
15+
doc.Meta = jcad_dict.get("metadata", {})
16+
17+
prop_handlers = {
18+
Cls.name(): Cls
19+
for Cls in Props.__dict__.values()
20+
if isinstance(Cls, type) and issubclass(Cls, BaseProp)
21+
}
2122

22-
fcstd = FCStd()
23-
fcstd._sources = b64_blank
23+
# LOCAL variable for guidata (no function attribute)
24+
guidata = {}
2425

25-
# 3) Replay your JCAD model into it
26-
objs = jcad_dict.get("objects", [])
27-
opts = jcad_dict.get("options", {})
28-
meta = jcad_dict.get("metadata", {})
29-
fcstd.save(objs, opts, meta)
26+
for jcad_obj in jcad_dict.get("objects", []):
27+
shape_type = jcad_obj["shape"]
28+
name = jcad_obj["name"]
29+
params = jcad_obj.get("parameters", {})
30+
opts = jcad_dict.get("options", {}).get(name, {})
3031

31-
# 4) Dump the new .FCStd and re‑open it
32-
with tempfile.NamedTemporaryFile(delete=False, suffix=".FCStd") as tmp_out:
33-
tmp_out.write(base64.b64decode(fcstd.sources))
34-
tmp_out.flush()
32+
fc_obj = doc.addObject(shape_type, name)
3533

36-
doc = fc.app.openDocument(tmp_out.name)
37-
return doc
34+
for prop, jval in params.items():
35+
if prop == "Color":
36+
continue
37+
if hasattr(fc_obj, prop):
38+
t = fc_obj.getTypeIdOfProperty(prop)
39+
Handler = prop_handlers.get(t)
40+
if Handler:
41+
try:
42+
new_val = Handler.jcad_to_fc(
43+
jval,
44+
jcad_object=jcad_obj,
45+
fc_prop=getattr(fc_obj, prop),
46+
fc_object=fc_obj,
47+
fc_file=doc
48+
)
49+
if new_val is not None:
50+
setattr(fc_obj, prop, new_val)
51+
except Exception as e:
52+
print(f"Error setting {prop} on {name}: {e}")
3853

54+
# Build guidata entry
55+
hexcol = params.get("Color") or opts.get("color") or "#808080"
56+
rgb = _hex_to_rgb(hexcol)
57+
guidata[name] = {
58+
"ShapeColor": {"type": "App::PropertyColor", "value": rgb},
59+
"Visibility": {"type": "App::PropertyBool", "value": opts.get("visible", True)}
60+
}
61+
62+
doc.recompute()
63+
64+
with tempfile.NamedTemporaryFile(delete=False, suffix=".FCStd") as tmp:
65+
tmp_path = tmp.name
66+
67+
OfflineRenderingUtils.save(
68+
doc,
69+
filename=tmp_path,
70+
guidata=guidata
71+
)
72+
73+
return fc.app.openDocument(tmp_path)

jupytercad_freecad/freecad/loader.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def _rgb_to_hex(rgb):
3131
def _hex_to_rgb(hex_color):
3232
"""Convert hex color string to an RGB tuple"""
3333
hex_color = hex_color.lstrip("#")
34-
return tuple(int(hex_color[i : i + 2], 16) / 255.0 for i in (0, 2, 4))
34+
result = tuple(int(hex_color[i : i + 2], 16) / 255.0 for i in (0, 2, 4))
35+
return result
3536

3637

3738
def _guidata_to_options(guidata):
@@ -78,7 +79,6 @@ def _options_to_guidata(options):
7879
if "color" in data:
7980
rgb_value = _hex_to_rgb(data["color"])
8081
obj_data["ShapeColor"] = dict(type="App::PropertyColor", value=rgb_value)
81-
8282
# Handle visibility property from JupyterCad to FreeCAD's Visibility
8383
if "visible" in data:
8484
obj_data["Visibility"] = dict(
@@ -189,6 +189,10 @@ def save(self, objects: List, options: Dict, metadata: Dict) -> None:
189189
fc_obj = fc_file.getObject(py_obj["name"])
190190

191191
for prop, jcad_prop_value in py_obj["parameters"].items():
192+
# Skip Color property as it's handled separately through guidata
193+
if prop == "Color":
194+
continue
195+
192196
if hasattr(fc_obj, prop):
193197
try:
194198
prop_type = fc_obj.getTypeIdOfProperty(prop)
@@ -217,7 +221,6 @@ def save(self, objects: List, options: Dict, metadata: Dict) -> None:
217221
new_hex_color = py_obj["parameters"]["Color"]
218222
else:
219223
new_hex_color = "#808080" # Default to gray if no color is provided
220-
221224
if obj_name in self._guidata:
222225
self._guidata[obj_name]["color"] = new_hex_color
223226
else:

jupytercad_freecad/freecad/props/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
from .property_link_list import * # noqa
88
from .property_map import * # noqa
99
from .property_partshape import * # noqa
10-
from .property_placement import * # noqa
10+
from .property_placement import * # noqa

jupytercad_freecad/freecad/props/geometry/geom_circle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
with redirect_stdout_stderr():
88
try:
99
import freecad as fc
10-
import Part
10+
import Part as Part
1111
except ImportError:
1212
fc = None
1313

jupytercad_freecad/freecad/props/geometry/geom_linesegment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
with redirect_stdout_stderr():
88
try:
99
import freecad as fc
10-
import Part
10+
import Part as Part
1111
except ImportError:
1212
fc = None
1313

jupytercad_freecad/freecad/props/property_partshape.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,28 @@ def fc_to_jcad(prop_value: Any, **kwargs) -> Any:
1717

1818
@staticmethod
1919
def jcad_to_fc(prop_value: str, **kwargs) -> Any:
20-
"""PropertyPartShape is readonly"""
21-
return None
20+
try:
21+
import freecad as fc
22+
import Part as Part
23+
except ImportError:
24+
print("Error: FreeCAD or Part module could not be imported in jcad_to_fc.")
25+
return None
26+
27+
if not prop_value:
28+
print("Warning: jcad_to_fc received empty prop_value for PartShape.")
29+
return None
30+
31+
try:
32+
shape = Part.Shape()
33+
# Use importBrepFromString as it's more specific for BREP strings
34+
shape.importBrepFromString(prop_value)
35+
36+
if shape.isNull():
37+
print(f"Warning: Reconstructed shape is Null after importBrepFromString. Input (first 100 chars): {prop_value[:100]}...")
38+
return None # Return None if shape is Null
39+
40+
return shape
41+
except Exception as e:
42+
# Log the actual exception and part of the problematic string
43+
print(f"Failed to rebuild BRep shape with importBrepFromString: {e}. Input (first 100 chars): {prop_value[:100]}...")
44+
return None

jupytercad_freecad/handlers.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import json
22
import base64
33
from pathlib import Path
4+
import os
5+
import shutil
46

57
from jupyter_server.base.handlers import APIHandler
68
from jupyter_server.utils import url_path_join, ApiPath, to_os_path
79
import tornado
810

911
from jupytercad_freecad.freecad.loader import FCStd
10-
from jupytercad_freecad.freecad.jcad_converter import convert_jcad_to_fcstd
12+
from jupytercad_freecad.freecad.jcad_converter import export_jcad_to_fcstd
1113

1214
class BackendCheckHandler(APIHandler):
1315
@tornado.web.authenticated
@@ -110,10 +112,17 @@ def post(self):
110112

111113
# convert to FreeCAD document
112114
try:
113-
doc = convert_jcad_to_fcstd(jcad_dict)
114-
doc.saveAs(str(out_path)) # write .FCStd
115+
doc = export_jcad_to_fcstd(jcad_dict)
116+
117+
temp_fcstd_path = doc.FileName
118+
shutil.copy2(temp_fcstd_path, str(out_path))
119+
120+
# Clean up the temporary file
121+
if os.path.exists(temp_fcstd_path):
122+
os.unlink(temp_fcstd_path)
123+
115124
except Exception as e:
116-
self.log.error(f"Conversion to FCStd failed: {e}")
125+
self.log.error(f"Error converting to FCStd: {e}")
117126
self.set_status(500)
118127
return self.finish(json.dumps({"error": str(e)}))
119128

0 commit comments

Comments
 (0)