mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
Start working on python package
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,4 +8,8 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
|
|
||||||
# TODO: Figure out if we want to keep a big default skybox image in the repo
|
# 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/
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo"
|
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@parcel/optimizer-data-url": "2.11.0",
|
||||||
|
"@parcel/transformer-inline-string": "2.11.0",
|
||||||
"@types/three": "^0.160.0",
|
"@types/three": "^0.160.0",
|
||||||
"buffer": "^5.5.0||^6.0.0",
|
"buffer": "^5.5.0||^6.0.0",
|
||||||
"parcel": "^2.11.0"
|
"parcel": "^2.11.0"
|
||||||
|
|||||||
22
pyproject.toml
Normal file
22
pyproject.toml
Normal file
@@ -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"
|
||||||
13
src/app.ts
13
src/app.ts
@@ -1,10 +1,8 @@
|
|||||||
import {ModelViewerElement} from '@google/model-viewer';
|
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 {$scene} from "@google/model-viewer/lib/model-viewer-base";
|
||||||
import {OrientationGizmo} from "./orientation";
|
import {OrientationGizmo} from "./orientation";
|
||||||
import {$controls} from "@google/model-viewer/lib/features/controls";
|
|
||||||
import {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
import {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||||
|
import {settings} from "./settings";
|
||||||
|
|
||||||
export class App {
|
export class App {
|
||||||
element: ModelViewerElement
|
element: ModelViewerElement
|
||||||
@@ -13,10 +11,12 @@ export class App {
|
|||||||
this.element = new ModelViewerElement();
|
this.element = new ModelViewerElement();
|
||||||
this.element.setAttribute('alt', 'The CAD Viewer is not supported on this browser.');
|
this.element.setAttribute('alt', 'The CAD Viewer is not supported on this browser.');
|
||||||
this.element.setAttribute('camera-controls', '');
|
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('max-camera-orbit', 'Infinity 180deg auto');
|
||||||
this.element.setAttribute('min-camera-orbit', '-Infinity 0deg auto');
|
this.element.setAttribute('min-camera-orbit', '-Infinity 0deg auto');
|
||||||
this.element.setAttribute('interaction-prompt', 'none'); // Quits selected views from gizmo
|
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)
|
// this.element.setAttribute('auto-rotate', ''); // Messes with the gizmo (rotates model instead of camera)
|
||||||
|
if (settings.autoplay) this.element.setAttribute('autoplay', '');
|
||||||
if (settings.arModes) {
|
if (settings.arModes) {
|
||||||
this.element.setAttribute('ar', '');
|
this.element.setAttribute('ar', '');
|
||||||
this.element.setAttribute('ar-modes', settings.arModes);
|
this.element.setAttribute('ar-modes', settings.arModes);
|
||||||
@@ -34,16 +34,13 @@ export class App {
|
|||||||
let scene: ModelScene = this.element[$scene];
|
let scene: ModelScene = this.element[$scene];
|
||||||
let gizmo = new OrientationGizmo(scene);
|
let gizmo = new OrientationGizmo(scene);
|
||||||
gizmo.install();
|
gizmo.install();
|
||||||
|
|
||||||
function updateGizmo() {
|
function updateGizmo() {
|
||||||
gizmo.update();
|
gizmo.update();
|
||||||
requestAnimationFrame(updateGizmo);
|
requestAnimationFrame(updateGizmo);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
replaceModel(url: string) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Yet Another CAD Viewer</title>
|
<title>Yet Another CAD Viewer</title>
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
<meta name='viewport' content='width=device-width, initial-scale=1'/>
|
||||||
<link rel="stylesheet" type="text/css" href="./index.css">
|
<link rel="stylesheet" type="text/css" href="./index.css">
|
||||||
<script type="module" src="./index.ts"></script>
|
<script type="module" src="./index.ts"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {App} from "./app";
|
import {App} from "./app";
|
||||||
|
import {settings} from "./settings";
|
||||||
const app = new App()
|
const app = new App()
|
||||||
|
|
||||||
app.install();
|
app.install();
|
||||||
|
|
||||||
app.replaceModel(`https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF-Binary/Duck.glb`)
|
app.replaceModel(settings.preloadModel)
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
// @ts-ignore
|
// @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 = {
|
export const settings = {
|
||||||
|
// ModelViewer settings
|
||||||
|
preloadModel: logo,
|
||||||
|
autoplay: true,
|
||||||
arModes: 'webxr scene-viewer quick-look',
|
arModes: 'webxr scene-viewer quick-look',
|
||||||
shadowIntensity: 1,
|
shadowIntensity: 1,
|
||||||
background: skyboxUrl,
|
background: skyboxUrl,
|
||||||
|
|||||||
0
yacv_server/__init__.py
Normal file
0
yacv_server/__init__.py
Normal file
15
yacv_server/logo/logo.py
Normal file
15
yacv_server/logo/logo.py
Normal file
@@ -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))
|
||||||
0
yacv_server/model.py
Normal file
0
yacv_server/model.py
Normal file
3
yacv_server/requirements.txt
Normal file
3
yacv_server/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
build123d==0.3.0
|
||||||
|
aiohttp==3.9.3
|
||||||
|
partcad==0.3.84
|
||||||
0
yacv_server/server.py
Normal file
0
yacv_server/server.py
Normal file
151
yacv_server/tessellate.py
Normal file
151
yacv_server/tessellate.py
Normal file
@@ -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
|
||||||
27
yarn.lock
27
yarn.lock
@@ -317,6 +317,16 @@
|
|||||||
lightningcss "^1.22.1"
|
lightningcss "^1.22.1"
|
||||||
nullthrows "^1.1.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":
|
"@parcel/optimizer-htmlnano@2.11.0":
|
||||||
version "2.11.0"
|
version "2.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.11.0.tgz#2d62e5a0f15a58feeee67cdd23bf54da528d0207"
|
resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.11.0.tgz#2d62e5a0f15a58feeee67cdd23bf54da528d0207"
|
||||||
@@ -590,6 +600,13 @@
|
|||||||
"@parcel/workers" "2.11.0"
|
"@parcel/workers" "2.11.0"
|
||||||
nullthrows "^1.1.1"
|
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":
|
"@parcel/transformer-js@2.11.0":
|
||||||
version "2.11.0"
|
version "2.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.11.0.tgz#1067d929a5c7f577b3c40068d5f6cb6428398771"
|
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"
|
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
|
||||||
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
|
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:
|
js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
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"
|
braces "^3.0.2"
|
||||||
picomatch "^2.3.1"
|
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:
|
msgpackr-extract@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz#e05ec1bb4453ddf020551bcd5daaf0092a2c279d"
|
resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz#e05ec1bb4453ddf020551bcd5daaf0092a2c279d"
|
||||||
|
|||||||
Reference in New Issue
Block a user