diff --git a/jupytercad_freecad/freecad/loader.py b/jupytercad_freecad/freecad/loader.py index e37cc28..b184237 100644 --- a/jupytercad_freecad/freecad/loader.py +++ b/jupytercad_freecad/freecad/loader.py @@ -21,6 +21,19 @@ fc = None +def _rgb_to_hex(rgb): + """Converts a list of RGB values [0-1] to a hex color string""" + return "#{:02x}{:02x}{:02x}".format( + int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255) + ) + + +def _hex_to_rgb(hex_color): + """Convert hex color string to an RGB tuple""" + hex_color = hex_color.lstrip("#") + return tuple(int(hex_color[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) + + def _guidata_to_options(guidata): """Converts freecad guidata into options that JupyterCad understands""" options = {} @@ -34,11 +47,14 @@ def _guidata_to_options(guidata): options[obj_name] = data continue + # Handle FreeCAD's ShapeColor property and map to JupyterCad's color if "ShapeColor" in data: - obj_options["color"] = list(data["ShapeColor"]["value"]) + color_rgb = data["ShapeColor"]["value"] + obj_options["color"] = _rgb_to_hex(color_rgb) + # Handle FreeCAD's Visibility property and map to JupyterCad's visible property if "Visibility" in data: - obj_options["visibility"] = data["Visibility"]["value"] + obj_options["visible"] = data["Visibility"]["value"] options[obj_name] = obj_options @@ -55,17 +71,18 @@ def _options_to_guidata(options): # We need to make a special case to "GuiCameraSettings" because freecad's # OfflineRenderingUtils mixes the camera settings with 3D objects if obj_name == "GuiCameraSettings": - options[obj_name] = data + gui_data[obj_name] = data continue + # Handle color property from JupyterCad to FreeCAD's ShapeColor if "color" in data: - obj_data["ShapeColor"] = dict( - type="App::PropertyColor", value=tuple(data["color"]) - ) + rgb_value = _hex_to_rgb(data["color"]) + obj_data["ShapeColor"] = dict(type="App::PropertyColor", value=rgb_value) - if "visibility" in data: + # Handle visibility property from JupyterCad to FreeCAD's Visibility + if "visible" in data: obj_data["Visibility"] = dict( - type="App::PropertyBool", value=data["visibility"] + type="App::PropertyBool", value=data["visible"] ) gui_data[obj_name] = obj_data @@ -81,6 +98,7 @@ def __init__(self) -> None: self._metadata = {} self._id = None self._visible = True + self._guidata = {} self._prop_handlers: Dict[str, Type[BaseProp]] = {} for Cls in Props.__dict__.values(): if isinstance(Cls, type) and issubclass(Cls, BaseProp): @@ -115,15 +133,31 @@ def load(self, base64_content: str) -> None: # Get metadata self._metadata = fc_file.Meta - # Get GuiData (metadata from the GuiDocument.xml file) - self._options["guidata"] = _guidata_to_options( - OfflineRenderingUtils.getGuiData(tmp.name) - ) + # Get GuiData and assign it to the internal attribute + self._guidata = _guidata_to_options(OfflineRenderingUtils.getGuiData(tmp.name)) # Get objects self._objects = [] for obj in fc_file.Objects: - self._objects.append(self._fc_to_jcad_obj(obj)) + obj_name = obj.Name + + obj_data = self._fc_to_jcad_obj(obj) + + if obj_name in self._guidata: + if "color" in self._guidata[obj_name]: + default_color = "#808080" + gui_data_color = self._guidata[obj_name]["color"] + + obj_data["parameters"]["Color"] = ( + gui_data_color if gui_data_color else default_color + ) + if "visible" in self._guidata[obj_name]: + gui_data_visible = self._guidata[obj_name]["visible"] + obj_data["visible"] = ( + gui_data_visible if gui_data_visible is not None else True + ) + + self._objects.append(obj_data) os.remove(tmp.name) @@ -152,29 +186,46 @@ def save(self, objects: List, options: Dict, metadata: Dict) -> None: for obj_name in to_update: py_obj = new_objs[obj_name] - fc_obj = fc_file.getObject(py_obj["name"]) for prop, jcad_prop_value in py_obj["parameters"].items(): - prop_type = fc_obj.getTypeIdOfProperty(prop) - prop_handler = self._prop_handlers.get(prop_type, None) - if prop_handler is not None: - fc_value = prop_handler.jcad_to_fc( - jcad_prop_value, - jcad_object=objects, - fc_prop=getattr(fc_obj, prop), - fc_object=fc_obj, - fc_file=fc_file, + if hasattr(fc_obj, prop): + try: + prop_type = fc_obj.getTypeIdOfProperty(prop) + prop_handler = self._prop_handlers.get(prop_type, None) + if prop_handler is not None: + fc_value = prop_handler.jcad_to_fc( + jcad_prop_value, + jcad_object=objects, + fc_prop=getattr(fc_obj, prop), + fc_object=fc_obj, + fc_file=fc_file, + ) + if fc_value: + setattr(fc_obj, prop, fc_value) + except AttributeError as e: + print( + f"Error accessing property '{prop}' on object '{fc_obj.Name}': {e}" + ) + else: + print( + f"Property '{prop}' does not exist on object '{fc_obj.Name}' and is not handled" ) - if fc_value: - try: - setattr(fc_obj, prop, fc_value) - except Exception: - pass + + # Handle updating the color in guidata + if "Color" in py_obj["parameters"]: + new_hex_color = py_obj["parameters"]["Color"] + else: + new_hex_color = "#808080" # Default to gray if no color is provided + + if obj_name in self._guidata: + self._guidata[obj_name]["color"] = new_hex_color + else: + self._guidata[obj_name] = {"color": new_hex_color} OfflineRenderingUtils.save( fc_file, - guidata=_options_to_guidata(options.get("guidata", {})), + guidata=_options_to_guidata(self._guidata), ) fc_file.recompute() diff --git a/package.json b/package.json index c59be39..f01533a 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,9 @@ "dependencies": { "@jupyter/collaboration": "^2.0.0", "@jupyter/docprovider": "^2.0.0", - "@jupytercad/base": "^2.0.0", - "@jupytercad/jupytercad-core": "^2.0.0", - "@jupytercad/schema": "^2.0.0", + "@jupytercad/base": "^3.0.0-alpha.1", + "@jupytercad/jupytercad-core": "^3.0.0-alpha.1", + "@jupytercad/schema": "^3.0.0-alpha.1", "@jupyterlab/application": "^4.0.0" }, "devDependencies": { diff --git a/pyproject.toml b/pyproject.toml index 18b9f90..df3cb67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ ] dependencies = [ "jupyter_ydoc>=2,<3", - "jupytercad_core>=2.0.0,<3", + "jupytercad_core>=3.0.0a1,<4", ] dynamic = ["version", "description", "authors", "urls", "keywords"]