add full support for locations, better helpers and more server fixes

This commit is contained in:
Yeicor
2024-03-02 23:00:07 +01:00
parent a2a14ca257
commit c51ddade4b
9 changed files with 198 additions and 224 deletions

View File

@@ -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);

View File

@@ -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 = [];
} }
} }
} }

View File

@@ -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;

View File

@@ -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
View 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.

View File

@@ -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,7 +12,8 @@ _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( def __init__(self):
self.gltf = GLTF2(
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"), asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
scene=0, scene=0,
scenes=[Scene(nodes=[0])], scenes=[Scene(nodes=[0])],
@@ -24,9 +26,8 @@ class GLTFMgr:
textures=[Texture(source=0, sampler=0)], textures=[Texture(source=0, sampler=0)],
images=[Image(bufferView=0, mimeType='image/png')], images=[Image(bufferView=0, mimeType='image/png')],
) )
def __init__(self):
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'))

View File

@@ -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))
if text:
text_at_plane = Plane.YZ text_at_plane = Plane.YZ
text_at_plane.origin = faces().group_by(Axis.X)[-1].face().center() text_at_plane.origin = faces().group_by(Axis.X)[-1].face().center()
with BuildSketch(text_at_plane.location): with BuildSketch(text_at_plane.location):
Text('Yet Another\nCAD Viewer', 7, font_path='/usr/share/fonts/TTF/OpenSans-Regular.ttf') Text('Yet Another\nCAD Viewer', 7, font_path='/usr/share/fonts/TTF/OpenSans-Regular.ttf')
extrude(amount=1) 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

View File

@@ -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)
if isinstance(obj, TopoDS_Shape):
obj = Shape(obj).rotate(Axis.X, -90).wrapped 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()

View File

@@ -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,7 +28,12 @@ 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)
if isinstance(cad_like, TopLoc_Location):
mgr.add_location(Location(cad_like))
elif isinstance(cad_like, TopoDS_Shape):
shape = Shape(cad_like)
# Perform tessellation tasks # Perform tessellation tasks
edge_to_faces: Dict[TopoDS_Edge, List[TopoDS_Face]] = {} edge_to_faces: Dict[TopoDS_Edge, List[TopoDS_Face]] = {}
@@ -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()