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

Conversation

trungleduc
Copy link
Member

@trungleduc trungleduc commented Jan 18, 2023

API example:

from jupytercad.notebook import CadDocument
a = CadDocument('examples/cut.FCStd')
box = a.get_object('Box')
box.parameters.Height = 1
box.update_object()
jcadapi.mp4

Done

  • Add mime renderer for jcad and fcstd content
  • Add APIs to interact with JupyterCad objects.

Todo

  • Make the Caddocument constructor waits for the content coming from the frontend. -> No solution yet

@trungleduc trungleduc linked an issue Jan 18, 2023 that may be closed by this pull request
2 tasks
@github-actions
Copy link
Contributor

Binder 👈 Launch a Binder on branch trungleduc/jupytercad/notebook

@trungleduc trungleduc force-pushed the notebook branch 3 times, most recently from cebd15e to 8ad858c Compare January 20, 2023 13:16
@trungleduc trungleduc self-assigned this Jan 20, 2023
@trungleduc trungleduc force-pushed the notebook branch 3 times, most recently from d044f42 to 5c5299c Compare January 27, 2023 15:08
@trungleduc
Copy link
Member Author

trungleduc commented Jan 27, 2023

@hbcarlos I'm still having issues with file saving with the latest commit.

def _repr_mimebundle_(self, **kwargs):
return self._caddoc.render()

def update_object(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this should be called automatically whenever the underlying parameters changed.

Instead of:

box.parameters.Height = 1
box.update_object()

You would just do:

box.parameters.Height = 1

This would be closer to the ipywidgets vision, if that is a goal we'd like to follow.

In ipywidgets, you can also hold changes so that we do not crowd the comm channel with too many messages:

with widget.hold_sync():
    widget.value1 = 1
    widget.value2 = 2
    widget.value3 = 3

We could maybe think of having something equivalent here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to do so, but pydantic does not have the field observation (I think @davidbrochart is working on it).
I use this approach in JupyterCad but we definitely should have something as you describe in the standalone version of ypywidgets frontend.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/notebookrenderer/index.ts Outdated Show resolved Hide resolved
Comment on lines +90 to +94
const awareness = jcadModel.sharedModel.awareness;
const _onUserChanged = (user: User.IManager) => {
awareness.setLocalStateField('user', user.identity);
};
user.ready
.then(() => {
_onUserChanged(user);
})
.catch(e => console.error(e));
user.userChanged.connect(_onUserChanged, this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this code here? I don't really understand what it has to do with the rest of the _handle_comm_open method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, this snippet is done by WebSocketProvider, in the case of opening a new widget without providing the file path, we don't initialize the WebSocketProvider but jcadModels (and then the 3D viewer) still expects that its awareness has the user information.

@trungleduc trungleduc force-pushed the notebook branch 2 times, most recently from 63d1079 to dd5bd57 Compare February 20, 2023 13:00
Comment on lines +57 to +108
class SingletonMeta(type):

_instances = {}

def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]


class ObjectFactoryManager(metaclass=SingletonMeta):
def __init__(self):
self._factories: Dict[str, type[BaseModel]] = {}

def register_factory(self, shape_type: str, cls: type[BaseModel]) -> None:
if shape_type not in self._factories:
self._factories[shape_type] = cls

def create_object(
self, data: Dict, parent: Optional[CadDocument] = None
) -> Optional[PythonJcadObject]:
object_type = data.get('shape', None)
name: str = data.get('name', None)
if object_type and object_type in self._factories:
Model = self._factories[object_type]
args = {}
params = data['parameters']
for field in Model.__fields__:
args[field] = params.get(field, None)
obj_params = Model(**args)
return PythonJcadObject(
parent=parent,
name=name,
shape=object_type,
parameters=obj_params,
)

return None


OBJECT_FACTORY = ObjectFactoryManager()

OBJECT_FACTORY.register_factory(Parts.Part__Box.value, IBox)
OBJECT_FACTORY.register_factory(Parts.Part__Cone.value, ICone)
OBJECT_FACTORY.register_factory(Parts.Part__Cut.value, ICut)
OBJECT_FACTORY.register_factory(Parts.Part__Cylinder.value, ICylinder)
OBJECT_FACTORY.register_factory(Parts.Part__Extrusion.value, IExtrusion)
OBJECT_FACTORY.register_factory(Parts.Part__MultiCommon.value, IIntersection)
OBJECT_FACTORY.register_factory(Parts.Part__MultiFuse.value, IFuse)
OBJECT_FACTORY.register_factory(Parts.Part__Sphere.value, ISphere)
OBJECT_FACTORY.register_factory(Parts.Part__Torus.value, ITorus)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code may be too complex? It's more or less the same as:

Suggested change
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class ObjectFactoryManager(metaclass=SingletonMeta):
def __init__(self):
self._factories: Dict[str, type[BaseModel]] = {}
def register_factory(self, shape_type: str, cls: type[BaseModel]) -> None:
if shape_type not in self._factories:
self._factories[shape_type] = cls
def create_object(
self, data: Dict, parent: Optional[CadDocument] = None
) -> Optional[PythonJcadObject]:
object_type = data.get('shape', None)
name: str = data.get('name', None)
if object_type and object_type in self._factories:
Model = self._factories[object_type]
args = {}
params = data['parameters']
for field in Model.__fields__:
args[field] = params.get(field, None)
obj_params = Model(**args)
return PythonJcadObject(
parent=parent,
name=name,
shape=object_type,
parameters=obj_params,
)
return None
OBJECT_FACTORY = ObjectFactoryManager()
OBJECT_FACTORY.register_factory(Parts.Part__Box.value, IBox)
OBJECT_FACTORY.register_factory(Parts.Part__Cone.value, ICone)
OBJECT_FACTORY.register_factory(Parts.Part__Cut.value, ICut)
OBJECT_FACTORY.register_factory(Parts.Part__Cylinder.value, ICylinder)
OBJECT_FACTORY.register_factory(Parts.Part__Extrusion.value, IExtrusion)
OBJECT_FACTORY.register_factory(Parts.Part__MultiCommon.value, IIntersection)
OBJECT_FACTORY.register_factory(Parts.Part__MultiFuse.value, IFuse)
OBJECT_FACTORY.register_factory(Parts.Part__Sphere.value, ISphere)
OBJECT_FACTORY.register_factory(Parts.Part__Torus.value, ITorus)
CLASSES = {
Parts.Part__Box.value: IBox,
Parts.Part__Cone.value: ICone,
Parts.Part__Cut.value: ICut,
Parts.Part__Cylinder.value: ICylinder,
Parts.Part__Extrusion.value: IExtrusion,
Parts.Part__MultiCommon.value: IIntersection,
Parts.Part__MultiFuse.value: IFuse,
Parts.Part__Sphere.value: ISphere,
Parts.Part__Torus.value: ITorus,
}
def create_object(
data: Dict, parent: Optional[CadDocument] = None
) -> Optional[PythonJcadObject]:
object_type = data.get('shape', None)
name: str = data.get('name', None)
if object_type and object_type in CLASSES:
Model = CLASSES[object_type]
args = {}
params = data['parameters']
for field in Model.__fields__:
args[field] = params.get(field, None)
obj_params = Model(**args)
return PythonJcadObject(
parent=parent,
name=name,
shape=object_type,
parameters=obj_params,
)
return None

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to use a factory pattern to register the shapes at the same place as their definition, the register_factory method should be called in the appropriate shape classes. But then shape classes are generated automatically and I didn't find a way to add this register_factory call to the generated files, so we ended up with this implementation.

Another advantage of the registering method is that we can prevent removing or overwriting existing factories.

Copy link
Member

@martinRenou martinRenou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@martinRenou martinRenou merged commit 7a46eda into jupytercad:main Mar 14, 2023
@trungleduc trungleduc deleted the notebook branch January 2, 2024 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

New frontends for JupyterCad
3 participants