Skip to content

Commit

Permalink
Minor improvements to custom textures feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Yeicor committed Nov 9, 2024
1 parent 8c2ccc4 commit 660a4ff
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 73 deletions.
38 changes: 19 additions & 19 deletions assets/licenses.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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@3.5.0
- @google/model-viewer@4.0.0

This package contains the following license and notice below:

Expand Down Expand Up @@ -980,7 +980,7 @@ third-party archives.

The following npm package may be included in this product:

- [email protected].2
- [email protected].3

This package contains the following license and notice below:

Expand Down Expand Up @@ -1458,7 +1458,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

The following npm package may be included in this product:

- [email protected].0
- [email protected].2

This package contains the following license and notice below:

Expand Down Expand Up @@ -1692,7 +1692,7 @@ THE SOFTWARE.

The following npm package may be included in this product:

- three@0.163.0
- three@0.169.0

This package contains the following license and notice below:

Expand Down Expand Up @@ -1782,7 +1782,7 @@ THE SOFTWARE.

The following npm package may be included in this product:

- [email protected].2
- [email protected].3

This package contains the following license and notice below:

Expand Down Expand Up @@ -1812,16 +1812,16 @@ THE SOFTWARE.

The following npm packages may be included in this product:

- @vue/[email protected].7
- @vue/[email protected].7
- @vue/[email protected].7
- @vue/[email protected].7
- @vue/[email protected].7
- @vue/[email protected].7
- @vue/[email protected].7
- @vue/[email protected].7
- @vue/[email protected].7
- [email protected].7
- @vue/[email protected].12
- @vue/[email protected].12
- @vue/[email protected].12
- @vue/[email protected].12
- @vue/[email protected].12
- @vue/[email protected].12
- @vue/[email protected].12
- @vue/[email protected].12
- @vue/[email protected].12
- [email protected].12

These packages each contain the following license and notice below:

Expand Down Expand Up @@ -1851,7 +1851,7 @@ THE SOFTWARE.

The following npm package may be included in this product:

- [email protected].0
- [email protected].1

This package contains the following license and notice below:

Expand Down Expand Up @@ -1913,9 +1913,9 @@ SOFTWARE.

The following npm packages may be included in this product:

- @gltf-transform/[email protected].8
- @gltf-transform/[email protected].8
- @gltf-transform/[email protected].8
- @gltf-transform/[email protected].10
- @gltf-transform/[email protected].10
- @gltf-transform/[email protected].10

These packages each contain the following license and notice below:

Expand Down
7 changes: 5 additions & 2 deletions example/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@
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)
# Show it in the frontend with hot-reloading (texture and other keyword arguments are optional)
texture = ( # MIT License Framework7 Line Icons: https://www.svgrepo.com/svg/437552/checkmark-seal
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAQAAAAB+tbP6AAAAQ0lEQVQI12P4b3+A4Z/8AYYHBw8w"
"HHxwgOH8HyD+AsRPDjDMP+fAYD+fgcESiGfYOTCcqTnAcK4GogakFqQHpBdoBgAbGiPSbdzkhgAAAABJRU5ErkJggg==")
show(example, example_hl, texture=texture)

# %%

Expand Down
29 changes: 14 additions & 15 deletions yacv_server/cad.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,20 @@
ColorTuple = Tuple[float, float, float, float]


def get_color(obj: CADLike) -> Optional[ColorTuple]:
"""Get color from a CAD Object"""
def get_color(obj: any) -> Optional[ColorTuple]:
"""Get color from a CAD Object or any other color-like object"""
if 'color' in dir(obj):
if isinstance(obj.color, tuple):
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()
obj = obj.color
if isinstance(obj, tuple):
c = None
if len(obj) == 3:
c = obj + (1,)
elif len(obj) == 4:
c = obj
# noinspection PyTypeChecker
return [min(max(float(x), 0), 1) for x in c]
if isinstance(obj, Color):
return obj.to_tuple()
return None


Expand Down Expand Up @@ -181,16 +182,14 @@ def vert(v: Vector) -> Vector:
return b''.join(mgr.build().save_to_bytes()), name


def _hashcode(obj: Union[bytes, CADCoreLike], color: Optional[ColorTuple], **extras) -> str:
def _hashcode(obj: Union[bytes, CADCoreLike], **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
1 change: 0 additions & 1 deletion yacv_server/logo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]:
with BuildSketch(text_at_plane.location):
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]
Expand Down
7 changes: 4 additions & 3 deletions yacv_server/tessellate.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ def tessellate(
edges: bool = True,
vertices: bool = True,
obj_color: Optional[ColorTuple] = None,
base_texture: Optional[Tuple[bytes, str]] = None,
texture: Optional[Tuple[bytes, str]] = None,
) -> GLTF2:
"""Tessellate a whole shape into a list of triangle vertices and a list of triangle indices."""
if base_texture is None:
print("tessellate, obj_color: ", obj_color)
if texture is None:
mgr = GLTFMgr()
else:
mgr = GLTFMgr(base_texture)
mgr = GLTFMgr(texture)

if isinstance(cad_like, TopLoc_Location):
mgr.add_location(Location(cad_like))
Expand Down
86 changes: 53 additions & 33 deletions yacv_server/yacv.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import atexit
import base64
import copy
import inspect
import os
Expand Down Expand Up @@ -47,19 +48,15 @@ class UpdatesApiData:
class UpdatesApiFullData(UpdatesApiData):
obj: YACVSupported
"""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)"""

def __init__(self, obj: YACVSupported, name: str, _hash: str, is_remove: Optional[bool] = False,
color: Optional[ColorTuple] = None,
kwargs: Optional[Dict[str, any]] = None):
self.name = name
self.hash = _hash
self.is_remove = is_remove
self.obj = obj
self.color = color
self.kwargs = kwargs

def to_json(self) -> str:
Expand Down Expand Up @@ -94,9 +91,13 @@ class YACV:
frontend_lock: RWLock
"""Lock to ensure that the frontend has finished working before we shut down"""

base_texture: Optional[Tuple[bytes, str]]
"""Base texture to use for model rendering, in (data, mimetype) format
If set to None, will use default checkerboard texture"""
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.
It can be set with the YACV_BASE_TEXTURE=<uri> and overriden by `show(..., texture="<uri>")`.
The <uri> can be file:<path> or data:<mime>;base64,<data> where <mime> is the mime type and
<data> is the base64 encoded image."""

def __init__(self):
self.server_thread = None
Expand All @@ -108,7 +109,7 @@ def __init__(self):
self.at_least_one_client = threading.Event()
self.shutting_down = threading.Event()
self.frontend_lock = RWLock()
self.base_texture = _resolve_base_texture()
self.texture = _read_texture_uri(os.getenv("YACV_BASE_TEXTURE"))
logger.info('Using yacv-server v%s', get_version())

def start(self):
Expand Down Expand Up @@ -176,12 +177,32 @@ def _run_server(self):
self.server.serve_forever()

def show(self, *objs: List[YACVSupported], names: Optional[Union[str, List[str]]] = None, **kwargs):
"""
Shows the given CAD objects in the frontend. The objects will be tessellated and converted to GLTF. Optionally,
the following keyword arguments can be used:
- auto_clear: Whether to clear the previous objects before showing the new ones (default: True)
- texture: The texture to use for the faces of the object (see `YACV.texture` for more info)
- color: The default color to use for the objects (can be overridden by the `color` attribute of each object)
- tolerance: The tolerance for tessellating the object (default: 0.1)
- angular_tolerance: The angular tolerance for tessellating the object (default: 0.1)
- faces: Whether to tessellate and show the faces of the object (default: True)
- edges: Whether to tessellate and show the edges of the object (default: True)
- vertices: Whether to tessellate and show the vertices of the object (default: True)
:param objs: The CAD objects to show. Can be CAD-like objects (solids, locations, etc.) or bytes (GLTF) objects.
:param names: The names of the objects. If None, the variable names will be used (if possible). The number of
names must match the number of objects. An object of the same name will be replaced in the frontend.
:param kwargs: Additional options for the show_object event.
"""
# Prepare the arguments
start = time.time()
names = names or [_find_var_name(obj) for obj in objs]
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'])

# Handle auto clearing of previous objects
if kwargs.get('auto_clear', True):
Expand All @@ -196,17 +217,20 @@ def show(self, *objs: List[YACVSupported], names: Optional[Union[str, List[str]]

# Publish the show event
for obj, name in zip(objs, names):
color = get_color(obj)
obj_color = get_color(obj)
if obj_color is not None:
kwargs = kwargs.copy()
kwargs['color'] = obj_color
if not isinstance(obj, bytes):
obj = _preprocess_cad(obj, **kwargs)
_hash = _hashcode(obj, color, **kwargs)
event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, color=color, kwargs=kwargs or {})
_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)

def show_cad_all(self, **kwargs):
"""Publishes all CAD objects in the current scope to the server"""
"""Publishes all CAD objects in the current scope to the server. See `show` for more details."""
all_cad = list(grab_all_cad()) # List for reproducible iteration order
self.show(*[cad for _, cad in all_cad], names=[name for name, _ in all_cad], **kwargs)

Expand Down Expand Up @@ -285,13 +309,17 @@ def export(self, name: str) -> Optional[Tuple[bytes, str]]:
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.color,
base_texture=self.base_texture)
obj_color=event.kwargs.get('color', None),
texture=texture_override or self.texture)
glb_list_of_bytes = gltf.save_to_bytes()
glb_bytes = b''.join(glb_list_of_bytes)
publish_to.publish(glb_bytes)
Expand All @@ -315,31 +343,23 @@ def export_all(self, folder: str,
f.write(self.export(name)[0])


def _resolve_base_texture() -> Optional[Tuple[bytes, str]]:
env_str = os.environ.get("YACV_BASE_TEXTURE")
if env_str is None:
def _read_texture_uri(uri: str) -> Optional[Tuple[bytes, str]]:
if uri is None:
return None
if env_str.startswith("file:"):
path = env_str[len("file:"):]
if uri.startswith("file:"):
path = uri[len("file:"):]
with open(path, 'rb') as f:
data = f.read()
buf = BytesIO(data)
img = Image.open(buf)
mtype = img.get_format_mimetype()
return (data, mtype)
if env_str.startswith("base64-png:"):
data = env_str[len("base64-png:"):]
data = base64.decodebytes(data.encode())
return (data, 'image/png')
if env_str.startswith("preset:"):
preset = env_str[len("preset:"):]
color = Color(preset)
img = Image.new("RGBA", (16, 16))
color_tuple = tuple(int(i*256) for i in color.to_tuple())
img.paste(color_tuple, (0, 0, 16, 16))
buf = BytesIO()
img.save(buf, 'PNG')
return (buf.getvalue(), 'image/png')
return data, mtype
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]
data = base64.b64decode(data_str)
return data, mtype
return None

# noinspection PyUnusedLocal
Expand Down

0 comments on commit 660a4ff

Please sign in to comment.