Skip to content

Commit

Permalink
Expose JupyterCad 3d view and APIs in notebook (#102)
Browse files Browse the repository at this point in the history
* wip

* Add mimtype for FCStd file

* Add mimetype for Jcad file

* mime renderer works

* Switch to widget

* Refactor widget

* WIP

* Issue with saving file

* switch back to rendermime

* Add async create method

* Update fcstd mimerenderer

* wip

* Use pydantic

* Comm works

* Update widget manager

* Render objects

* Update import

* Update dependencies

* Update file path

* Remove auto generated files
  • Loading branch information
trungleduc authored Mar 14, 2023
1 parent 36881f4 commit 7a46eda
Show file tree
Hide file tree
Showing 45 changed files with 2,260 additions and 583 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
channels: conda-forge/label/jupyterlab_alpha,conda-forge
extra-specs: |
python=3.9
nodejs=16
nodejs=18
yarn
jupyterlab=4.0.0a32
freecad
Expand Down Expand Up @@ -91,6 +91,7 @@ jobs:
name: extension-artifacts

- name: Install and Test
# TODO Update JupyterLab version
run: |
set -eux
# Remove NodeJS, twice to take care of system and locally installed node versions.
Expand Down Expand Up @@ -130,7 +131,7 @@ jobs:
channels: conda-forge/label/jupyterlab_alpha,conda-forge
extra-specs: |
python=3.9
nodejs=16
nodejs=18
yarn
jupyterlab=4.0.0a32
freecad
Expand Down Expand Up @@ -205,7 +206,7 @@ jobs:
channels: conda-forge/label/jupyterlab_alpha,conda-forge
extra-specs: |
python=3.9
nodejs=16
nodejs=18
yarn
jupyterlab=4.0.0a32
freecad
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
matrix:
group: [check_release]
python-version: ["3.9"]
node-version: ["14.x"]
node-version: ["18.x"]

steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update_galata_references.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
run: |
whereis python
python -m pip install jupytercad*.whl
python -m pip install "jupyterlab>=4.0.0a32"
python -m pip install "jupyterlab==4.0.0a32"
- name: Install dependencies
shell: bash -l {0}
Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,9 @@ src/_interface/
ui-tests/test-results/
ui-tests/playwright-report/

examples/*.ipynb
examples/*.ipynb
# Hatchling
jupytercad/_version.py

#Schema
jupytercad/notebook/objects/_schema
2 changes: 1 addition & 1 deletion binder/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ dependencies:
- yarn

# Dependencies
- jupyterlab >=4.0.0a32,<5.0.0a0
- jupyterlab ==4.0.0a32,<5.0.0a0
- freecad

# Binder
Expand Down
Binary file modified examples/cut.FCStd
Binary file not shown.
1 change: 1 addition & 0 deletions jupytercad/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from ._version import __version__


def _jupyter_labextension_paths():
return [{'src': 'labextension', 'dest': 'jupytercad'}]
16 changes: 9 additions & 7 deletions jupytercad/fcstd_ydoc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from jupyter_ydoc.ydoc import YBaseDoc
import y_py as Y
from jupyter_ydoc.ydoc import YBaseDoc

from jupytercad.freecad.loader import FCStd

Expand All @@ -14,34 +14,36 @@ def __init__(self, *args, **kwargs):
self._virtual_file = FCStd()

@property
def source(self):
def objects(self) -> Y.YArray:
return self._yobjects

def get(self):
fc_objects = self._yobjects.to_json()
options = self._yoptions.to_json()
meta = self._ymeta.to_json()
self._virtual_file.save(fc_objects, options, meta)
return self._virtual_file.sources

@source.setter
def source(self, value):
def set(self, value):
virtual_file = self._virtual_file
virtual_file.load(value)
objects = []

for obj in virtual_file.objects:
objects.append(Y.YMap(obj))

with self._ydoc.begin_transaction() as t:
length = len(self._yobjects)
self._yobjects.delete_range(t, 0, length)
self._yobjects.extend(t, objects)

self._yoptions.update(t, virtual_file.options.items())
self._ymeta.update(t, virtual_file.metadata.items())

def observe(self, callback):
self.unobserve()
self._subscriptions[self._ystate] = self._ystate.observe(callback)
self._subscriptions[self._ysource] = self._ysource.observe(callback)
self._subscriptions[self._yobjects] = self._yobjects.observe_deep(callback)
self._subscriptions[self._yobjects] = self._yobjects.observe_deep(
callback
)
self._subscriptions[self._yoptions] = self._yoptions.observe(callback)
self._subscriptions[self._ymeta] = self._ymeta.observe_deep(callback)
65 changes: 33 additions & 32 deletions jupytercad/freecad/loader.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import traceback
from typing import Dict, List, Type
import tempfile
import base64
from pathlib import Path
import logging
import os
import xml
import zipfile
import tempfile
import traceback
from typing import Dict, List, Type

from .props.base_prop import BaseProp
from . import props as Props
import logging
from .props.base_prop import BaseProp

logger = logging.getLogger(__file__)

try:
import freecad as fc

import OfflineRenderingUtils

except ImportError:
logger.warn("[JupyterCad] Freecad is not installed!")
logger.warn('[JupyterCad] Freecad is not installed!')
fc = None


Expand All @@ -32,15 +28,15 @@ def _guidata_to_options(guidata):

# We need to make a special case to "GuiCameraSettings" because freecad's
# OfflineRenderingUtils mixes the camera settings with 3D objects
if obj_name == "GuiCameraSettings":
if obj_name == 'GuiCameraSettings':
options[obj_name] = data
continue

if "ShapeColor" in data:
obj_options["color"] = list(data["ShapeColor"]["value"])
if 'ShapeColor' in data:
obj_options['color'] = list(data['ShapeColor']['value'])

if "Visibility" in data:
obj_options["visibility"] = data["Visibility"]["value"]
if 'Visibility' in data:
obj_options['visibility'] = data['Visibility']['value']

options[obj_name] = obj_options

Expand All @@ -56,18 +52,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":
if obj_name == 'GuiCameraSettings':
options[obj_name] = data
continue

if "color" in data:
obj_data["ShapeColor"] = dict(
type="App::PropertyColor", value=tuple(data["color"])
if 'color' in data:
obj_data['ShapeColor'] = dict(
type='App::PropertyColor', value=tuple(data['color'])
)

if "visibility" in data:
obj_data["Visibility"] = dict(
type="App::PropertyBool", value=data["visibility"]
if 'visibility' in data:
obj_data['Visibility'] = dict(
type='App::PropertyBool', value=data['visibility']
)

gui_data[obj_name] = obj_data
Expand All @@ -77,7 +73,7 @@ def _options_to_guidata(options):

class FCStd:
def __init__(self) -> None:
self._sources = ""
self._sources = ''
self._objects = []
self._options = {}
self._metadata = {}
Expand Down Expand Up @@ -108,7 +104,7 @@ def load(self, base64_content: str) -> None:
if not fc:
return
self._sources = base64_content
with tempfile.NamedTemporaryFile(delete=False, suffix=".FCStd") as tmp:
with tempfile.NamedTemporaryFile(delete=False, suffix='.FCStd') as tmp:
file_content = base64.b64decode(base64_content)
tmp.write(file_content)

Expand All @@ -118,11 +114,12 @@ def load(self, base64_content: str) -> None:
self._metadata = fc_file.Meta

# Get GuiData (metadata from the GuiDocument.xml file)
self._options["guidata"] = _guidata_to_options(
self._options['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))

Expand All @@ -133,27 +130,31 @@ def save(self, objects: List, options: Dict, metadata: Dict) -> None:
if not fc or len(self._sources) == 0:
return

with tempfile.NamedTemporaryFile(delete=False, suffix=".FCStd") as tmp:
with tempfile.NamedTemporaryFile(
delete=False, suffix='.FCStd'
) as tmp:
file_content = base64.b64decode(self._sources)
tmp.write(file_content)
fc_file = fc.app.openDocument(tmp.name)
fc_file.Meta = metadata

new_objs = dict([(o["name"], o) for o in objects])
new_objs = dict([(o['name'], o) for o in objects])

current_objs = dict([(o.Name, o) for o in fc_file.Objects])

to_remove = [x for x in current_objs if x not in new_objs]
to_add = [x for x in new_objs if x not in current_objs]
for obj_name in to_remove:
fc_file.removeObject(obj_name)
for obj_name in to_add:
py_obj = new_objs[obj_name]
fc_file.addObject(py_obj["shape"], py_obj["name"])
fc_file.addObject(py_obj['shape'], py_obj['name'])
to_update = [x for x in new_objs if x in current_objs] + to_add

for obj_name in to_update:
py_obj = new_objs[obj_name]

fc_obj = fc_file.getObject(py_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)
Expand All @@ -174,11 +175,11 @@ def save(self, objects: List, options: Dict, metadata: Dict) -> None:

OfflineRenderingUtils.save(
fc_file,
guidata=_options_to_guidata(options.get("guidata", {})),
guidata=_options_to_guidata(options.get('guidata', {})),
)

fc_file.recompute()
with open(tmp.name, "rb") as f:
with open(tmp.name, 'rb') as f:
encoded = base64.b64encode(f.read())
self._sources = encoded.decode()
os.remove(tmp.name)
Expand All @@ -200,5 +201,5 @@ def _fc_to_jcad_obj(self, obj) -> Dict:
value = prop_handler.fc_to_jcad(prop_value, fc_object=obj)
else:
value = None
obj_data["parameters"][prop] = value
obj_data['parameters'][prop] = value
return obj_data
10 changes: 5 additions & 5 deletions jupytercad/freecad/props/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from .property_length import *
from .property_placement import *
from .property_angle import *
from .property_bool import *
from .property_distance import *
from .property_geometrylist import *
from .property_length import *
from .property_link import *
from .property_link_list import *
from .property_partshape import *
from .property_map import *
from .property_geometrylist import *
from .property_distance import *
from .property_partshape import *
from .property_placement import *
2 changes: 1 addition & 1 deletion jupytercad/freecad/props/base_prop.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any, Dict
from typing import Any


class BaseProp(ABC):
Expand Down
3 changes: 2 additions & 1 deletion jupytercad/freecad/props/property_geometrylist.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any, List
from .geometry import geom_handlers

from .base_prop import BaseProp
from .geometry import geom_handlers


class Part_PropertyGeometryList(BaseProp):
Expand Down
2 changes: 1 addition & 1 deletion jupytercad/freecad/props/property_map.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict
from typing import Any

from .base_prop import BaseProp

Expand Down
3 changes: 2 additions & 1 deletion jupytercad/freecad/props/property_partshape.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Any
from io import StringIO
from typing import Any

from .base_prop import BaseProp


Expand Down
1 change: 1 addition & 0 deletions jupytercad/freecad/props/property_placement.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import math
from typing import Any

from .base_prop import BaseProp

try:
Expand Down
3 changes: 2 additions & 1 deletion jupytercad/jcad_ydoc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from jupyter_ydoc.ydoc import YBaseDoc

import y_py as Y
from jupyter_ydoc.ydoc import YBaseDoc


class YJCad(YBaseDoc):
Expand Down
2 changes: 2 additions & 0 deletions jupytercad/notebook/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .cad_document import CadDocument
from .objects import OBJECT_FACTORY
Loading

0 comments on commit 7a46eda

Please sign in to comment.