From 8fd3a2247ae82c4dec585938eca091db8c598ac8 Mon Sep 17 00:00:00 2001 From: Yeicor <4929005+Yeicor@users.noreply.github.com> Date: Thu, 1 Feb 2024 21:03:20 +0100 Subject: [PATCH] Start working on python package --- .gitignore | 6 +- README.md | 1 + package.json | 2 + pyproject.toml | 22 +++++ src/app.ts | 13 ++- src/index.html | 2 +- src/index.ts | 4 +- src/settings.ts | 7 +- yacv_server/__init__.py | 0 yacv_server/logo/logo.py | 15 ++++ yacv_server/model.py | 0 yacv_server/requirements.txt | 3 + yacv_server/server.py | 0 yacv_server/tessellate.py | 151 +++++++++++++++++++++++++++++++++++ yarn.lock | 27 +++++++ 15 files changed, 240 insertions(+), 13 deletions(-) create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 yacv_server/__init__.py create mode 100644 yacv_server/logo/logo.py create mode 100644 yacv_server/model.py create mode 100644 yacv_server/requirements.txt create mode 100644 yacv_server/server.py create mode 100644 yacv_server/tessellate.py diff --git a/.gitignore b/.gitignore index 2888e41..8d7d5bc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,8 @@ /.idea/ # TODO: Figure out if we want to keep a big default skybox image in the repo -/img/st_peters_square_night_8k.jpg +/assets/st_peters_square_night_8k.jpg +/assets/fox.glb + +*.iml +venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6abc401 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Yet Another CAD Viewer \ No newline at end of file diff --git a/package.json b/package.json index 44b3095..639f5e9 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo" }, "devDependencies": { + "@parcel/optimizer-data-url": "2.11.0", + "@parcel/transformer-inline-string": "2.11.0", "@types/three": "^0.160.0", "buffer": "^5.5.0||^6.0.0", "parcel": "^2.11.0" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e9bc489 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "yacv_server" +version = "0.0.1" +authors = [ + { name = "Yeicor" }, +] +description = "Yet Another CAD Viewer (server)" +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +Homepage = "https://github.com/yeicor-3d/yet-another-cad-viewer" +Issues = "https://github.com/yeicor-3d/yet-another-cad-viewer/issues" diff --git a/src/app.ts b/src/app.ts index cafd590..fd0bf7c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,10 +1,8 @@ import {ModelViewerElement} from '@google/model-viewer'; -import {settings} from "./settings"; -import {Renderer} from "@google/model-viewer/lib/three-components/Renderer"; import {$scene} from "@google/model-viewer/lib/model-viewer-base"; import {OrientationGizmo} from "./orientation"; -import {$controls} from "@google/model-viewer/lib/features/controls"; import {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene"; +import {settings} from "./settings"; export class App { element: ModelViewerElement @@ -13,10 +11,12 @@ export class App { this.element = new ModelViewerElement(); this.element.setAttribute('alt', 'The CAD Viewer is not supported on this browser.'); this.element.setAttribute('camera-controls', ''); + this.element.setAttribute('camera-orbit', '30deg 75deg auto'); this.element.setAttribute('max-camera-orbit', 'Infinity 180deg auto'); this.element.setAttribute('min-camera-orbit', '-Infinity 0deg auto'); this.element.setAttribute('interaction-prompt', 'none'); // Quits selected views from gizmo // this.element.setAttribute('auto-rotate', ''); // Messes with the gizmo (rotates model instead of camera) + if (settings.autoplay) this.element.setAttribute('autoplay', ''); if (settings.arModes) { this.element.setAttribute('ar', ''); this.element.setAttribute('ar-modes', settings.arModes); @@ -34,16 +34,13 @@ export class App { let scene: ModelScene = this.element[$scene]; let gizmo = new OrientationGizmo(scene); gizmo.install(); + function updateGizmo() { gizmo.update(); requestAnimationFrame(updateGizmo); } + updateGizmo(); - // document.body.appendChild(this.stats.dom) - // this.stats.dom.style.left = ''; - // this.stats.dom.style.right = '0px'; - // this.stats.dom.style.top = '120px'; - // this.stats.showPanel(1); // 0: fps, 1: ms, 2: mb, 3+: custom } replaceModel(url: string) { diff --git a/src/index.html b/src/index.html index ab673ce..dd755b9 100644 --- a/src/index.html +++ b/src/index.html @@ -3,7 +3,7 @@ Yet Another CAD Viewer - + diff --git a/src/index.ts b/src/index.ts index 735aa8d..68ec3d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import {App} from "./app"; - +import {settings} from "./settings"; const app = new App() app.install(); -app.replaceModel(`https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF-Binary/Duck.glb`) +app.replaceModel(settings.preloadModel) diff --git a/src/settings.ts b/src/settings.ts index 04ed650..50f28f5 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,7 +1,12 @@ // @ts-ignore -import skyboxUrl from './../img/st_peters_square_night_8k.jpg'; +import skyboxUrl from '../assets/st_peters_square_night_8k.jpg'; +// @ts-ignore +import logo from "url:../assets/fox.glb"; export const settings = { + // ModelViewer settings + preloadModel: logo, + autoplay: true, arModes: 'webxr scene-viewer quick-look', shadowIntensity: 1, background: skyboxUrl, diff --git a/yacv_server/__init__.py b/yacv_server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yacv_server/logo/logo.py b/yacv_server/logo/logo.py new file mode 100644 index 0000000..d1abfb7 --- /dev/null +++ b/yacv_server/logo/logo.py @@ -0,0 +1,15 @@ +from build123d import * + +from tessellate import tessellate + + +def logo() -> Compound: + """Builds the CAD part of the logo""" + with BuildPart() as logo_obj: + Box(1, 2, 3) + return logo_obj.part + + +if __name__ == "__main__": + obj = logo() + tessellate(obj.wrapped, lambda *args: print(args)) diff --git a/yacv_server/model.py b/yacv_server/model.py new file mode 100644 index 0000000..e69de29 diff --git a/yacv_server/requirements.txt b/yacv_server/requirements.txt new file mode 100644 index 0000000..3daafbb --- /dev/null +++ b/yacv_server/requirements.txt @@ -0,0 +1,3 @@ +build123d==0.3.0 +aiohttp==3.9.3 +partcad==0.3.84 \ No newline at end of file diff --git a/yacv_server/server.py b/yacv_server/server.py new file mode 100644 index 0000000..e69de29 diff --git a/yacv_server/tessellate.py b/yacv_server/tessellate.py new file mode 100644 index 0000000..d66e007 --- /dev/null +++ b/yacv_server/tessellate.py @@ -0,0 +1,151 @@ +import concurrent +import copyreg +from concurrent.futures import ProcessPoolExecutor, Executor +from dataclasses import dataclass +from enum import Enum +from typing import Optional, Tuple, Callable + +import OCP +from OCP.BRepAdaptor import BRepAdaptor_Curve +from OCP.GCPnts import GCPnts_TangentialDeflection +from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape +from build123d import Face, Vector, Shape +from partcad.wrappers import cq_serialize + + +class UVMode(Enum): + """UV mode for tesselation""" + + TRIPLANAR = 0 + """Triplanar UV mapping""" + FACE = 1 + """Use UV coordinates from each face""" + + +@dataclass +class TessellationUpdate: + """Tessellation update""" + + # Progress + root: TopoDS_Shape + """The root shape that is being tessellated""" + progress: float + """Progress in percent""" + + # Current shape + shape: TopoDS_Shape + """Shape that was tessellated""" + vertices: list[Vector] + """List of vertices""" + indices: Optional[list[Tuple[int, int, int]]] + """List of indices (only for faces)""" + + +progress_callback_t = Callable[[TessellationUpdate], None] + + +def _inflate_vec(*values: float): + pnt = OCP.gp.gp_Vec(values[0], values[1], values[2]) + return pnt + + +def _reduce_vec(pnt: OCP.gp.gp_Vec): + return _inflate_vec, (pnt.X(), pnt.Y(), pnt.Z()) + + +def tessellate( + ocp_shape: TopoDS_Shape, + progress_callback: progress_callback_t = None, + tolerance: float = 0.1, + angular_tolerance: float = 0.1, + uv: Optional[UVMode] = None, + executor: Executor = ProcessPoolExecutor(), # Set to ThreadPoolExecutor if pickling fails... +): + """Tessellate a whole shape into a list of triangle vertices and a list of triangle indices. + + It uses multiprocessing to speed up the process, and publishes progress updates to the callback. + """ + shape = Shape(ocp_shape) + _register_pickle_if_needed() + with executor: + futures = [] + + # Submit tessellation tasks + for face in shape.faces(): + futures.append(executor.submit(_tessellate_element, face.wrapped, tolerance, angular_tolerance, uv)) + for edge in shape.edges(): + futures.append(executor.submit(_tessellate_element, edge.wrapped, tolerance, angular_tolerance, uv)) + + # Collect results as they come in + for i, future in enumerate(concurrent.futures.as_completed(futures)): + tessellation, shape = future.result() + is_face = isinstance(shape, TopoDS_Face) + update = TessellationUpdate( + root=ocp_shape, + progress=(i + 1) / len(futures), + shape=shape, + vertices=tessellation[0] if is_face else tessellation, + indices=tessellation[1] if is_face else None, + ) + progress_callback(update) + + +_pickle_registered = False + + +def _register_pickle_if_needed(): + global _pickle_registered + if _pickle_registered: + return + cq_serialize.register() + copyreg.pickle(OCP.gp.gp_Vec, _reduce_vec) + + +# Define the function that will tessellate each element in parallel +def _tessellate_element(element: TopoDS_Shape, tolerance, angular_tolerance, uv): + if isinstance(element, TopoDS_Face): + return _tessellate_face(element, tolerance, angular_tolerance, uv), element + elif isinstance(element, TopoDS_Edge): + return _tessellate_edge(element, angular_tolerance, angular_tolerance), element + + +TriMesh = Tuple[list[Vector], list[Tuple[int, int, int]]] + + +def _tessellate_face( + ocp_face: TopoDS_Face, + tolerance: float = 0.1, + angular_tolerance: float = 0.1, + uv: Optional[UVMode] = None, +) -> TriMesh: + """Tessellate a face into a list of triangle vertices and a list of triangle indices""" + face = Face(ocp_face) + tri_mesh = face.tessellate(tolerance, angular_tolerance) + + # TODO: UV mapping + + return tri_mesh + + +def _tessellate_edge( + ocp_edge: TopoDS_Edge, + angular_deflection: float = 0.1, + curvature_deflection: float = 0.1, +) -> list[Vector]: + """Tessellate a wire or edge into a list of ordered vertices""" + curve = BRepAdaptor_Curve(ocp_edge) + discretizer = GCPnts_TangentialDeflection(curve, angular_deflection, curvature_deflection) + assert discretizer.NbPoints() > 1, "Edge is too small??" + + # TODO: get transformation?? + + # add vertices + vertices: list[Vector] = [ + Vector(v.X(), v.Y(), v.Z()) + for v in ( + discretizer.Value(i) # .Transformed(transformation) + for i in range(1, discretizer.NbPoints() + 1) + ) + ] + + return vertices diff --git a/yarn.lock b/yarn.lock index 758817a..2093ea8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -317,6 +317,16 @@ lightningcss "^1.22.1" nullthrows "^1.1.1" +"@parcel/optimizer-data-url@2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-data-url/-/optimizer-data-url-2.11.0.tgz#db0681ff38b356b29ddc40d059126bb4641d3ed8" + integrity sha512-k/BCJMNhqN+3vykp1jVR8AvCrbV8sYBC3gYh/qNzeS+mcmWaF4VIeoJj/nwIu7qYgb6LIC1Ib0ph6i16YmcRFg== + dependencies: + "@parcel/plugin" "2.11.0" + "@parcel/utils" "2.11.0" + isbinaryfile "^4.0.2" + mime "^2.4.4" + "@parcel/optimizer-htmlnano@2.11.0": version "2.11.0" resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.11.0.tgz#2d62e5a0f15a58feeee67cdd23bf54da528d0207" @@ -590,6 +600,13 @@ "@parcel/workers" "2.11.0" nullthrows "^1.1.1" +"@parcel/transformer-inline-string@2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-inline-string/-/transformer-inline-string-2.11.0.tgz#36ef8add36ac45b48df6db379def2f920ade6a09" + integrity sha512-yLLjVqS7/P/ySOOjwdl2mElNHmCtJK81+7mnoA2oLEsf4kTKlW3JnIvX5BqJj6Dy6Ek0V1M48E86T9U3fwzWzg== + dependencies: + "@parcel/plugin" "2.11.0" + "@parcel/transformer-js@2.11.0": version "2.11.0" resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.11.0.tgz#1067d929a5c7f577b3c40068d5f6cb6428398771" @@ -1290,6 +1307,11 @@ is-promise@^2.1.0: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== +isbinaryfile@^4.0.2: + version "4.0.10" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" + integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1454,6 +1476,11 @@ micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" +mime@^2.4.4: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + msgpackr-extract@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz#e05ec1bb4453ddf020551bcd5daaf0092a2c279d"