mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
add full support for locations, better helpers and more server fixes
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import {Document, TypedArray} from '@gltf-transform/core'
|
import {Document, TypedArray} from '@gltf-transform/core'
|
||||||
import {Vector2} from 'three/src/math/Vector2'
|
import {Vector2} from 'three/src/math/Vector2'
|
||||||
import {Vector3} from 'three/src/math/Vector3'
|
import {Vector3} from 'three/src/math/Vector3'
|
||||||
import {Box3} from 'three/src/math/Box3'
|
|
||||||
import {Matrix4} from 'three/src/math/Matrix4'
|
import {Matrix4} from 'three/src/math/Matrix4'
|
||||||
|
|
||||||
|
|
||||||
@@ -47,20 +46,20 @@ function buildSimpleGltf(doc: Document, rawPositions: number[], rawIndices: numb
|
|||||||
*/
|
*/
|
||||||
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
|
export function newAxes(doc: Document, size: Vector3, transform: Matrix4) {
|
||||||
let rawPositions = [
|
let rawPositions = [
|
||||||
0, 0, 0,
|
[0, 0, 0, size.x, 0, 0],
|
||||||
size.x, 0, 0,
|
[0, 0, 0, 0, size.y, 0],
|
||||||
0, 0, 0,
|
[0, 0, 0, 0, 0, -size.z],
|
||||||
0, size.y, 0,
|
|
||||||
0, 0, 0,
|
|
||||||
0, 0, -size.z,
|
|
||||||
];
|
];
|
||||||
let rawIndices = [0, 1, 2, 3, 4, 5];
|
let rawIndices = [0, 1];
|
||||||
let rawColors = [
|
let rawColors = [
|
||||||
...(AxesColors.x[0]), ...(AxesColors.x[1]),
|
[...(AxesColors.x[0]), ...(AxesColors.x[1])],
|
||||||
...(AxesColors.y[0]), ...(AxesColors.y[1]),
|
[...(AxesColors.y[0]), ...(AxesColors.y[1])],
|
||||||
...(AxesColors.z[0]), ...(AxesColors.z[1]),
|
[...(AxesColors.z[0]), ...(AxesColors.z[1])],
|
||||||
].map(x => x / 255.0);
|
].map(g => g.map(x => x / 255.0));
|
||||||
buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, transform, '__helper_axes');
|
buildSimpleGltf(doc, rawPositions[0], rawIndices, rawColors[0], transform, '__helper_axes');
|
||||||
|
buildSimpleGltf(doc, rawPositions[1], rawIndices, rawColors[1], transform, '__helper_axes');
|
||||||
|
buildSimpleGltf(doc, rawPositions[2], rawIndices, rawColors[2], transform, '__helper_axes');
|
||||||
|
buildSimpleGltf(doc, [0, 0, 0], [0], null, transform, '__helper_axes', WebGL2RenderingContext.POINTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,7 +85,7 @@ export function newGridBox(doc: Document, size: Vector3, baseTransform: Matrix4
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newGridPlane(doc: Document, size: Vector2, transform: Matrix4 = new Matrix4(), divisions = 10, divisionWidth = 0.2) {
|
export function newGridPlane(doc: Document, size: Vector2, transform: Matrix4 = new Matrix4(), divisions = 10, divisionWidth = 0.002) {
|
||||||
const rawPositions = [];
|
const rawPositions = [];
|
||||||
const rawIndices = [];
|
const rawIndices = [];
|
||||||
// Build the grid as triangles
|
// Build the grid as triangles
|
||||||
@@ -95,19 +94,19 @@ export function newGridPlane(doc: Document, size: Vector2, transform: Matrix4 =
|
|||||||
const y = -size.y / 2 + size.y * i / divisions;
|
const y = -size.y / 2 + size.y * i / divisions;
|
||||||
|
|
||||||
// Vertical quad (two triangles)
|
// Vertical quad (two triangles)
|
||||||
rawPositions.push(x - divisionWidth / 2, -size.y / 2, 0);
|
rawPositions.push(x - divisionWidth * size.x / 2, -size.y / 2, 0);
|
||||||
rawPositions.push(x + divisionWidth / 2, -size.y / 2, 0);
|
rawPositions.push(x + divisionWidth * size.x / 2, -size.y / 2, 0);
|
||||||
rawPositions.push(x + divisionWidth / 2, size.y / 2, 0);
|
rawPositions.push(x + divisionWidth * size.x / 2, size.y / 2, 0);
|
||||||
rawPositions.push(x - divisionWidth / 2, size.y / 2, 0);
|
rawPositions.push(x - divisionWidth * size.x / 2, size.y / 2, 0);
|
||||||
const baseIndex = i * 4;
|
const baseIndex = i * 4;
|
||||||
rawIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
|
rawIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
|
||||||
rawIndices.push(baseIndex, baseIndex + 2, baseIndex + 3);
|
rawIndices.push(baseIndex, baseIndex + 2, baseIndex + 3);
|
||||||
|
|
||||||
// Horizontal quad (two triangles)
|
// Horizontal quad (two triangles)
|
||||||
rawPositions.push(-size.x / 2, y - divisionWidth / 2, 0);
|
rawPositions.push(-size.x / 2, y - divisionWidth * size.y / 2, 0);
|
||||||
rawPositions.push(size.x / 2, y - divisionWidth / 2, 0);
|
rawPositions.push(size.x / 2, y - divisionWidth * size.y / 2, 0);
|
||||||
rawPositions.push(size.x / 2, y + divisionWidth / 2, 0);
|
rawPositions.push(size.x / 2, y + divisionWidth * size.y / 2, 0);
|
||||||
rawPositions.push(-size.x / 2, y + divisionWidth / 2, 0);
|
rawPositions.push(-size.x / 2, y + divisionWidth * size.y / 2, 0);
|
||||||
const baseIndex2 = (divisions + 1 + i) * 4;
|
const baseIndex2 = (divisions + 1 + i) * 4;
|
||||||
rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
|
rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2);
|
||||||
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
|
rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3);
|
||||||
|
|||||||
@@ -147,10 +147,10 @@ function onClipPlanesChange() {
|
|||||||
if (!enabledY) planes.splice(1, 1);
|
if (!enabledY) planes.splice(1, 1);
|
||||||
if (!enabledX) planes.shift();
|
if (!enabledX) planes.shift();
|
||||||
child.material.clippingPlanes = planes;
|
child.material.clippingPlanes = planes;
|
||||||
if (child.userData.backChild) child.userData.backChild.material.clippingPlanes = planes;
|
if (child.userData.backChild && child.userData.backChild.material) child.userData.backChild.material.clippingPlanes = planes;
|
||||||
} else {
|
} else {
|
||||||
child.material.clippingPlanes = [];
|
child.material.clippingPlanes = [];
|
||||||
if (child.userData.backChild) child.userData.backChild.material.clippingPlanes = [];
|
if (child.userData.backChild && child.userData.backChild.material) child.userData.backChild.material.clippingPlanes = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,9 +89,10 @@ let selectionListener = (event: MouseEvent) => {
|
|||||||
const hits = raycaster.intersectObject(scene, true);
|
const hits = raycaster.intersectObject(scene, true);
|
||||||
let hit = hits.find((hit) => {
|
let hit = hits.find((hit) => {
|
||||||
const kind = hit.object.type
|
const kind = hit.object.type
|
||||||
|
console.log(kind)
|
||||||
const kindOk = (selectFilter.value === 'Any (S)') ||
|
const kindOk = (selectFilter.value === 'Any (S)') ||
|
||||||
((kind === 'Mesh' || kind === 'SkinnedMesh') && selectFilter.value === '(F)aces') ||
|
((kind === 'Mesh' || kind === 'SkinnedMesh') && selectFilter.value === '(F)aces') ||
|
||||||
(kind === 'Line' && selectFilter.value === '(E)dges') ||
|
((kind === 'Line' || kind === 'LineSegments') && selectFilter.value === '(E)dges') ||
|
||||||
(kind === 'Points' && selectFilter.value === '(V)ertices');
|
(kind === 'Points' && selectFilter.value === '(V)ertices');
|
||||||
return hit.object.visible && !hit.object.userData.noHit && kindOk;
|
return hit.object.visible && !hit.object.userData.noHit && kindOk;
|
||||||
}) as Intersection<MObject3D> | undefined;
|
}) as Intersection<MObject3D> | undefined;
|
||||||
|
|||||||
@@ -18,13 +18,17 @@ if 'YACV_DISABLE_SERVER' not in os.environ:
|
|||||||
# Expose some nice aliases using the default server instance
|
# Expose some nice aliases using the default server instance
|
||||||
show = server.show
|
show = server.show
|
||||||
show_object = show
|
show_object = show
|
||||||
|
show_all = server.show_cad_all
|
||||||
|
|
||||||
|
|
||||||
def _get_app() -> web.Application:
|
def _get_app() -> web.Application:
|
||||||
"""Required by aiohttp-devtools"""
|
"""Required by aiohttp-devtools"""
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
from logo import build_logo
|
from logo import build_logo
|
||||||
server.show_cad(build_logo(), 'logo')
|
from build123d import Axis
|
||||||
|
logo = build_logo(False)
|
||||||
|
server.show_cad(logo, 'Logo')
|
||||||
|
server.show_cad(logo.faces().group_by(Axis.X)[0].face().center_location, 'Location')
|
||||||
return server.app
|
return server.app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
57
yacv_server/cad.py
Normal file
57
yacv_server/cad.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""
|
||||||
|
Utilities to work with CAD objects
|
||||||
|
"""
|
||||||
|
from typing import Optional, Union, List, Tuple
|
||||||
|
|
||||||
|
from OCP.TopLoc import TopLoc_Location
|
||||||
|
from OCP.TopoDS import TopoDS_Shape
|
||||||
|
|
||||||
|
CADLike = Union[TopoDS_Shape, TopLoc_Location] # Faces, Edges, Vertices and Locations for now
|
||||||
|
|
||||||
|
|
||||||
|
def get_shape(obj: any, error: bool = True) -> Optional[CADLike]:
|
||||||
|
""" Get the shape of a CAD-like object """
|
||||||
|
|
||||||
|
# Try to grab a shape if a different type of object was passed
|
||||||
|
if isinstance(obj, TopoDS_Shape) or isinstance(obj, TopLoc_Location):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
# Return locations (drawn as axes)
|
||||||
|
if 'wrapped' in dir(obj) and isinstance(obj.wrapped, TopLoc_Location):
|
||||||
|
return obj.wrapped
|
||||||
|
|
||||||
|
# Build123D
|
||||||
|
if 'part' in dir(obj):
|
||||||
|
obj = obj.part
|
||||||
|
if 'sketch' in dir(obj):
|
||||||
|
obj = obj.sketch
|
||||||
|
if 'line' in dir(obj):
|
||||||
|
obj = obj.line
|
||||||
|
|
||||||
|
# Build123D & CadQuery
|
||||||
|
while 'wrapped' in dir(obj) and not isinstance(obj, TopoDS_Shape) and not isinstance(obj, TopLoc_Location):
|
||||||
|
obj = obj.wrapped
|
||||||
|
|
||||||
|
# Return shapes
|
||||||
|
if isinstance(obj, TopoDS_Shape):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
if error:
|
||||||
|
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)')
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def grab_all_cad() -> List[Tuple[str, CADLike]]:
|
||||||
|
""" Grab all shapes by inspecting the stack """
|
||||||
|
import inspect
|
||||||
|
stack = inspect.stack()
|
||||||
|
shapes = []
|
||||||
|
for frame in stack:
|
||||||
|
for key, value in frame.frame.f_locals.items():
|
||||||
|
shape = get_shape(value, error=False)
|
||||||
|
if shape:
|
||||||
|
shapes.append((key, shape))
|
||||||
|
return shapes
|
||||||
|
|
||||||
|
# TODO: Image to CAD utility and show_image shortcut on server.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from build123d import Location, Plane, Vector
|
||||||
from pygltflib import *
|
from pygltflib import *
|
||||||
|
|
||||||
_checkerboard_image_bytes = base64.decodebytes(
|
_checkerboard_image_bytes = base64.decodebytes(
|
||||||
@@ -11,22 +12,22 @@ _checkerboard_image_bytes = base64.decodebytes(
|
|||||||
class GLTFMgr:
|
class GLTFMgr:
|
||||||
"""A utility class to build our GLTF2 objects easily and incrementally"""
|
"""A utility class to build our GLTF2 objects easily and incrementally"""
|
||||||
|
|
||||||
gltf: GLTF2 = GLTF2(
|
|
||||||
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
|
|
||||||
scene=0,
|
|
||||||
scenes=[Scene(nodes=[0])],
|
|
||||||
nodes=[Node(mesh=0)],
|
|
||||||
meshes=[Mesh(primitives=[])],
|
|
||||||
accessors=[],
|
|
||||||
bufferViews=[BufferView(buffer=0, byteLength=len(_checkerboard_image_bytes), byteOffset=0)],
|
|
||||||
buffers=[Buffer(byteLength=len(_checkerboard_image_bytes))],
|
|
||||||
samplers=[Sampler(magFilter=NEAREST)],
|
|
||||||
textures=[Texture(source=0, sampler=0)],
|
|
||||||
images=[Image(bufferView=0, mimeType='image/png')],
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.gltf = GLTF2(
|
||||||
|
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
|
||||||
|
scene=0,
|
||||||
|
scenes=[Scene(nodes=[0])],
|
||||||
|
nodes=[Node(mesh=0)],
|
||||||
|
meshes=[Mesh(primitives=[])],
|
||||||
|
accessors=[],
|
||||||
|
bufferViews=[BufferView(buffer=0, byteLength=len(_checkerboard_image_bytes), byteOffset=0)],
|
||||||
|
buffers=[Buffer(byteLength=len(_checkerboard_image_bytes))],
|
||||||
|
samplers=[Sampler(magFilter=NEAREST)],
|
||||||
|
textures=[Texture(source=0, sampler=0)],
|
||||||
|
images=[Image(bufferView=0, mimeType='image/png')],
|
||||||
|
)
|
||||||
self.gltf.set_binary_blob(_checkerboard_image_bytes)
|
self.gltf.set_binary_blob(_checkerboard_image_bytes)
|
||||||
|
# TODO: Custom image support for loading textured planes as CAD objects
|
||||||
|
|
||||||
def add_face(self, vertices_raw: List[Tuple[float, float, float]], indices_raw: List[Tuple[int, int, int]],
|
def add_face(self, vertices_raw: List[Tuple[float, float, float]], indices_raw: List[Tuple[int, int, int]],
|
||||||
tex_coord_raw: List[Tuple[float, float]]):
|
tex_coord_raw: List[Tuple[float, float]]):
|
||||||
@@ -36,12 +37,12 @@ class GLTFMgr:
|
|||||||
tex_coord = np.array([[t[0], t[1]] for t in tex_coord_raw], dtype=np.float32)
|
tex_coord = np.array([[t[0], t[1]] for t in tex_coord_raw], dtype=np.float32)
|
||||||
self._add_any(vertices, indices, tex_coord, mode=TRIANGLES, material="face")
|
self._add_any(vertices, indices, tex_coord, mode=TRIANGLES, material="face")
|
||||||
|
|
||||||
def add_edge(self, vertices_raw: List[Tuple[float, float, float]]):
|
def add_edge(self, vertices_raw: List[Tuple[float, float, float]], mat: str = None):
|
||||||
"""Add an edge to the GLTF as a new primitive of the unique mesh"""
|
"""Add an edge to the GLTF as a new primitive of the unique mesh"""
|
||||||
vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32)
|
vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32)
|
||||||
indices = np.array(list(map(lambda i: [i, i + 1], range(len(vertices) - 1))), dtype=np.uint32)
|
indices = np.array(list(map(lambda i: [i, i + 1], range(len(vertices) - 1))), dtype=np.uint32)
|
||||||
tex_coord = np.array([])
|
tex_coord = np.array([])
|
||||||
self._add_any(vertices, indices, tex_coord, mode=LINE_STRIP, material="edge")
|
self._add_any(vertices, indices, tex_coord, mode=LINE_STRIP, material=mat or "edge")
|
||||||
|
|
||||||
def add_vertex(self, vertex: Tuple[float, float, float]):
|
def add_vertex(self, vertex: Tuple[float, float, float]):
|
||||||
"""Add a vertex to the GLTF as a new primitive of the unique mesh"""
|
"""Add a vertex to the GLTF as a new primitive of the unique mesh"""
|
||||||
@@ -50,6 +51,19 @@ class GLTFMgr:
|
|||||||
tex_coord = np.array([], dtype=np.float32)
|
tex_coord = np.array([], dtype=np.float32)
|
||||||
self._add_any(vertices, indices, tex_coord, mode=POINTS, material="vertex")
|
self._add_any(vertices, indices, tex_coord, mode=POINTS, material="vertex")
|
||||||
|
|
||||||
|
def add_location(self, loc: Location):
|
||||||
|
"""Add a location to the GLTF as a new primitive of the unique mesh"""
|
||||||
|
pl = Plane(loc)
|
||||||
|
|
||||||
|
def vert(v: Vector) -> Tuple[float, float, float]:
|
||||||
|
return v.X, v.Y, v.Z
|
||||||
|
|
||||||
|
# Add 1 origin vertex and 3 edges with custom colors to identify the X, Y and Z axis
|
||||||
|
self.add_vertex(vert(pl.origin))
|
||||||
|
self.add_edge([vert(pl.origin), vert(pl.origin + pl.x_dir)], mat="locX")
|
||||||
|
self.add_edge([vert(pl.origin), vert(pl.origin + pl.y_dir)], mat="locY")
|
||||||
|
self.add_edge([vert(pl.origin), vert(pl.origin + pl.z_dir)], mat="locZ")
|
||||||
|
|
||||||
def add_material(self, kind: str) -> int:
|
def add_material(self, kind: str) -> int:
|
||||||
"""It is important to use a different material for each primitive to be able to change them at runtime"""
|
"""It is important to use a different material for each primitive to be able to change them at runtime"""
|
||||||
new_material: Material
|
new_material: Material
|
||||||
@@ -62,6 +76,15 @@ class GLTFMgr:
|
|||||||
elif kind == "vertex":
|
elif kind == "vertex":
|
||||||
new_material = Material(name="vertex", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
new_material = Material(name="vertex", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
||||||
baseColorFactor=[0, 0.3, 0.3, 1]))
|
baseColorFactor=[0, 0.3, 0.3, 1]))
|
||||||
|
elif kind == "locX":
|
||||||
|
new_material = Material(name="locX", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
||||||
|
baseColorFactor=[0.97, 0.24, 0.24, 1]))
|
||||||
|
elif kind == "locY":
|
||||||
|
new_material = Material(name="locY", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
||||||
|
baseColorFactor=[0.42, 0.8, 0.15, 1]))
|
||||||
|
elif kind == "locZ":
|
||||||
|
new_material = Material(name="locZ", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness(
|
||||||
|
baseColorFactor=[0.09, 0.55, 0.94, 1]))
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown material kind {kind}")
|
raise ValueError(f"Unknown material kind {kind}")
|
||||||
self.gltf.materials.append(new_material)
|
self.gltf.materials.append(new_material)
|
||||||
@@ -129,9 +152,9 @@ class GLTFMgr:
|
|||||||
) if len(tex_coord) > 0 else None
|
) if len(tex_coord) > 0 else None
|
||||||
] if it is not None])
|
] if it is not None])
|
||||||
|
|
||||||
binary_blob = self.gltf.binary_blob()
|
prev_binary_blob = self.gltf.binary_blob()
|
||||||
byte_offset_base = len(binary_blob)
|
byte_offset_base = len(prev_binary_blob)
|
||||||
self.gltf.bufferViews.extend([it for it in [
|
self.gltf.bufferViews.extend([bv for bv in [
|
||||||
BufferView(
|
BufferView(
|
||||||
buffer=0,
|
buffer=0,
|
||||||
byteOffset=byte_offset_base,
|
byteOffset=byte_offset_base,
|
||||||
@@ -149,131 +172,7 @@ class GLTFMgr:
|
|||||||
byteOffset=byte_offset_base + len(indices_blob) + len(vertices_blob),
|
byteOffset=byte_offset_base + len(indices_blob) + len(vertices_blob),
|
||||||
byteLength=len(tex_coord_blob),
|
byteLength=len(tex_coord_blob),
|
||||||
target=ARRAY_BUFFER,
|
target=ARRAY_BUFFER,
|
||||||
) if len(tex_coord) > 0 else None
|
)
|
||||||
] if it is not None])
|
] if bv.byteLength > 0])
|
||||||
|
|
||||||
self.gltf.set_binary_blob(binary_blob + indices_blob + vertices_blob + tex_coord_blob)
|
self.gltf.set_binary_blob(prev_binary_blob + indices_blob + vertices_blob + tex_coord_blob)
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# def create_gltf(vertices: np.ndarray, indices: np.ndarray, tex_coord: np.ndarray, mode: int = TRIANGLES,
|
|
||||||
# material: Optional[Material] = None, images: Optional[List[Image]] = None) -> GLTF2:
|
|
||||||
# """Create a glTF object from vertices and optionally indices.
|
|
||||||
#
|
|
||||||
# If indices are not set, vertices are interpreted as line_strip."""
|
|
||||||
#
|
|
||||||
# assert vertices.ndim == 2
|
|
||||||
# assert vertices.shape[1] == 3
|
|
||||||
# vertices = vertices.astype(np.float32)
|
|
||||||
# vertices_blob = vertices.tobytes()
|
|
||||||
# # print(vertices)
|
|
||||||
#
|
|
||||||
# indices = indices.astype(np.uint8)
|
|
||||||
# indices_blob = indices.flatten().tobytes()
|
|
||||||
# # print(indices)
|
|
||||||
#
|
|
||||||
# tex_coord = tex_coord.astype(np.float32)
|
|
||||||
# tex_coord_blob = tex_coord.tobytes()
|
|
||||||
# # print(tex_coord)
|
|
||||||
#
|
|
||||||
# if images is None:
|
|
||||||
# images = []
|
|
||||||
# image_blob = b''
|
|
||||||
# image_blob_pointers = []
|
|
||||||
# for i, img in enumerate(images):
|
|
||||||
# image_blob = img_to_blob(i, image_blob, image_blob_pointers, images, img)
|
|
||||||
#
|
|
||||||
# gltf = GLTF2(
|
|
||||||
# scene=0,
|
|
||||||
# scenes=[Scene(nodes=[0])],
|
|
||||||
# nodes=[Node(mesh=0)],
|
|
||||||
# meshes=[
|
|
||||||
# Mesh(
|
|
||||||
# primitives=[
|
|
||||||
# Primitive(
|
|
||||||
# attributes=Attributes(POSITION=1, TEXCOORD_0=2) if len(tex_coord) > 0 else Attributes(
|
|
||||||
# POSITION=1),
|
|
||||||
# indices=0,
|
|
||||||
# mode=mode,
|
|
||||||
# material=0 if material is not None else None,
|
|
||||||
# )
|
|
||||||
# ]
|
|
||||||
# )
|
|
||||||
# ],
|
|
||||||
# materials=[material] if material is not None else [],
|
|
||||||
# accessors=[
|
|
||||||
# Accessor(
|
|
||||||
# bufferView=0,
|
|
||||||
# componentType=UNSIGNED_BYTE,
|
|
||||||
# count=indices.size,
|
|
||||||
# type=SCALAR,
|
|
||||||
# max=[int(indices.max())],
|
|
||||||
# min=[int(indices.min())],
|
|
||||||
# ),
|
|
||||||
# Accessor(
|
|
||||||
# bufferView=1,
|
|
||||||
# componentType=FLOAT,
|
|
||||||
# count=len(vertices),
|
|
||||||
# type=VEC3,
|
|
||||||
# max=vertices.max(axis=0).tolist(),
|
|
||||||
# min=vertices.min(axis=0).tolist(),
|
|
||||||
# ),
|
|
||||||
# ] + ([
|
|
||||||
# Accessor(
|
|
||||||
# bufferView=2,
|
|
||||||
# componentType=FLOAT,
|
|
||||||
# count=len(tex_coord),
|
|
||||||
# type=VEC2,
|
|
||||||
# max=tex_coord.max(axis=0).tolist(),
|
|
||||||
# min=tex_coord.min(axis=0).tolist(),
|
|
||||||
# )] if len(tex_coord) > 0 else [])
|
|
||||||
# ,
|
|
||||||
# bufferViews=[
|
|
||||||
# BufferView(
|
|
||||||
# buffer=0,
|
|
||||||
# byteLength=len(indices_blob),
|
|
||||||
# target=ELEMENT_ARRAY_BUFFER,
|
|
||||||
# ),
|
|
||||||
# BufferView(
|
|
||||||
# buffer=0,
|
|
||||||
# byteOffset=len(indices_blob),
|
|
||||||
# byteLength=len(vertices_blob),
|
|
||||||
# target=ARRAY_BUFFER,
|
|
||||||
# ),
|
|
||||||
# ] + (
|
|
||||||
# [
|
|
||||||
# BufferView(
|
|
||||||
# buffer=0,
|
|
||||||
# byteOffset=len(indices_blob) + len(vertices_blob),
|
|
||||||
# byteLength=len(tex_coord_blob),
|
|
||||||
# target=ARRAY_BUFFER,
|
|
||||||
# ),
|
|
||||||
# ] if len(tex_coord) > 0 else []) + (
|
|
||||||
# [
|
|
||||||
# BufferView(
|
|
||||||
# buffer=0,
|
|
||||||
# byteOffset=len(indices_blob) + len(
|
|
||||||
# vertices_blob) + len(tex_coord_blob) + image_blob_pointers[i],
|
|
||||||
# byteLength=image_blob_pointers[i + 1] - image_blob_pointers[i] if i + 1 < len(
|
|
||||||
# image_blob_pointers) else len(image_blob) - image_blob_pointers[i],
|
|
||||||
# )
|
|
||||||
# for i, img in enumerate(images)
|
|
||||||
# ] if len(images) > 0 else []),
|
|
||||||
# buffers=[
|
|
||||||
# Buffer(
|
|
||||||
# byteLength=len(indices_blob) + len(vertices_blob) + len(tex_coord_blob) + len(image_blob),
|
|
||||||
# )
|
|
||||||
# ],
|
|
||||||
# samplers=[Sampler(magFilter=NEAREST)] if len(images) > 0 else [],
|
|
||||||
# textures=[Texture(source=i, sampler=0) for i, _ in enumerate(images)],
|
|
||||||
# images=images,
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# gltf.set_binary_blob(indices_blob + vertices_blob + tex_coord_blob + image_blob)
|
|
||||||
#
|
|
||||||
# return gltf
|
|
||||||
|
|
||||||
|
|
||||||
def img_blob(img: Image) -> bytes:
|
|
||||||
return base64.decodebytes(img.uri.split('base64,', maxsplit=1)[1].encode('ascii'))
|
|
||||||
|
|||||||
@@ -2,29 +2,29 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from OCP.TopoDS import TopoDS_Shape
|
|
||||||
from build123d import *
|
from build123d import *
|
||||||
|
|
||||||
|
|
||||||
def build_logo() -> TopoDS_Shape:
|
def build_logo(text: bool = True) -> Part:
|
||||||
"""Builds the CAD part of the logo"""
|
"""Builds the CAD part of the logo"""
|
||||||
with BuildPart(Plane.XY.offset(50)) as logo_obj:
|
with BuildPart(Plane.XY.offset(50)) as logo_obj:
|
||||||
Box(22, 40, 30)
|
Box(22, 40, 30)
|
||||||
fillet(edges().filter_by(Axis.Y).group_by(Axis.Z)[-1], 10)
|
fillet(edges().filter_by(Axis.Y).group_by(Axis.Z)[-1], 10)
|
||||||
offset(solid(), 2, openings=faces().group_by(Axis.Z)[0] + faces().filter_by(Plane.XZ))
|
offset(solid(), 2, openings=faces().group_by(Axis.Z)[0] + faces().filter_by(Plane.XZ))
|
||||||
text_at_plane = Plane.YZ
|
if text:
|
||||||
text_at_plane.origin = faces().group_by(Axis.X)[-1].face().center()
|
text_at_plane = Plane.YZ
|
||||||
with BuildSketch(text_at_plane.location):
|
text_at_plane.origin = faces().group_by(Axis.X)[-1].face().center()
|
||||||
Text('Yet Another\nCAD Viewer', 7, font_path='/usr/share/fonts/TTF/OpenSans-Regular.ttf')
|
with BuildSketch(text_at_plane.location):
|
||||||
extrude(amount=1)
|
Text('Yet Another\nCAD Viewer', 7, font_path='/usr/share/fonts/TTF/OpenSans-Regular.ttf')
|
||||||
|
extrude(amount=1)
|
||||||
|
|
||||||
return logo_obj.part.wrapped
|
return logo_obj.part
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
# Start an offline "server" to merge the CAD part of the logo with the animated GLTF part of the logo
|
# Start an offline "server" to export the CAD part of the logo in a way compatible with the frontend
|
||||||
os.environ['YACV_DISABLE_SERVER'] = '1'
|
os.environ['YACV_DISABLE_SERVER'] = '1'
|
||||||
from yacv_server import show_object, server
|
from yacv_server import show_object, server
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ from threading import Thread
|
|||||||
from typing import Optional, Dict, Union
|
from typing import Optional, Dict, Union
|
||||||
|
|
||||||
import aiohttp_cors
|
import aiohttp_cors
|
||||||
|
from OCP.TopLoc import TopLoc_Location
|
||||||
from OCP.TopoDS import TopoDS_Shape
|
from OCP.TopoDS import TopoDS_Shape
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from build123d import Shape, Axis
|
from build123d import Shape, Axis, Location, Vector
|
||||||
from dataclasses_json import dataclass_json
|
from dataclasses_json import dataclass_json
|
||||||
|
|
||||||
|
from cad import get_shape, grab_all_cad
|
||||||
from mylogger import logger
|
from mylogger import logger
|
||||||
from pubsub import BufferedPubSub
|
from pubsub import BufferedPubSub
|
||||||
from tessellate import _hashcode, tessellate
|
from tessellate import _hashcode, tessellate
|
||||||
@@ -202,27 +204,26 @@ class Server:
|
|||||||
"""Publishes a CAD object to the server"""
|
"""Publishes a CAD object to the server"""
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
# Try to grab a shape if a different type of object was passed
|
# Get the shape of a CAD-like object
|
||||||
if not isinstance(obj, TopoDS_Shape):
|
obj = get_shape(obj)
|
||||||
# Build123D
|
|
||||||
if 'part' in dir(obj):
|
|
||||||
obj = obj.part
|
|
||||||
if 'sketch' in dir(obj):
|
|
||||||
obj = obj.sketch
|
|
||||||
if 'line' in dir(obj):
|
|
||||||
obj = obj.line
|
|
||||||
# Build123D & CadQuery
|
|
||||||
while 'wrapped' in dir(obj) and not isinstance(obj, TopoDS_Shape):
|
|
||||||
obj = obj.wrapped
|
|
||||||
# TODO: Support locations (drawn as axes)
|
|
||||||
if not isinstance(obj, TopoDS_Shape):
|
|
||||||
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)')
|
|
||||||
|
|
||||||
# Convert Z-up (OCCT convention) to Y-up (GLTF convention)
|
# Convert Z-up (OCCT convention) to Y-up (GLTF convention)
|
||||||
obj = Shape(obj).rotate(Axis.X, -90).wrapped
|
if isinstance(obj, TopoDS_Shape):
|
||||||
|
obj = Shape(obj).rotate(Axis.X, -90).wrapped
|
||||||
|
elif isinstance(obj, TopLoc_Location):
|
||||||
|
tmp_location = Location(obj)
|
||||||
|
tmp_location.position = Vector(tmp_location.position.X, tmp_location.position.Z, -tmp_location.position.Y)
|
||||||
|
tmp_location.orientation = Vector(tmp_location.orientation.X - 90, tmp_location.orientation.Y,
|
||||||
|
tmp_location.orientation.Z)
|
||||||
|
obj = tmp_location.wrapped
|
||||||
|
|
||||||
self._show_common(name, _hashcode(obj, **kwargs), start, obj, kwargs)
|
self._show_common(name, _hashcode(obj, **kwargs), start, obj, kwargs)
|
||||||
|
|
||||||
|
def show_cad_all(self, **kwargs):
|
||||||
|
"""Publishes all CAD objects to the server"""
|
||||||
|
for name, obj in grab_all_cad():
|
||||||
|
self.show_cad(obj, name, **kwargs)
|
||||||
|
|
||||||
async def _api_object(self, request: web.Request) -> web.Response:
|
async def _api_object(self, request: web.Request) -> web.Response:
|
||||||
"""Returns the object file with the matching name, building it if necessary."""
|
"""Returns the object file with the matching name, building it if necessary."""
|
||||||
# Export the object (or fail if not found)
|
# Export the object (or fail if not found)
|
||||||
@@ -276,7 +277,7 @@ class Server:
|
|||||||
|
|
||||||
# We should build it fully even if we are cancelled, so we use a separate task
|
# We should build it fully even if we are cancelled, so we use a separate task
|
||||||
# Furthermore, building is CPU-bound, so we use the default executor
|
# Furthermore, building is CPU-bound, so we use the default executor
|
||||||
asyncio.get_running_loop().run_in_executor(None, _build_object)
|
await asyncio.get_running_loop().run_in_executor(None, _build_object)
|
||||||
|
|
||||||
# In either case return the elements of a subscription to the async generator
|
# In either case return the elements of a subscription to the async generator
|
||||||
subscription = self.object_events[name].subscribe()
|
subscription = self.object_events[name].subscribe()
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ from OCP.TopExp import TopExp
|
|||||||
from OCP.TopLoc import TopLoc_Location
|
from OCP.TopLoc import TopLoc_Location
|
||||||
from OCP.TopTools import TopTools_IndexedMapOfShape
|
from OCP.TopTools import TopTools_IndexedMapOfShape
|
||||||
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
|
from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex
|
||||||
from build123d import Shape, Vertex, Face
|
from build123d import Shape, Vertex, Face, Location
|
||||||
from pygltflib import GLTF2
|
from pygltflib import GLTF2
|
||||||
|
|
||||||
import mylogger
|
import mylogger
|
||||||
|
from cad import CADLike
|
||||||
from gltf import GLTFMgr
|
from gltf import GLTFMgr
|
||||||
|
|
||||||
|
|
||||||
def tessellate(
|
def tessellate(
|
||||||
ocp_shape: TopoDS_Shape,
|
cad_like: CADLike,
|
||||||
tolerance: float = 0.1,
|
tolerance: float = 0.1,
|
||||||
angular_tolerance: float = 0.1,
|
angular_tolerance: float = 0.1,
|
||||||
faces: bool = True,
|
faces: bool = True,
|
||||||
@@ -26,27 +28,32 @@ def tessellate(
|
|||||||
) -> GLTF2:
|
) -> GLTF2:
|
||||||
"""Tessellate a whole shape into a list of triangle vertices and a list of triangle indices."""
|
"""Tessellate a whole shape into a list of triangle vertices and a list of triangle indices."""
|
||||||
mgr = GLTFMgr()
|
mgr = GLTFMgr()
|
||||||
shape = Shape(ocp_shape)
|
|
||||||
|
|
||||||
# Perform tessellation tasks
|
if isinstance(cad_like, TopLoc_Location):
|
||||||
edge_to_faces: Dict[TopoDS_Edge, List[TopoDS_Face]] = {}
|
mgr.add_location(Location(cad_like))
|
||||||
vertex_to_faces: Dict[TopoDS_Vertex, List[TopoDS_Face]] = {}
|
|
||||||
if faces:
|
elif isinstance(cad_like, TopoDS_Shape):
|
||||||
for face in shape.faces():
|
shape = Shape(cad_like)
|
||||||
_tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance)
|
|
||||||
if edges:
|
# Perform tessellation tasks
|
||||||
for edge in face.edges():
|
edge_to_faces: Dict[TopoDS_Edge, List[TopoDS_Face]] = {}
|
||||||
edge_to_faces[edge.wrapped] = edge_to_faces.get(edge.wrapped, []) + [face.wrapped]
|
vertex_to_faces: Dict[TopoDS_Vertex, List[TopoDS_Face]] = {}
|
||||||
if vertices:
|
if faces:
|
||||||
for vertex in face.vertices():
|
for face in shape.faces():
|
||||||
vertex_to_faces[vertex.wrapped] = vertex_to_faces.get(vertex.wrapped, []) + [face.wrapped]
|
_tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance)
|
||||||
if edges:
|
if edges:
|
||||||
for edge in shape.edges():
|
for edge in face.edges():
|
||||||
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), angular_tolerance,
|
edge_to_faces[edge.wrapped] = edge_to_faces.get(edge.wrapped, []) + [face.wrapped]
|
||||||
angular_tolerance)
|
if vertices:
|
||||||
if vertices:
|
for vertex in face.vertices():
|
||||||
for vertex in shape.vertices():
|
vertex_to_faces[vertex.wrapped] = vertex_to_faces.get(vertex.wrapped, []) + [face.wrapped]
|
||||||
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []))
|
if edges:
|
||||||
|
for edge in shape.edges():
|
||||||
|
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), angular_tolerance,
|
||||||
|
angular_tolerance)
|
||||||
|
if vertices:
|
||||||
|
for vertex in shape.vertices():
|
||||||
|
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []))
|
||||||
|
|
||||||
return mgr.gltf
|
return mgr.gltf
|
||||||
|
|
||||||
@@ -124,15 +131,19 @@ def _hashcode(obj: Union[bytes, TopoDS_Shape], **extras) -> str:
|
|||||||
"""Utility to compute the hash code of a shape recursively without the need to tessellate it"""
|
"""Utility to compute the hash code of a shape recursively without the need to tessellate it"""
|
||||||
# NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program
|
# NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program
|
||||||
# This is best-effort and not guaranteed to be unique
|
# This is best-effort and not guaranteed to be unique
|
||||||
map_of_shapes = TopTools_IndexedMapOfShape()
|
|
||||||
TopExp.MapShapes_s(obj, map_of_shapes)
|
|
||||||
hasher = hashlib.md5(usedforsecurity=False)
|
hasher = hashlib.md5(usedforsecurity=False)
|
||||||
for k, v in extras.items():
|
for k, v in extras.items():
|
||||||
hasher.update(str(k).encode())
|
hasher.update(str(k).encode())
|
||||||
hasher.update(str(v).encode())
|
hasher.update(str(v).encode())
|
||||||
if isinstance(obj, bytes):
|
if isinstance(obj, bytes):
|
||||||
hasher.update(obj)
|
hasher.update(obj)
|
||||||
else:
|
elif isinstance(obj, TopLoc_Location):
|
||||||
|
sub_data = io.BytesIO()
|
||||||
|
obj.DumpJson(sub_data)
|
||||||
|
hasher.update(sub_data.getvalue())
|
||||||
|
elif isinstance(obj, TopoDS_Shape):
|
||||||
|
map_of_shapes = TopTools_IndexedMapOfShape()
|
||||||
|
TopExp.MapShapes_s(obj, map_of_shapes)
|
||||||
for i in range(1, map_of_shapes.Extent() + 1):
|
for i in range(1, map_of_shapes.Extent() + 1):
|
||||||
sub_shape = map_of_shapes.FindKey(i)
|
sub_shape = map_of_shapes.FindKey(i)
|
||||||
sub_data = io.BytesIO()
|
sub_data = io.BytesIO()
|
||||||
@@ -140,4 +151,6 @@ def _hashcode(obj: Union[bytes, TopoDS_Shape], **extras) -> str:
|
|||||||
val = sub_data.getvalue()
|
val = sub_data.getvalue()
|
||||||
val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address
|
val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address
|
||||||
hasher.update(val)
|
hasher.update(val)
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Cannot hash object of type {type(obj)}')
|
||||||
return hasher.hexdigest()
|
return hasher.hexdigest()
|
||||||
|
|||||||
Reference in New Issue
Block a user