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
from build123d import * # Also works with cadquery objects!
from build123d import Compound
logging.basicConfig(level=logging.DEBUG)
@@ -15,9 +16,14 @@ with BuildPart() as example:
Box(10, 10, 5)
Cylinder(4, 5, mode=Mode.SUBTRACT)
# Show it in the frontend with hot-reloading
show(example)
# Custom colors (optional)
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
ColorTuple = Tuple[float, float, float, float]
def get_color(obj: CADLike) -> Optional[ColorTuple]:
"""Get color from a CAD Object"""
if 'color' in dir(obj):
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):
return obj.color.to_tuple()
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
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"""
# 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
@@ -183,6 +189,8 @@ def _hashcode(obj: Union[bytes, CADCoreLike], **extras) -> str:
for k, v in extras.items():
hasher.update(str(k).encode())
hasher.update(str(v).encode())
if color:
hasher.update(str(color).encode())
if isinstance(obj, bytes):
hasher.update(obj)
elif isinstance(obj, TopLoc_Location):

View File

@@ -8,6 +8,12 @@ _checkerboard_image_bytes = base64.decodebytes(
b'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVQI12N49OjR////Gf'
b'/////48WMATwULS8tcyj8AAAAASUVORK5CYII=')
def get_version() -> str:
try:
return importlib.metadata.version("yacv_server")
except importlib.metadata.PackageNotFoundError:
return "unknown"
class GLTFMgr:
"""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')):
self.gltf = GLTF2(
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
asset=Asset(generator=f"yacv_server@{get_version()}"),
scene=0,
scenes=[Scene(nodes=[0])],
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]
def add_face(self, vertices_raw: List[Vector], indices_raw: List[Tuple[int, int, int]],
tex_coord_raw: List[Tuple[float, float]],
color: Tuple[float, float, float, float] = (1.0, 0.75, 0.0, 1.0)):
tex_coord_raw: List[Tuple[float, float]], color: Optional[Tuple[float, float, float, float]] = None):
"""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"
@@ -85,8 +91,9 @@ class GLTFMgr:
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: 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"""
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))])
@@ -94,9 +101,9 @@ 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: Tuple[float, float, float, float] = (0.1, 0.1, 0.1, 1.0)):
def add_vertex(self, vertex: Tuple[float, float, float], color: Optional[Tuple[float, float, float, float]] = None):
"""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)

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.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')
Text('Yet Another\nCAD Viewer', 6, font_path='/usr/share/fonts/TTF/Hack-Regular.ttf')
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 *= Location((0, 0, 4e-2), (0, 0, 90)) # Avoid overlapping and adjust placement
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)
# 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, '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__":

View File

@@ -35,7 +35,8 @@ def tessellate(
edge_to_faces: Dict[str, List[TopoDS_Face]] = {}
vertex_to_faces: Dict[str, List[TopoDS_Face]] = {}
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)
if edges:
for edge in face.edges():
@@ -43,13 +44,16 @@ def tessellate(
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 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,
angular_tolerance)
angular_tolerance, obj_color)
if len(shape_edges) > 0: obj_color = None # Don't color vertices if edges are colored
if 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()
@@ -77,10 +81,7 @@ def _tessellate_face(
vertices = tri_mesh[0]
indices = tri_mesh[1]
if color is None:
mgr.add_face(vertices, indices, uv)
else:
mgr.add_face(vertices, indices, uv, color)
mgr.add_face(vertices, indices, uv, color)
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],
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)
@@ -122,11 +124,12 @@ def _tessellate_edge(
# Convert strip of vertices to a list of pairs of vertices
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()
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
from dataclasses import dataclass
from http.server import ThreadingHTTPServer
from importlib.metadata import version
from threading import Thread
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 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.gltf import get_version
from yacv_server.myhttp import HTTPHandler
from yacv_server.mylogger import logger
from yacv_server.pubsub import BufferedPubSub
from yacv_server.rwlock import RWLock
from yacv_server.tessellate import tessellate
from yacv_server.cad import _hashcode, ColorTuple, get_color
@dataclass_json
@@ -44,7 +44,9 @@ YACVSupported = Union[bytes, CADCoreLike]
class UpdatesApiFullData(UpdatesApiData):
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]]
"""The show_object options, if any (not serialized)"""
@@ -100,7 +102,7 @@ class YACV:
self.at_least_one_client = threading.Event()
self.shutting_down = threading.Event()
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):
"""Starts the web server in the background"""
@@ -190,7 +192,7 @@ class YACV:
color = get_color(obj)
if not isinstance(obj, bytes):
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 {})
self.show_events.publish(event)