From 874f9e8d6e4214d6ea9704d61c4ffa806aea1728 Mon Sep 17 00:00:00 2001 From: Yeicor <4929005+Yeicor@users.noreply.github.com> Date: Sat, 9 Nov 2024 18:20:46 +0100 Subject: [PATCH] Minor improvements to custom textures feature --- assets/licenses.txt | 38 ++++++++--------- example/object.py | 7 +++- yacv_server/cad.py | 29 +++++++------ yacv_server/logo.py | 1 - yacv_server/tessellate.py | 7 ++-- yacv_server/yacv.py | 86 ++++++++++++++++++++++++--------------- 6 files changed, 95 insertions(+), 73 deletions(-) diff --git a/assets/licenses.txt b/assets/licenses.txt index e07dbbd..b129728 100644 --- a/assets/licenses.txt +++ b/assets/licenses.txt @@ -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: diff --git a/example/object.py b/example/object.py index 56d217f..1a8d1cb 100644 --- a/example/object.py +++ b/example/object.py @@ -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) # %% diff --git a/yacv_server/cad.py b/yacv_server/cad.py index c0d4be8..12128ff 100644 --- a/yacv_server/cad.py +++ b/yacv_server/cad.py @@ -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): - 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 @@ -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): diff --git a/yacv_server/logo.py b/yacv_server/logo.py index f998bc2..697d039 100644 --- a/yacv_server/logo.py +++ b/yacv_server/logo.py @@ -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] diff --git a/yacv_server/tessellate.py b/yacv_server/tessellate.py index 6f2ba06..12b9549 100644 --- a/yacv_server/tessellate.py +++ b/yacv_server/tessellate.py @@ -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)) diff --git a/yacv_server/yacv.py b/yacv_server/yacv.py index e8be6ed..87c7d0f 100644 --- a/yacv_server/yacv.py +++ b/yacv_server/yacv.py @@ -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= and overriden by `show(..., texture="")`. + The can be file: or data:;base64, where is the mime type and + 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