Minor backend improvements: better color and textured handling, smooth shading, better demo

This commit is contained in:
Yeicor
2025-07-19 12:43:05 +02:00
parent 41662944d6
commit 10ed5e2e9e
6 changed files with 126 additions and 74 deletions

View File

@@ -78,7 +78,8 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
# Sorting is required to improve hashcode consistency # Sorting is required to improve hashcode consistency
shapes_raw_filtered_sorted = sorted(shapes_raw_filtered, key=lambda x: _hashcode(x)) 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) # 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) return get_shape(Compound(shapes_bd), error)
except TypeError: except TypeError:
pass 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.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, 2, 1),
(0, 3, 2), (0, 3, 2),
], [ ], [

View File

@@ -4,10 +4,6 @@ import numpy as np
from build123d import Location, Plane, Vector from build123d import Location, Plane, Vector
from pygltflib import * from pygltflib import *
# PNG file containing 1x1 while pixel
_default_tex = (b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=')
_default_color = (1.0, 0.75, 0.0, 1.0)
def get_version() -> str: def get_version() -> str:
try: try:
@@ -25,6 +21,7 @@ class GLTFMgr:
# - Face data # - Face data
face_indices: List[int] # 3 indices per triangle face_indices: List[int] # 3 indices per triangle
face_positions: List[float] # x, y, z face_positions: List[float] # x, y, z
face_normals: List[float] # x, y, z
face_tex_coords: List[float] # u, v face_tex_coords: List[float] # u, v
face_colors: List[float] # r, g, b, a face_colors: List[float] # r, g, b, a
image: Optional[Tuple[bytes, str]] # image/png image: Optional[Tuple[bytes, str]] # image/png
@@ -37,7 +34,7 @@ class GLTFMgr:
vertex_positions: List[float] # x, y, z vertex_positions: List[float] # x, y, z
vertex_colors: List[float] # r, g, b, a vertex_colors: List[float] # r, g, b, a
def __init__(self, image: Optional[Tuple[bytes, str]] = (_default_tex, 'image/png')): def __init__(self, image: Optional[Tuple[bytes, str]] = None):
self.gltf = GLTF2( self.gltf = GLTF2(
asset=Asset(generator=f"yacv_server@{get_version()}"), asset=Asset(generator=f"yacv_server@{get_version()}"),
scene=0, scene=0,
@@ -55,6 +52,7 @@ class GLTFMgr:
) )
self.face_indices = [] self.face_indices = []
self.face_positions = [] self.face_positions = []
self.face_normals = []
self.face_tex_coords = [] self.face_tex_coords = []
self.face_colors = [] self.face_colors = []
self.image = image self.image = image
@@ -77,24 +75,23 @@ class GLTFMgr:
def _vertices_primitive(self) -> Primitive: def _vertices_primitive(self) -> Primitive:
return [p for p in self.gltf.meshes[0].primitives if p.mode == POINTS][0] 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]], 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: Optional[Tuple[float, float, float, float]] = None): tex_coord_raw: List[Tuple[float, float]], color: Tuple[float, float, float, float]):
"""Add a face to the GLTF mesh""" """Add a face to the GLTF mesh"""
if color is None: color = _default_color
# assert len(vertices_raw) == len(tex_coord_raw), f"Vertices and texture coordinates have different lengths" # 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 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" # 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 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_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_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_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.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)) 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]]], 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""" """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. 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 base_index = len(self.edge_positions) // 3
self.edge_indices.extend([base_index + i for i in range(len(vertices_flat))]) self.edge_indices.extend([base_index + i for i in range(len(vertices_flat))])
@@ -102,9 +99,8 @@ class GLTFMgr:
self.edge_colors.extend([col for _ in range(len(vertices_flat)) for col in color]) 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)) 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""" """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 base_index = len(self.vertex_positions) // 3
self.vertex_indices.append(base_index) self.vertex_indices.append(base_index)
self.vertex_positions.extend(vertex) self.vertex_positions.extend(vertex)
@@ -118,10 +114,11 @@ class GLTFMgr:
return v.X, v.Y, v.Z 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 # Add 1 origin vertex and 3 edges with custom colors to identify the X, Y and Z axis
self.add_vertex(vert(pl.origin)) # The colors are hardcoded. You can add vertices and edges manually to change them.
self.add_edge([(vert(pl.origin), vert(pl.origin + pl.x_dir))], color=(0.97, 0.24, 0.24, 1)) 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.y_dir))], color=(0.42, 0.8, 0.15, 1)) 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.z_dir))], color=(0.09, 0.55, 0.94, 1)) 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: def build(self) -> GLTF2:
"""Merge the intermediate data into the GLTF object and return it""" """Merge the intermediate data into the GLTF object and return it"""
@@ -132,6 +129,8 @@ class GLTFMgr:
buffers_list.append(_gen_buffer_metadata(self.face_indices, 1)) buffers_list.append(_gen_buffer_metadata(self.face_indices, 1))
self._faces_primitive.attributes.POSITION = len(buffers_list) self._faces_primitive.attributes.POSITION = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(self.face_positions, 3)) 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) self._faces_primitive.attributes.TEXCOORD_0 = len(buffers_list)
buffers_list.append(_gen_buffer_metadata(self.face_tex_coords, 2)) buffers_list.append(_gen_buffer_metadata(self.face_tex_coords, 2))
self._faces_primitive.attributes.COLOR_0 = len(buffers_list) self._faces_primitive.attributes.COLOR_0 = len(buffers_list)

View File

@@ -6,24 +6,30 @@ from build123d import *
ASSETS_DIR = os.getenv('ASSETS_DIR', os.path.join(os.path.dirname(__file__), '..', 'assets')) 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""" """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: if text:
text_at_plane = Plane.YZ with BuildSketch(Plane.YZ.move(Pos(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):
Text('Yet Another\nCAD Viewer', 6, font_path='/usr/share/fonts/TTF/Hack-Regular.ttf') Text('Yet Another\nCAD Viewer', 6, font_path='/usr/share/fonts/TTF/Hack-Regular.ttf')
extrude(amount=1) 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 # Highlight text edges with a custom color
to_highlight = logo_obj.edges().group_by(Axis.X)[-1] 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 = Compound(to_highlight).translate((1e-3, 0, 0)) # To avoid z-fighting
logo_obj_hl.color = (0, 0.3, 0.3, 1) 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 # 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 = 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 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 # Add an animated fox to the CAD part
fox_glb_bytes = open(os.path.join(ASSETS_DIR, 'fox.glb'), 'rb').read() 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__": if __name__ == "__main__":

View File

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

View File

@@ -3,9 +3,11 @@ from typing import List, Dict, Tuple, Optional
from OCP.BRep import BRep_Tool from OCP.BRep import BRep_Tool
from OCP.BRepAdaptor import BRepAdaptor_Curve from OCP.BRepAdaptor import BRepAdaptor_Curve
from OCP.GCPnts import GCPnts_TangentialDeflection 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.TopLoc import TopLoc_Location
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 Vertex, Face, Location, Compound from build123d import Vertex, Face, Location, Compound, Vector
from pygltflib import GLTF2 from pygltflib import GLTF2
from yacv_server.cad import CADCoreLike, ColorTuple from yacv_server.cad import CADCoreLike, ColorTuple
@@ -14,14 +16,9 @@ from yacv_server.mylogger import logger
def tessellate( def tessellate(
cad_like: CADCoreLike, cad_like: CADCoreLike, color_faces: ColorTuple, color_edges: ColorTuple, color_vertices: ColorTuple,
tolerance: float = 0.1, color_obj: Optional[ColorTuple] = None, tolerance: float = 0.1, angular_tolerance: float = 0.1,
angular_tolerance: float = 0.1, faces: bool = True, edges: bool = True, vertices: bool = True, texture: Optional[Tuple[bytes, str]] = None,
faces: bool = True,
edges: bool = True,
vertices: bool = True,
obj_color: Optional[ColorTuple] = None,
texture: Optional[Tuple[bytes, str]] = None,
) -> 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."""
if texture is None: if texture is None:
@@ -41,23 +38,24 @@ def tessellate(
if faces and hasattr(shape, 'faces'): if faces and hasattr(shape, 'faces'):
shape_faces = shape.faces() shape_faces = shape.faces()
for face in 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: if edges:
for edge in face.edges(): for edge in face.edges():
edge_to_faces[edge.wrapped] = edge_to_faces.get(edge.wrapped, []) + [face.wrapped] edge_to_faces[edge.wrapped] = edge_to_faces.get(edge.wrapped, []) + [face.wrapped]
if vertices: if vertices:
for vertex in face.vertices(): for vertex in face.vertices():
vertex_to_faces[vertex.wrapped] = vertex_to_faces.get(vertex.wrapped, []) + [face.wrapped] 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'): if edges and hasattr(shape, 'edges'):
shape_edges = shape.edges() shape_edges = shape.edges()
for edge in shape_edges: for edge in shape_edges:
_tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), angular_tolerance, _tessellate_edge(mgr, edge.wrapped, edge_to_faces.get(edge.wrapped, []), color_obj or color_edges,
angular_tolerance, obj_color) angular_tolerance, angular_tolerance)
if len(shape_edges) > 0: obj_color = None # Don't color vertices if edges are colored if len(shape_edges) > 0: color_obj = None # Don't color vertices if edges are colored
if vertices and hasattr(shape, 'vertices'): if vertices and hasattr(shape, 'vertices'):
for vertex in 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: else:
raise TypeError(f"Unsupported type: {type(cad_like)}: {cad_like}") raise TypeError(f"Unsupported type: {type(cad_like)}: {cad_like}")
@@ -68,9 +66,9 @@ def tessellate(
def _tessellate_face( def _tessellate_face(
mgr: GLTFMgr, mgr: GLTFMgr,
ocp_face: TopoDS_Face, ocp_face: TopoDS_Face,
color: ColorTuple,
tolerance: float = 1e-3, tolerance: float = 1e-3,
angular_tolerance: float = 0.1, angular_tolerance: float = 0.1,
color: Optional[ColorTuple] = None,
): ):
face = Compound(ocp_face) face = Compound(ocp_face)
# face.mesh(tolerance, angular_tolerance) # face.mesh(tolerance, angular_tolerance)
@@ -81,6 +79,14 @@ def _tessellate_face(
logger.warn("No triangulation found for face") logger.warn("No triangulation found for face")
return GLTF2() 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 # Get UV of each face from the parameters
uv = [ uv = [
(v.X(), v.Y()) (v.X(), v.Y())
@@ -89,7 +95,7 @@ def _tessellate_face(
vertices = tri_mesh[0] vertices = tri_mesh[0]
indices = tri_mesh[1] indices = tri_mesh[1]
mgr.add_face(vertices, indices, uv, color) mgr.add_face(vertices, normals, indices, uv, color)
return None return None
@@ -113,9 +119,9 @@ def _tessellate_edge(
mgr: GLTFMgr, mgr: GLTFMgr,
ocp_edge: TopoDS_Edge, ocp_edge: TopoDS_Edge,
faces: List[TopoDS_Face], faces: List[TopoDS_Face],
color: ColorTuple,
angular_deflection: float = 0.1, angular_deflection: float = 0.1,
curvature_deflection: float = 0.1, curvature_deflection: float = 0.1,
color: Optional[ColorTuple] = None,
): ):
# Use a curve discretizer to get the vertices # Use a curve discretizer to get the vertices
curve = BRepAdaptor_Curve(ocp_edge) curve = BRepAdaptor_Curve(ocp_edge)
@@ -136,9 +142,6 @@ def _tessellate_edge(
mgr.add_edge(vertices, color) mgr.add_edge(vertices, color)
def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[TopoDS_Face], def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[TopoDS_Face], color: ColorTuple):
color: Optional[ColorTuple] = None):
c = Vertex(ocp_vertex).center() c = Vertex(ocp_vertex).center()
mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces), color) mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces), color)

View File

@@ -20,7 +20,7 @@ from PIL import Image
from build123d import Shape, Axis, Location, Vector from build123d import Shape, Axis, Location, Vector
from dataclasses_json import dataclass_json 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.cad import get_shape, grab_all_cad, CADCoreLike, CADLike
from yacv_server.gltf import get_version from yacv_server.gltf import get_version
from yacv_server.myhttp import HTTPHandler from yacv_server.myhttp import HTTPHandler
@@ -93,12 +93,39 @@ class YACV:
texture: Optional[Tuple[bytes, str]] texture: Optional[Tuple[bytes, str]]
"""Default texture to use for model faces, in (data, mimetype) format. """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=<uri> and overridden by `show(..., texture="<uri>")`. It can be set with the YACV_TEXTURE=<uri> and overridden by the custom `yacv_texture` attribute of an object.
The <uri> can be file:<path> or data:<mime>;base64,<data> where <mime> is the mime type and The <uri> can be file:<path> or data:<mime>;base64,<data> where <mime> is the mime type and
<data> is the base64 encoded image.""" <data> 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=<color> environment variable, where <color> 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=<color> environment variable, where <color> 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=<color> environment variable, where <color> is a color
in the hexadecimal format #RRGGBB or #RRGGBBAA."""
def __init__(self): def __init__(self):
self.server_thread = None self.server_thread = None
self.server = None self.server = None
@@ -109,7 +136,10 @@ class YACV:
self.at_least_one_client = threading.Event() self.at_least_one_client = threading.Event()
self.shutting_down = threading.Event() self.shutting_down = threading.Event()
self.frontend_lock = RWLock() 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()) logger.info('Using yacv-server v%s', get_version())
def start(self): def start(self):
@@ -201,8 +231,9 @@ class YACV:
if isinstance(names, str): if isinstance(names, str):
names = [names] names = [names]
assert len(names) == len(objs), 'Number of names must match the number of objects' assert len(names) == len(objs), 'Number of names must match the number of objects'
if 'color' in kwargs: for color_name in ('color_faces', 'color_edges', 'color_vertices'):
kwargs['color'] = get_color(kwargs['color']) 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 # Handle auto clearing of previous objects
if kwargs.get('auto_clear', True): if kwargs.get('auto_clear', True):
@@ -218,13 +249,15 @@ class YACV:
# Publish the show event # Publish the show event
for obj, name in zip(objs, names): for obj, name in zip(objs, names):
obj_color = get_color(obj) 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: if obj_color is not None:
kwargs = kwargs.copy() _kwargs['color_obj'] = obj_color # Only applies to highest-dimensional objects
kwargs['color'] = obj_color _kwargs['texture'] = _read_texture_uri(getattr(obj, 'yacv_texture', None) or kwargs.get('texture', None))
if not isinstance(obj, bytes): if not isinstance(obj, bytes):
obj = _preprocess_cad(obj, **kwargs) obj = _preprocess_cad(obj, **_kwargs)
_hash = _hashcode(obj, **kwargs) _hash = _hashcode(obj, **_kwargs)
event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, kwargs=kwargs or {}) event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, kwargs=_kwargs or {})
self.show_events.publish(event) self.show_events.publish(event)
logger.info('show %s took %.3f seconds', names, time.time() - start) 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 if isinstance(event.obj, bytes): # Already a GLTF
publish_to.publish(event.obj) publish_to.publish(event.obj)
else: # CAD object to tessellate and convert to GLTF else: # CAD object to tessellate and convert to GLTF
texture_override_uri = event.kwargs.get('texture', None) gltf = tessellate(
texture_override = None event.obj,
if isinstance(texture_override_uri, str): color_faces=event.kwargs.get('color_faces', self.color_faces),
texture_override = _read_texture_uri(texture_override_uri) color_edges=event.kwargs.get('color_edges', self.color_edges),
gltf = tessellate(event.obj, tolerance=event.kwargs.get('tolerance', 0.1), 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), angular_tolerance=event.kwargs.get('angular_tolerance', 0.1),
faces=event.kwargs.get('faces', True), faces=event.kwargs.get('faces', True), edges=event.kwargs.get('edges', True),
edges=event.kwargs.get('edges', True),
vertices=event.kwargs.get('vertices', True), vertices=event.kwargs.get('vertices', True),
obj_color=event.kwargs.get('color', None), texture=event.kwargs.get('texture', self.texture))
texture=texture_override or self.texture)
glb_list_of_bytes = gltf.save_to_bytes() glb_list_of_bytes = gltf.save_to_bytes()
glb_bytes = b''.join(glb_list_of_bytes) glb_bytes = b''.join(glb_list_of_bytes)
publish_to.publish(glb_bytes) publish_to.publish(glb_bytes)
@@ -363,6 +396,20 @@ def _read_texture_uri(uri: str) -> Optional[Tuple[bytes, str]]:
return data, mtype return data, mtype
return None 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 # noinspection PyUnusedLocal
def _preprocess_cad(obj: CADLike, **kwargs) -> CADCoreLike: def _preprocess_cad(obj: CADLike, **kwargs) -> CADCoreLike:
# Get the shape of a CAD-like object # Get the shape of a CAD-like object
@@ -384,6 +431,7 @@ def _preprocess_cad(obj: CADLike, **kwargs) -> CADCoreLike:
_obj_name_counts = {} _obj_name_counts = {}
def _find_var_name(obj: any, avoid_levels: int = 2) -> str: 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""" """A hacky way to get a stable name for an object that may change over time"""