mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 14:14:13 +01:00
Minor improvements to custom textures feature
This commit is contained in:
@@ -3,7 +3,7 @@ https://www.npmjs.com/package/generate-license-file
|
|||||||
|
|
||||||
The following npm package may be included in this product:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
The following npm packages may be included in this product:
|
||||||
|
|
||||||
- @vue/compiler-core@3.5.7
|
- @vue/compiler-core@3.5.12
|
||||||
- @vue/compiler-dom@3.5.7
|
- @vue/compiler-dom@3.5.12
|
||||||
- @vue/compiler-sfc@3.5.7
|
- @vue/compiler-sfc@3.5.12
|
||||||
- @vue/compiler-ssr@3.5.7
|
- @vue/compiler-ssr@3.5.12
|
||||||
- @vue/reactivity@3.5.7
|
- @vue/reactivity@3.5.12
|
||||||
- @vue/runtime-core@3.5.7
|
- @vue/runtime-core@3.5.12
|
||||||
- @vue/runtime-dom@3.5.7
|
- @vue/runtime-dom@3.5.12
|
||||||
- @vue/server-renderer@3.5.7
|
- @vue/server-renderer@3.5.12
|
||||||
- @vue/shared@3.5.7
|
- @vue/shared@3.5.12
|
||||||
- vue@3.5.7
|
- vue@3.5.12
|
||||||
|
|
||||||
These packages each contain the following license and notice below:
|
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:
|
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:
|
This package contains the following license and notice below:
|
||||||
|
|
||||||
@@ -1913,9 +1913,9 @@ SOFTWARE.
|
|||||||
|
|
||||||
The following npm packages may be included in this product:
|
The following npm packages may be included in this product:
|
||||||
|
|
||||||
- @gltf-transform/core@4.0.8
|
- @gltf-transform/core@4.0.10
|
||||||
- @gltf-transform/extensions@4.0.8
|
- @gltf-transform/extensions@4.0.10
|
||||||
- @gltf-transform/functions@4.0.8
|
- @gltf-transform/functions@4.0.10
|
||||||
|
|
||||||
These packages each contain the following license and notice below:
|
These packages each contain the following license and notice below:
|
||||||
|
|
||||||
|
|||||||
@@ -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 = Compound(to_highlight).translate((0, 0, 1e-3)) # To avoid z-fighting
|
||||||
example_hl.color = (1, 1, .0, 1)
|
example_hl.color = (1, 1, .0, 1)
|
||||||
|
|
||||||
# Show it in the frontend with hot-reloading
|
# Show it in the frontend with hot-reloading (texture and other keyword arguments are optional)
|
||||||
show(example, example_hl)
|
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)
|
||||||
|
|
||||||
# %%
|
# %%
|
||||||
|
|
||||||
|
|||||||
@@ -19,19 +19,20 @@ CADLike = Union[CADCoreLike, any] # build123d and cadquery types
|
|||||||
ColorTuple = Tuple[float, float, float, float]
|
ColorTuple = Tuple[float, float, float, float]
|
||||||
|
|
||||||
|
|
||||||
def get_color(obj: CADLike) -> Optional[ColorTuple]:
|
def get_color(obj: any) -> Optional[ColorTuple]:
|
||||||
"""Get color from a CAD Object"""
|
"""Get color from a CAD Object or any other color-like object"""
|
||||||
if 'color' in dir(obj):
|
if 'color' in dir(obj):
|
||||||
if isinstance(obj.color, tuple):
|
obj = obj.color
|
||||||
c = None
|
if isinstance(obj, tuple):
|
||||||
if len(obj.color) == 3:
|
c = None
|
||||||
c = obj.color + (1,)
|
if len(obj) == 3:
|
||||||
elif len(obj.color) == 4:
|
c = obj + (1,)
|
||||||
c = obj.color
|
elif len(obj) == 4:
|
||||||
# noinspection PyTypeChecker
|
c = obj
|
||||||
return [min(max(float(x), 0), 1) for x in c]
|
# noinspection PyTypeChecker
|
||||||
if isinstance(obj.color, Color):
|
return [min(max(float(x), 0), 1) for x in c]
|
||||||
return obj.color.to_tuple()
|
if isinstance(obj, Color):
|
||||||
|
return obj.to_tuple()
|
||||||
return None
|
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
|
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"""
|
"""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
|
# 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
|
# 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():
|
for k, v in extras.items():
|
||||||
hasher.update(str(k).encode())
|
hasher.update(str(k).encode())
|
||||||
hasher.update(str(v).encode())
|
hasher.update(str(v).encode())
|
||||||
if color:
|
|
||||||
hasher.update(str(color).encode())
|
|
||||||
if isinstance(obj, bytes):
|
if isinstance(obj, bytes):
|
||||||
hasher.update(obj)
|
hasher.update(obj)
|
||||||
elif isinstance(obj, TopLoc_Location):
|
elif isinstance(obj, TopLoc_Location):
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]:
|
|||||||
with BuildSketch(text_at_plane.location):
|
with BuildSketch(text_at_plane.location):
|
||||||
Text('Yet Another\nCAD Viewer', 6, font_path='/usr/share/fonts/TTF/Hack-Regular.ttf')
|
Text('Yet Another\nCAD Viewer', 6, font_path='/usr/share/fonts/TTF/Hack-Regular.ttf')
|
||||||
extrude(amount=1)
|
extrude(amount=1)
|
||||||
logo_obj.color = (0.7, 0.4, 0.1, 1) # Custom color for faces
|
|
||||||
|
|
||||||
# Highlight text edges with a custom color
|
# Highlight text edges with a custom color
|
||||||
to_highlight = logo_obj.edges().group_by(Axis.X)[-1]
|
to_highlight = logo_obj.edges().group_by(Axis.X)[-1]
|
||||||
|
|||||||
@@ -21,13 +21,14 @@ def tessellate(
|
|||||||
edges: bool = True,
|
edges: bool = True,
|
||||||
vertices: bool = True,
|
vertices: bool = True,
|
||||||
obj_color: Optional[ColorTuple] = None,
|
obj_color: Optional[ColorTuple] = None,
|
||||||
base_texture: Optional[Tuple[bytes, str]] = None,
|
texture: Optional[Tuple[bytes, str]] = None,
|
||||||
) -> GLTF2:
|
) -> GLTF2:
|
||||||
"""Tessellate a whole shape into a list of triangle vertices and a list of triangle indices."""
|
"""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()
|
mgr = GLTFMgr()
|
||||||
else:
|
else:
|
||||||
mgr = GLTFMgr(base_texture)
|
mgr = GLTFMgr(texture)
|
||||||
|
|
||||||
if isinstance(cad_like, TopLoc_Location):
|
if isinstance(cad_like, TopLoc_Location):
|
||||||
mgr.add_location(Location(cad_like))
|
mgr.add_location(Location(cad_like))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import atexit
|
import atexit
|
||||||
|
import base64
|
||||||
import copy
|
import copy
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
@@ -47,19 +48,15 @@ YACVSupported = Union[bytes, CADCoreLike]
|
|||||||
class UpdatesApiFullData(UpdatesApiData):
|
class UpdatesApiFullData(UpdatesApiData):
|
||||||
obj: YACVSupported
|
obj: YACVSupported
|
||||||
"""The OCCT object (not serialized)"""
|
"""The OCCT object (not serialized)"""
|
||||||
color: Optional[ColorTuple]
|
|
||||||
"""The color of the object, if any (not serialized)"""
|
|
||||||
kwargs: Optional[Dict[str, any]]
|
kwargs: Optional[Dict[str, any]]
|
||||||
"""The show_object options, if any (not serialized)"""
|
"""The show_object options, if any (not serialized)"""
|
||||||
|
|
||||||
def __init__(self, obj: YACVSupported, name: str, _hash: str, is_remove: Optional[bool] = False,
|
def __init__(self, obj: YACVSupported, name: str, _hash: str, is_remove: Optional[bool] = False,
|
||||||
color: Optional[ColorTuple] = None,
|
|
||||||
kwargs: Optional[Dict[str, any]] = None):
|
kwargs: Optional[Dict[str, any]] = None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.hash = _hash
|
self.hash = _hash
|
||||||
self.is_remove = is_remove
|
self.is_remove = is_remove
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
self.color = color
|
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
def to_json(self) -> str:
|
def to_json(self) -> str:
|
||||||
@@ -94,9 +91,13 @@ class YACV:
|
|||||||
frontend_lock: RWLock
|
frontend_lock: RWLock
|
||||||
"""Lock to ensure that the frontend has finished working before we shut down"""
|
"""Lock to ensure that the frontend has finished working before we shut down"""
|
||||||
|
|
||||||
base_texture: Optional[Tuple[bytes, str]]
|
texture: Optional[Tuple[bytes, str]]
|
||||||
"""Base texture to use for model rendering, in (data, mimetype) format
|
"""Default texture to use for model faces, in (data, mimetype) format.
|
||||||
If set to None, will use default checkerboard texture"""
|
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):
|
def __init__(self):
|
||||||
self.server_thread = None
|
self.server_thread = None
|
||||||
@@ -108,7 +109,7 @@ class YACV:
|
|||||||
self.at_least_one_client = threading.Event()
|
self.at_least_one_client = threading.Event()
|
||||||
self.shutting_down = threading.Event()
|
self.shutting_down = threading.Event()
|
||||||
self.frontend_lock = RWLock()
|
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())
|
logger.info('Using yacv-server v%s', get_version())
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
@@ -176,12 +177,32 @@ class YACV:
|
|||||||
self.server.serve_forever()
|
self.server.serve_forever()
|
||||||
|
|
||||||
def show(self, *objs: List[YACVSupported], names: Optional[Union[str, List[str]]] = None, **kwargs):
|
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
|
# Prepare the arguments
|
||||||
start = time.time()
|
start = time.time()
|
||||||
names = names or [_find_var_name(obj) for obj in objs]
|
names = names or [_find_var_name(obj) for obj in objs]
|
||||||
if isinstance(names, str):
|
if isinstance(names, str):
|
||||||
names = [names]
|
names = [names]
|
||||||
assert len(names) == len(objs), 'Number of names must match the number of objects'
|
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
|
# Handle auto clearing of previous objects
|
||||||
if kwargs.get('auto_clear', True):
|
if kwargs.get('auto_clear', True):
|
||||||
@@ -196,17 +217,20 @@ class YACV:
|
|||||||
|
|
||||||
# Publish the show event
|
# Publish the show event
|
||||||
for obj, name in zip(objs, names):
|
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):
|
if not isinstance(obj, bytes):
|
||||||
obj = _preprocess_cad(obj, **kwargs)
|
obj = _preprocess_cad(obj, **kwargs)
|
||||||
_hash = _hashcode(obj, color, **kwargs)
|
_hash = _hashcode(obj, **kwargs)
|
||||||
event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, color=color, kwargs=kwargs or {})
|
event = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, kwargs=kwargs or {})
|
||||||
self.show_events.publish(event)
|
self.show_events.publish(event)
|
||||||
|
|
||||||
logger.info('show %s took %.3f seconds', names, time.time() - start)
|
logger.info('show %s took %.3f seconds', names, time.time() - start)
|
||||||
|
|
||||||
def show_cad_all(self, **kwargs):
|
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
|
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)
|
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
|
if isinstance(event.obj, bytes): # Already a GLTF
|
||||||
publish_to.publish(event.obj)
|
publish_to.publish(event.obj)
|
||||||
else: # CAD object to tessellate and convert to GLTF
|
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),
|
gltf = tessellate(event.obj, tolerance=event.kwargs.get('tolerance', 0.1),
|
||||||
angular_tolerance=event.kwargs.get('angular_tolerance', 0.1),
|
angular_tolerance=event.kwargs.get('angular_tolerance', 0.1),
|
||||||
faces=event.kwargs.get('faces', True),
|
faces=event.kwargs.get('faces', True),
|
||||||
edges=event.kwargs.get('edges', True),
|
edges=event.kwargs.get('edges', True),
|
||||||
vertices=event.kwargs.get('vertices', True),
|
vertices=event.kwargs.get('vertices', True),
|
||||||
obj_color=event.color,
|
obj_color=event.kwargs.get('color', None),
|
||||||
base_texture=self.base_texture)
|
texture=texture_override or self.texture)
|
||||||
glb_list_of_bytes = gltf.save_to_bytes()
|
glb_list_of_bytes = gltf.save_to_bytes()
|
||||||
glb_bytes = b''.join(glb_list_of_bytes)
|
glb_bytes = b''.join(glb_list_of_bytes)
|
||||||
publish_to.publish(glb_bytes)
|
publish_to.publish(glb_bytes)
|
||||||
@@ -315,31 +343,23 @@ class YACV:
|
|||||||
f.write(self.export(name)[0])
|
f.write(self.export(name)[0])
|
||||||
|
|
||||||
|
|
||||||
def _resolve_base_texture() -> Optional[Tuple[bytes, str]]:
|
def _read_texture_uri(uri: str) -> Optional[Tuple[bytes, str]]:
|
||||||
env_str = os.environ.get("YACV_BASE_TEXTURE")
|
if uri is None:
|
||||||
if env_str is None:
|
|
||||||
return None
|
return None
|
||||||
if env_str.startswith("file:"):
|
if uri.startswith("file:"):
|
||||||
path = env_str[len("file:"):]
|
path = uri[len("file:"):]
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
buf = BytesIO(data)
|
buf = BytesIO(data)
|
||||||
img = Image.open(buf)
|
img = Image.open(buf)
|
||||||
mtype = img.get_format_mimetype()
|
mtype = img.get_format_mimetype()
|
||||||
return (data, mtype)
|
return data, mtype
|
||||||
if env_str.startswith("base64-png:"):
|
if uri.startswith("data:"): # https://en.wikipedia.org/wiki/Data_URI_scheme#Syntax (limited)
|
||||||
data = env_str[len("base64-png:"):]
|
mtype_and_data = uri[len("data:"):]
|
||||||
data = base64.decodebytes(data.encode())
|
mtype = mtype_and_data.split(";", 1)[0]
|
||||||
return (data, 'image/png')
|
data_str = mtype_and_data.split(",", 1)[1]
|
||||||
if env_str.startswith("preset:"):
|
data = base64.b64decode(data_str)
|
||||||
preset = env_str[len("preset:"):]
|
return data, mtype
|
||||||
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 None
|
return None
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
|
|||||||
Reference in New Issue
Block a user