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"