diff --git a/.generatelicensefile.json b/.generatelicensefile.json index 14f4198..4163731 100644 --- a/.generatelicensefile.json +++ b/.generatelicensefile.json @@ -1,6 +1,7 @@ { "append": [ "assets/fox.glb.license", + "assets/qwantani_afternoon_1k.hdr.license", "LICENSE" ], "replace": { diff --git a/assets/licenses.txt b/assets/licenses.txt index 76315f4..f869012 100644 --- a/assets/licenses.txt +++ b/assets/licenses.txt @@ -3,7 +3,7 @@ https://www.npmjs.com/package/generate-license-file The following npm package may be included in this product: - - @google/model-viewer@4.0.0 + - @google/model-viewer@4.1.0 This package contains the following license: @@ -522,7 +522,7 @@ Apache License The following npm package may be included in this product: - - detect-libc@2.0.3 + - detect-libc@2.0.4 This package contains the following license: @@ -1045,9 +1045,9 @@ END OF TERMS AND CONDITIONS The following npm packages may be included in this product: - - @lit/reactive-element@1.6.3 - - lit-element@3.3.3 - - lit@2.8.0 + - @lit/reactive-element@2.1.1 + - lit-element@4.2.1 + - lit@3.3.1 These packages each contain the following license: @@ -1084,7 +1084,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following npm package may be included in this product: - - lit-html@2.8.0 + - lit-html@3.3.1 This package contains the following license: @@ -1121,7 +1121,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The following npm package may be included in this product: - - @lit-labs/ssr-dom-shim@1.3.0 + - @lit-labs/ssr-dom-shim@1.4.0 This package contains the following license: @@ -1247,7 +1247,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The following npm package may be included in this product: - - @babel/parser@7.27.0 + - @babel/parser@7.28.0 This package contains the following license: @@ -1381,6 +1381,34 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ----------- +The following npm package may be included in this product: + + - @jridgewell/sourcemap-codec@1.5.4 + +This package contains the following license: + +Copyright 2024 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----------- + The following npm package may be included in this product: - picocolors@1.1.1 @@ -1418,9 +1446,9 @@ LGPL-3.0-or-later The following npm packages may be included in this product: - - @babel/helper-string-parser@7.25.9 - - @babel/helper-validator-identifier@7.25.9 - - @babel/types@7.27.0 + - @babel/helper-string-parser@7.27.1 + - @babel/helper-validator-identifier@7.27.1 + - @babel/types@7.28.1 These packages each contain the following license: @@ -1451,7 +1479,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The following npm package may be included in this product: - - three-mesh-bvh@0.9.0 + - three-mesh-bvh@0.9.1 This package contains the following license: @@ -1601,7 +1629,7 @@ The MIT license applies to all non-font and non-icon files. The following npm package may be included in this product: - - semver@7.7.1 + - semver@7.7.2 This package contains the following license: @@ -1623,36 +1651,6 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ----------- -The following npm package may be included in this product: - - - @jridgewell/sourcemap-codec@1.5.0 - -This package contains the following license: - -The MIT License - -Copyright (c) 2015 Rich Harris - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ------------ - The following npm package may be included in this product: - three@0.125.2 @@ -1685,7 +1683,7 @@ THE SOFTWARE. The following npm package may be included in this product: - - three@0.175.0 + - three@0.178.0 This package contains the following license: @@ -1775,13 +1773,13 @@ THE SOFTWARE. The following npm package may be included in this product: - - vuetify@3.8.0 + - vuetify@3.9.0 This package contains the following license: The MIT License (MIT) -Copyright (c) 2016-2023 John Jeremy Leider +Copyright (c) 2016-now Vuetify, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -1805,16 +1803,16 @@ THE SOFTWARE. The following npm packages may be included in this product: - - @vue/compiler-core@3.5.13 - - @vue/compiler-dom@3.5.13 - - @vue/compiler-sfc@3.5.13 - - @vue/compiler-ssr@3.5.13 - - @vue/reactivity@3.5.13 - - @vue/runtime-core@3.5.13 - - @vue/runtime-dom@3.5.13 - - @vue/server-renderer@3.5.13 - - @vue/shared@3.5.13 - - vue@3.5.13 + - @vue/compiler-core@3.5.17 + - @vue/compiler-dom@3.5.17 + - @vue/compiler-sfc@3.5.17 + - @vue/compiler-ssr@3.5.17 + - @vue/reactivity@3.5.17 + - @vue/runtime-core@3.5.17 + - @vue/runtime-dom@3.5.17 + - @vue/server-renderer@3.5.17 + - @vue/shared@3.5.17 + - vue@3.5.17 These packages each contain the following license: @@ -1844,7 +1842,7 @@ THE SOFTWARE. The following npm package may be included in this product: - - ktx-parse@1.0.0 + - ktx-parse@1.0.1 This package contains the following license: @@ -1906,9 +1904,9 @@ SOFTWARE. The following npm packages may be included in this product: - - @gltf-transform/core@4.1.3 - - @gltf-transform/extensions@4.1.3 - - @gltf-transform/functions@4.1.3 + - @gltf-transform/core@4.2.0 + - @gltf-transform/extensions@4.2.0 + - @gltf-transform/functions@4.2.0 These packages each contain the following license: @@ -1968,7 +1966,7 @@ THE SOFTWARE. The following npm package may be included in this product: - - postcss@8.5.3 + - postcss@8.5.6 This package contains the following license: @@ -2049,6 +2047,11 @@ glTF conversion by @AsoboStudio and @scurest ----------- +CC0: Qwantani Afternoon by Greg Zaal (Photography) and Jarod Guest (Processing) +https://polyhaven.com/a/qwantani_afternoon + +----------- + MIT License Copyright (c) 2024 Yeicor diff --git a/assets/qwantani_afternoon_1k.hdr b/assets/qwantani_afternoon_1k.hdr new file mode 100644 index 0000000..bd310ca Binary files /dev/null and b/assets/qwantani_afternoon_1k.hdr differ diff --git a/assets/qwantani_afternoon_1k.hdr.license b/assets/qwantani_afternoon_1k.hdr.license new file mode 100644 index 0000000..6509f15 --- /dev/null +++ b/assets/qwantani_afternoon_1k.hdr.license @@ -0,0 +1,2 @@ +CC0: Qwantani Afternoon by Greg Zaal (Photography) and Jarod Guest (Processing) +https://polyhaven.com/a/qwantani_afternoon \ No newline at end of file diff --git a/frontend/misc/settings.ts b/frontend/misc/settings.ts index 0a37825..c1534ae 100644 --- a/frontend/misc/settings.ts +++ b/frontend/misc/settings.ts @@ -29,7 +29,14 @@ export async function settings() { panSensitivity: 1, exposure: 1, shadowIntensity: 0, - background: '', + // Nice low-res outdoor/high-contrast HDRI image (CC0 licensed) for lighting + background: new URL('../../assets/qwantani_afternoon_1k.hdr', import.meta.url).href, + // Uniform (1x1 pixel) medium gray background for visibility (following dark/light mode) + skybox: (window.matchMedia("(prefers-color-scheme: dark)").matches ? + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEU4ODiyn42XAAAACklEQVQI" + + "12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==" : + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEW6urpaLVq8AAAACklEQVQI" + + "12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="), }; // Auto-override any settings from the URL diff --git a/frontend/viewer/ModelViewerWrapper.vue b/frontend/viewer/ModelViewerWrapper.vue index e35bd4f..b94ac42 100644 --- a/frontend/viewer/ModelViewerWrapper.vue +++ b/frontend/viewer/ModelViewerWrapper.vue @@ -215,7 +215,7 @@ watch(disableTap, (newDisableTap) => { @@ -302,4 +302,4 @@ watch(disableTap, (newDisableTap) => { float: left; transition: width 0.3s; } - \ No newline at end of file + diff --git a/vite.config.ts b/vite.config.ts index 081facb..2980db4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -28,7 +28,7 @@ export default defineConfig({ build: { assetsDir: '.', // Support deploying to a subdirectory using relative URLs cssCodeSplit: false, // Small enough to inline - chunkSizeWarningLimit: 550, // Three.js is big. Draco is even bigger but not likely to be used. + chunkSizeWarningLimit: 1024, // Three.js is big. Draco is even bigger but not likely to be used. sourcemap: true, // For debugging production }, define: { diff --git a/yacv_server/cad.py b/yacv_server/cad.py index fd06fd9..1a464ba 100644 --- a/yacv_server/cad.py +++ b/yacv_server/cad.py @@ -78,7 +78,8 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]: # Sorting is required to improve hashcode consistency shapes_raw_filtered_sorted = sorted(shapes_raw_filtered, key=lambda x: _hashcode(x)) # Build a single compound shape (skip locations/axes here, they can't be in a Compound) - shapes_bd = [Compound(shape) for shape in shapes_raw_filtered_sorted if shape is not None and not isinstance(shape, TopLoc_Location)] + shapes_bd = [Compound(shape) for shape in shapes_raw_filtered_sorted if + shape is not None and not isinstance(shape, TopLoc_Location)] return get_shape(Compound(shapes_bd), error) except TypeError: pass @@ -168,7 +169,7 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non vert(plane.origin + plane.x_dir * width / 2 + plane.y_dir * height / 2), vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2), vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2), - ], [ + ], [vert(plane.z_dir)] * 4, [ (0, 2, 1), (0, 3, 2), ], [ diff --git a/yacv_server/gltf.py b/yacv_server/gltf.py index 6c4a2df..94386c1 100644 --- a/yacv_server/gltf.py +++ b/yacv_server/gltf.py @@ -4,9 +4,6 @@ import numpy as np from build123d import Location, Plane, Vector from pygltflib import * -_checkerboard_image_bytes = base64.decodebytes( - b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVQI12N49OjR////Gf' - b'/////48WMATwULS8tcyj8AAAAASUVORK5CYII=') def get_version() -> str: try: @@ -24,6 +21,7 @@ class GLTFMgr: # - Face data face_indices: List[int] # 3 indices per triangle face_positions: List[float] # x, y, z + face_normals: List[float] # x, y, z face_tex_coords: List[float] # u, v face_colors: List[float] # r, g, b, a image: Optional[Tuple[bytes, str]] # image/png @@ -36,7 +34,7 @@ class GLTFMgr: vertex_positions: List[float] # x, y, z vertex_colors: List[float] # r, g, b, a - def __init__(self, image: Optional[Tuple[bytes, str]] = (_checkerboard_image_bytes, 'image/png')): + def __init__(self, image: Optional[Tuple[bytes, str]] = None): self.gltf = GLTF2( asset=Asset(generator=f"yacv_server@{get_version()}"), scene=0, @@ -54,6 +52,7 @@ class GLTFMgr: ) self.face_indices = [] self.face_positions = [] + self.face_normals = [] self.face_tex_coords = [] self.face_colors = [] self.image = image @@ -76,24 +75,23 @@ class GLTFMgr: def _vertices_primitive(self) -> Primitive: return [p for p in self.gltf.meshes[0].primitives if p.mode == POINTS][0] - def add_face(self, vertices_raw: List[Vector], indices_raw: List[Tuple[int, int, int]], - tex_coord_raw: List[Tuple[float, float]], color: Optional[Tuple[float, float, float, float]] = None): + def add_face(self, vertices_raw: List[Vector], normals: List[Vector], indices_raw: List[Tuple[int, int, int]], + tex_coord_raw: List[Tuple[float, float]], color: Tuple[float, float, float, float]): """Add a face to the GLTF mesh""" - if color is None: color = (1.0, 0.75, 0.0, 1.0) # assert len(vertices_raw) == len(tex_coord_raw), f"Vertices and texture coordinates have different lengths" # assert min([i for t in indices_raw for i in t]) == 0, f"Face indices start at {min(indices_raw)}" # assert max([e for t in indices_raw for e in t]) < len(vertices_raw), f"Indices have non-existing vertices" base_index = len(self.face_positions) // 3 # All the new indices reference the new vertices self.face_indices.extend([base_index + i for t in indices_raw for i in t]) self.face_positions.extend([v for t in vertices_raw for v in t]) + self.face_normals.extend([n for t in normals for n in t]) self.face_tex_coords.extend([c for t in tex_coord_raw for c in t]) self.face_colors.extend([col for _ in range(len(vertices_raw)) for col in color]) self._faces_primitive.extras["face_triangles_end"].append(len(self.face_indices)) def add_edge(self, vertices_raw: List[Tuple[Tuple[float, float, float], Tuple[float, float, float]]], - color: Optional[Tuple[float, float, float, float]] = None): + color: Tuple[float, float, float, float]): """Add an edge to the GLTF mesh""" - if color is None: color = (0.1, 0.1, 1.0, 1.0) vertices_flat = [v for t in vertices_raw for v in t] # Line from 0 to 1, 2 to 3, 4 to 5, etc. base_index = len(self.edge_positions) // 3 self.edge_indices.extend([base_index + i for i in range(len(vertices_flat))]) @@ -101,9 +99,8 @@ class GLTFMgr: self.edge_colors.extend([col for _ in range(len(vertices_flat)) for col in color]) self._edges_primitive.extras["edge_points_end"].append(len(self.edge_indices)) - def add_vertex(self, vertex: Tuple[float, float, float], color: Optional[Tuple[float, float, float, float]] = None): + def add_vertex(self, vertex: Tuple[float, float, float], color: Tuple[float, float, float, float]): """Add a vertex to the GLTF mesh""" - if color is None: color = (0.1, 0.1, 0.1, 1.0) base_index = len(self.vertex_positions) // 3 self.vertex_indices.append(base_index) self.vertex_positions.extend(vertex) @@ -117,10 +114,11 @@ class GLTFMgr: 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))], color=(0.97, 0.24, 0.24, 1)) - self.add_edge([(vert(pl.origin), vert(pl.origin + pl.y_dir))], color=(0.42, 0.8, 0.15, 1)) - self.add_edge([(vert(pl.origin), vert(pl.origin + pl.z_dir))], color=(0.09, 0.55, 0.94, 1)) + # The colors are hardcoded. You can add vertices and edges manually to change them. + self.add_vertex(vert(pl.origin), color=(0.1, 0.1, 0.1, 1.0)) + self.add_edge([(vert(pl.origin), vert(pl.origin + pl.x_dir))], color=(0.97, 0.24, 0.24, 1.0)) + self.add_edge([(vert(pl.origin), vert(pl.origin + pl.y_dir))], color=(0.42, 0.8, 0.15, 1.0)) + self.add_edge([(vert(pl.origin), vert(pl.origin + pl.z_dir))], color=(0.09, 0.55, 0.94, 1.0)) def build(self) -> GLTF2: """Merge the intermediate data into the GLTF object and return it""" @@ -131,6 +129,8 @@ class GLTFMgr: buffers_list.append(_gen_buffer_metadata(self.face_indices, 1)) self._faces_primitive.attributes.POSITION = len(buffers_list) buffers_list.append(_gen_buffer_metadata(self.face_positions, 3)) + self._faces_primitive.attributes.NORMAL = len(buffers_list) + buffers_list.append(_gen_buffer_metadata(self.face_normals, 3)) self._faces_primitive.attributes.TEXCOORD_0 = len(buffers_list) buffers_list.append(_gen_buffer_metadata(self.face_tex_coords, 2)) self._faces_primitive.attributes.COLOR_0 = len(buffers_list) diff --git a/yacv_server/logo.py b/yacv_server/logo.py index 697d039..d74d563 100644 --- a/yacv_server/logo.py +++ b/yacv_server/logo.py @@ -6,24 +6,30 @@ from build123d import * ASSETS_DIR = os.getenv('ASSETS_DIR', os.path.join(os.path.dirname(__file__), '..', 'assets')) -def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]: +def build_logo(text: bool = True) -> Dict[str, Union[Compound, Location, str]]: """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)) 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): + with BuildSketch(Plane.YZ.move(Pos(faces().group_by(Axis.X)[-1].face().center()))): Text('Yet Another\nCAD Viewer', 6, font_path='/usr/share/fonts/TTF/Hack-Regular.ttf') extrude(amount=1) + logo_face_curved_front = faces().filter_by(GeomType.CYLINDER).group_by(Axis.X)[-1].face() # Highlight text edges with a custom color to_highlight = logo_obj.edges().group_by(Axis.X)[-1] logo_obj_hl = Compound(to_highlight).translate((1e-3, 0, 0)) # To avoid z-fighting logo_obj_hl.color = (0, 0.3, 0.3, 1) + # Highlight face with custom texture + logo_face_curved_front.yacv_texture = \ + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVQI12N49OjR////Gf' \ + '/////48WMATwULS8tcyj8AAAAASUVORK5CYII=' + logo_face_curved_front.color = (0, 0.5, 0.0, 1) + logo_obj = Compound(logo_obj.faces() - ShapeList([logo_face_curved_front])) # Remove face from the main object + # Add a logo image to the CAD part logo_img_location = logo_obj.faces().group_by(Axis.X)[0].face().center_location logo_img_location *= Location((0, 0, 4e-2), (0, 0, 90)) # Avoid overlapping and adjust placement @@ -33,7 +39,8 @@ def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]: # Add an animated fox to the CAD part fox_glb_bytes = open(os.path.join(ASSETS_DIR, 'fox.glb'), 'rb').read() - return {'fox': fox_glb_bytes, 'logo': logo_obj, 'logo_hl': logo_obj_hl, 'location': logo_img_location, img_name: img_glb_bytes} + return {'fox': fox_glb_bytes, 'logo': logo_obj, 'logo_hl': logo_obj_hl, 'logo_hl_tex': logo_face_curved_front, + 'location': logo_img_location, img_name: img_glb_bytes} if __name__ == "__main__": diff --git a/yacv_server/main.py b/yacv_server/main.py deleted file mode 100644 index 404c411..0000000 --- a/yacv_server/main.py +++ /dev/null @@ -1,6 +0,0 @@ -from os import system - -if __name__ == '__main__': - # Just a reminder that a hot-reloading server can be started with the following command: - # Need to disable auto-start to avoid conflicts with the hot-reloading server - system('YACV_DISABLE_SERVER=true aiohttp-devtools runserver __init__.py --port 32323 --app-factory _get_app') diff --git a/yacv_server/tessellate.py b/yacv_server/tessellate.py index e8a3eb0..15b5bb0 100644 --- a/yacv_server/tessellate.py +++ b/yacv_server/tessellate.py @@ -3,9 +3,11 @@ from typing import List, Dict, Tuple, Optional from OCP.BRep import BRep_Tool from OCP.BRepAdaptor import BRepAdaptor_Curve from OCP.GCPnts import GCPnts_TangentialDeflection +from OCP.OCP.BRepLib import BRepLib_ToolTriangulatedShape +from OCP.OCP.TopAbs import TopAbs_Orientation from OCP.TopLoc import TopLoc_Location from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex -from build123d import Vertex, Face, Location, Compound +from build123d import Vertex, Face, Location, Compound, Vector from pygltflib import GLTF2 from yacv_server.cad import CADCoreLike, ColorTuple @@ -14,14 +16,9 @@ from yacv_server.mylogger import logger def tessellate( - cad_like: CADCoreLike, - tolerance: float = 0.1, - angular_tolerance: float = 0.1, - faces: bool = True, - edges: bool = True, - vertices: bool = True, - obj_color: Optional[ColorTuple] = None, - texture: Optional[Tuple[bytes, str]] = None, + cad_like: CADCoreLike, color_faces: ColorTuple, color_edges: ColorTuple, color_vertices: ColorTuple, + color_obj: Optional[ColorTuple] = None, tolerance: float = 0.1, angular_tolerance: float = 0.1, + faces: bool = True, edges: bool = True, vertices: bool = True, texture: Optional[Tuple[bytes, str]] = None, ) -> GLTF2: """Tessellate a whole shape into a list of triangle vertices and a list of triangle indices.""" if texture is None: @@ -41,23 +38,24 @@ def tessellate( if faces and hasattr(shape, 'faces'): shape_faces = shape.faces() for face in shape_faces: - _tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance, obj_color) + _tessellate_face(mgr, face.wrapped, color_obj or color_faces, 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 len(shape_faces) > 0: obj_color = None # Don't color edges/vertices if faces are colored + if len(shape_faces) > 0: color_obj = None # Don't color edges/vertices if faces are colored if edges and hasattr(shape, 'edges'): shape_edges = shape.edges() for edge in shape_edges: - _tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), angular_tolerance, - angular_tolerance, obj_color) - if len(shape_edges) > 0: obj_color = None # Don't color vertices if edges are colored + _tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), color_obj or color_edges, + angular_tolerance, angular_tolerance) + if len(shape_edges) > 0: color_obj = None # Don't color vertices if edges are colored if vertices and hasattr(shape, 'vertices'): for vertex in shape.vertices(): - _tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []), obj_color) + _tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []), + color_obj or color_vertices) else: raise TypeError(f"Unsupported type: {type(cad_like)}: {cad_like}") @@ -68,9 +66,9 @@ def tessellate( def _tessellate_face( mgr: GLTFMgr, ocp_face: TopoDS_Face, + color: ColorTuple, tolerance: float = 1e-3, angular_tolerance: float = 0.1, - color: Optional[ColorTuple] = None, ): face = Compound(ocp_face) # face.mesh(tolerance, angular_tolerance) @@ -81,6 +79,14 @@ def _tessellate_face( logger.warn("No triangulation found for face") return GLTF2() + # Get the normal for each vertex (for smooth instead of flat shading!) + BRepLib_ToolTriangulatedShape.ComputeNormals_s(face.wrapped, poly) + reversed_face = face.wrapped.Orientation() == TopAbs_Orientation.TopAbs_REVERSED + normals = [ + -Vector(v) if reversed_face else Vector(v) + for v in (poly.Normal(i) for i in range(1, poly.NbNodes() + 1)) + ] + # Get UV of each face from the parameters uv = [ (v.X(), v.Y()) @@ -89,7 +95,7 @@ def _tessellate_face( vertices = tri_mesh[0] indices = tri_mesh[1] - mgr.add_face(vertices, indices, uv, color) + mgr.add_face(vertices, normals, indices, uv, color) return None @@ -113,9 +119,9 @@ def _tessellate_edge( mgr: GLTFMgr, ocp_edge: TopoDS_Edge, faces: List[TopoDS_Face], + color: ColorTuple, angular_deflection: float = 0.1, curvature_deflection: float = 0.1, - color: Optional[ColorTuple] = None, ): # Use a curve discretizer to get the vertices curve = BRepAdaptor_Curve(ocp_edge) @@ -136,9 +142,6 @@ def _tessellate_edge( mgr.add_edge(vertices, color) -def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[TopoDS_Face], - color: Optional[ColorTuple] = None): +def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[TopoDS_Face], color: ColorTuple): c = Vertex(ocp_vertex).center() mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces), color) - - diff --git a/yacv_server/yacv.py b/yacv_server/yacv.py index 3d1a22e..868f985 100644 --- a/yacv_server/yacv.py +++ b/yacv_server/yacv.py @@ -20,7 +20,7 @@ from PIL import Image from build123d import Shape, Axis, Location, Vector from dataclasses_json import dataclass_json -from yacv_server.cad import _hashcode, get_color +from yacv_server.cad import _hashcode, get_color, ColorTuple from yacv_server.cad import get_shape, grab_all_cad, CADCoreLike, CADLike from yacv_server.gltf import get_version from yacv_server.myhttp import HTTPHandler @@ -93,12 +93,39 @@ class YACV: texture: Optional[Tuple[bytes, str]] """Default texture to use for model faces, in (data, mimetype) format. - If left as None, a default checkerboard texture will be used. + If left as None, no texture will be used. - It can be set with the YACV_BASE_TEXTURE= and overridden by `show(..., texture="")`. + It can be set with the YACV_TEXTURE= and overridden by the custom `yacv_texture` attribute of an object. The can be file: or data:;base64, where is the mime type and is the base64 encoded image.""" + color_faces: Optional[ColorTuple] + """Overrides the default color to use for model faces. Applies even if a texture is used. + + You can use `show(..., color_faces=...)` or the standard way of setting colors for build123d/cadquery objects to + override this color. + + It can be set with the YACV_COLOR_FACES= environment variable, where is a color + in the hexadecimal format #RRGGBB or #RRGGBBAA.""" + + color_edges: Optional[ColorTuple] + """Overrides the default color to use for model edges. + + You can use `show(..., color_edges=...) or the standard way of setting colors for build123d/cadquery objects to + override this color. + + It can be set with the YACV_COLOR_EDGES= environment variable, where is a color + in the hexadecimal format #RRGGBB or #RRGGBBAA.""" + + color_vertices: Optional[ColorTuple] + """Overrides the default color to use for model vertices. + + You can use `show(..., color_vertices=...)` or the standard way of setting colors for build123d/cadquery objects to + override this color. + + It can be set with the YACV_COLOR_VERTICES= environment variable, where is a color + in the hexadecimal format #RRGGBB or #RRGGBBAA.""" + def __init__(self): self.server_thread = None self.server = None @@ -109,7 +136,10 @@ class YACV: self.at_least_one_client = threading.Event() self.shutting_down = threading.Event() self.frontend_lock = RWLock() - self.texture = _read_texture_uri(os.getenv("YACV_BASE_TEXTURE")) + self.texture = _read_texture_uri(os.getenv("YACV_TEXTURE")) + self.color_faces = _read_color(os.getenv("YACV_COLOR_FACES", "#ffbf00")) # Default yellow + self.color_edges = _read_color(os.getenv("YACV_COLOR_EDGES", "#1a1aff")) # Default blue + self.color_vertices = _read_color(os.getenv("YACV_COLOR_VERTICES", "#1a1a1a")) # Default dark gray logger.info('Using yacv-server v%s', get_version()) def start(self): @@ -201,8 +231,9 @@ class YACV: if isinstance(names, str): names = [names] assert len(names) == len(objs), 'Number of names must match the number of objects' - if 'color' in kwargs: - kwargs['color'] = get_color(kwargs['color']) + for color_name in ('color_faces', 'color_edges', 'color_vertices'): + if color_name in kwargs: + kwargs[color_name] = get_color(kwargs[color_name]) or _read_color(kwargs[color_name]) # Handle auto clearing of previous objects if kwargs.get('auto_clear', True): @@ -218,13 +249,15 @@ class YACV: # Publish the show event for obj, name in zip(objs, names): obj_color = get_color(obj) + # Some properties may be lost in preprocessing, so save them in kwargs + _kwargs = kwargs.copy() if obj_color is not None: - kwargs = kwargs.copy() - kwargs['color'] = obj_color + _kwargs['color_obj'] = obj_color # Only applies to highest-dimensional objects + _kwargs['texture'] = _read_texture_uri(getattr(obj, 'yacv_texture', None) or kwargs.get('texture', None)) if not isinstance(obj, bytes): - obj = _preprocess_cad(obj, **kwargs) - _hash = _hashcode(obj, **kwargs) - event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, kwargs=kwargs or {}) + obj = _preprocess_cad(obj, **_kwargs) + _hash = _hashcode(obj, **_kwargs) + event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, kwargs=_kwargs or {}) self.show_events.publish(event) logger.info('show %s took %.3f seconds', names, time.time() - start) @@ -309,17 +342,17 @@ class YACV: if isinstance(event.obj, bytes): # Already a GLTF publish_to.publish(event.obj) else: # CAD object to tessellate and convert to GLTF - texture_override_uri = event.kwargs.get('texture', None) - texture_override = None - if isinstance(texture_override_uri, str): - texture_override = _read_texture_uri(texture_override_uri) - gltf = tessellate(event.obj, tolerance=event.kwargs.get('tolerance', 0.1), - angular_tolerance=event.kwargs.get('angular_tolerance', 0.1), - faces=event.kwargs.get('faces', True), - edges=event.kwargs.get('edges', True), - vertices=event.kwargs.get('vertices', True), - obj_color=event.kwargs.get('color', None), - texture=texture_override or self.texture) + gltf = tessellate( + event.obj, + color_faces=event.kwargs.get('color_faces', self.color_faces), + color_edges=event.kwargs.get('color_edges', self.color_edges), + color_vertices=event.kwargs.get('color_vertices', self.color_vertices), + color_obj=event.kwargs.get('color_obj', None), + tolerance=event.kwargs.get('tolerance', 0.1), + angular_tolerance=event.kwargs.get('angular_tolerance', 0.1), + faces=event.kwargs.get('faces', True), edges=event.kwargs.get('edges', True), + vertices=event.kwargs.get('vertices', True), + texture=event.kwargs.get('texture', self.texture)) glb_list_of_bytes = gltf.save_to_bytes() glb_bytes = b''.join(glb_list_of_bytes) publish_to.publish(glb_bytes) @@ -355,7 +388,7 @@ def _read_texture_uri(uri: str) -> Optional[Tuple[bytes, str]]: img = Image.open(buf) mtype = img.get_format_mimetype() return data, mtype - if uri.startswith("data:"): # https://en.wikipedia.org/wiki/Data_URI_scheme#Syntax (limited) + if uri.startswith("data:"): # https://en.wikipedia.org/wiki/Data_URI_scheme#Syntax (limited) mtype_and_data = uri[len("data:"):] mtype = mtype_and_data.split(";", 1)[0] data_str = mtype_and_data.split(",", 1)[1] @@ -363,6 +396,20 @@ def _read_texture_uri(uri: str) -> Optional[Tuple[bytes, str]]: return data, mtype return None + +def _read_color(color: str) -> Optional[ColorTuple]: + """Reads a color from a string in the format #RRGGBB or #RRGGBBAA""" + if color is None: + return None + if not color.startswith('#') or len(color) not in (7, 9): + raise ValueError(f'Invalid color format: {color}') + r = float(int(color[1:3], 16)) / 255.0 + g = float(int(color[3:5], 16)) / 255.0 + b = float(int(color[5:7], 16)) / 255.0 + a = float(int(color[7:9], 16)) / 255.0 if len(color) == 9 else 1.0 + return r, g, b, a + + # noinspection PyUnusedLocal def _preprocess_cad(obj: CADLike, **kwargs) -> CADCoreLike: # Get the shape of a CAD-like object @@ -384,6 +431,7 @@ def _preprocess_cad(obj: CADLike, **kwargs) -> CADCoreLike: _obj_name_counts = {} + def _find_var_name(obj: any, avoid_levels: int = 2) -> str: """A hacky way to get a stable name for an object that may change over time"""