From 3402da1ca4cf116ebff6fb93e410ee8f8d58db90 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Fri, 15 Dec 2023 11:05:13 +0100 Subject: [PATCH] Update worker --- .pre-commit-config.yaml | 2 +- .prettierignore | 2 + examples/test.jcad | 80 ++++--- examples/test22.jcad | 186 ++++++++++++++++ jupytercad/fcstd_ydoc.py | 68 ------ jupytercad/freecad/__init__.py | 0 jupytercad/freecad/loader.py | 204 ------------------ jupytercad/freecad/props/__init__.py | 10 - jupytercad/freecad/props/base_prop.py | 48 ----- jupytercad/freecad/props/geometry/__init__.py | 10 - .../freecad/props/geometry/geom_circle.py | 62 ------ .../props/geometry/geom_linesegment.py | 50 ----- jupytercad/freecad/props/property_angle.py | 17 -- jupytercad/freecad/props/property_bool.py | 17 -- jupytercad/freecad/props/property_distance.py | 17 -- .../freecad/props/property_geometrylist.py | 47 ---- jupytercad/freecad/props/property_length.py | 17 -- jupytercad/freecad/props/property_link.py | 19 -- .../freecad/props/property_link_list.py | 19 -- jupytercad/freecad/props/property_map.py | 17 -- .../freecad/props/property_partshape.py | 21 -- .../freecad/props/property_placement.py | 45 ---- jupytercad/freecad/tools.py | 10 - packages/base/src/commands.ts | 50 +++++ packages/base/src/fcplugin/modelfactory.ts | 101 --------- packages/base/src/fcplugin/plugins.ts | 177 --------------- packages/base/src/mainview.tsx | 121 +++++++---- .../base/src/panelview/objectproperties.tsx | 13 +- packages/base/src/panelview/rightpanel.tsx | 6 +- packages/base/src/toolbar/widget.tsx | 10 +- packages/base/src/tools.ts | 6 + packages/base/style/icon/grid.svg | 6 + packages/occ-worker/package.json | 5 +- packages/occ-worker/src/actions.ts | 22 +- packages/occ-worker/src/occapi.ts | 50 ++++- packages/occ-worker/src/occparser.ts | 13 +- packages/occ-worker/src/occworker.ts | 60 +++--- packages/occ-worker/src/types.ts | 70 ++---- packages/occ-worker/src/worker.ts | 9 +- packages/opencascade/build.yml | 1 + packages/opencascade/package.json | 3 - packages/opencascade/src/index.ts | 38 +++- .../src/jupytercad.opencascade.d.ts | 1 + packages/schema/src/interfaces.ts | 126 ++++++++++- packages/schema/src/schema/jcad.json | 3 +- packages/schema/src/schema/postOperator.json | 17 ++ packages/schema/src/token.ts | 7 +- packages/schema/src/types.ts | 1 + python/jupytercad_app/.gitignore | 1 + python/jupytercad_app/package.json | 2 +- python/jupytercad_core/package.json | 18 +- python/jupytercad_core/src/index.ts | 4 +- python/jupytercad_core/src/plugin.ts | 21 +- python/jupytercad_core/src/schemaregistry.ts | 27 +++ python/jupytercad_core/src/workerregistry.ts | 11 + python/jupytercad_lab/package.json | 6 +- python/jupytercad_lab/src/index.ts | 17 +- ui-tests/playwright.config.js | 2 +- yarn.lock | 47 ++-- 59 files changed, 815 insertions(+), 1225 deletions(-) create mode 100644 examples/test22.jcad delete mode 100644 jupytercad/fcstd_ydoc.py delete mode 100644 jupytercad/freecad/__init__.py delete mode 100644 jupytercad/freecad/loader.py delete mode 100644 jupytercad/freecad/props/__init__.py delete mode 100644 jupytercad/freecad/props/base_prop.py delete mode 100644 jupytercad/freecad/props/geometry/__init__.py delete mode 100644 jupytercad/freecad/props/geometry/geom_circle.py delete mode 100644 jupytercad/freecad/props/geometry/geom_linesegment.py delete mode 100644 jupytercad/freecad/props/property_angle.py delete mode 100644 jupytercad/freecad/props/property_bool.py delete mode 100644 jupytercad/freecad/props/property_distance.py delete mode 100644 jupytercad/freecad/props/property_geometrylist.py delete mode 100644 jupytercad/freecad/props/property_length.py delete mode 100644 jupytercad/freecad/props/property_link.py delete mode 100644 jupytercad/freecad/props/property_link_list.py delete mode 100644 jupytercad/freecad/props/property_map.py delete mode 100644 jupytercad/freecad/props/property_partshape.py delete mode 100644 jupytercad/freecad/props/property_placement.py delete mode 100644 jupytercad/freecad/tools.py delete mode 100644 packages/base/src/fcplugin/modelfactory.ts delete mode 100644 packages/base/src/fcplugin/plugins.ts create mode 100644 packages/base/style/icon/grid.svg create mode 100644 packages/schema/src/schema/postOperator.json create mode 100644 python/jupytercad_core/src/schemaregistry.ts diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad06b898..01a84a17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: hooks: - id: forbid-new-submodules - id: end-of-file-fixer - exclude: galata/.*-snapshots + exclude: galata/.*-snapshots, *.jcad - id: check-case-conflict - id: requirements-txt-fixer - id: check-added-large-files diff --git a/.prettierignore b/.prettierignore index 83d70a32..93fb34ae 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,5 @@ jupytercad **/*.d.ts **/*.js packages/opencascade/build.yml +yarn.lock +*.jcad diff --git a/examples/test.jcad b/examples/test.jcad index 56a3e77d..2db333ab 100644 --- a/examples/test.jcad +++ b/examples/test.jcad @@ -2,53 +2,69 @@ "objects": [ { "name": "box", + "visible": true, "shape": "Part::Box", + "shapeMetadata": { + "mass": 1995, + "centerOfMass": [ + 3.5, + 7.5, + 9.499999999999998 + ], + "matrixOfInertia": [ + [ + 0, + -2.9103830456733704e-11, + 45552.5 + ], + [ + 97422.50000000006, + 0, + 0 + ], + [ + 0, + 68162.50000000006, + -2.9103830456733704e-11 + ] + ] + }, "parameters": { - "Height": 19.0, "Placement": { "Axis": [ - 1.0, - 0.0, - 0.0 + 1, + 0, + 0 ], "Position": [ - 0.0, - 0.0, - 0.0 + 0, + 0, + 0 ], - "Angle": 0.0 + "Angle": 0 }, - "Width": 15.0, - "Length": 7.0 - }, - "visible": true + "Width": 15, + "Length": 7, + "Height": 19 + } }, { - "visible": true, + "name": "Mesh 1", + "shape": "Post::Operator", "parameters": { - "Length": 10.0, - "Placement": { - "Position": [ - 0.0, - 0.0, - 0.0 - ], - "Axis": [ - 1.0, - 0.0, - 0.0 - ], - "Angle": 2.0 - }, - "Height": 9.0, - "Width": 25.0 + "NumberOfSegment": 5, + "Object": "box" }, - "shape": "Part::Box", - "name": "box2" + "visible": true } ], "options": { - "foo": 1.0 + "foo": 1, + "guidata": { + "box": { + "visibility": false + } + } }, "metadata": {} } diff --git a/examples/test22.jcad b/examples/test22.jcad new file mode 100644 index 00000000..0d6c9c5c --- /dev/null +++ b/examples/test22.jcad @@ -0,0 +1,186 @@ +{ + "objects": [ + { + "visible": true, + "name": "box", + "shapeMetadata": { + "matrixOfInertia": [ + [ + 0, + 0, + 75735 + ], + [ + 175477.5, + 0, + 0 + ], + [ + 0, + 139837.5, + 0 + ] + ], + "mass": 2970, + "centerOfMass": [ + 4.5, + 7.5, + 11 + ] + }, + "parameters": { + "Width": 15, + "Height": 22, + "Length": 9, + "Placement": { + "Angle": 0, + "Position": [ + 0, + 0, + 0 + ], + "Axis": [ + 1, + 0, + 0 + ] + } + }, + "shape": "Part::Box" + }, + { + "visible": true, + "shapeMetadata": { + "mass": 3900, + "centerOfMass": [ + 6.5, + 12.28298835752369, + 6.4325886708958375 + ], + "matrixOfInertia": [ + [ + 0, + -5452.340379025205, + 257859.60007843364 + ], + [ + 249925, + 0, + 0 + ], + [ + 0, + 101915.39992156648, + -5452.340379025205 + ] + ] + }, + "name": "box2", + "parameters": { + "Width": 25, + "Placement": { + "Position": [ + 0, + 0, + 0 + ], + "Angle": 2, + "Axis": [ + 1, + 0, + 0 + ] + }, + "Height": 12, + "Length": 13 + }, + "shape": "Part::Box" + }, + { + "parameters": { + "Refine": false, + "Shapes": [ + "box", + "box2" + ], + "Placement": { + "Position": [ + 0, + 0, + 0 + ], + "Angle": 0, + "Axis": [ + 0, + 0, + 1 + ] + } + }, + "shape": "Part::MultiCommon", + "shapeMetadata": { + "mass": 1620.9874617640914, + "centerOfMass": [ + 4.5, + 7.500000000000002, + 6.26556303698104 + ], + "matrixOfInertia": [ + [ + 0, + -1061.3649281489343, + 41335.18027498432 + ], + [ + 49906.14888444988, + 0, + 0 + ], + [ + 0, + 30454.299343280843, + -1061.3649281489343 + ] + ] + }, + "name": "Intersection 1", + "visible": true + }, + { + "name": "Salome mesh 1", + "shape": "Post::Operator", + "visible": true, + "parameters": { + "Object": "box", + "Placement": { + "Position": [ + 0, + 0, + 0 + ], + "Axis": [ + 0, + 0, + 1 + ], + "Angle": 0 + } + } + } + ], + "options": { + "guidata": { + "box2": { + "visibility": true + }, + "Intersection 1": { + "visibility": true + }, + "box": { + "visibility": true + } + }, + "foo": 1 + }, + "metadata": {} +} diff --git a/jupytercad/fcstd_ydoc.py b/jupytercad/fcstd_ydoc.py deleted file mode 100644 index b5b3eebf..00000000 --- a/jupytercad/fcstd_ydoc.py +++ /dev/null @@ -1,68 +0,0 @@ -import json -from typing import Any, Callable -from functools import partial - -import y_py as Y -from jupyter_ydoc.ybasedoc import YBaseDoc - -from jupytercad.freecad.loader import FCStd - - -class YFCStd(YBaseDoc): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._ysource = self._ydoc.get_text("source") - self._yobjects = self._ydoc.get_array("objects") - self._yoptions = self._ydoc.get_map("options") - self._ymeta = self._ydoc.get_map("metadata") - self._virtual_file = FCStd() - - @property - def objects(self) -> Y.YArray: - return self._yobjects - - def version(self) -> str: - return "0.1.0" - - def get(self): - fc_objects = json.loads(self._yobjects.to_json()) - options = json.loads(self._yoptions.to_json()) - meta = json.loads(self._ymeta.to_json()) - - self._virtual_file.save(fc_objects, options, meta) - return self._virtual_file.sources - - 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) - # workaround for https://github.com/y-crdt/ypy/issues/126: - # self._yobjects.extend(t, objects) - for o in objects: - self._yobjects.append(t, o) - self._yoptions.update(t, virtual_file.options.items()) - self._ymeta.update(t, virtual_file.metadata.items()) - - def observe(self, callback: Callable[[str, Any], None]): - self.unobserve() - self._subscriptions[self._ystate] = self._ystate.observe( - partial(callback, "state") - ) - self._subscriptions[self._ysource] = self._ysource.observe( - partial(callback, "source") - ) - self._subscriptions[self._yobjects] = self._yobjects.observe_deep( - partial(callback, "objects") - ) - self._subscriptions[self._yoptions] = self._yoptions.observe_deep( - partial(callback, "options") - ) - self._subscriptions[self._ymeta] = self._ymeta.observe_deep( - partial(callback, "meta") - ) diff --git a/jupytercad/freecad/__init__.py b/jupytercad/freecad/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/jupytercad/freecad/loader.py b/jupytercad/freecad/loader.py deleted file mode 100644 index e37cc284..00000000 --- a/jupytercad/freecad/loader.py +++ /dev/null @@ -1,204 +0,0 @@ -import base64 -import logging -import os -import tempfile -import traceback -from typing import Dict, List, Type - -from .tools import redirect_stdout_stderr - -from . import props as Props -from .props.base_prop import BaseProp - -logger = logging.getLogger(__file__) - -with redirect_stdout_stderr(): - try: - import freecad as fc - import OfflineRenderingUtils - - except ImportError: - fc = None - - -def _guidata_to_options(guidata): - """Converts freecad guidata into options that JupyterCad understands""" - options = {} - - for obj_name, data in guidata.items(): - obj_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 - continue - - if "ShapeColor" in data: - obj_options["color"] = list(data["ShapeColor"]["value"]) - - if "Visibility" in data: - obj_options["visibility"] = data["Visibility"]["value"] - - options[obj_name] = obj_options - - return options - - -def _options_to_guidata(options): - """Converts JupyterCad options into freecad guidata""" - gui_data = {} - - for obj_name, data in options.items(): - obj_data = {} - - # 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 - continue - - 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"] - ) - - gui_data[obj_name] = obj_data - - return gui_data - - -class FCStd: - def __init__(self) -> None: - self._sources = "" - self._objects = [] - self._options = {} - self._metadata = {} - self._id = None - self._visible = True - self._prop_handlers: Dict[str, Type[BaseProp]] = {} - for Cls in Props.__dict__.values(): - if isinstance(Cls, type) and issubclass(Cls, BaseProp): - self._prop_handlers[Cls.name()] = Cls - - @property - def sources(self): - return self._sources - - @property - def objects(self): - return self._objects - - @property - def metadata(self): - return self._metadata - - @property - def options(self): - return self._options - - def load(self, base64_content: str) -> None: - if not fc: - return - self._sources = base64_content - with tempfile.NamedTemporaryFile(delete=False, suffix=".FCStd") as tmp: - file_content = base64.b64decode(base64_content) - tmp.write(file_content) - - fc_file = fc.app.openDocument(tmp.name) - - # 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 objects - self._objects = [] - for obj in fc_file.Objects: - self._objects.append(self._fc_to_jcad_obj(obj)) - - os.remove(tmp.name) - - def save(self, objects: List, options: Dict, metadata: Dict) -> None: - try: - if not fc or len(self._sources) == 0: - return - - 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]) - - 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"]) - 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"]) - - 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 fc_value: - try: - setattr(fc_obj, prop, fc_value) - except Exception: - pass - - OfflineRenderingUtils.save( - fc_file, - guidata=_options_to_guidata(options.get("guidata", {})), - ) - - fc_file.recompute() - with open(tmp.name, "rb") as f: - encoded = base64.b64encode(f.read()) - self._sources = encoded.decode() - os.remove(tmp.name) - except Exception: - print(traceback.print_exc()) - - def _fc_to_jcad_obj(self, obj) -> Dict: - obj_data = dict( - shape=obj.TypeId, - visible=obj.Visibility, - parameters={}, - name=obj.Name, - ) - for prop in obj.PropertiesList: - prop_type = obj.getTypeIdOfProperty(prop) - prop_value = getattr(obj, prop) - prop_handler = self._prop_handlers.get(prop_type, None) - if prop_handler is not None and prop_value is not None: - value = prop_handler.fc_to_jcad(prop_value, fc_object=obj) - else: - value = None - obj_data["parameters"][prop] = value - return obj_data diff --git a/jupytercad/freecad/props/__init__.py b/jupytercad/freecad/props/__init__.py deleted file mode 100644 index 5671dd49..00000000 --- a/jupytercad/freecad/props/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .property_angle import * # noqa -from .property_bool import * # noqa -from .property_distance import * # noqa -from .property_geometrylist import * # noqa -from .property_length import * # noqa -from .property_link import * # noqa -from .property_link_list import * # noqa -from .property_map import * # noqa -from .property_partshape import * # noqa -from .property_placement import * # noqa diff --git a/jupytercad/freecad/props/base_prop.py b/jupytercad/freecad/props/base_prop.py deleted file mode 100644 index 14e621a5..00000000 --- a/jupytercad/freecad/props/base_prop.py +++ /dev/null @@ -1,48 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any - - -class BaseProp(ABC): - @staticmethod - @abstractmethod - def name() -> str: - pass - - @staticmethod - @abstractmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - """Method to translate a FreeCAD property into jcad property - - Keyword Args: - prop_value (Any): Value of the FreeCAD property - - fc_object (FreeCAD object): The current FreeCAD object that - we are reading. - - Returns: - Any: - """ - pass - - @staticmethod - @abstractmethod - def jcad_to_fc(prop_value: Any, **kwargs) -> Any: - """Method to translate jcad property value into FreeCAD object - - Keyword Args: - prop_value (Any): Value of the property - - jcad_object (List): The current list of jcad object - - fc_prop (FreeCAD object property): The current property that - we are updating. - - fc_object (FreeCAD object): The current FreeCAD object that - we are updating. - - fc_file (FreeCAD document): The current FreeCAD document. - - Returns: - Any: - """ - pass diff --git a/jupytercad/freecad/props/geometry/__init__.py b/jupytercad/freecad/props/geometry/__init__.py deleted file mode 100644 index 43f07757..00000000 --- a/jupytercad/freecad/props/geometry/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Dict, Type - -from ..base_prop import BaseProp -from .geom_circle import Part_GeomCircle -from .geom_linesegment import Part_GeomLineSegment - -geom_handlers: Dict[str, Type[BaseProp]] = {} - -geom_handlers[Part_GeomCircle.name()] = Part_GeomCircle -geom_handlers[Part_GeomLineSegment.name()] = Part_GeomLineSegment diff --git a/jupytercad/freecad/props/geometry/geom_circle.py b/jupytercad/freecad/props/geometry/geom_circle.py deleted file mode 100644 index 13ab1519..00000000 --- a/jupytercad/freecad/props/geometry/geom_circle.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import Any, Dict - -from ...tools import redirect_stdout_stderr - -from ..base_prop import BaseProp - -with redirect_stdout_stderr(): - try: - import freecad as fc - import Part - except ImportError: - fc = None - - -class Part_GeomCircle(BaseProp): - @staticmethod - def name() -> str: - return "Part::GeomCircle" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - center = prop_value.Center - radius = prop_value.Radius - angle = prop_value.AngleXU - normal = prop_value.Axis - return { - "TypeId": Part_GeomCircle.name(), - "CenterX": center.x, - "CenterY": center.y, - "CenterZ": center.z, - "NormalX": normal.x, - "NormalY": normal.y, - "NormalZ": normal.z, - "AngleXU": angle, - "Radius": radius, - } - - @staticmethod - def jcad_to_fc(prop_value: Dict, fc_object: Any, **kwargs) -> Any: - if not fc: - return - Center = fc.app.Base.Vector( - prop_value["CenterX"], - prop_value["CenterY"], - prop_value["CenterZ"], - ) - - Axis = fc.app.Base.Vector( - prop_value["NormalX"], - prop_value["NormalY"], - prop_value["NormalZ"], - ) - - Radius = prop_value["Radius"] - if fc_object: - fc_object.Center = Center - fc_object.Axis = Axis - fc_object.AngleXU = prop_value["AngleXU"] - fc_object.Radius = Radius - return None - else: - return Part.Circle(Center, Axis, Radius) diff --git a/jupytercad/freecad/props/geometry/geom_linesegment.py b/jupytercad/freecad/props/geometry/geom_linesegment.py deleted file mode 100644 index e1940c5c..00000000 --- a/jupytercad/freecad/props/geometry/geom_linesegment.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import Any, Dict - -from ...tools import redirect_stdout_stderr - -from ..base_prop import BaseProp - -with redirect_stdout_stderr(): - try: - import freecad as fc - import Part - except ImportError: - fc = None - - -class Part_GeomLineSegment(BaseProp): - @staticmethod - def name() -> str: - return "Part::GeomLineSegment" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - start = prop_value.StartPoint - end = prop_value.EndPoint - - return { - "TypeId": Part_GeomLineSegment.name(), - "StartX": start.x, - "StartY": start.y, - "StartZ": start.z, - "EndX": end.x, - "EndY": end.y, - "EndZ": end.z, - } - - @staticmethod - def jcad_to_fc(prop_value: Dict, fc_object: Any, **kwargs) -> Any: - if not fc: - return - StartPoint = fc.app.Base.Vector( - prop_value["StartX"], prop_value["StartY"], prop_value["StartZ"] - ) - EndPoint = fc.app.Base.Vector( - prop_value["EndX"], prop_value["EndY"], prop_value["EndZ"] - ) - if fc_object: - fc_object.StartPoint = StartPoint - fc_object.EndPoint = EndPoint - return None - else: - return Part.LineSegment(StartPoint, EndPoint) diff --git a/jupytercad/freecad/props/property_angle.py b/jupytercad/freecad/props/property_angle.py deleted file mode 100644 index 663d9382..00000000 --- a/jupytercad/freecad/props/property_angle.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -from .base_prop import BaseProp - - -class App_PropertyAngle(BaseProp): - @staticmethod - def name() -> str: - return "App::PropertyAngle" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - return prop_value.getValueAs("deg").Value - - @staticmethod - def jcad_to_fc(prop_value: float, **kwargs) -> Any: - return prop_value diff --git a/jupytercad/freecad/props/property_bool.py b/jupytercad/freecad/props/property_bool.py deleted file mode 100644 index 316847d2..00000000 --- a/jupytercad/freecad/props/property_bool.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -from .base_prop import BaseProp - - -class App_PropertyBool(BaseProp): - @staticmethod - def name() -> str: - return "App::PropertyBool" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - return prop_value - - @staticmethod - def jcad_to_fc(prop_value: bool, **kwargs) -> Any: - return prop_value diff --git a/jupytercad/freecad/props/property_distance.py b/jupytercad/freecad/props/property_distance.py deleted file mode 100644 index dfa87862..00000000 --- a/jupytercad/freecad/props/property_distance.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -from .base_prop import BaseProp - - -class App_PropertyDistance(BaseProp): - @staticmethod - def name() -> str: - return "App::PropertyDistance" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - return prop_value.Value - - @staticmethod - def jcad_to_fc(prop_value: Any, **kwargs) -> Any: - return prop_value diff --git a/jupytercad/freecad/props/property_geometrylist.py b/jupytercad/freecad/props/property_geometrylist.py deleted file mode 100644 index 47c215e0..00000000 --- a/jupytercad/freecad/props/property_geometrylist.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Any, List - -from .base_prop import BaseProp -from .geometry import geom_handlers - - -class Part_PropertyGeometryList(BaseProp): - @staticmethod - def name() -> str: - return "Part::PropertyGeometryList" - - @staticmethod - def fc_to_jcad(prop_value: List, **kwargs) -> Any: - ret = [] - for geo in prop_value: - if geo.TypeId in geom_handlers: - ret.append(geom_handlers[geo.TypeId].fc_to_jcad(geo)) - return ret - - @staticmethod - def jcad_to_fc(prop_value: List, fc_prop: List = [], **kwargs) -> Any: - # We do not handle the case of adding or removing yet. - n_objects = len(fc_prop) - - n_new_objects = len(prop_value) - if n_objects > 0 and n_objects == n_new_objects: - # Update existing geometries - for idx, jcad_geo in enumerate(prop_value): - if jcad_geo["TypeId"] in geom_handlers: - geom_handlers[jcad_geo["TypeId"]].jcad_to_fc( - jcad_geo, fc_object=fc_prop[idx] - ) - return fc_prop - - if n_objects == 0 and n_new_objects > 0: - # Create new geometries - for jcad_geo in prop_value: - if jcad_geo["TypeId"] in geom_handlers: - fc_geo = geom_handlers[jcad_geo["TypeId"]].jcad_to_fc( - jcad_geo, fc_object=None - ) - - fc_prop.append(fc_geo) - - return fc_prop - - return None diff --git a/jupytercad/freecad/props/property_length.py b/jupytercad/freecad/props/property_length.py deleted file mode 100644 index 21c88cf2..00000000 --- a/jupytercad/freecad/props/property_length.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -from .base_prop import BaseProp - - -class App_PropertyLength(BaseProp): - @staticmethod - def name() -> str: - return "App::PropertyLength" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - return prop_value.Value - - @staticmethod - def jcad_to_fc(prop_value: Any, **kwargs) -> Any: - return prop_value diff --git a/jupytercad/freecad/props/property_link.py b/jupytercad/freecad/props/property_link.py deleted file mode 100644 index 331cc33c..00000000 --- a/jupytercad/freecad/props/property_link.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Any - -from .base_prop import BaseProp - - -class App_PropertyLink(BaseProp): - @staticmethod - def name() -> str: - return "App::PropertyLink" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - return prop_value.Name - - @staticmethod - def jcad_to_fc(prop_value: str, fc_file=None, **kwargs) -> Any: - if prop_value is None: - return None - return fc_file.getObject(prop_value) diff --git a/jupytercad/freecad/props/property_link_list.py b/jupytercad/freecad/props/property_link_list.py deleted file mode 100644 index 14ff7fcb..00000000 --- a/jupytercad/freecad/props/property_link_list.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Any, List - -from .base_prop import BaseProp - - -class App_PropertyLinkList(BaseProp): - @staticmethod - def name() -> str: - return "App::PropertyLinkList" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - return [o.Name for o in prop_value] - - @staticmethod - def jcad_to_fc(prop_value: List, fc_file=None, **kwargs) -> Any: - if prop_value is None: - return None - return [fc_file.getObject(name) for name in prop_value] diff --git a/jupytercad/freecad/props/property_map.py b/jupytercad/freecad/props/property_map.py deleted file mode 100644 index 2829f202..00000000 --- a/jupytercad/freecad/props/property_map.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -from .base_prop import BaseProp - - -class App_PropertyMap(BaseProp): - @staticmethod - def name() -> str: - return "App::PropertyMap" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - return prop_value - - @staticmethod - def jcad_to_fc(prop_value: Any, **kwargs) -> Any: - return prop_value diff --git a/jupytercad/freecad/props/property_partshape.py b/jupytercad/freecad/props/property_partshape.py deleted file mode 100644 index d49e3153..00000000 --- a/jupytercad/freecad/props/property_partshape.py +++ /dev/null @@ -1,21 +0,0 @@ -from io import StringIO -from typing import Any - -from .base_prop import BaseProp - - -class Part_PropertyPartShape(BaseProp): - @staticmethod - def name() -> str: - return "Part::PropertyPartShape" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - buffer = StringIO() - prop_value.exportBrep(buffer) - return buffer.getvalue() - - @staticmethod - def jcad_to_fc(prop_value: str, **kwargs) -> Any: - """PropertyPartShape is readonly""" - return None diff --git a/jupytercad/freecad/props/property_placement.py b/jupytercad/freecad/props/property_placement.py deleted file mode 100644 index 1ea2e2f4..00000000 --- a/jupytercad/freecad/props/property_placement.py +++ /dev/null @@ -1,45 +0,0 @@ -import math -from typing import Any - -from ..tools import redirect_stdout_stderr - -from .base_prop import BaseProp - - -with redirect_stdout_stderr(): - try: - import freecad as fc - except ImportError: - fc = None - - -class App_PropertyPlacement(BaseProp): - @staticmethod - def name() -> str: - return "App::PropertyPlacement" - - @staticmethod - def fc_to_jcad(prop_value: Any, **kwargs) -> Any: - return { - "Position": [ - prop_value.Base.x, - prop_value.Base.y, - prop_value.Base.z, - ], - "Axis": [ - prop_value.Rotation.Axis.x, - prop_value.Rotation.Axis.y, - prop_value.Rotation.Axis.z, - ], - "Angle": 180 * prop_value.Rotation.Angle / math.pi, - } - - @staticmethod - def jcad_to_fc(prop_value: Any, **kwargs) -> Any: - if not fc: - return - - base = fc.app.Base.Vector(prop_value["Position"]) - axis = fc.app.Base.Vector(prop_value["Axis"]) - angle = prop_value["Angle"] - return fc.app.Placement(base, axis, angle) diff --git a/jupytercad/freecad/tools.py b/jupytercad/freecad/tools.py deleted file mode 100644 index 504d575f..00000000 --- a/jupytercad/freecad/tools.py +++ /dev/null @@ -1,10 +0,0 @@ -from contextlib import contextmanager, redirect_stderr, redirect_stdout -from os import devnull - - -@contextmanager -def redirect_stdout_stderr(): - """A context manager that redirects stdout and stderr to devnull""" - with open(devnull, "w") as fnull: - with redirect_stderr(fnull) as err, redirect_stdout(fnull) as out: - yield (err, out) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 97bd1df6..fd620abb 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -21,6 +21,7 @@ import { cylinderIcon, explodedViewIcon, extrusionIcon, + gridIcon, intersectionIcon, sphereIcon, torusIcon, @@ -302,6 +303,47 @@ const OPERATORS = { setVisible(sharedModel, shape, false); }); + if (!sharedModel.objectExists(objectModel.name)) { + sharedModel.addObject(objectModel); + } else { + showErrorMessage( + 'The object already exists', + 'There is an existing object with the same name.' + ); + } + }); + } + }; + } + }, + mesh: { + title: 'Mesh parameters', + shape: 'Post::Operator', + default: (model: IJupyterCadModel) => { + const objects = model.getAllObject(); + const selected = model.localState?.selected.value || []; + return { + Name: newName('Mesh', model), + Object: selected.length > 0 ? selected[0] : objects[0].name ?? '', + NumberOfSegment: 15 + }; + }, + syncData: (model: IJupyterCadModel) => { + return (props: IDict) => { + const { Name, ...parameters } = props; + const objectModel: IJCadObject = { + shape: 'Post::Operator', + parameters, + visible: true, + name: Name + }; + const sharedModel = model.sharedModel; + if (sharedModel) { + sharedModel.transact(() => { + if (parameters['Object'].length > 0) { + setVisible(sharedModel, parameters['Object'], false); + } + if (!sharedModel.objectExists(objectModel.name)) { sharedModel.addObject(objectModel); } else { @@ -587,6 +629,13 @@ export function addCommands( execute: Private.executeOperator('intersection', tracker) }); + commands.addCommand(CommandIDs.mesh, { + label: trans.__('Mesh creation'), + isEnabled: () => Boolean(tracker.currentWidget), + icon: gridIcon, + execute: Private.executeOperator('mesh', tracker) + }); + commands.addCommand(CommandIDs.updateAxes, { label: trans.__('Axes Helper'), isEnabled: () => Boolean(tracker.currentWidget), @@ -676,6 +725,7 @@ export namespace CommandIDs { export const extrusion = 'jupytercad:extrusion'; export const union = 'jupytercad:union'; export const intersection = 'jupytercad:intersection'; + export const mesh = 'jupytercad:mesh'; export const updateAxes = 'jupytercad:updateAxes'; export const updateExplodedView = 'jupytercad:updateExplodedView'; diff --git a/packages/base/src/fcplugin/modelfactory.ts b/packages/base/src/fcplugin/modelfactory.ts deleted file mode 100644 index 2f5774b9..00000000 --- a/packages/base/src/fcplugin/modelfactory.ts +++ /dev/null @@ -1,101 +0,0 @@ -// import { -// IAnnotationModel, -// IJupyterCadDoc, -// JupyterCadModel -// } from '@jupytercad/schema'; -// import { DocumentRegistry } from '@jupyterlab/docregistry'; -// import { Contents } from '@jupyterlab/services'; - -// /** -// * A Model factory to create new instances of JupyterCadModel. -// */ -// export class JupyterCadFCModelFactory -// implements DocumentRegistry.IModelFactory -// { -// constructor(options: JupyterCadFCModelFactory.IOptions) { -// this._annotationModel = options.annotationModel; -// } - -// /** -// * Whether the model is collaborative or not. -// */ -// readonly collaborative = true; - -// /** -// * The name of the model. -// * -// * @returns The name -// */ -// get name(): string { -// return 'jupytercad-fcmodel'; -// } - -// /** -// * The content type of the file. -// * -// * @returns The content type -// */ -// get contentType(): Contents.ContentType { -// return 'FCStd'; -// } - -// /** -// * The format of the file. -// * -// * @returns the file format -// */ -// get fileFormat(): Contents.FileFormat { -// return 'base64'; -// } - -// /** -// * Get whether the model factory has been disposed. -// * -// * @returns disposed status -// */ -// get isDisposed(): boolean { -// return this._disposed; -// } - -// /** -// * Dispose the model factory. -// */ -// dispose(): void { -// this._disposed = true; -// } - -// /** -// * Get the preferred language given the path on the file. -// * -// * @param path path of the file represented by this document model -// * @returns The preferred language -// */ -// preferredLanguage(path: string): string { -// return ''; -// } - -// /** -// * Create a new instance of JupyterCadModel. -// * -// * @returns The model -// */ -// createNew( -// options: DocumentRegistry.IModelOptions -// ): JupyterCadModel { -// const model = new JupyterCadModel({ -// sharedModel: options.sharedModel, -// languagePreference: options.languagePreference, -// annotationModel: this._annotationModel -// }); -// return model; -// } - -// private _annotationModel: IAnnotationModel; -// private _disposed = false; -// } - -// export namespace JupyterCadFCModelFactory { -// export interface IOptions { -// annotationModel: IAnnotationModel; -// } -// } diff --git a/packages/base/src/fcplugin/plugins.ts b/packages/base/src/fcplugin/plugins.ts deleted file mode 100644 index 961ecc18..00000000 --- a/packages/base/src/fcplugin/plugins.ts +++ /dev/null @@ -1,177 +0,0 @@ -// import { -// ICollaborativeDrive, -// SharedDocumentFactory -// } from '@jupyter/docprovider'; -// import { IAnnotationModel, JupyterCadDoc } from '@jupytercad/schema'; -// import { -// JupyterFrontEnd, -// JupyterFrontEndPlugin -// } from '@jupyterlab/application'; -// import { -// ICommandPalette, -// IThemeManager, -// showErrorMessage, -// WidgetTracker -// } from '@jupyterlab/apputils'; -// import { IFileBrowserFactory } from '@jupyterlab/filebrowser'; -// import { ILauncher } from '@jupyterlab/launcher'; -// import { fileIcon } from '@jupyterlab/ui-components'; - -// import { JupyterCadWidgetFactory } from '../factory'; -// import { IAnnotationToken, IJupyterCadDocTracker } from '../token'; -// import { requestAPI } from '../tools'; -// import { IJupyterCadWidget } from '../types'; -// import { JupyterCadFCModelFactory } from './modelfactory'; - -// const FACTORY = 'Jupytercad Freecad Factory'; - -// // const PALETTE_CATEGORY = 'JupyterCAD'; - -// namespace CommandIDs { -// export const createNew = 'jupytercad:create-new-FCStd-file'; -// } - -// const activate = async ( -// app: JupyterFrontEnd, -// tracker: WidgetTracker, -// themeManager: IThemeManager, -// annotationModel: IAnnotationModel, -// browserFactory: IFileBrowserFactory, -// drive: ICollaborativeDrive, -// launcher: ILauncher | null, -// palette: ICommandPalette | null -// ): Promise => { -// const fcCheck = await requestAPI<{ installed: boolean }>( -// 'cad/backend-check', -// { -// method: 'POST', -// body: JSON.stringify({ -// backend: 'FreeCAD' -// }) -// } -// ); -// const { installed } = fcCheck; -// const backendCheck = () => { -// if (!installed) { -// showErrorMessage( -// 'FreeCAD is not installed', -// 'FreeCAD is required to open FCStd files' -// ); -// } -// return installed; -// }; -// const widgetFactory = new JupyterCadWidgetFactory({ -// name: FACTORY, -// modelName: 'jupytercad-fcmodel', -// fileTypes: ['FCStd'], -// defaultFor: ['FCStd'], -// tracker, -// commands: app.commands, -// backendCheck -// }); - -// // Registering the widget factory -// app.docRegistry.addWidgetFactory(widgetFactory); - -// // Creating and registering the model factory for our custom DocumentModel -// const modelFactory = new JupyterCadFCModelFactory({ annotationModel }); -// app.docRegistry.addModelFactory(modelFactory); -// // register the filetype -// app.docRegistry.addFileType({ -// name: 'FCStd', -// displayName: 'FCStd', -// mimeTypes: ['application/octet-stream'], -// extensions: ['.FCStd', 'fcstd'], -// fileFormat: 'base64', -// contentType: 'FCStd' -// }); - -// const FCStdSharedModelFactory: SharedDocumentFactory = () => { -// return new JupyterCadDoc(); -// }; -// drive.sharedModelFactory.registerDocumentFactory( -// 'FCStd', -// FCStdSharedModelFactory -// ); - -// widgetFactory.widgetCreated.connect((sender, widget) => { -// // Notify the instance tracker if restore data needs to update. -// widget.context.pathChanged.connect(() => { -// tracker.save(widget); -// }); -// themeManager.themeChanged.connect((_, changes) => -// widget.context.model.themeChanged.emit(changes) -// ); - -// tracker.add(widget); -// app.shell.activateById('jupytercad::leftControlPanel'); -// app.shell.activateById('jupytercad::rightControlPanel'); -// }); - -// app.commands.addCommand(CommandIDs.createNew, { -// label: args => (args['isPalette'] ? 'New FCStd Editor' : 'FCStd Editor'), -// caption: 'Create a new FCStd Editor', -// icon: args => (args['isPalette'] ? undefined : fileIcon), -// execute: async args => { -// // Get the directory in which the FCStd file must be created; -// // otherwise take the current filebrowser directory -// const cwd = (args['cwd'] || -// browserFactory.tracker.currentWidget?.model.path) as string; - -// // Create a new untitled Blockly file -// let model = await app.serviceManager.contents.newUntitled({ -// path: cwd, -// type: 'file', -// ext: '.FCStd' -// }); - -// console.debug('Model:', model); -// model = await app.serviceManager.contents.save(model.path, { -// ...model, -// format: 'base64', -// size: undefined, -// content: btoa('') -// }); - -// // Open the newly created file with the 'Editor' -// return app.commands.execute('docmanager:open', { -// path: model.path, -// factory: FACTORY -// }); -// } -// }); - -// // Add the command to the launcher -// if (launcher) { -// /* launcher.add({ -// command: CommandIDs.createNew, -// category: 'Other', -// rank: 1 -// }); */ -// } - -// // Add the command to the palette -// if (palette) { -// /* palette.addItem({ -// command: CommandIDs.createNew, -// args: { isPalette: true }, -// category: PALETTE_CATEGORY -// }); */ -// } -// }; - -// const fcplugin: JupyterFrontEndPlugin = { -// id: 'jupytercad:fcplugin', -// requires: [ -// IJupyterCadDocTracker, -// IThemeManager, -// IAnnotationToken, -// IFileBrowserFactory, -// ICollaborativeDrive -// ], -// optional: [ILauncher, ICommandPalette], -// autoStart: true, -// activate -// }; - -// export default fcplugin; diff --git a/packages/base/src/mainview.tsx b/packages/base/src/mainview.tsx index 4c394d4a..6888d0dc 100644 --- a/packages/base/src/mainview.tsx +++ b/packages/base/src/mainview.tsx @@ -1,12 +1,5 @@ import { MapChange } from '@jupyter/ydoc'; -import { - IDisplayShape, - IMainMessage, - IWorkerMessage, - MainAction, - OCC_WORKER_ID, - WorkerAction -} from '@jupytercad/occ-worker'; +import { IWorkerMessage } from '@jupytercad/occ-worker'; import { IAnnotation, IDict, @@ -15,7 +8,11 @@ import { IJCadWorkerRegistry, IJupyterCadClientState, IJupyterCadDoc, - IJupyterCadModel + IJupyterCadModel, + MainAction, + IDisplayShape, + IMainMessage, + WorkerAction } from '@jupytercad/schema'; import { IObservableMap, ObservableMap } from '@jupyterlab/observables'; import { User } from '@jupyterlab/services'; @@ -31,6 +28,8 @@ import { disposeBoundsTree } from 'three-mesh-bvh'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; +import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'; +// import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' import { v4 as uuid } from 'uuid'; import { FloatingAnnotation } from './annotation/view'; @@ -101,21 +100,6 @@ export class MainView extends React.Component { this.props.view.changed.connect(this._onViewChanged, this); - const lightTheme = - document.body.getAttribute('data-jp-theme-light') === 'true'; - - this._worker = props.workerRegistry.getWorker(OCC_WORKER_ID)!; - const id = this._worker.initChannel(); - this._worker.registerHandler(id, this.messageHandler.bind(this)); - - this.state = { - id, - lightTheme, - loading: true, - annotations: {}, - firstLoad: true - }; - this._model = this.props.jcadModel; this._pointer = new THREE.Vector2(); @@ -142,6 +126,26 @@ export class MainView extends React.Component { if (this._raycaster.params.Line) { this._raycaster.params.Line.threshold = 0.1; } + + this._worker = props.workerRegistry.getDefaultWorker(); + const id = this._worker.register({ + messageHandler: this.messageHandler.bind(this) + }); + props.workerRegistry.getAllWorkers().forEach(wk => { + const id = wk.register({ + messageHandler: this.postProcessWorkerHandler.bind(this) + }); + this._postWorkerId.set(id, wk); + }); + const lightTheme = + document.body.getAttribute('data-jp-theme-light') === 'true'; + this.state = { + id, + lightTheme, + loading: true, + annotations: {}, + firstLoad: true + }; } componentDidMount(): void { @@ -387,8 +391,16 @@ export class MainView extends React.Component { messageHandler = (msg: IMainMessage): void => { switch (msg.action) { case MainAction.DISPLAY_SHAPE: { - this._saveMeta(msg.payload); - this._shapeToMesh(msg.payload); + this._saveMeta(msg.payload.result); + this._shapeToMesh(msg.payload.result); + this._postWorkerId.forEach((wk, id) => { + wk.postMessage({ + id, + action: WorkerAction.POSTPROCESS, + payload: msg.payload.postResult + }); + }); + break; } case MainAction.INITIALIZED: { @@ -405,6 +417,35 @@ export class MainView extends React.Component { } }; + postProcessWorkerHandler = (msg: IMainMessage): void => { + switch (msg.action) { + case MainAction.DISPLAY_POST: { + msg.payload.forEach(element => { + const { jcObject, postResult } = element; + const geoObj = postResult.mesh; + const loader = new STLLoader(); + const obj = loader.parse(geoObj); + const material = new THREE.MeshPhongMaterial({ + color: DEFAULT_MESH_COLOR, + polygonOffset: true, + polygonOffsetFactor: 1, + polygonOffsetUnits: 1 + }); + const mesh = new THREE.Mesh(obj, material); + + const lineGeo = new THREE.WireframeGeometry(mesh.geometry); + const mat = new THREE.LineBasicMaterial({ color: 'black' }); + const wireframe = new THREE.LineSegments(lineGeo, mat); + mesh.add(wireframe); + mesh.name = jcObject.name; + mesh.visible = true; + this._meshGroup?.add(mesh); + }); + break; + } + } + }; + private _projectVector = (vector: THREE.Vector3): THREE.Vector2 => { const copy = new THREE.Vector3().copy(vector); const canvas = this._renderer.domElement; @@ -535,7 +576,7 @@ export class MainView extends React.Component { this._model.syncSelectedObject(names, this.state.id); } } - private _saveMeta = (payload: IDisplayShape['payload']) => { + private _saveMeta = (payload: IDisplayShape['payload']['result']) => { if (!this._model) { return; } @@ -543,7 +584,7 @@ export class MainView extends React.Component { this._model.sharedModel.setShapeMeta(objName, data.meta); }); }; - private _shapeToMesh = (payload: IDisplayShape['payload']) => { + private _shapeToMesh = (payload: IDisplayShape['payload']['result']) => { if (this._meshGroup !== null) { this._scene.remove(this._meshGroup); } @@ -749,8 +790,9 @@ export class MainView extends React.Component { const rgba = guidata[selectedMesh.name]['color'] as number[]; originalColor = new THREE.Color(rgba[0], rgba[1], rgba[2]); } - - selectedMesh.material.color = originalColor; + if (selectedMesh?.material?.color) { + selectedMesh.material.color = originalColor; + } } // Set new selection @@ -764,7 +806,9 @@ export class MainView extends React.Component { } this._selectedMeshes.push(selected); - selected.material.color = SELECTED_MESH_COLOR; + if (selected?.material?.color) { + selected.material.color = SELECTED_MESH_COLOR; + } } } @@ -951,12 +995,14 @@ export class MainView extends React.Component { } } } - if ('color' in guidata[objName]) { - const rgba = guidata[objName]['color'] as number[]; - const color = new THREE.Color(rgba[0], rgba[1], rgba[2]); - obj.material.color = color; - } else { - obj.material.color = DEFAULT_MESH_COLOR; + if (obj.material.color) { + if ('color' in guidata[objName]) { + const rgba = guidata[objName]['color'] as number[]; + const color = new THREE.Color(rgba[0], rgba[1], rgba[2]); + obj.material.color = color; + } else { + obj.material.color = DEFAULT_MESH_COLOR; + } } } } @@ -1233,4 +1279,5 @@ export class MainView extends React.Component { private _collaboratorPointers: IDict; private _pointerGeometry: THREE.SphereGeometry; private _contextMenu: ContextMenu; + private _postWorkerId: Map = new Map(); } diff --git a/packages/base/src/panelview/objectproperties.tsx b/packages/base/src/panelview/objectproperties.tsx index dfa7ecd4..6f942666 100644 --- a/packages/base/src/panelview/objectproperties.tsx +++ b/packages/base/src/panelview/objectproperties.tsx @@ -1,12 +1,12 @@ import { IDict, + IJCadFormSchemaRegistry, IJCadModel, IJcadObjectDocChange, IJupyterCadClientState, IJupyterCadDoc, IJupyterCadModel } from '@jupytercad/schema'; -import formSchema from '@jupytercad/schema/lib/_interface/forms.json'; import { ReactWidget } from '@jupyterlab/apputils'; import { PanelWithToolbar } from '@jupyterlab/ui-components'; import { Panel } from '@lumino/widgets'; @@ -26,7 +26,10 @@ export class ObjectProperties extends PanelWithToolbar { super(params); this.title.label = 'Objects Properties'; const body = ReactWidget.create( - + ); this.addWidget(body); this.addClass('jpcad-sidebar-propertiespanel'); @@ -47,6 +50,7 @@ interface IStates { interface IProps { cpModel: IControlPanelModel; + formSchemaRegistry: IJCadFormSchemaRegistry; } class ObjectPropertiesReact extends React.Component { @@ -58,6 +62,7 @@ class ObjectPropertiesReact extends React.Component { clientId: null, id: uuid() }; + this._formSchema = props.formSchemaRegistry.getSchemas(); this.props.cpModel.jcadModel?.sharedObjectsChanged.connect( this._sharedJcadModelChanged ); @@ -219,7 +224,7 @@ class ObjectPropertiesReact extends React.Component { } if (selectedObj.shape) { - schema = formSchema[selectedObj.shape]; + schema = this._formSchema.get(selectedObj.shape); } const selectedObjectData = selectedObj['parameters']; this.setState(old => ({ @@ -251,6 +256,7 @@ class ObjectPropertiesReact extends React.Component { } private _lastSelectedPropFieldId?: string; + private _formSchema: Map; } export namespace ObjectProperties { @@ -259,5 +265,6 @@ export namespace ObjectProperties { */ export interface IOptions extends Panel.IOptions { controlPanelModel: IControlPanelModel; + formSchemaRegistry: IJCadFormSchemaRegistry; } } diff --git a/packages/base/src/panelview/rightpanel.tsx b/packages/base/src/panelview/rightpanel.tsx index fe49a38b..f9226bf6 100644 --- a/packages/base/src/panelview/rightpanel.tsx +++ b/packages/base/src/panelview/rightpanel.tsx @@ -1,4 +1,4 @@ -import { JupyterCadDoc } from '@jupytercad/schema'; +import { IJCadFormSchemaRegistry, JupyterCadDoc } from '@jupytercad/schema'; import { SidePanel } from '@jupyterlab/ui-components'; import { IControlPanelModel } from '../types'; @@ -13,7 +13,8 @@ export class RightPanelWidget extends SidePanel { const header = new ControlPanelHeader(); this.header.addWidget(header); const properties = new ObjectProperties({ - controlPanelModel: this._model + controlPanelModel: this._model, + formSchemaRegistry: options.formSchemaRegistry }); this.addWidget(properties); @@ -35,6 +36,7 @@ export class RightPanelWidget extends SidePanel { export namespace RightPanelWidget { export interface IOptions { model: IControlPanelModel; + formSchemaRegistry: IJCadFormSchemaRegistry; } export interface IProps { filePath?: string; diff --git a/packages/base/src/toolbar/widget.tsx b/packages/base/src/toolbar/widget.tsx index 317a487a..19253222 100644 --- a/packages/base/src/toolbar/widget.tsx +++ b/packages/base/src/toolbar/widget.tsx @@ -170,7 +170,15 @@ export class ToolbarWidget extends Toolbar { commands: options.commands }) ); - + this.addItem('separator5', new Separator()); + this.addItem( + 'Mesh', + new CommandToolbarButton({ + id: CommandIDs.mesh, + label: '', + commands: options.commands + }) + ); this.addItem('spacer', Toolbar.createSpacerItem()); // Users diff --git a/packages/base/src/tools.ts b/packages/base/src/tools.ts index ae80f770..004ab4e0 100644 --- a/packages/base/src/tools.ts +++ b/packages/base/src/tools.ts @@ -15,6 +15,7 @@ import minimizeIconStr from '../style/icon/minimize.svg'; import sphereIconStr from '../style/icon/sphere.svg'; import torusIconStr from '../style/icon/torus.svg'; import unionIconStr from '../style/icon/union.svg'; +import gridIconStr from '../style/icon/grid.svg'; export const jcLightIcon = new LabIcon({ name: 'jupytercad:control-light', @@ -81,6 +82,11 @@ export const explodedViewIcon = new LabIcon({ svgstr: jvControlLight }); +export const gridIcon = new LabIcon({ + name: 'jupytercad:grid-icon', + svgstr: gridIconStr +}); + export const debounce = ( func: CallableFunction, timeout = 100 diff --git a/packages/base/style/icon/grid.svg b/packages/base/style/icon/grid.svg new file mode 100644 index 00000000..6358c8d6 --- /dev/null +++ b/packages/base/style/icon/grid.svg @@ -0,0 +1,6 @@ + + + diff --git a/packages/occ-worker/package.json b/packages/occ-worker/package.json index 5101281a..6876db34 100644 --- a/packages/occ-worker/package.json +++ b/packages/occ-worker/package.json @@ -31,7 +31,9 @@ "clean": "rimraf tsconfig.tsbuildinfo", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", "clean:all": "jlpm run clean:lib", - "watch": "webpack --config worker.webpack.config.js --watch --mode=development" + "watch:webpack": "webpack --config worker.webpack.config.js --watch --mode=development", + "watch:lib": "tsc -w", + "watch": "tsc-watch --onSuccess \"webpack --config worker.webpack.config.js --mode=development\"" }, "dependencies": { "@jupytercad/opencascade": "^1.0.0-alpha.2", @@ -47,6 +49,7 @@ "rimraf": "^3.0.2", "source-map-loader": "^3.0.0", "ts-loader": "^9.2.6", + "tsc-watch": "^6.0.0", "typescript": "^5", "webpack": "^5.76.3" }, diff --git a/packages/occ-worker/src/actions.ts b/packages/occ-worker/src/actions.ts index 7e8f6562..8c7b38e7 100644 --- a/packages/occ-worker/src/actions.ts +++ b/packages/occ-worker/src/actions.ts @@ -1,8 +1,14 @@ import { OCC } from '@jupytercad/opencascade'; -import { IJCadContent, IJCadObject } from '@jupytercad/schema'; +import { + IDict, + IJCadContent, + IJCadObject, + IPostOperatorInput, + WorkerAction +} from '@jupytercad/schema'; -import { IDict, WorkerAction } from './types'; import { ObjectFile, ShapesFactory } from './occapi'; + import { OccParser } from './occparser'; import { IOperatorArg, IOperatorFuncOutput } from './types'; @@ -51,9 +57,19 @@ function buildModel( function loadFile(payload: { content: IJCadContent }): IDict | null { const { content } = payload; const outputModel = buildModel(content); + const parser = new OccParser(outputModel); const result = parser.execute(); - return result; + const postResult: IDict = {}; + outputModel.forEach(item => { + if (item.jcObject.shape?.startsWith('Post::')) { + postResult[item.jcObject.name] = { + jcObject: item.jcObject, + occBrep: item.shapeData.occBrep + }; + } + }); + return { result, postResult }; } const WorkerHandler: { diff --git a/packages/occ-worker/src/occapi.ts b/packages/occ-worker/src/occapi.ts index fdb28ac6..fdd60cbb 100644 --- a/packages/occ-worker/src/occapi.ts +++ b/packages/occ-worker/src/occapi.ts @@ -143,7 +143,7 @@ function _Cut(arg: ICut, content: IJCadContent): OCC.TopoDS_Shape | undefined { toolObject[0].parameters as IOperatorArg, content ); - if (base && tool) { + if (base && tool && base.occShape && tool.occShape) { baseObject[0].visible = false; toolObject[0].visible = false; const operator = new oc.BRepAlgoAPI_Cut_3( @@ -176,7 +176,7 @@ function _Fuse( baseObject[0].parameters as IOperatorArg, content ); - if (base) { + if (base && base.occShape) { occShapes.push(base.occShape); baseObject[0].visible = false; } @@ -211,7 +211,7 @@ function _Intersection( baseObject[0].parameters as IOperatorArg, content ); - if (base) { + if (base && base.occShape) { occShapes.push(base.occShape); baseObject[0].visible = false; } @@ -274,7 +274,7 @@ function _Extrude( baseObject[0].parameters as IOperatorArg, content ); - if (!base) { + if (!base || !base.occShape) { return; } const dirVec = new oc.gp_Vec_4(Dir[0], Dir[1], Dir[2]); @@ -335,6 +335,28 @@ export function _Any( } } +export function _PostOperator( + arg: any, + content: IJCadContent +): { occBrep: string } { + const baseObject = content.objects.filter(obj => obj.name === arg.Object); + if (baseObject.length === 0) { + return { occBrep: '' }; + } + const baseShape = baseObject[0].shape; + if (baseShape && ShapesFactory[baseShape]) { + const base = ShapesFactory[baseShape]( + baseObject[0].parameters as IOperatorArg, + content + ); + if (base?.occShape) { + const occBrep = _writeBrep(base.occShape); + return { occBrep }; + } + } + return { occBrep: '' }; +} + export function _loadBrepFile(content: string): OCC.TopoDS_Shape | undefined { const oc = getOcc(); const fakeFileName = `${uuid()}.brep`; @@ -381,6 +403,23 @@ export function _loadObjectFile(arg: { } } +export function _writeBrep(shape: OCC.TopoDS_Shape): string { + const oc = getOcc(); + const fakeFileName = `${uuid()}.brep`; + const progress = new oc.Message_ProgressRange_1(); + oc.BRepTools.Write_4( + shape, + fakeFileName, + false, + false, + oc.TopTools_FormatVersion.TopTools_FormatVersion_VERSION_1 as any, + progress + ); + const value = oc.FS.readFile('/' + fakeFileName, { encoding: 'utf8' }); + oc.FS.unlink('/' + fakeFileName); + return value; +} + const Any = operatorCache('Part::Any', _Any); const Box = operatorCache('Part::Box', _Box); @@ -426,5 +465,6 @@ export const ShapesFactory: { 'Part::MultiFuse': Fuse, 'Part::Extrusion': Extrude, 'Part::MultiCommon': Intersection, - 'Sketcher::SketchObject': SketchObject + 'Sketcher::SketchObject': SketchObject, + 'Post::Operator': _PostOperator }; diff --git a/packages/occ-worker/src/occparser.ts b/packages/occ-worker/src/occparser.ts index 0229a5f7..145a5786 100644 --- a/packages/occ-worker/src/occparser.ts +++ b/packages/occ-worker/src/occparser.ts @@ -1,13 +1,7 @@ import { OCC } from '@jupytercad/opencascade'; -import { IJCadObject } from '@jupytercad/schema'; +import { IEdge, IFace, IJCadObject, IParsedShape } from '@jupytercad/schema'; -import { - IDict, - IEdge, - IFace, - IOperatorFuncOutput, - IParsedShape -} from './types'; +import { IDict, IOperatorFuncOutput } from './types'; interface IShapeList { shapeData: IOperatorFuncOutput; @@ -28,6 +22,9 @@ export class OccParser { this._shapeList.forEach(data => { const { shapeData, jcObject } = data; const { occShape, metadata } = shapeData; + if (!occShape) { + return; + } new this._occ.BRepMesh_IncrementalMesh_2( occShape, maxDeviation, diff --git a/packages/occ-worker/src/occworker.ts b/packages/occ-worker/src/occworker.ts index fcccb8c8..6243ea4d 100644 --- a/packages/occ-worker/src/occworker.ts +++ b/packages/occ-worker/src/occworker.ts @@ -1,11 +1,12 @@ -import { IJCadWorker } from '@jupytercad/schema'; +import { + IJCadWorker, + IMessageHandler, + MainAction, + WorkerAction +} from '@jupytercad/schema'; import { PromiseDelegate } from '@lumino/coreutils'; import { v4 as uuid } from 'uuid'; -import { MainAction, WorkerAction } from './types'; - -export const OCC_WORKER_ID = 'jupytercadOccWorker'; - export class OccWorker implements IJCadWorker { constructor(options: OccWorker.IOptions) { this._nativeWorker = options.worker; @@ -15,10 +16,17 @@ export class OccWorker implements IJCadWorker { return this._ready.promise; } - initChannel(): string { - const messageChannel = new MessageChannel(); - messageChannel.port1.onmessage = this._processMessage.bind(this); + register(options: { + messageHandler: IMessageHandler; + thisArg?: any; + }): string { + const { messageHandler, thisArg } = options; const id = uuid(); + const messageChannel = new MessageChannel(); + if (thisArg) { + messageHandler.bind(thisArg); + } + messageChannel.port1.onmessage = this._handlerFactory(messageHandler); this._messageChannels.set(id, messageChannel); const initMessage = { id, @@ -26,42 +34,28 @@ export class OccWorker implements IJCadWorker { payload: { id } }; this._nativeWorker.postMessage(initMessage, [messageChannel.port2]); + return id; } - removeChannel(id: string): void { - this._handlers.delete(id); - this._messageChannels.delete(id); - } - registerHandler( - id: string, - messageHandler: ((msg: any) => void) | ((msg: any) => Promise), - thisArg?: any - ): void { - if (!this._handlers.has(id)) { - if (thisArg) { - messageHandler.bind(thisArg); - } - this._handlers.set(id, messageHandler); - } else { - console.error( - `${id} is already registered, remove the handler first before re-registering ` - ); - } + unregister(id: string): void { + this._messageChannels.delete(id); } postMessage(msg: { id: string; [key: string]: any }): void { this._nativeWorker.postMessage(msg); } - private _processMessage(msg: any): void { - if (msg.data.action === MainAction.INITIALIZED) { - this._ready.resolve(); - } - this._handlers.forEach(cb => cb(msg.data)); + private _handlerFactory(messageHandler: IMessageHandler) { + return (msg: any) => { + if (msg.data.action === MainAction.INITIALIZED) { + this._ready.resolve(); + } + messageHandler(msg.data); + }; } + private _ready = new PromiseDelegate(); - private _handlers = new Map(); private _messageChannels = new Map(); private _nativeWorker: Worker; } diff --git a/packages/occ-worker/src/types.ts b/packages/occ-worker/src/types.ts index ec20098a..1bfeff15 100644 --- a/packages/occ-worker/src/types.ts +++ b/packages/occ-worker/src/types.ts @@ -9,11 +9,13 @@ import { IFuse, IIntersection, IJCadContent, - IJCadObject, IShapeMetadata, ISketchObject, ISphere, - ITorus + ITorus, + IPostOperator, + WorkerAction, + IWorkerMessageBase } from '@jupytercad/schema'; export interface IDict { @@ -22,27 +24,14 @@ export interface IDict { export type ValueOf = T[keyof T]; -/** - * Action definitions for worker - */ -export enum WorkerAction { - LOAD_FILE = 'LOAD_FILE', - SAVE_FILE = 'SAVE_FILE', - REGISTER = 'REGISTER' -} - -interface IMainId { - id: string; -} - -export interface IRegister extends IMainId { +export interface IRegister extends IWorkerMessageBase { action: WorkerAction.REGISTER; payload: { id: string; }; } -export interface ILoadFile extends IMainId { +export interface ILoadFile extends IWorkerMessageBase { action: WorkerAction.LOAD_FILE; payload: { content: IJCadContent; @@ -51,46 +40,10 @@ export interface ILoadFile extends IMainId { export type IWorkerMessage = ILoadFile | IRegister; -/** - * Action definitions for main thread - */ -export enum MainAction { - DISPLAY_SHAPE = 'DISPLAY_SHAPE', - INITIALIZED = 'INITIALIZED' -} - -export interface IFace { - vertexCoord: Array; - normalCoord: Array; - triIndexes: Array; - numberOfTriangles: number; -} - -export interface IEdge { - vertexCoord: number[]; - numberOfCoords: number; -} -export interface IParsedShape { - jcObject: IJCadObject; - faceList: Array; - edgeList: Array; - meta?: IDict; - guiData?: IDict; -} -export interface IDisplayShape { - action: MainAction.DISPLAY_SHAPE; - payload: IDict; -} -export interface IWorkerInitialized { - action: MainAction.INITIALIZED; - payload: boolean; -} - -export type IMainMessage = IDisplayShape | IWorkerInitialized; - export interface IOperatorFuncOutput { - occShape: OCC.TopoDS_Shape; + occShape?: OCC.TopoDS_Shape; metadata?: IShapeMetadata | undefined; + occBrep?: string; } type IOperatorFunc = ( @@ -109,7 +62,9 @@ export type IAllOperatorFunc = | IOperatorFunc | IOperatorFunc | IOperatorFunc - | IOperatorFunc; + | IOperatorFunc + | IOperatorFunc; + export type IOperatorArg = IAny & IBox & ICylinder & @@ -119,4 +74,5 @@ export type IOperatorArg = IAny & ICut & IFuse & IExtrusion & - ISketchObject; + ISketchObject & + IPostOperator; diff --git a/packages/occ-worker/src/worker.ts b/packages/occ-worker/src/worker.ts index 3593b5fd..2593f8bf 100644 --- a/packages/occ-worker/src/worker.ts +++ b/packages/occ-worker/src/worker.ts @@ -1,13 +1,8 @@ import { initializeOpenCascade, OCC } from '@jupytercad/opencascade'; import WorkerHandler from './actions'; -import { - IDict, - IMainMessage, - IWorkerMessage, - MainAction, - WorkerAction -} from './types'; +import { IDict, IWorkerMessage } from './types'; +import { IMainMessage, MainAction, WorkerAction } from '@jupytercad/schema'; let occ: OCC.OpenCascadeInstance; const ports: IDict = {}; diff --git a/packages/opencascade/build.yml b/packages/opencascade/build.yml index 59d9d174..5dba5e2f 100644 --- a/packages/opencascade/build.yml +++ b/packages/opencascade/build.yml @@ -88,6 +88,7 @@ mainBuild: - symbol: TopoDS_Shape - symbol: TopoDS_Shell - symbol: TopoDS_Wire + - symbol: TopTools_FormatVersion - symbol: TopTools_IndexedDataMapOfShapeListOfShape - symbol: TopTools_IndexedMapOfShape - symbol: TopTools_ListOfShape diff --git a/packages/opencascade/package.json b/packages/opencascade/package.json index dbcb9ac3..74880787 100644 --- a/packages/opencascade/package.json +++ b/packages/opencascade/package.json @@ -40,8 +40,5 @@ }, "publishConfig": { "access": "public" - }, - "dependencies": { - "opencascade.js": "beta" } } diff --git a/packages/opencascade/src/index.ts b/packages/opencascade/src/index.ts index 51cef5fb..025934b6 100644 --- a/packages/opencascade/src/index.ts +++ b/packages/opencascade/src/index.ts @@ -1,10 +1,42 @@ -import initOpenCascade from 'opencascade.js'; - -import opencascade from './jupytercad.opencascade'; +import opencascade, { OpenCascadeInstance } from './jupytercad.opencascade'; import opencascadeWasm from './jupytercad.opencascade.wasm'; export * as OCC from './jupytercad.opencascade'; +const initOpenCascade = (options: { + mainJS: any; + mainWasm: any; + worker?: any; + libs?: any[]; + module?: any[]; +}): Promise => { + return new Promise((resolve, reject) => { + const { mainJS, mainWasm, worker, libs = [], module = [] } = options; + new mainJS({ + locateFile(path) { + if (path.endsWith('.wasm')) { + return mainWasm; + } + if (path.endsWith('.worker.js') && !!worker) { + return worker; + } + return path; + }, + ...module + }).then(async oc => { + for (const lib of libs) { + await oc.loadDynamicLibrary(lib, { + loadAsync: true, + global: true, + nodelete: true, + allowUndefined: false + }); + } + resolve(oc); + }); + }); +}; + export async function initializeOpenCascade() { return initOpenCascade({ mainJS: opencascade, diff --git a/packages/opencascade/src/jupytercad.opencascade.d.ts b/packages/opencascade/src/jupytercad.opencascade.d.ts index e3dff3ef..9cbc6632 100644 --- a/packages/opencascade/src/jupytercad.opencascade.d.ts +++ b/packages/opencascade/src/jupytercad.opencascade.d.ts @@ -1 +1,2 @@ export default null as any; +export interface OpenCascadeInstance {} diff --git a/packages/schema/src/interfaces.ts b/packages/schema/src/interfaces.ts index 0125aeaa..4ca1b961 100644 --- a/packages/schema/src/interfaces.ts +++ b/packages/schema/src/interfaces.ts @@ -182,17 +182,92 @@ export interface IAnnotation { parent: string; } +export interface IFace { + vertexCoord: Array; + normalCoord: Array; + triIndexes: Array; + numberOfTriangles: number; +} + +export interface IEdge { + vertexCoord: number[]; + numberOfCoords: number; +} +export interface IParsedShape { + jcObject: IJCadObject; + faceList: Array; + edgeList: Array; + meta?: IDict; + guiData?: IDict; +} + +export interface IPostOperatorInput { + jcObject: IJCadObject; + occBrep?: string; +} + +/** + * Action definitions for worker + */ +export enum WorkerAction { + LOAD_FILE = 'LOAD_FILE', + SAVE_FILE = 'SAVE_FILE', + REGISTER = 'REGISTER', + POSTPROCESS = 'POSTPROCESS' +} + +/** + * Action definitions for main thread + */ +export enum MainAction { + DISPLAY_SHAPE = 'DISPLAY_SHAPE', + INITIALIZED = 'INITIALIZED', + DISPLAY_POST = 'DISPLAY_POST' +} + +export interface IMainMessageBase { + action: MainAction; + payload: any; +} + +export interface IDisplayShape extends IMainMessageBase { + action: MainAction.DISPLAY_SHAPE; + payload: { + result: IDict; + postResult: IDict; + }; +} +export interface IWorkerInitialized extends IMainMessageBase { + action: MainAction.INITIALIZED; + payload: boolean; +} + +export interface IDisplayPost extends IMainMessageBase { + action: MainAction.DISPLAY_POST; + payload: { + jcObject: IJCadObject; + postResult: any; + }[]; +} + +export type IMainMessage = IDisplayShape | IWorkerInitialized | IDisplayPost; + +export interface IWorkerMessageBase { + id: string; + action: WorkerAction; + payload: any; +} +export type IMessageHandler = + | ((msg: IMainMessageBase) => void) + | ((msg: IMainMessageBase) => Promise); + export interface IJCadWorker { ready: Promise; - initChannel(): string; - registerHandler( - id: string, - messageHandler: ((msg: any) => void) | ((msg: any) => Promise), - thisArg?: any - ): void; - removeChannel(id: string): void; - postMessage(msg: any): void; + postMessage(msg: IWorkerMessageBase): void; + register(options: { messageHandler: IMessageHandler; thisArg?: any }): string; + unregister(id: string): void; } + export interface IJCadWorkerRegistry { /** * @@ -216,6 +291,13 @@ export interface IJCadWorkerRegistry { * @return {*} {(IJCadWorker | undefined)} */ getWorker(workerId: string): IJCadWorker | undefined; + /** + * + * + * @param {string} workerId + * @return {*} {(IJCadWorker | undefined)} + */ + getDefaultWorker(): IJCadWorker; /** * @@ -228,3 +310,31 @@ export interface IJCadWorkerRegistry { export type IJupyterCadWidget = IDocumentWidget; export type IJupyterCadTracker = IWidgetTracker; + +export interface IJCadFormSchemaRegistry { + /** + * + * + * @return {*} {IDict} + * @memberof IJCadFormSchemaRegistry + */ + getSchemas(): Map; + + /** + * + * + * @param {string} name + * @param {IDict} schema + * @memberof IJCadFormSchemaRegistry + */ + registerSchema(name: string, schema: IDict): void; + + /** + * + * + * @param {string} name + * @return {*} {boolean} + * @memberof IJCadFormSchemaRegistry + */ + has(name: string): boolean; +} diff --git a/packages/schema/src/schema/jcad.json b/packages/schema/src/schema/jcad.json index b33f78cf..cf9cc62a 100644 --- a/packages/schema/src/schema/jcad.json +++ b/packages/schema/src/schema/jcad.json @@ -34,7 +34,8 @@ "Part::MultiFuse", "Part::MultiCommon", "Part::Extrusion", - "Sketcher::SketchObject" + "Sketcher::SketchObject", + "Post::Operator" ] }, "shapeMetadata": { diff --git a/packages/schema/src/schema/postOperator.json b/packages/schema/src/schema/postOperator.json new file mode 100644 index 00000000..6bcefaf6 --- /dev/null +++ b/packages/schema/src/schema/postOperator.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "description": "Post::Operator", + "title": "IPostOperator", + "required": ["Object"], + "additionalProperties": true, + "properties": { + "Object": { + "type": "string", + "description": "The name of input object" + }, + "NumberOfSegment": { + "type": "number", + "description": "The number of segment" + } + } +} diff --git a/packages/schema/src/token.ts b/packages/schema/src/token.ts index db35a7fc..c859597e 100644 --- a/packages/schema/src/token.ts +++ b/packages/schema/src/token.ts @@ -3,7 +3,8 @@ import { Token } from '@lumino/coreutils'; import { IJCadWorkerRegistry, IJupyterCadTracker, - IAnnotationModel + IAnnotationModel, + IJCadFormSchemaRegistry } from './interfaces'; export const IJupyterCadDocTracker = new Token( @@ -17,3 +18,7 @@ export const IAnnotationToken = new Token( export const IJCadWorkerRegistryToken = new Token( 'jupytercadWorkerRegistry' ); + +export const IJCadFormSchemaRegistryToken = new Token( + 'jupytercadFormSchemaRegistry' +); diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts index 0fd7fa1c..3a2440ff 100644 --- a/packages/schema/src/types.ts +++ b/packages/schema/src/types.ts @@ -11,6 +11,7 @@ export * from './_interface/placement'; export * from './_interface/sketch'; export * from './_interface/sphere'; export * from './_interface/torus'; +export * from './_interface/postOperator'; export * from './interfaces'; export * from './model'; export * from './token'; diff --git a/python/jupytercad_app/.gitignore b/python/jupytercad_app/.gitignore index 3ab3a637..6dedb4cb 100644 --- a/python/jupytercad_app/.gitignore +++ b/python/jupytercad_app/.gitignore @@ -3,3 +3,4 @@ jupytercad_app/schemas jupytercad_app/themes # Version file is handled by hatchling jupytercad_app/_version.py +build_dir/ diff --git a/python/jupytercad_app/package.json b/python/jupytercad_app/package.json index f9d4ef1d..226c4a50 100644 --- a/python/jupytercad_app/package.json +++ b/python/jupytercad_app/package.json @@ -39,7 +39,7 @@ "eslint:check": "eslint . --cache --ext .ts,.tsx", "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", - "prettier": "jlpm prettier:base --write --list-different", + "prettier": "jlpm prettier:base --write", "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "prettier:check": "jlpm prettier:base --check", "stylelint": "jlpm stylelint:check --fix", diff --git a/python/jupytercad_core/package.json b/python/jupytercad_core/package.json index 75343b77..70e82dd0 100644 --- a/python/jupytercad_core/package.json +++ b/python/jupytercad_core/package.json @@ -41,7 +41,7 @@ "install:extension": "jlpm build", "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", - "prettier": "jlpm prettier:base --write --list-different", + "prettier": "jlpm prettier:base --write", "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "prettier:check": "jlpm prettier:base --check", "stylelint": "jlpm stylelint:check --fix", @@ -104,6 +104,20 @@ }, "extension": true, "outputDir": "jupytercad_core/labextension", - "webpackConfig": "./extension.webpack.config.js" + "webpackConfig": "./extension.webpack.config.js", + "sharedPackages": { + "@jupytercad/base": { + "singleton": true, + "bundled": true + }, + "@jupytercad/schema": { + "singleton": true, + "bundled": true + }, + "@jupyter/docprovider": { + "singleton": true, + "bundled": false + } + } } } diff --git a/python/jupytercad_core/src/index.ts b/python/jupytercad_core/src/index.ts index 7e51e094..4d848bb9 100644 --- a/python/jupytercad_core/src/index.ts +++ b/python/jupytercad_core/src/index.ts @@ -2,6 +2,7 @@ import jcadPlugin from './jcadplugin/plugins'; import stepPlugin from './stepplugin/plugins'; import { annotationPlugin, + formSchemaRegistryPlugin, trackerPlugin, workerRegistryPlugin } from './plugin'; @@ -13,5 +14,6 @@ export default [ annotationPlugin, workerRegistryPlugin, jcadPlugin, - stepPlugin + stepPlugin, + formSchemaRegistryPlugin ]; diff --git a/python/jupytercad_core/src/plugin.ts b/python/jupytercad_core/src/plugin.ts index 7a0c5251..377f2159 100644 --- a/python/jupytercad_core/src/plugin.ts +++ b/python/jupytercad_core/src/plugin.ts @@ -1,8 +1,9 @@ import { AnnotationModel, JupyterCadWidget } from '@jupytercad/base'; -import { OCC_WORKER_ID, OccWorker } from '@jupytercad/occ-worker'; import { IAnnotationModel, IAnnotationToken, + IJCadFormSchemaRegistry, + IJCadFormSchemaRegistryToken, IJCadWorkerRegistry, IJCadWorkerRegistryToken, IJupyterCadDocTracker, @@ -17,6 +18,7 @@ import { IMainMenu } from '@jupyterlab/mainmenu'; import { ITranslator } from '@jupyterlab/translation'; import { JupyterCadWorkerRegistry } from './workerregistry'; +import { JupyterCadFormSchemaRegistry } from './schemaregistry'; const NAME_SPACE = 'jupytercad'; @@ -64,11 +66,18 @@ export const workerRegistryPlugin: JupyterFrontEndPlugin = provides: IJCadWorkerRegistryToken, activate: (app: JupyterFrontEnd): IJCadWorkerRegistry => { const workerRegistry = new JupyterCadWorkerRegistry(); - const worker = new Worker( - new URL('@jupytercad/occ-worker/lib/worker', (import.meta as any).url) - ); - const occWorker = new OccWorker({ worker }); - workerRegistry.registerWorker(OCC_WORKER_ID, occWorker); return workerRegistry; } }; + +export const formSchemaRegistryPlugin: JupyterFrontEndPlugin = + { + id: 'jupytercad:core:form-schema-registry', + autoStart: true, + requires: [], + provides: IJCadFormSchemaRegistryToken, + activate: (app: JupyterFrontEnd): IJCadFormSchemaRegistry => { + const registry = new JupyterCadFormSchemaRegistry(); + return registry; + } + }; diff --git a/python/jupytercad_core/src/schemaregistry.ts b/python/jupytercad_core/src/schemaregistry.ts new file mode 100644 index 00000000..d894e93a --- /dev/null +++ b/python/jupytercad_core/src/schemaregistry.ts @@ -0,0 +1,27 @@ +import { IDict, IJCadFormSchemaRegistry } from '@jupytercad/schema'; +import formSchema from '@jupytercad/schema/lib/_interface/forms.json'; + +export class JupyterCadFormSchemaRegistry implements IJCadFormSchemaRegistry { + constructor() { + this._registry = new Map(Object.entries(formSchema)); + console.log('_registry', this._registry); + } + + registerSchema(name: string, schema: IDict): void { + if (!this._registry.has(name)) { + this._registry.set(name, schema); + } else { + console.error('Worker is already registered!'); + } + } + + has(name: string): boolean { + return this._registry.has(name); + } + + getSchemas(): Map { + return this._registry; + } + + private _registry: Map; +} diff --git a/python/jupytercad_core/src/workerregistry.ts b/python/jupytercad_core/src/workerregistry.ts index bab4115b..21882c0b 100644 --- a/python/jupytercad_core/src/workerregistry.ts +++ b/python/jupytercad_core/src/workerregistry.ts @@ -1,9 +1,15 @@ +import { OccWorker } from '@jupytercad/occ-worker'; import { IJCadWorker, IJCadWorkerRegistry } from '@jupytercad/schema'; export class JupyterCadWorkerRegistry implements IJCadWorkerRegistry { constructor() { this._registry = new Map(); + const worker = new Worker( + new URL('@jupytercad/occ-worker/lib/worker', (import.meta as any).url) + ); + this._defaultWorker = new OccWorker({ worker }); } + registerWorker(workerId: string, worker: IJCadWorker): void { if (!this._registry.has(workerId)) { this._registry.set(workerId, worker); @@ -18,6 +24,10 @@ export class JupyterCadWorkerRegistry implements IJCadWorkerRegistry { } } + getDefaultWorker(): IJCadWorker { + return this._defaultWorker; + } + getWorker(workerId: string): IJCadWorker | undefined { return this._registry.get(workerId); } @@ -27,4 +37,5 @@ export class JupyterCadWorkerRegistry implements IJCadWorkerRegistry { } private _registry: Map; + private _defaultWorker: IJCadWorker; } diff --git a/python/jupytercad_lab/package.json b/python/jupytercad_lab/package.json index 2a5cac0e..12868d85 100644 --- a/python/jupytercad_lab/package.json +++ b/python/jupytercad_lab/package.json @@ -41,7 +41,7 @@ "install:extension": "jlpm build", "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", - "prettier": "jlpm prettier:base --write --list-different", + "prettier": "jlpm prettier:base --write", "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "prettier:check": "jlpm prettier:base --check", "stylelint": "jlpm stylelint:check --fix", @@ -112,6 +112,10 @@ "@jupytercad/jupytercad-core": { "singleton": true, "bundled": false + }, + "@jupyter/docprovider": { + "singleton": true, + "bundled": false } } } diff --git a/python/jupytercad_lab/src/index.ts b/python/jupytercad_lab/src/index.ts index 3e1f704a..77aa594e 100644 --- a/python/jupytercad_lab/src/index.ts +++ b/python/jupytercad_lab/src/index.ts @@ -21,6 +21,8 @@ import { import { IAnnotationModel, IAnnotationToken, + IJCadFormSchemaRegistry, + IJCadFormSchemaRegistryToken, IJupyterCadDocTracker, IJupyterCadTracker } from '@jupytercad/schema'; @@ -61,12 +63,18 @@ const plugin: JupyterFrontEndPlugin = { const controlPanel: JupyterFrontEndPlugin = { id: 'jupytercad:lab:controlpanel', autoStart: true, - requires: [ILayoutRestorer, IJupyterCadDocTracker, IAnnotationToken], + requires: [ + ILayoutRestorer, + IJupyterCadDocTracker, + IAnnotationToken, + IJCadFormSchemaRegistryToken + ], activate: ( app: JupyterFrontEnd, restorer: ILayoutRestorer, tracker: IJupyterCadTracker, - annotationModel: IAnnotationModel + annotationModel: IAnnotationModel, + formSchemaRegistry: IJCadFormSchemaRegistry ) => { const controlModel = new ControlPanelModel({ tracker }); @@ -79,7 +87,10 @@ const controlPanel: JupyterFrontEndPlugin = { leftControlPanel.title.caption = 'JupyterCad Control Panel'; leftControlPanel.title.icon = jcLightIcon; - const rightControlPanel = new RightPanelWidget({ model: controlModel }); + const rightControlPanel = new RightPanelWidget({ + model: controlModel, + formSchemaRegistry + }); rightControlPanel.id = 'jupytercad::rightControlPanel'; rightControlPanel.title.caption = 'JupyterCad Control Panel'; rightControlPanel.title.icon = jcLightIcon; diff --git a/ui-tests/playwright.config.js b/ui-tests/playwright.config.js index 8062b249..86a3d7e9 100644 --- a/ui-tests/playwright.config.js +++ b/ui-tests/playwright.config.js @@ -9,7 +9,7 @@ module.exports = { command: 'jlpm start', url: 'http://localhost:8888/lab', timeout: 120 * 1000, - reuseExistingServer: !process.env.CI + reuseExistingServer: false }, retries: 0, expect: { diff --git a/yarn.lock b/yarn.lock index 2e371155..520455b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -514,13 +514,13 @@ __metadata: linkType: hard "@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.7.0, @codemirror/view@npm:^6.9.3, @codemirror/view@npm:^6.9.6": - version: 6.22.3 - resolution: "@codemirror/view@npm:6.22.3" + version: 6.22.2 + resolution: "@codemirror/view@npm:6.22.2" dependencies: "@codemirror/state": ^6.1.4 style-mod: ^4.1.0 w3c-keyname: ^2.2.4 - checksum: 89d011afa87b754a4207d18393109c1972c9403762d288711e96c77f51c693a825431c8e35c26268a6eb0889f7184602080d8dcf27fa21354a87dff80636d971 + checksum: a56f5d1f5cf8f3de290e912c96c57406cceb33e99d3463bfce1ffda5f6bd709ab75f3016d289f808bb01d797a1a484f001bf787a15002ffdf7579e48e3bd5c09 languageName: node linkType: hard @@ -1089,6 +1089,7 @@ __metadata: rimraf: ^3.0.2 source-map-loader: ^3.0.0 ts-loader: ^9.2.6 + tsc-watch: ^6.0.0 typescript: ^5 uuid: ^8.3.2 webpack: ^5.76.3 @@ -1101,7 +1102,6 @@ __metadata: dependencies: copyfiles: ^2.4.1 js-yaml: ^4.1.0 - opencascade.js: beta rimraf: ^3.0.2 typescript: ^5 languageName: unknown @@ -3341,12 +3341,12 @@ __metadata: linkType: hard "@types/eslint@npm:*": - version: 8.44.9 - resolution: "@types/eslint@npm:8.44.9" + version: 8.44.8 + resolution: "@types/eslint@npm:8.44.8" dependencies: "@types/estree": "*" "@types/json-schema": "*" - checksum: 6f8889e94e67a5e43c15f5a2530798f864ace08c270bfb3f153cb705da4e30a80e0e9a0fc05317c8642c8dda909d528968172089eb4d52aca9f212761df25d90 + checksum: c3bc70166075e6e9f7fb43978882b9ac0b22596b519900b08dc8a1d761bbbddec4c48a60cc4eb674601266223c6f11db30f3fb6ceaae96c23c54b35ad88022bc languageName: node linkType: hard @@ -3460,13 +3460,13 @@ __metadata: linkType: hard "@types/react@npm:*, @types/react@npm:^18.0.26": - version: 18.2.45 - resolution: "@types/react@npm:18.2.45" + version: 18.2.43 + resolution: "@types/react@npm:18.2.43" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 40b256bdce67b026348022b4f8616a693afdad88cf493b77f7b4e6c5f4b0e4ba13a6068e690b9b94572920840ff30d501ea3d8518e1f21cc8fb8204d4b140c8a + checksum: e7e4da18c7b14e4b8b1ea630d1d8224835698f2bb2485766455bfb9dea93b8ee6cc549a9fe1cb10c984e62685db44fcfb61e8fac913b008cd30a92087f25b9df languageName: node linkType: hard @@ -4586,9 +4586,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001565": - version: 1.0.30001570 - resolution: "caniuse-lite@npm:1.0.30001570" - checksum: 460be2c7a9b1c8a83b6aae4226661c276d9dada6c84209dee547699cf4b28030b9d1fc29ddd7626acee77412b6401993878ea0ef3eadbf3a63ded9034896ae20 + version: 1.0.30001568 + resolution: "caniuse-lite@npm:1.0.30001568" + checksum: 7092aaa246dc8531fbca5b47be91e92065db7e5c04cc9e3d864e848f8f1be769ac6754429e843a5e939f7331a771e8b0a1bc3b13495c66b748c65e2f5bdb1220 languageName: node linkType: hard @@ -5553,9 +5553,9 @@ __metadata: linkType: hard "electron-to-chromium@npm:^1.4.601": - version: 1.4.611 - resolution: "electron-to-chromium@npm:1.4.611" - checksum: 17a4d83219f52cbfdc7b585e755cef0ea695c5992109ec1009a724bca4a44fad2f6245bfc2d5f4dad99d576a9fff3057d8a03703ff69196180dc40e8a91d803b + version: 1.4.610 + resolution: "electron-to-chromium@npm:1.4.610" + checksum: 30e57a1483717e6e27882e7e35b167258b669f44a4e66f4f40460825b77c12646140d220f5e1f95668890fc76dd511c93fa73c6374cbf443fc78077d9634864d languageName: node linkType: hard @@ -9517,15 +9517,6 @@ __metadata: languageName: node linkType: hard -opencascade.js@beta: - version: 2.0.0-beta.b5ff984 - resolution: "opencascade.js@npm:2.0.0-beta.b5ff984" - peerDependencies: - ws: ^8.5.0 - checksum: 751fa25e3625497fb562d4c586e884020478b9c5aeae61c43ce10bac9a011826f5c7518c2126fc6b1f79792c83bbc4d43069bff40dfbee4e48137cb9a496424e - languageName: node - linkType: hard - "optionator@npm:^0.9.1": version: 0.9.3 resolution: "optionator@npm:0.9.3" @@ -12747,8 +12738,8 @@ opencascade.js@beta: linkType: hard "ws@npm:^8.11.0": - version: 8.15.1 - resolution: "ws@npm:8.15.1" + version: 8.15.0 + resolution: "ws@npm:8.15.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -12757,7 +12748,7 @@ opencascade.js@beta: optional: true utf-8-validate: optional: true - checksum: 8c67365f6e6134278ad635d558bfce466d7ef7543a043baea333aaa430429f0af8a130c0c36e7dd78f918d68167a659ba9b5067330b77c4b279e91533395952b + checksum: ca15c590aa49bc0197223b8ab7d15e7362ae6c4011d91ed0e5cd5867cdd5497fd71470ea36314833b4b078929fe64dc4ba7748b1e58e50a0f8e41f147db2b464 languageName: node linkType: hard