Minor improvements to custom textures feature

This commit is contained in:
Yeicor
2024-11-09 18:20:46 +01:00
parent 822672c288
commit 874f9e8d6e
6 changed files with 95 additions and 73 deletions

View File

@@ -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:
@@ -980,7 +980,7 @@ third-party archives.
The following npm package may be included in this product:
- typescript@5.6.2
- typescript@5.6.3
This package contains the following license and notice below:
@@ -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:
- three-mesh-bvh@0.8.0
- three-mesh-bvh@0.8.2
This package contains the following license and notice below:
@@ -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:
@@ -1782,7 +1782,7 @@ THE SOFTWARE.
The following npm package may be included in this product:
- vuetify@3.7.2
- vuetify@3.7.3
This package contains the following license and notice below:
@@ -1812,16 +1812,16 @@ THE SOFTWARE.
The following npm packages may be included in this product:
- @vue/compiler-core@3.5.7
- @vue/compiler-dom@3.5.7
- @vue/compiler-sfc@3.5.7
- @vue/compiler-ssr@3.5.7
- @vue/reactivity@3.5.7
- @vue/runtime-core@3.5.7
- @vue/runtime-dom@3.5.7
- @vue/server-renderer@3.5.7
- @vue/shared@3.5.7
- vue@3.5.7
- @vue/compiler-core@3.5.12
- @vue/compiler-dom@3.5.12
- @vue/compiler-sfc@3.5.12
- @vue/compiler-ssr@3.5.12
- @vue/reactivity@3.5.12
- @vue/runtime-core@3.5.12
- @vue/runtime-dom@3.5.12
- @vue/server-renderer@3.5.12
- @vue/shared@3.5.12
- vue@3.5.12
These packages each contain the following license and notice below:
@@ -1851,7 +1851,7 @@ THE SOFTWARE.
The following npm package may be included in this product:
- ktx-parse@0.7.0
- ktx-parse@0.7.1
This package contains the following license and notice below:
@@ -1913,9 +1913,9 @@ SOFTWARE.
The following npm packages may be included in this product:
- @gltf-transform/core@4.0.8
- @gltf-transform/extensions@4.0.8
- @gltf-transform/functions@4.0.8
- @gltf-transform/core@4.0.10
- @gltf-transform/extensions@4.0.10
- @gltf-transform/functions@4.0.10
These packages each contain the following license and notice below:

View File

@@ -22,8 +22,11 @@ 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)
# 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)
# %%

View File

@@ -19,19 +19,20 @@ 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"""
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):
obj = obj.color
if isinstance(obj, tuple):
c = None
if len(obj.color) == 3:
c = obj.color + (1,)
elif len(obj.color) == 4:
c = obj.color
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, Color):
return obj.color.to_tuple()
if isinstance(obj, Color):
return obj.to_tuple()
return None
@@ -181,7 +182,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], 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
@@ -189,8 +190,6 @@ def _hashcode(obj: Union[bytes, CADCoreLike], color: Optional[ColorTuple], **ext
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

@@ -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]

View File

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

View File

@@ -1,4 +1,5 @@
import atexit
import base64
import copy
import inspect
import os
@@ -47,19 +48,15 @@ YACVSupported = Union[bytes, CADCoreLike]
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:
@@ -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
@@ -108,7 +109,7 @@ class YACV:
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):
@@ -176,12 +177,32 @@ class YACV:
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):
@@ -196,17 +217,20 @@ class YACV:
# 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)
@@ -285,13 +309,17 @@ class YACV:
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)
@@ -315,31 +343,23 @@ class YACV:
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