diff --git a/src/misc/helpers.ts b/src/misc/helpers.ts index 645ac51..1db463f 100644 --- a/src/misc/helpers.ts +++ b/src/misc/helpers.ts @@ -1,7 +1,6 @@ import {Document, TypedArray} from '@gltf-transform/core' import {Vector2} from 'three/src/math/Vector2' import {Vector3} from 'three/src/math/Vector3' -import {Box3} from 'three/src/math/Box3' 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) { let rawPositions = [ - 0, 0, 0, - size.x, 0, 0, - 0, 0, 0, - 0, size.y, 0, - 0, 0, 0, - 0, 0, -size.z, + [0, 0, 0, size.x, 0, 0], + [0, 0, 0, 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 = [ - ...(AxesColors.x[0]), ...(AxesColors.x[1]), - ...(AxesColors.y[0]), ...(AxesColors.y[1]), - ...(AxesColors.z[0]), ...(AxesColors.z[1]), - ].map(x => x / 255.0); - buildSimpleGltf(doc, rawPositions, rawIndices, rawColors, transform, '__helper_axes'); + [...(AxesColors.x[0]), ...(AxesColors.x[1])], + [...(AxesColors.y[0]), ...(AxesColors.y[1])], + [...(AxesColors.z[0]), ...(AxesColors.z[1])], + ].map(g => g.map(x => x / 255.0)); + 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 rawIndices = []; // 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; // Vertical quad (two triangles) - rawPositions.push(x - divisionWidth / 2, -size.y / 2, 0); - rawPositions.push(x + divisionWidth / 2, -size.y / 2, 0); - rawPositions.push(x + divisionWidth / 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 * size.x / 2, -size.y / 2, 0); + rawPositions.push(x + divisionWidth * size.x / 2, size.y / 2, 0); + rawPositions.push(x - divisionWidth * size.x / 2, size.y / 2, 0); const baseIndex = i * 4; rawIndices.push(baseIndex, baseIndex + 1, baseIndex + 2); rawIndices.push(baseIndex, baseIndex + 2, baseIndex + 3); // Horizontal quad (two triangles) - rawPositions.push(-size.x / 2, y - divisionWidth / 2, 0); - rawPositions.push(size.x / 2, y - divisionWidth / 2, 0); - rawPositions.push(size.x / 2, y + divisionWidth / 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 * size.y / 2, 0); + rawPositions.push(size.x / 2, y + divisionWidth * size.y / 2, 0); + rawPositions.push(-size.x / 2, y + divisionWidth * size.y / 2, 0); const baseIndex2 = (divisions + 1 + i) * 4; rawIndices.push(baseIndex2, baseIndex2 + 1, baseIndex2 + 2); rawIndices.push(baseIndex2, baseIndex2 + 2, baseIndex2 + 3); diff --git a/src/models/Model.vue b/src/models/Model.vue index 32090d4..99662e2 100644 --- a/src/models/Model.vue +++ b/src/models/Model.vue @@ -147,10 +147,10 @@ function onClipPlanesChange() { if (!enabledY) planes.splice(1, 1); if (!enabledX) planes.shift(); 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 { 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 = []; } } } diff --git a/src/tools/Selection.vue b/src/tools/Selection.vue index e54890d..12c1510 100644 --- a/src/tools/Selection.vue +++ b/src/tools/Selection.vue @@ -89,9 +89,10 @@ let selectionListener = (event: MouseEvent) => { const hits = raycaster.intersectObject(scene, true); let hit = hits.find((hit) => { const kind = hit.object.type + console.log(kind) const kindOk = (selectFilter.value === 'Any (S)') || ((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'); return hit.object.visible && !hit.object.userData.noHit && kindOk; }) as Intersection | undefined; diff --git a/yacv_server/__init__.py b/yacv_server/__init__.py index e2dc7f8..2463b78 100644 --- a/yacv_server/__init__.py +++ b/yacv_server/__init__.py @@ -18,13 +18,17 @@ if 'YACV_DISABLE_SERVER' not in os.environ: # Expose some nice aliases using the default server instance show = server.show show_object = show +show_all = server.show_cad_all def _get_app() -> web.Application: """Required by aiohttp-devtools""" logging.basicConfig(level=logging.DEBUG) 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 diff --git a/yacv_server/cad.py b/yacv_server/cad.py new file mode 100644 index 0000000..05a1f56 --- /dev/null +++ b/yacv_server/cad.py @@ -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. diff --git a/yacv_server/gltf.py b/yacv_server/gltf.py index afead89..73dbd0c 100644 --- a/yacv_server/gltf.py +++ b/yacv_server/gltf.py @@ -1,6 +1,7 @@ import importlib.metadata import numpy as np +from build123d import Location, Plane, Vector from pygltflib import * _checkerboard_image_bytes = base64.decodebytes( @@ -11,22 +12,22 @@ _checkerboard_image_bytes = base64.decodebytes( class GLTFMgr: """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): + 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) + # 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]], 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) 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""" 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) 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]): """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) 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: """It is important to use a different material for each primitive to be able to change them at runtime""" new_material: Material @@ -62,6 +76,15 @@ class GLTFMgr: elif kind == "vertex": new_material = Material(name="vertex", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness( 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: raise ValueError(f"Unknown material kind {kind}") self.gltf.materials.append(new_material) @@ -129,9 +152,9 @@ class GLTFMgr: ) if len(tex_coord) > 0 else None ] if it is not None]) - binary_blob = self.gltf.binary_blob() - byte_offset_base = len(binary_blob) - self.gltf.bufferViews.extend([it for it in [ + prev_binary_blob = self.gltf.binary_blob() + byte_offset_base = len(prev_binary_blob) + self.gltf.bufferViews.extend([bv for bv in [ BufferView( buffer=0, byteOffset=byte_offset_base, @@ -149,131 +172,7 @@ class GLTFMgr: byteOffset=byte_offset_base + len(indices_blob) + len(vertices_blob), byteLength=len(tex_coord_blob), 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) - - -# -# -# 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')) + self.gltf.set_binary_blob(prev_binary_blob + indices_blob + vertices_blob + tex_coord_blob) diff --git a/yacv_server/logo.py b/yacv_server/logo.py index ce14193..f79a605 100644 --- a/yacv_server/logo.py +++ b/yacv_server/logo.py @@ -2,29 +2,29 @@ import asyncio import logging import os -from OCP.TopoDS import TopoDS_Shape from build123d import * -def build_logo() -> TopoDS_Shape: +def build_logo(text: bool = True) -> Part: """Builds the CAD part of the logo""" with BuildPart(Plane.XY.offset(50)) as logo_obj: Box(22, 40, 30) 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)) - text_at_plane = Plane.YZ - text_at_plane.origin = faces().group_by(Axis.X)[-1].face().center() - with BuildSketch(text_at_plane.location): - Text('Yet Another\nCAD Viewer', 7, font_path='/usr/share/fonts/TTF/OpenSans-Regular.ttf') - extrude(amount=1) + if text: + text_at_plane = Plane.YZ + text_at_plane.origin = faces().group_by(Axis.X)[-1].face().center() + with BuildSketch(text_at_plane.location): + 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__": 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' from yacv_server import show_object, server diff --git a/yacv_server/server.py b/yacv_server/server.py index 87d9d45..d4536e3 100644 --- a/yacv_server/server.py +++ b/yacv_server/server.py @@ -9,11 +9,13 @@ from threading import Thread from typing import Optional, Dict, Union import aiohttp_cors +from OCP.TopLoc import TopLoc_Location from OCP.TopoDS import TopoDS_Shape from aiohttp import web -from build123d import Shape, Axis +from build123d import Shape, Axis, Location, Vector from dataclasses_json import dataclass_json +from cad import get_shape, grab_all_cad from mylogger import logger from pubsub import BufferedPubSub from tessellate import _hashcode, tessellate @@ -202,27 +204,26 @@ class Server: """Publishes a CAD object to the server""" start = time.time() - # Try to grab a shape if a different type of object was passed - if not isinstance(obj, TopoDS_Shape): - # 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?)') + # Get the shape of a CAD-like object + obj = get_shape(obj) # 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) + 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: """Returns the object file with the matching name, building it if necessary.""" # 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 # 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 subscription = self.object_events[name].subscribe() diff --git a/yacv_server/tessellate.py b/yacv_server/tessellate.py index 041835e..60cd15d 100644 --- a/yacv_server/tessellate.py +++ b/yacv_server/tessellate.py @@ -10,14 +10,16 @@ from OCP.TopExp import TopExp from OCP.TopLoc import TopLoc_Location from OCP.TopTools import TopTools_IndexedMapOfShape 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 import mylogger +from cad import CADLike from gltf import GLTFMgr + def tessellate( - ocp_shape: TopoDS_Shape, + cad_like: CADLike, tolerance: float = 0.1, angular_tolerance: float = 0.1, faces: bool = True, @@ -26,27 +28,32 @@ def tessellate( ) -> GLTF2: """Tessellate a whole shape into a list of triangle vertices and a list of triangle indices.""" mgr = GLTFMgr() - shape = Shape(ocp_shape) - # Perform tessellation tasks - edge_to_faces: Dict[TopoDS_Edge, List[TopoDS_Face]] = {} - vertex_to_faces: Dict[TopoDS_Vertex, List[TopoDS_Face]] = {} - if faces: - for face in shape.faces(): - _tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance) - if edges: - for edge in face.edges(): - edge_to_faces[edge.wrapped] = edge_to_faces.get(edge.wrapped, []) + [face.wrapped] - if vertices: - for vertex in face.vertices(): - vertex_to_faces[vertex.wrapped] = vertex_to_faces.get(vertex.wrapped, []) + [face.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, [])) + 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 + edge_to_faces: Dict[TopoDS_Edge, List[TopoDS_Face]] = {} + vertex_to_faces: Dict[TopoDS_Vertex, List[TopoDS_Face]] = {} + if faces: + for face in shape.faces(): + _tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance) + if edges: + for edge in face.edges(): + edge_to_faces[edge.wrapped] = edge_to_faces.get(edge.wrapped, []) + [face.wrapped] + if vertices: + for vertex in face.vertices(): + vertex_to_faces[vertex.wrapped] = vertex_to_faces.get(vertex.wrapped, []) + [face.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 @@ -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""" # 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 - map_of_shapes = TopTools_IndexedMapOfShape() - TopExp.MapShapes_s(obj, map_of_shapes) hasher = hashlib.md5(usedforsecurity=False) for k, v in extras.items(): hasher.update(str(k).encode()) hasher.update(str(v).encode()) if isinstance(obj, bytes): 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): sub_shape = map_of_shapes.FindKey(i) sub_data = io.BytesIO() @@ -140,4 +151,6 @@ def _hashcode(obj: Union[bytes, TopoDS_Shape], **extras) -> str: val = sub_data.getvalue() val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address hasher.update(val) + else: + raise ValueError(f'Cannot hash object of type {type(obj)}') return hasher.hexdigest()