Also color edges and vertices, add examples and minor improvements

This commit is contained in:
Yeicor
2024-10-11 20:07:38 +02:00
parent 0939e25da2
commit 09c0994a34
6 changed files with 63 additions and 30 deletions

View File

@@ -3,6 +3,7 @@ import logging
import os import os
from build123d import * # Also works with cadquery objects! from build123d import * # Also works with cadquery objects!
from build123d import Compound
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
@@ -15,9 +16,14 @@ with BuildPart() as example:
Box(10, 10, 5) Box(10, 10, 5)
Cylinder(4, 5, mode=Mode.SUBTRACT) Cylinder(4, 5, mode=Mode.SUBTRACT)
# Show it in the frontend with hot-reloading # Custom colors (optional)
show(example) example.color = (0.1, 0.3, 0.1, 1) # RGBA
to_highlight = example.edges().group_by(Axis.Z)[-1]
example_hl = Compound(to_highlight).translate((0, 0, 1e-3)) # To avoid z-fighting
example_hl.color = (1, 1, .0, 1)
# Show it in the frontend with hot-reloading
show(example, example_hl)
# %% # %%

View File

@@ -18,12 +18,18 @@ CADCoreLike = Union[TopoDS_Shape, TopLoc_Location] # Faces, Edges, Vertices and
CADLike = Union[CADCoreLike, any] # build123d and cadquery types CADLike = Union[CADCoreLike, any] # build123d and cadquery types
ColorTuple = Tuple[float, float, float, float] ColorTuple = Tuple[float, float, float, float]
def get_color(obj: CADLike) -> Optional[ColorTuple]: def get_color(obj: CADLike) -> Optional[ColorTuple]:
"""Get color from a CAD Object""" """Get color from a CAD Object"""
if 'color' in dir(obj): if 'color' in dir(obj):
if isinstance(obj.color, tuple): if isinstance(obj.color, tuple):
return obj.color c = None
if len(obj.color) == 3:
c = obj.color + (1,)
elif len(obj.color) == 4:
c = obj.color
# noinspection PyTypeChecker
return [min(max(float(x), 0), 1) for x in c]
if isinstance(obj.color, Color): if isinstance(obj.color, Color):
return obj.color.to_tuple() return obj.color.to_tuple()
return None return None
@@ -175,7 +181,7 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
return b''.join(mgr.build().save_to_bytes()), name return b''.join(mgr.build().save_to_bytes()), name
def _hashcode(obj: Union[bytes, CADCoreLike], **extras) -> str: def _hashcode(obj: Union[bytes, CADCoreLike], color: Optional[ColorTuple], **extras) -> str:
"""Utility to compute the STABLE hash code of a shape""" """Utility to compute the STABLE hash code of a shape"""
# 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
@@ -183,6 +189,8 @@ def _hashcode(obj: Union[bytes, CADCoreLike], **extras) -> str:
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 color:
hasher.update(str(color).encode())
if isinstance(obj, bytes): if isinstance(obj, bytes):
hasher.update(obj) hasher.update(obj)
elif isinstance(obj, TopLoc_Location): elif isinstance(obj, TopLoc_Location):

View File

@@ -8,6 +8,12 @@ _checkerboard_image_bytes = base64.decodebytes(
b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVQI12N49OjR////Gf' b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVQI12N49OjR////Gf'
b'/////48WMATwULS8tcyj8AAAAASUVORK5CYII=') b'/////48WMATwULS8tcyj8AAAAASUVORK5CYII=')
def get_version() -> str:
try:
return importlib.metadata.version("yacv_server")
except importlib.metadata.PackageNotFoundError:
return "unknown"
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"""
@@ -32,7 +38,7 @@ class GLTFMgr:
def __init__(self, image: Optional[Tuple[bytes, str]] = (_checkerboard_image_bytes, 'image/png')): def __init__(self, image: Optional[Tuple[bytes, str]] = (_checkerboard_image_bytes, 'image/png')):
self.gltf = GLTF2( self.gltf = GLTF2(
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"), asset=Asset(generator=f"yacv_server@{get_version()}"),
scene=0, scene=0,
scenes=[Scene(nodes=[0])], scenes=[Scene(nodes=[0])],
nodes=[Node(mesh=0)], # TODO: Server-side detection of shallow copies --> nodes nodes=[Node(mesh=0)], # TODO: Server-side detection of shallow copies --> nodes
@@ -71,9 +77,9 @@ class GLTFMgr:
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], indices_raw: List[Tuple[int, int, int]],
tex_coord_raw: List[Tuple[float, float]], tex_coord_raw: List[Tuple[float, float]], color: Optional[Tuple[float, float, float, float]] = None):
color: Tuple[float, float, float, float] = (1.0, 0.75, 0.0, 1.0)):
"""Add a face to the GLTF mesh""" """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 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"
@@ -85,8 +91,9 @@ class GLTFMgr:
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: Tuple[float, float, float, float] = (0.1, 0.1, 1.0, 1.0)): color: Optional[Tuple[float, float, float, float]] = None):
"""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))])
@@ -94,9 +101,9 @@ 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], def add_vertex(self, vertex: Tuple[float, float, float], color: Optional[Tuple[float, float, float, float]] = None):
color: Tuple[float, float, float, float] = (0.1, 0.1, 0.1, 1.0)):
"""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)

View File

@@ -16,18 +16,25 @@ def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]:
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', 6, font_path='/usr/share/fonts/TTF/Hack-Regular.ttf')
extrude(amount=1) extrude(amount=1)
logo_obj.color = (0.7, 0.4, 0.1, 1) # Custom color for faces
# 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)
# 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
logo_img_path = os.path.join(ASSETS_DIR, 'img.jpg') logo_img_path = os.path.join(ASSETS_DIR, 'img.jpg')
img_glb_bytes, img_name = image_to_gltf(logo_img_path, logo_img_location, height=18) img_glb_bytes, img_name = image_to_gltf(logo_img_path, logo_img_location, height=18)
# 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, 'location': logo_img_location, img_name: img_glb_bytes} return {'fox': fox_glb_bytes, 'logo': logo_obj, 'logo_hl': logo_obj_hl, 'location': logo_img_location, img_name: img_glb_bytes}
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -35,7 +35,8 @@ def tessellate(
edge_to_faces: Dict[str, List[TopoDS_Face]] = {} edge_to_faces: Dict[str, List[TopoDS_Face]] = {}
vertex_to_faces: Dict[str, List[TopoDS_Face]] = {} vertex_to_faces: Dict[str, List[TopoDS_Face]] = {}
if faces: if faces:
for face in 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, tolerance, angular_tolerance, obj_color)
if edges: if edges:
for edge in face.edges(): for edge in face.edges():
@@ -43,13 +44,16 @@ def tessellate(
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 edges: if edges:
for edge in shape.edges(): shape_edges = 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, []), angular_tolerance,
angular_tolerance) angular_tolerance, obj_color)
if len(shape_edges) > 0: obj_color = None # Don't color vertices if edges are colored
if vertices: if vertices:
for vertex in shape.vertices(): for vertex in shape.vertices():
_tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, [])) _tessellate_vertex(mgr, vertex.wrapped, vertex_to_faces.get(vertex.wrapped, []), obj_color)
return mgr.build() return mgr.build()
@@ -77,10 +81,7 @@ def _tessellate_face(
vertices = tri_mesh[0] vertices = tri_mesh[0]
indices = tri_mesh[1] indices = tri_mesh[1]
if color is None: mgr.add_face(vertices, indices, uv, color)
mgr.add_face(vertices, indices, uv)
else:
mgr.add_face(vertices, indices, uv, color)
def _push_point(v: Tuple[float, float, float], faces: List[TopoDS_Face]) -> Tuple[float, float, float]: def _push_point(v: Tuple[float, float, float], faces: List[TopoDS_Face]) -> Tuple[float, float, float]:
@@ -105,6 +106,7 @@ def _tessellate_edge(
faces: List[TopoDS_Face], faces: List[TopoDS_Face],
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)
@@ -122,11 +124,12 @@ def _tessellate_edge(
# Convert strip of vertices to a list of pairs of vertices # Convert strip of vertices to a list of pairs of vertices
vertices = [(vertices[i], vertices[i + 1]) for i in range(len(vertices) - 1)] vertices = [(vertices[i], vertices[i + 1]) for i in range(len(vertices) - 1)]
mgr.add_edge(vertices) 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: 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)) mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces), color)

View File

@@ -8,7 +8,6 @@ import threading
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from http.server import ThreadingHTTPServer from http.server import ThreadingHTTPServer
from importlib.metadata import version
from threading import Thread from threading import Thread
from typing import Optional, Dict, Union, Callable, List, Tuple from typing import Optional, Dict, Union, Callable, List, Tuple
@@ -18,13 +17,14 @@ from OCP.TopoDS import TopoDS_Shape
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, ColorTuple, get_color
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.myhttp import HTTPHandler from yacv_server.myhttp import HTTPHandler
from yacv_server.mylogger import logger from yacv_server.mylogger import logger
from yacv_server.pubsub import BufferedPubSub from yacv_server.pubsub import BufferedPubSub
from yacv_server.rwlock import RWLock from yacv_server.rwlock import RWLock
from yacv_server.tessellate import tessellate from yacv_server.tessellate import tessellate
from yacv_server.cad import _hashcode, ColorTuple, get_color
@dataclass_json @dataclass_json
@@ -44,7 +44,9 @@ YACVSupported = Union[bytes, CADCoreLike]
class UpdatesApiFullData(UpdatesApiData): class UpdatesApiFullData(UpdatesApiData):
obj: YACVSupported obj: YACVSupported
"""The OCCT object, if any (not serialized)""" """The OCCT object (not serialized)"""
color: Optional[ColorTuple]
"""The color of the object, if any (not serialized)"""
kwargs: Optional[Dict[str, any]] kwargs: Optional[Dict[str, any]]
"""The show_object options, if any (not serialized)""" """The show_object options, if any (not serialized)"""
@@ -100,7 +102,7 @@ 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()
logger.info('Using yacv-server v%s', version('yacv-server')) logger.info('Using yacv-server v%s', get_version())
def start(self): def start(self):
"""Starts the web server in the background""" """Starts the web server in the background"""
@@ -190,7 +192,7 @@ class YACV:
color = get_color(obj) color = get_color(obj)
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, color, **kwargs)
event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, color=color, kwargs=kwargs or {}) event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, color=color, kwargs=kwargs or {})
self.show_events.publish(event) self.show_events.publish(event)