Skip to content

Commit

Permalink
Also color edges and vertices, add examples and minor improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
Yeicor committed Oct 11, 2024
1 parent 59a7e09 commit 167d00c
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 30 deletions.
10 changes: 8 additions & 2 deletions example/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os

from build123d import * # Also works with cadquery objects!
from build123d import Compound

logging.basicConfig(level=logging.DEBUG)

Expand All @@ -15,9 +16,14 @@
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)

# %%

Expand Down
14 changes: 11 additions & 3 deletions yacv_server/cad.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
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
Expand Down Expand Up @@ -175,14 +181,16 @@ def vert(v: Vector) -> Vector:
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
hasher = hashlib.md5(usedforsecurity=False)
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):
Expand Down
19 changes: 13 additions & 6 deletions yacv_server/gltf.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
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"""
Expand All @@ -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
Expand Down Expand Up @@ -71,9 +77,9 @@ 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: 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"
Expand All @@ -85,18 +91,19 @@ def add_face(self, vertices_raw: List[Vector], indices_raw: List[Tuple[int, int,
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))])
self.edge_positions.extend([v for t in vertices_flat for v in t])
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)
Expand Down
13 changes: 10 additions & 3 deletions yacv_server/logo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down
25 changes: 14 additions & 11 deletions yacv_server/tessellate.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,25 @@ 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():
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 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()

Expand Down Expand Up @@ -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]:
Expand All @@ -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)
Expand All @@ -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)


12 changes: 7 additions & 5 deletions yacv_server/yacv.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
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

Expand All @@ -18,13 +17,14 @@
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
Expand All @@ -44,7 +44,9 @@ class UpdatesApiData:

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)"""

Expand Down Expand Up @@ -100,7 +102,7 @@ def __init__(self):
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"""
Expand Down Expand Up @@ -190,7 +192,7 @@ def show(self, *objs: List[YACVSupported], names: Optional[Union[str, List[str]]
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)

Expand Down

0 comments on commit 167d00c

Please sign in to comment.