Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose JupyterCad 3d view and APIs in notebook #102

Merged
merged 20 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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