mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2026-01-01 04:14:21 +01:00
add support for loading images as quads
This commit is contained in:
@@ -3,6 +3,7 @@ import os
|
||||
import time
|
||||
|
||||
from aiohttp import web
|
||||
from build123d import Vector
|
||||
|
||||
from server import Server
|
||||
|
||||
@@ -18,6 +19,7 @@ if 'YACV_DISABLE_SERVER' not in os.environ:
|
||||
# Expose some nice aliases using the default server instance
|
||||
show = server.show
|
||||
show_object = show
|
||||
show_image = server.show_image
|
||||
show_all = server.show_cad_all
|
||||
|
||||
|
||||
@@ -26,9 +28,13 @@ def _get_app() -> web.Application:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
from logo import build_logo
|
||||
from build123d import Axis
|
||||
logo = build_logo(False)
|
||||
logo = build_logo()
|
||||
server.show_cad(logo, 'Logo')
|
||||
server.show_cad(logo.faces().group_by(Axis.X)[0].face().center_location, 'Location')
|
||||
img_location = logo.faces().group_by(Axis.X)[0].face().center_location # Avoid overlapping:
|
||||
img_location.position = Vector(img_location.position.X - 1e-2, img_location.position.Y, img_location.position.Z)
|
||||
server.show_cad(img_location, 'Location')
|
||||
img_path = os.path.join(os.path.dirname(__file__), '..', 'assets', 'img.jpg')
|
||||
server.show_image(img_path, img_location, 20)
|
||||
return server.app
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""
|
||||
Utilities to work with CAD objects
|
||||
"""
|
||||
import hashlib
|
||||
from typing import Optional, Union, List, Tuple
|
||||
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopoDS import TopoDS_Shape
|
||||
|
||||
from gltf import GLTFMgr
|
||||
|
||||
CADLike = Union[TopoDS_Shape, TopLoc_Location] # Faces, Edges, Vertices and Locations for now
|
||||
|
||||
|
||||
@@ -54,4 +57,68 @@ def grab_all_cad() -> List[Tuple[str, CADLike]]:
|
||||
shapes.append((key, shape))
|
||||
return shapes
|
||||
|
||||
# TODO: Image to CAD utility and show_image shortcut on server.
|
||||
|
||||
def image_to_gltf(source: str | bytes, center: any, ppmm: int, name: Optional[str] = None,
|
||||
save_mime: str = 'image/jpeg') -> Tuple[bytes, str]:
|
||||
"""Convert an image to a GLTF CAD object, indicating the center location and pixels per millimeter."""
|
||||
from PIL import Image
|
||||
import io
|
||||
import os
|
||||
from build123d import Plane
|
||||
from build123d import Location
|
||||
from build123d import Vector
|
||||
|
||||
# Handle arguments
|
||||
if name is None:
|
||||
if isinstance(source, str):
|
||||
name = os.path.basename(source)
|
||||
else:
|
||||
hasher = hashlib.md5()
|
||||
hasher.update(source)
|
||||
name = 'image_' + hasher.hexdigest()
|
||||
format: str
|
||||
if save_mime == 'image/jpeg':
|
||||
format = 'JPEG'
|
||||
elif save_mime == 'image/png':
|
||||
format = 'PNG'
|
||||
else:
|
||||
raise ValueError(f'Unsupported save MIME type (for GLTF files): {save_mime}')
|
||||
|
||||
# Get the plane of the image
|
||||
center_loc = get_shape(center)
|
||||
if not isinstance(center_loc, TopLoc_Location):
|
||||
raise ValueError('Center location not valid')
|
||||
plane = Plane(Location(center_loc))
|
||||
# Convert coordinates system
|
||||
plane.origin = Vector(plane.origin.X, plane.origin.Z, -plane.origin.Y)
|
||||
plane.z_dir = -plane.y_dir
|
||||
plane.y_dir = plane.z_dir
|
||||
|
||||
def vert(v: Vector) -> Tuple[float, float, float]:
|
||||
return v.X, v.Y, v.Z
|
||||
|
||||
# Load the image to a byte buffer
|
||||
img = Image.open(source)
|
||||
img_buf = io.BytesIO()
|
||||
img.save(img_buf, format=format)
|
||||
img_buf = img_buf.getvalue()
|
||||
|
||||
# Build the gltf
|
||||
mgr = GLTFMgr(image=(img_buf, save_mime))
|
||||
mgr.add_face([
|
||||
vert(plane.origin - plane.x_dir * img.width / (2 * ppmm) - plane.y_dir * img.height / (2 * ppmm)),
|
||||
vert(plane.origin + plane.x_dir * img.width / (2 * ppmm) - plane.y_dir * img.height / (2 * ppmm)),
|
||||
vert(plane.origin + plane.x_dir * img.width / (2 * ppmm) + plane.y_dir * img.height / (2 * ppmm)),
|
||||
vert(plane.origin - plane.x_dir * img.width / (2 * ppmm) + plane.y_dir * img.height / (2 * ppmm)),
|
||||
], [
|
||||
(0, 2, 1),
|
||||
(0, 3, 2),
|
||||
], [
|
||||
(0, 0),
|
||||
(1, 0),
|
||||
(1, 1),
|
||||
(0, 1),
|
||||
])
|
||||
|
||||
# Return the GLTF binary blob and the suggested name of the image
|
||||
return b''.join(mgr.gltf.save_to_bytes()), name
|
||||
|
||||
@@ -12,7 +12,7 @@ _checkerboard_image_bytes = base64.decodebytes(
|
||||
class GLTFMgr:
|
||||
"""A utility class to build our GLTF2 objects easily and incrementally"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, image: Tuple[bytes, str] = (_checkerboard_image_bytes, 'image/png')):
|
||||
self.gltf = GLTF2(
|
||||
asset=Asset(generator=f"yacv_server@{importlib.metadata.version('yacv_server')}"),
|
||||
scene=0,
|
||||
@@ -20,14 +20,13 @@ class GLTFMgr:
|
||||
nodes=[Node(mesh=0)],
|
||||
meshes=[Mesh(primitives=[])],
|
||||
accessors=[],
|
||||
bufferViews=[BufferView(buffer=0, byteLength=len(_checkerboard_image_bytes), byteOffset=0)],
|
||||
buffers=[Buffer(byteLength=len(_checkerboard_image_bytes))],
|
||||
bufferViews=[BufferView(buffer=0, byteLength=len(image[0]), byteOffset=0)],
|
||||
buffers=[Buffer(byteLength=len(image[0]))],
|
||||
samplers=[Sampler(magFilter=NEAREST)],
|
||||
textures=[Texture(source=0, sampler=0)],
|
||||
images=[Image(bufferView=0, mimeType='image/png')],
|
||||
images=[Image(bufferView=0, mimeType=image[1])],
|
||||
)
|
||||
self.gltf.set_binary_blob(_checkerboard_image_bytes)
|
||||
# TODO: Custom image support for loading textured planes as CAD objects
|
||||
self.gltf.set_binary_blob(image[0])
|
||||
|
||||
def add_face(self, vertices_raw: List[Tuple[float, float, float]], indices_raw: List[Tuple[int, int, int]],
|
||||
tex_coord_raw: List[Tuple[float, float]]):
|
||||
|
||||
@@ -15,7 +15,7 @@ from aiohttp import web
|
||||
from build123d import Shape, Axis, Location, Vector
|
||||
from dataclasses_json import dataclass_json
|
||||
|
||||
from cad import get_shape, grab_all_cad
|
||||
from cad import get_shape, grab_all_cad, image_to_gltf
|
||||
from mylogger import logger
|
||||
from pubsub import BufferedPubSub
|
||||
from tessellate import _hashcode, tessellate
|
||||
@@ -190,7 +190,7 @@ class Server:
|
||||
self.show_cad(any_object, name, **kwargs)
|
||||
|
||||
def show_gltf(self, gltf: bytes, name: Optional[str] = None, **kwargs):
|
||||
"""Publishes any single-file GLTF object to the server (GLB format recommended)."""
|
||||
"""Publishes any single-file GLTF object to the server."""
|
||||
start = time.time()
|
||||
# Precompute the info and send it to the client as if it was a CAD object
|
||||
precomputed_info = self._show_common(name, _hashcode(gltf, **kwargs), start, kwargs=kwargs)
|
||||
@@ -200,6 +200,14 @@ class Server:
|
||||
publish_to.publish_nowait(b'') # Signal the end of the stream
|
||||
self.object_events[precomputed_info.name] = publish_to
|
||||
|
||||
def show_image(self, source: str | bytes, center: any, ppmm: int, name: Optional[str] = None,
|
||||
save_mime: str = 'image/jpeg', **kwargs):
|
||||
"""Publishes an image as a quad GLTF object, indicating the center location and pixels per millimeter."""
|
||||
# Convert the image to a GLTF CAD object
|
||||
gltf, name = image_to_gltf(source, center, ppmm, name, save_mime)
|
||||
# Publish it like any other GLTF object
|
||||
self.show_gltf(gltf, name, **kwargs)
|
||||
|
||||
def show_cad(self, obj: Union[TopoDS_Shape, any], name: Optional[str] = None, **kwargs):
|
||||
"""Publishes a CAD object to the server"""
|
||||
start = time.time()
|
||||
|
||||
Reference in New Issue
Block a user