193 lines
6.6 KiB
Python
193 lines
6.6 KiB
Python
|
|
import os
|
|
import sys
|
|
import inspect
|
|
import tempfile
|
|
import shutil
|
|
import jinja2
|
|
import time
|
|
|
|
# web content
|
|
if sys.version_info[0] >= 3:
|
|
# python 3.x
|
|
import http.server as SimpleHTTPServer
|
|
import socketserver as SocketServer
|
|
else:
|
|
# python 2.x
|
|
import SimpleHTTPServer
|
|
import SocketServer
|
|
|
|
import threading
|
|
import webbrowser
|
|
|
|
import logging
|
|
log = logging.getLogger(__name__)
|
|
|
|
from ..utils import working_dir
|
|
from .environment import map_environment, DisplayEnvironment
|
|
|
|
# Get this file's location
|
|
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
|
|
|
# Set template web-content directory
|
|
# note: can be changed prior to calling web_display()
|
|
#
|
|
# >>> from cqparts.display import web
|
|
# >>> web.TEMPLATE_CONTENT_DIR = './my/alternative/template'
|
|
# >>> web.web_display(some_thing)
|
|
#
|
|
# This would typically be used for testing, or development purposes.
|
|
TEMPLATE_CONTENT_DIR = os.path.join(_this_path, 'web-template')
|
|
|
|
SocketServer.TCPServer.allow_reuse_address = True # stops crash on re-use of port
|
|
|
|
|
|
@map_environment(
|
|
# named 'cmdline'?
|
|
# This is a fallback display environment if no other method is available.
|
|
# Therefore it's assumed that the environment that's been detected is a
|
|
# no-frills command line.
|
|
name='cmdline',
|
|
order=99,
|
|
condition=lambda: True, # fallback
|
|
)
|
|
class WebDisplayEnv(DisplayEnvironment):
|
|
"""
|
|
Display given component in a browser window
|
|
|
|
This display exports the model, then exposes a http service on *localhost*
|
|
for a browser to use.
|
|
The http service does not know when the browser window has been closed, so
|
|
it will continue to serve the model's data until the user halts the
|
|
process with a :class:`KeyboardInterrupt` (by pressing ``Ctrl+C``)
|
|
|
|
When run, you should see output similar to::
|
|
|
|
>>> from cqparts.display import WebDisplayEnv
|
|
>>> from cqparts_misc.basic.primatives import Cube
|
|
>>> WebDisplayEnv().display(Cube())
|
|
press [ctrl+c] to stop server
|
|
127.0.0.1 - - [27/Dec/2017 16:06:37] "GET / HTTP/1.1" 200 -
|
|
Created new window in existing browser session.
|
|
127.0.0.1 - - [27/Dec/2017 16:06:39] "GET /model/out.gltf HTTP/1.1" 200 -
|
|
127.0.0.1 - - [27/Dec/2017 16:06:39] "GET /model/out.bin HTTP/1.1" 200 -
|
|
|
|
A new browser window should appear with a render that looks like:
|
|
|
|
.. image:: /_static/img/web_display.cube.png
|
|
|
|
Then, when you press ``Ctrl+C``, you should see::
|
|
|
|
^C[server shutdown successfully]
|
|
|
|
and any further request on the opened browser window will return
|
|
an errorcode 404 (file not found), because the http service has stopped.
|
|
"""
|
|
|
|
def display_callback(self, component, port=9041, autorotate=False):
|
|
"""
|
|
:param component: the component to render
|
|
:type component: :class:`Component <cqparts.Component>`
|
|
:param port: port to expose http service on
|
|
:type port: :class:`int`
|
|
:param autorotate: if ``True``, rendered component will rotate
|
|
as if on a turntable.
|
|
:type autorotate: :class:`bool`
|
|
"""
|
|
|
|
# Verify Parameter(s)
|
|
from .. import Component
|
|
|
|
if not isinstance(component, Component):
|
|
raise TypeError("given component must be a %r, not a %r" % (
|
|
Component, type(component)
|
|
))
|
|
|
|
# Create temporary file to host files
|
|
temp_dir = tempfile.mkdtemp()
|
|
host_dir = os.path.join(temp_dir, 'html')
|
|
print("host temp folder: %s" % host_dir)
|
|
|
|
# Copy template content to temporary location
|
|
shutil.copytree(TEMPLATE_CONTENT_DIR, host_dir)
|
|
|
|
# Export model
|
|
exporter = component.exporter('gltf')
|
|
exporter(
|
|
filename=os.path.join(host_dir, 'model', 'out.gltf'),
|
|
embed=False,
|
|
)
|
|
|
|
# Modify templated content
|
|
# index.html
|
|
with open(os.path.join(host_dir, 'index.html'), 'r') as fh:
|
|
index_template = jinja2.Template(fh.read())
|
|
with open(os.path.join(host_dir, 'index.html'), 'w') as fh:
|
|
# camera location & target
|
|
cam_t = [
|
|
(((a + b) / 2.0) / 1000) # midpoint (unit: meters)
|
|
for (a, b) in zip(exporter.scene_min, exporter.scene_max)
|
|
]
|
|
cam_p = [
|
|
(((b - a) * 1.0) / 1000) + t # max point * 200% (unit: meters)
|
|
for (a, b, t) in zip(exporter.scene_min, exporter.scene_max, cam_t)
|
|
]
|
|
|
|
# write
|
|
xzy = lambda a: (a[0], a[2], -a[1]) # x,z,y coordinates (not x,y,z)
|
|
fh.write(index_template.render(
|
|
model_filename='model/out.gltf',
|
|
autorotate = str(autorotate).lower(),
|
|
camera_target=','.join("%g" % (val) for val in xzy(cam_t)),
|
|
camera_pos=','.join("%g" % (val) for val in xzy(cam_p)),
|
|
))
|
|
|
|
try:
|
|
# Start web-service (loop forever)
|
|
server = SocketServer.ThreadingTCPServer(
|
|
server_address=("0.0.0.0", port),
|
|
RequestHandlerClass=SimpleHTTPServer.SimpleHTTPRequestHandler,
|
|
)
|
|
server_addr = "http://%s:%i/" % server.server_address
|
|
def thread_target():
|
|
with working_dir(host_dir):
|
|
server.serve_forever()
|
|
|
|
print("serving: %s" % server_addr)
|
|
sys.stdout.flush()
|
|
server_thread = threading.Thread(target=thread_target)
|
|
server_thread.daemon = True
|
|
server_thread.start()
|
|
|
|
# Open in browser
|
|
print("opening in browser: %s" % server_addr)
|
|
sys.stdout.flush()
|
|
webbrowser.open(server_addr)
|
|
|
|
# workaround for https://github.com/dcowden/cadquery/issues/211
|
|
import signal
|
|
def _handler_sigint(signum, frame):
|
|
raise KeyboardInterrupt()
|
|
signal.signal(signal.SIGINT, _handler_sigint)
|
|
|
|
print("press [ctrl+c] to stop server")
|
|
sys.stdout.flush()
|
|
while True: # wait for Ctrl+C
|
|
time.sleep(1)
|
|
|
|
except KeyboardInterrupt:
|
|
log.info("\n[keyboard interrupt]")
|
|
sys.stdout.flush()
|
|
|
|
finally:
|
|
# Stop web-service
|
|
server.shutdown()
|
|
server.server_close()
|
|
server_thread.join()
|
|
print("[server shutdown successfully]")
|
|
|
|
# Delete temporary content
|
|
if os.path.exists(os.path.join(host_dir, 'cqparts-display.txt')):
|
|
# just making sure we're deleting the right folder
|
|
shutil.rmtree(temp_dir)
|