"""Drag-and-drop launcher for the NC toolpath viewer.
Starts a tiny local web server (Python standard-library ``http.server`` only —
no extra installs beyond what the viewer already needs) that serves a dark
full-screen **drop zone**. Drop an ``.nc`` file — or a whole folder of them —
onto the page and it is parsed with the package ``parser`` and rendered with the
exact same Plotly viewer (``viewer.build_figure``), shown inline in an iframe.
Drop another at any time; pick from a list when several are dropped.
Why a server (instead of pure client-side drag-drop): the parser and the viewer
already live in Python and must not be rewritten in JavaScript. The browser only
reads the dropped file's text and POSTs it; Python does the parsing + figure
building and returns ready-to-display HTML.
Run:
python -m wired3d_viewer.server # serves on http://127.0.0.1:8765
python -m wired3d_viewer.server 9000 # custom port
# or, after `pip install -e .`: wired3d serve [port]
Then open the printed URL and drag NC files onto it. Ctrl-C to stop.
Dependencies: plotly, numpy (same as the viewer). Imports the sibling
``parser`` and ``viewer`` modules from this package.
"""
import os
import sys
import tempfile
import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import plotly.io as pio
from .parser import parse_nc
from .viewer import build_figure, LEGEND_CLICK_JS, logo_data_uri
DEFAULT_PORT = 8765
# --- the drop-zone page (served at "/") -------------------------------------
INDEX_HTML = """
Visualizer
__LOGO_IMG__
Visualizer
Drag an .nc file (or a folder of them) anywhere on this page.
⤓ Drop another
__LOGO_LOADING__
Rendering…
building the 3D toolpath — this can take a moment for large files
"""
def render_nc_text(nc_text: str, name: str) -> str:
"""Parse NC source text and return a full viewer HTML document.
Inputs:
nc_text -- raw contents of an .nc file.
name -- original file name (used for the title and as the part-name
fallback when the file has no "(Part name: ...)" comment).
Output:
a complete, self-contained HTML string (plotly.js from CDN), full
viewport, identical to what ``nc_viewer.py`` writes to disk — just
returned as a string for inline display instead of saved.
"""
# parse_nc reads from a path; write to a temp file, parse, then clean up.
with tempfile.NamedTemporaryFile("w", suffix=".nc", delete=False) as fh:
fh.write(nc_text)
tmp = fh.name
try:
parsed = parse_nc(tmp)
finally:
os.unlink(tmp)
# Without a "(Part name: ...)" comment, parse_nc falls back to the temp
# file's random stem — restore the real dropped name instead.
if "(Part name:" not in nc_text:
parsed["part_name"] = Path(name).stem
fig = build_figure(parsed, name)
return pio.to_html(
fig, include_plotlyjs="cdn", full_html=True,
default_width="100%", default_height="100vh",
config={"responsive": True},
post_script=LEGEND_CLICK_JS,
)
class _Handler(BaseHTTPRequestHandler):
"""Serves the drop page (GET /) and renders posted NC text (POST /render)."""
def _send(self, code: int, body: str, ctype: str = "text/html") -> None:
data = body.encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", f"{ctype}; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self): # noqa: N802 (http.server naming)
if self.path in ("/", "/index.html"):
uri = logo_data_uri()
bar_img = f'
' if uri else ""
load_img = f'
' if uri else ""
page = (INDEX_HTML
.replace("__LOGO_IMG__", bar_img)
.replace("__LOGO_LOADING__", load_img))
self._send(200, page)
else:
self._send(404, "not found", "text/plain")
def do_POST(self): # noqa: N802
if not self.path.startswith("/render"):
self._send(404, "not found", "text/plain")
return
# Original filename is passed as a query param (?name=...).
name = "dropped.nc"
if "?" in self.path:
from urllib.parse import parse_qs, urlparse, unquote
q = parse_qs(urlparse(self.path).query)
if "name" in q:
name = unquote(q["name"][0])
length = int(self.headers.get("Content-Length", 0))
nc_text = self.rfile.read(length).decode("utf-8", errors="replace")
try:
html = render_nc_text(nc_text, name)
self._send(200, html)
except Exception as exc: # noqa: BLE001 - report to the page
self._send(500, f"Failed to render {name}: {exc}", "text/plain")
def log_message(self, *_args):
pass # quiet; we print our own status line
def main(port: int = DEFAULT_PORT) -> None:
"""Start the drop-zone server and open it in the browser.
Inputs:
port -- TCP port to listen on (default 8765).
Output:
None. Runs until Ctrl-C.
"""
ThreadingHTTPServer.allow_reuse_address = True
try:
server = ThreadingHTTPServer(("127.0.0.1", port), _Handler)
except OSError as exc:
print(f"Could not bind port {port}: {exc}")
print(f"Another server is likely already running. Try a different port: "
f"python -m wired3d_viewer.server {port + 1}")
return
url = f"http://127.0.0.1:{port}"
print(f"NC Viewer drop zone running at {url}")
print("Drag an .nc file (or a folder of them) onto the page. Ctrl-C to stop.")
try:
webbrowser.open(url)
except Exception:
pass
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopped.")
server.server_close()
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PORT
main(port)