"""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 an NC file here
a single .nc file or a whole folder — parsed locally, nothing is uploaded anywhere
__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'wired3d' 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)