30.05
This commit is contained in:
1 parent
e2978bba83
commit
a4b9a311dd
13 files changed
+320
-58
No files matched your search
+12
@@ -0,0 +1,12 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Generated viewer output
|
||||||
|
*_viewer.html
|
||||||
|
/output/
|
||||||
@@ -1 +1,64 @@
|
|||||||
# Visualizer
|
# wired3d viewer
|
||||||
|
|
||||||
|
Interactive 3D viewer for the Beckhoff 5-axis metal-DED NC toolpaths produced by
|
||||||
|
`stl_slicer.py`. It reads the machine NC dialect and renders a self-contained,
|
||||||
|
offline-capable HTML page: a 3D toolpath scene (rapid moves + per-layer welds),
|
||||||
|
an A/B tilt-angle chart, a statistics table, layer isolation, and a
|
||||||
|
progressive-reveal print animation.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
visualizer/
|
||||||
|
├── pyproject.toml # package metadata + the `wired3d` console script
|
||||||
|
├── README.md
|
||||||
|
├── wired3d_viewer/ # the Python package
|
||||||
|
│ ├── __init__.py # lightweight; no heavy imports
|
||||||
|
│ ├── __main__.py # CLI: `python -m wired3d_viewer view|serve`
|
||||||
|
│ ├── parser.py # NC dialect reader (standard library only)
|
||||||
|
│ ├── viewer.py # Plotly figure builder + HTML writer
|
||||||
|
│ ├── server.py # drag-and-drop local web front end
|
||||||
|
│ └── assets/
|
||||||
|
│ └── wired3d.avif # logo, embedded into the HTML as a data URI
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e . # installs plotly + numpy and the `wired3d` command
|
||||||
|
```
|
||||||
|
|
||||||
|
The package runs fine without installing too — just call the modules directly
|
||||||
|
(see below). Only `viewer`/`server` need `plotly` + `numpy`; `parser` is pure
|
||||||
|
standard library.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Render an NC file to a standalone HTML viewer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m wired3d_viewer view path/to/part.nc # writes <stem>_viewer.html
|
||||||
|
wired3d view path/to/part.nc # same, if installed
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the drag-and-drop web front end (drop an `.nc` file or a folder onto the
|
||||||
|
page):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m wired3d_viewer serve # http://127.0.0.1:8765
|
||||||
|
python -m wired3d_viewer serve 9000 # custom port
|
||||||
|
wired3d serve # same, if installed
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse only (no visualisation, no third-party deps):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wired3d_viewer.parser import parse_nc, summarise
|
||||||
|
summarise(parse_nc("part.nc"))
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the parser self-test:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m wired3d_viewer.parser
|
||||||
|
```
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,11 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head><meta charset="utf-8" /></head>
|
|
||||||
<body>
|
|
||||||
<div> <script>window.PlotlyConfig = {MathJaxConfig: 'local'};</script>
|
|
||||||
<script charset="utf-8" src="https://cdn.plot.ly/plotly-3.5.0.min.js" integrity="sha256-fHbNLP+GlIXN+efbQec78UkemUz3NJp7UmfGxC1tNxs=" crossorigin="anonymous"></script> <div id="aa543e9f-0324-4175-aabb-bf3c354d6f7f" class="plotly-graph-div" style="height:100vh; width:100%;"></div> <script> window.PLOTLYENV=window.PLOTLYENV || {}; if (document.getElementById("aa543e9f-0324-4175-aabb-bf3c354d6f7f")) { Plotly.newPlot( "aa543e9f-0324-4175-aabb-bf3c354d6f7f", [{"customdata":[[37.89,-12.44,2.5,0.0,0.0,"off",1,1,0.0,-0.0,1.0],[47.5,-15.25,2.5,0.0,0.0,"off",1,2,0.0,-0.0,1.0],[47.08,-14.93,7.5,0.0,0.0,"off",2,1,0.0,-0.0,1.0],[42.36,-13.27,7.5,0.0,0.0,"off",2,2,0.0,-0.0,1.0],[37.33,-12.15,7.5,0.0,0.0,"off",2,3,0.0,-0.0,1.0],[36.08,-11.79,12.5,0.0,0.0,"off",3,1,0.0,-0.0,1.0],[41.2,-13.55,12.5,0.0,0.0,"off",3,2,0.0,-0.0,1.0],[46.0,-14.95,12.5,0.0,0.0,"off",3,3,0.0,-0.0,1.0],[44.36,-14.93,17.5,0.0,0.0,"off",4,1,0.0,-0.0,1.0],[39.71,-13.04,17.5,0.0,0.0,"off",4,2,0.0,-0.0,1.0],[34.04,-11.51,17.5,0.0,0.0,"off",4,3,0.0,-0.0,1.0],[31.27,-10.65,22.5,0.0,0.0,"off",5,1,0.0,-0.0,1.0],[37.3,-13.31,22.5,0.0,0.0,"off",5,2,0.0,-0.0,1.0],[41.99,-15.04,22.5,0.0,0.0,"off",5,3,0.0,-0.0,1.0],[39.22,-14.28,27.5,0.0,0.0,"off",6,1,0.0,-0.0,1.0],[34.54,-12.51,27.5,0.0,0.0,"off",6,2,0.0,-0.0,1.0],[27.2,-10.09,27.5,0.0,0.0,"off",6,3,0.0,-0.0,1.0],[31.03,-11.05,32.5,0.0,0.0,"off",7,1,0.0,-0.0,1.0],[35.74,-12.71,32.5,0.0,0.0,"off",7,2,0.0,-0.0,1.0],[22.04,-7.49,32.5,0.0,0.0,"off",7,3,0.0,-0.0,1.0],[26.66,-8.58,37.5,0.0,0.0,"off",8,1,0.0,-0.0,1.0],[31.44,-10.04,37.5,0.0,0.0,"off",8,2,0.0,-0.0,1.0],[13.2,-4.18,37.5,0.0,0.0,"off",8,3,0.0,-0.0,1.0],[15.5,-4.93,42.5,0.0,0.0,"off",9,1,0.0,-0.0,1.0],[10.73,-3.43,42.5,0.0,0.0,"off",9,2,0.0,-0.0,1.0],[6.06,-1.49,42.5,0.0,0.0,"off",9,3,0.0,-0.0,1.0],[1.26,0.07,42.5,0.0,0.0,"off",9,4,0.0,-0.0,1.0],[21.17,1.99,42.5,0.0,0.0,"off",9,5,0.0,-0.0,1.0],[26.17,2.25,42.5,0.0,0.0,"off",9,6,0.0,-0.0,1.0],[15.37,1.75,47.5,0.0,0.0,"off",10,1,0.0,-0.0,1.0],[10.43,0.83,47.5,0.0,0.0,"off",10,2,0.0,-0.0,1.0],[5.43,0.05,47.5,0.0,0.0,"off",10,3,0.0,-0.0,1.0]],"hovertemplate":"\u003cb\u003ePosition\u003c\u002fb\u003e\u003cbr\u003eX: %{customdata[0]:.2f} mm\u003cbr\u003eY: %{customdata[1]:.2f} mm\u003cbr\u003eZ: %{customdata[2]:.2f} mm\u003cbr\u003e\u003cb\u003eExtruder\u003c\u002fb\u003e\u003cbr\u003eA: %{customdata[3]:.2f}\u00b0\u003cbr\u003eB: %{customdata[4]:.2f}\u00b0\u003cbr\u003eVector: (%{customdata[8]:.3f}, %{customdata[9]:.3f}, %{customdata[10]:.3f})\u003cbr\u003e\u003cb\u003eStatus:\u003c\u002fb\u003e %{customdata[5]}\u003cbr\u003eSlice: %{customdata[6]} Fraction: %{customdata[7]}\u003cextra\u003e\u003c\u002fextra\u003e","line":{"color":"#888888","dash":"dot","width":1},"mode":"lines","name":"Rapid moves","showlegend":true,"x":[37.89,47.5,47.08,42.36,37.33,36.08,41.2,46.0,44.36,39.71,34.04,31.27,37.3,41.99,39.22,34.54,27.2,31.03,35.74,22.04,26.66,31.44,13.2,15.5,10.73,6.06,1.26,21.17,26.17,15.37,10.43,5.43],"y":[-12.44,-15.25,-14.93,-13.27,-12.15,-11.79,-13.55,-14.95,-14.93,-13.04,-11.51,-10.65,-13.31,-15.04,-14.28,-12.51,-10.09,-11.05,-12.71,-7.49,-8.58,-10.04,-4.18,-4.93,-3.43,-1.49,0.07,1.99,2.25,1.75,0.83,0.05],"z":[2.5,2.5,7.5,7.5,7.5,12.5,12.5,12.5,17.5,17.5,17.5,22.5,22.5,22.5,27.5,27.5,27.5,32.5,32.5,32.5,37.5,37.5,37.5,42.5,42.5,42.5,42.5,42.5,42.5,47.5,47.5,47.5],"type":"scatter3d","scene":"scene"},{"customdata":[[37.58,-13.39,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[37.22,-14.32,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[36.85,-15.25,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[36.47,-16.17,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[36.05,-17.08,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[35.61,-17.97,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[35.15,-18.86,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[34.66,-19.74,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[34.16,-20.6,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[33.64,-21.45,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[33.09,-22.28,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[32.52,-23.11,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[31.93,-23.91,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[31.33,-24.71,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[30.7,-25.48,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[30.05,-26.24,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[29.38,-26.98,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[28.7,-27.71,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[28.0,-28.42,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[27.28,-29.12,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[26.54,-29.79,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[25.78,-30.44,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[25.01,-31.08,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[24.23,-31.69,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[23.43,-32.29,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[22.62,-32.87,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[21.78,-33.42,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[20.94,-33.96,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[20.09,-34.49,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[19.21,-34.96,2.5,0.0,0.0,"on",1,1,0.0,-0.0,1.0],[18.34,-35.4Line truncated
|
|
||||||
Plotly.addFrames('aa543e9f-0324-4175-aabb-bf3c354d6f7f', [{"data":[{"x":[37.89],"y":[-12.44],"z":[2.5],"type":"scatter3d"},{"x":[37.89,37.89],"y":[-12.44,-12.44],"z":[2.5,12.48],"type":"scatter3d"},{"visible":false,"type":"scatter3d"},{"visible":false,"type":"scatter3d"},{"visible":false,"type":"scatter3d"},{"visible":false,"type":"scatter3d"},{"visible":false,"type":"scatter3d"},{"visible":false,"type":"scatter3d"},{"visible":false,"type":"scatter3d"},{"visible":false,"type":"scatter3d"},{"visible":false,"type":"scatter3d"},{"visible":false,"type":"scatter3d"},{"visible":false,"type":"scatter3d"}],"layout":{"scene":{"camera":{"center":{"x":0.0,"y":0.0,"z":0.0},"eye":{"x":1.6,"y":1.6,"z":1.2},"projection":{"type":"perspective"},"up":{"x":0.0,"y":0.0,"z":1.0}},"uirevision":"play"}},"name":"0","traces":[12,11,0,1,2,3,4,5,6,7,8,9,10]},{"data":[{"x":[37.89,37.58,37.22,36.85,36.47,36.05,35.61,35.15,34.66,34.16,33.64,33.09,32.52,31.93,31.33,30.7,30.05,29.38,28.7,28.0,27.28,26.54,25.78,25.01,24.23,23.43,22.62,21.78,20.94,20.09,19.21,18.34,17.45,16.54,15.63,14.7,13.77,12.83,11.88,10.92],"y":[-12.44,-13.39,-14.32,-15.25,-16.17,-17.08,-17.97,-18.86,-19.74,-20.6,-21.45,-22.28,-23.11,-23.91,-24.71,-25.48,-26.24,-26.98,-27.71,-28.42,-29.12,-29.79,-30.44,-31.08,-31.69,-32.29,-32.87,-33.42,-33.96,-34.49,-34.96,-35.44,-35.89,-36.31,-36.72,-37.09,-37.44,-37.79,-38.09,-38.37],"z":[2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5],"type":"scatter3d"},{"x":[10.92,10.92],"y":[-38.37,-38.37],"z":[2.5,12.48],"type":"scatter3d"}],"name":"39","traces":[12,11]},{"data":[{"x":[37.89,37.58,37.22,36.85,36.47,36.05,35.61,35.15,34.66,34.16,33.64,33.09,32.52,31.93,31.33,30.7,30.05,29.38,28.7,28.0,27.28,26.54,25.78,25.01,24.23,23.43,22.62,21.78,20.94,20.09,19.21,18.34,17.45,16.54,15.63,14.7,13.77,12.83,11.88,10.92,9.96,8.99,8.01,7.03,6.05,5.06,4.07,3.07,2.07,1.08,0.08,-0.92,-1.92,-2.92,-3.91,-4.9,-5.89,-6.88,-7.86,-8.83,-9.81,-10.77,-11.73,-12.69,-13.62,-14.56,-15.49,-16.4,-17.3,-18.2,-19.08,-19.96,-20.81,-21.65,-22.48,-23.3,-24.11,-24.89,-25.67],"y":[-12.44,-13.39,-14.32,-15.25,-16.17,-17.08,-17.97,-18.86,-19.74,-20.6,-21.45,-22.28,-23.11,-23.91,-24.71,-25.48,-26.24,-26.98,-27.71,-28.42,-29.12,-29.79,-30.44,-31.08,-31.69,-32.29,-32.87,-33.42,-33.96,-34.49,-34.96,-35.44,-35.89,-36.31,-36.72,-37.09,-37.44,-37.79,-38.09,-38.37,-38.65,-38.87,-39.09,-39.29,-39.43,-39.58,-39.7,-39.78,-39.85,-39.88,-39.88,-39.88,-39.86,-39.79,-39.71,-39.6,-39.46,-39.31,-39.12,-38.9,-38.69,-38.41,-38.13,-37.85,-37.5,-37.15,-36.78,-36.37,-35.96,-35.51,-35.04,-34.56,-34.04,-33.5,-32.96,-32.38,-31.79,-31.18,-30.54],"z":[2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5],"type":"scatter3d"},{"x":[-25.67,-25.67],"y":[-30.54,-30.54],"z":[2.5,12.48],"type":"scatter3d"}],"name":"78","traces":[12,11]},{"data":[{"x":[37.89,37.58,37.22,36.85,36.47,36.05,35.61,35.15,34.66,34.16,33.64,33.09,32.52,31.93,31.33,30.7,30.05,29.38,28.7,28.0,27.28,26.54,25.78,25.01,24.23,23.43,22.62,21.78,20.94,20.09,19.21,18.34,17.45,16.54,15.63,14.7,13.77,12.83,11.88,10.92,9.96,8.99,8.01,7.03,6.05,5.06,4.07,3.07,2.07,1.08,0.08,-0.92,-1.92,-2.92,-3.91,-4.9,-5.89,-6.88,-7.86,-8.83,-9.81,-10.77,-11.73,-12.69,-13.62,-14.56,-15.49,-16.4,-17.3,-18.2,-19.08,-19.96,-20.81,-21.65,-22.48,-23.3,-24.11,-24.89,-25.67,-26.42,-27.16,-27.89,-28.59,-29.28,-29.94,-30.6,-31.23,-31.84,-32.43,-33.0,-33.56,-34.09,-34.59,-35.07,-35.54,-35.98,-36.4,-36.8,-37.17,-37.52,-37.85,-38.15,-38.44,-38.69,-38.92,-39.13,-39.31,-39.47,-39.6,-39.71,-39.8,-39.85,-39.89,-39.91,-39.88,-39.83,-39.77,-39.67],"y":[-12.44,-13.39,-14.32,-15.25,-16.17,-17.08,-17.97,-18.86,-19.74,-20.6,-21.45,-22.28,-23.11,-23.91,-24.71,-25.48,-26.24,-26.98,-27.71,-28.42,-29.12,-29.79,-30.44,-31.08,-31.69,-32.29,-32.87,-33.42,-33.96,-34.49,-34.96,-35.44,-35.89,-36.31,-36.72,-37.09,-37.44,-37.79,-38.09,-38.37,-38.65,-38.87,-39.09,-39.29,-39.43,-39.58,-39.7,-39.78,-39.85,-39.88,-39.88,-39.88,-39.86,-39.79,-39.71,-39.6,-39.46,-39.31,-39.12,-38.9,-38.69,-38.41,-38.13,-37.85,-37.5,-37.15,-36.78,-36.37,-35.96,-35.51,-35.04,-34.56,-34.04,-33.5,-32.96,-32.38,-31.79,-31.18,-30.54,-29.89,-29.22,-28.53,-27.82,-27.1,-26.36,-25.6,-24.83,-24.04,-23.23,-22.41,-21.58,-20.73,-19.87,-19.0,-18.11,-17.22,-16.31,-15.4,-14.47,-13.54,-12.59,-11.64,-10.68,-9.72,-8.74,-7.77,-6.78,-5.8,-4.81,-3.82,-2.82,-1.82,-0.83,0.17,1.17,2.17,3.16,4.16],"z":[2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.5,2.Line truncated
|
|
||||||
}).then(function(){
|
|
||||||
Plotly.animate('aa543e9f-0324-4175-aabb-bf3c354d6f7f', null);
|
|
||||||
}) }; </script> </div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "wired3d-viewer"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Interactive 3D viewer for Beckhoff 5-axis DED NC toolpaths"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"plotly>=5",
|
||||||
|
"numpy>=1.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
wired3d = "wired3d_viewer.__main__:main"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ["wired3d_viewer"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
wired3d_viewer = ["assets/*.avif"]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""wired3d_viewer — interactive 3D viewer for Beckhoff 5-axis DED NC toolpaths.
|
||||||
|
|
||||||
|
Modules
|
||||||
|
-------
|
||||||
|
parser
|
||||||
|
NC-dialect reader. Standard library only — importable with zero installs.
|
||||||
|
viewer
|
||||||
|
Plotly figure builder and self-contained HTML writer (needs plotly, numpy).
|
||||||
|
server
|
||||||
|
Drag-and-drop local web front end around the viewer.
|
||||||
|
|
||||||
|
This top-level package deliberately imports nothing heavy: ``import
|
||||||
|
wired3d_viewer`` stays cheap and ``wired3d_viewer.parser`` keeps working without
|
||||||
|
plotly/numpy installed. Import the submodule you need explicitly, e.g.::
|
||||||
|
|
||||||
|
from wired3d_viewer.parser import parse_nc
|
||||||
|
from wired3d_viewer.viewer import build_figure
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Command-line entry point for the wired3d viewer package.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python -m wired3d_viewer view <file.nc> # write a standalone HTML viewer
|
||||||
|
python -m wired3d_viewer serve [port] # start the drag-and-drop server
|
||||||
|
|
||||||
|
After ``pip install -e .`` the same commands are available as::
|
||||||
|
|
||||||
|
wired3d view <file.nc>
|
||||||
|
wired3d serve [port]
|
||||||
|
|
||||||
|
Submodules are imported lazily inside each handler so that ``view``/``serve``
|
||||||
|
only pull in plotly/numpy when actually used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> None:
|
||||||
|
"""Parse argv and dispatch to the viewer or server.
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
argv -- optional argument list (defaults to ``sys.argv[1:]``).
|
||||||
|
Output:
|
||||||
|
None. Runs the chosen subcommand.
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="wired3d_viewer",
|
||||||
|
description="Interactive 3D viewer for Beckhoff 5-axis DED NC toolpaths.",
|
||||||
|
)
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
p_view = sub.add_parser(
|
||||||
|
"view", help="render an NC file to a standalone HTML viewer")
|
||||||
|
p_view.add_argument("nc_file", help="path to the .nc file to visualise")
|
||||||
|
|
||||||
|
p_serve = sub.add_parser(
|
||||||
|
"serve", help="start the drag-and-drop web server")
|
||||||
|
p_serve.add_argument(
|
||||||
|
"port", nargs="?", type=int, default=8765,
|
||||||
|
help="TCP port to listen on (default 8765)")
|
||||||
|
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.command == "view":
|
||||||
|
from .viewer import main as view_main
|
||||||
|
view_main(args.nc_file)
|
||||||
|
elif args.command == "serve":
|
||||||
|
from .server import main as serve_main
|
||||||
|
serve_main(args.port)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
File renamed without changes.
@@ -3,9 +3,9 @@
|
|||||||
Starts a tiny local web server (Python standard-library ``http.server`` only —
|
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
|
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 —
|
full-screen **drop zone**. Drop an ``.nc`` file — or a whole folder of them —
|
||||||
onto the page and it is parsed with ``nc_parser`` and rendered with the exact
|
onto the page and it is parsed with the package ``parser`` and rendered with the
|
||||||
same Plotly viewer as ``nc_viewer.py`` (``build_figure``), shown inline in an
|
exact same Plotly viewer (``viewer.build_figure``), shown inline in an iframe.
|
||||||
iframe. Drop another at any time; pick from a list when several are dropped.
|
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
|
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
|
already live in Python and must not be rewritten in JavaScript. The browser only
|
||||||
@@ -13,13 +13,14 @@ reads the dropped file's text and POSTs it; Python does the parsing + figure
|
|||||||
building and returns ready-to-display HTML.
|
building and returns ready-to-display HTML.
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
python nc_server.py # serves on http://127.0.0.1:8765
|
python -m wired3d_viewer.server # serves on http://127.0.0.1:8765
|
||||||
python nc_server.py 9000 # custom port
|
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.
|
Then open the printed URL and drag NC files onto it. Ctrl-C to stop.
|
||||||
|
|
||||||
Dependencies: plotly, numpy (same as the viewer). ``nc_parser.py`` and
|
Dependencies: plotly, numpy (same as the viewer). Imports the sibling
|
||||||
``nc_viewer.py`` must sit in the same folder.
|
``parser`` and ``viewer`` modules from this package.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -31,8 +32,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
import plotly.io as pio
|
import plotly.io as pio
|
||||||
|
|
||||||
from nc_parser import parse_nc
|
from .parser import parse_nc
|
||||||
from nc_viewer import build_figure
|
from .viewer import build_figure, LEGEND_CLICK_JS, logo_data_uri
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_PORT = 8765
|
DEFAULT_PORT = 8765
|
||||||
@@ -44,13 +45,14 @@ INDEX_HTML = """<!doctype html>
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>wired3d visualizer</title>
|
<title>Visualizer</title>
|
||||||
<style>
|
<style>
|
||||||
html, body { margin: 0; height: 100%; background: #0f0f1a; color: #e8e8f0;
|
html, body { margin: 0; height: 100%; background: #0f0f1a; color: #e8e8f0;
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
|
||||||
#wrap { display: flex; flex-direction: column; height: 100vh; }
|
#wrap { display: flex; flex-direction: column; height: 100vh; }
|
||||||
#bar { flex: 0 0 auto; padding: 10px 16px; display: flex; align-items: center;
|
#bar { flex: 0 0 auto; padding: 10px 16px; display: flex; align-items: center;
|
||||||
gap: 14px; border-bottom: 1px solid #222; background: #16213e; }
|
gap: 14px; border-bottom: 1px solid #222; background: #16213e; }
|
||||||
|
#bar img.logo { height: 28px; width: auto; display: block; }
|
||||||
#bar h1 { font-size: 15px; margin: 0; font-weight: 600; color: #fff; }
|
#bar h1 { font-size: 15px; margin: 0; font-weight: 600; color: #fff; }
|
||||||
#status { font-size: 13px; color: #9aa0b5; }
|
#status { font-size: 13px; color: #9aa0b5; }
|
||||||
#drop { flex: 1 1 auto; position: relative; }
|
#drop { flex: 1 1 auto; position: relative; }
|
||||||
@@ -77,18 +79,25 @@ INDEX_HTML = """<!doctype html>
|
|||||||
align-items: center; justify-content: center; gap: 22px;
|
align-items: center; justify-content: center; gap: 22px;
|
||||||
background: rgba(15,15,26,.92); z-index: 5; }
|
background: rgba(15,15,26,.92); z-index: 5; }
|
||||||
#loading.show { display: flex; }
|
#loading.show { display: flex; }
|
||||||
#loading .spinner { width: 64px; height: 64px; border-radius: 50%;
|
#loading .logo-big { height: 72px; width: auto; display: block;
|
||||||
|
animation: pulse 1.3s ease-in-out infinite; }
|
||||||
|
#loading .spinner { width: 56px; height: 56px; border-radius: 50%;
|
||||||
border: 6px solid #1d2b4d; border-top-color: #00bfff;
|
border: 6px solid #1d2b4d; border-top-color: #00bfff;
|
||||||
animation: spin 0.9s linear infinite; }
|
animation: spin 0.9s linear infinite; }
|
||||||
#loading .msg { font-size: 16px; color: #cfd3e6; }
|
#loading .msg { font-size: 16px; color: #cfd3e6; }
|
||||||
#loading .sub2 { font-size: 12px; color: #7f86a0; }
|
#loading .sub2 { font-size: 12px; color: #7f86a0; }
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: .45; transform: scale(.96); }
|
||||||
|
50% { opacity: 1; transform: scale(1.05); }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="wrap">
|
<div id="wrap">
|
||||||
<div id="bar">
|
<div id="bar">
|
||||||
<h1>wired3d visualizer</h1>
|
__LOGO_IMG__
|
||||||
|
<h1>Visualizer</h1>
|
||||||
<span id="status">Drag an .nc file (or a folder of them) anywhere on this page.</span>
|
<span id="status">Drag an .nc file (or a folder of them) anywhere on this page.</span>
|
||||||
<span style="flex:1"></span>
|
<span style="flex:1"></span>
|
||||||
<a class="file" id="another" style="display:none">⤓ Drop another</a>
|
<a class="file" id="another" style="display:none">⤓ Drop another</a>
|
||||||
@@ -107,6 +116,7 @@ INDEX_HTML = """<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
<iframe id="view"></iframe>
|
<iframe id="view"></iframe>
|
||||||
<div id="loading">
|
<div id="loading">
|
||||||
|
__LOGO_LOADING__
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<div class="msg" id="loadingMsg">Rendering…</div>
|
<div class="msg" id="loadingMsg">Rendering…</div>
|
||||||
<div class="sub2">building the 3D toolpath — this can take a moment for large files</div>
|
<div class="sub2">building the 3D toolpath — this can take a moment for large files</div>
|
||||||
@@ -280,6 +290,7 @@ def render_nc_text(nc_text: str, name: str) -> str:
|
|||||||
fig, include_plotlyjs="cdn", full_html=True,
|
fig, include_plotlyjs="cdn", full_html=True,
|
||||||
default_width="100%", default_height="100vh",
|
default_width="100%", default_height="100vh",
|
||||||
config={"responsive": True},
|
config={"responsive": True},
|
||||||
|
post_script=LEGEND_CLICK_JS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -296,7 +307,13 @@ class _Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def do_GET(self): # noqa: N802 (http.server naming)
|
def do_GET(self): # noqa: N802 (http.server naming)
|
||||||
if self.path in ("/", "/index.html"):
|
if self.path in ("/", "/index.html"):
|
||||||
self._send(200, INDEX_HTML)
|
uri = logo_data_uri()
|
||||||
|
bar_img = f'<img class="logo" src="{uri}" alt="wired3d">' if uri else ""
|
||||||
|
load_img = f'<img class="logo-big" src="{uri}" alt="wired3d">' if uri else ""
|
||||||
|
page = (INDEX_HTML
|
||||||
|
.replace("__LOGO_IMG__", bar_img)
|
||||||
|
.replace("__LOGO_LOADING__", load_img))
|
||||||
|
self._send(200, page)
|
||||||
else:
|
else:
|
||||||
self._send(404, "not found", "text/plain")
|
self._send(404, "not found", "text/plain")
|
||||||
|
|
||||||
@@ -338,7 +355,7 @@ def main(port: int = DEFAULT_PORT) -> None:
|
|||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
print(f"Could not bind port {port}: {exc}")
|
print(f"Could not bind port {port}: {exc}")
|
||||||
print(f"Another server is likely already running. Try a different port: "
|
print(f"Another server is likely already running. Try a different port: "
|
||||||
f"python nc_server.py {port + 1}")
|
f"python -m wired3d_viewer.server {port + 1}")
|
||||||
return
|
return
|
||||||
url = f"http://127.0.0.1:{port}"
|
url = f"http://127.0.0.1:{port}"
|
||||||
print(f"NC Viewer drop zone running at {url}")
|
print(f"NC Viewer drop zone running at {url}")
|
||||||
@@ -15,12 +15,14 @@ Units: X/Y/Z in millimetres, A/B in degrees, path lengths in mm, time in
|
|||||||
seconds (feed rate F = 800 mm/min).
|
seconds (feed rate F = 800 mm/min).
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
python nc_viewer.py <path_to_nc_file>
|
python -m wired3d_viewer.viewer <path_to_nc_file>
|
||||||
|
# or, after `pip install -e .`: wired3d view <path_to_nc_file>
|
||||||
|
|
||||||
Dependencies: plotly, numpy. ``nc_parser.py`` must sit in the same folder.
|
Dependencies: plotly, numpy. Imports the sibling ``parser`` module from this
|
||||||
The output HTML embeds plotly.js from CDN (``include_plotlyjs="cdn"``).
|
package. The output HTML embeds plotly.js from CDN (``include_plotlyjs="cdn"``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -29,7 +31,30 @@ import plotly.graph_objects as go
|
|||||||
import plotly.io as pio
|
import plotly.io as pio
|
||||||
from plotly.subplots import make_subplots
|
from plotly.subplots import make_subplots
|
||||||
|
|
||||||
from nc_parser import parse_nc, compute_tool_vector
|
from .parser import parse_nc, compute_tool_vector
|
||||||
|
|
||||||
|
|
||||||
|
# wired3d logo, base64-encoded once into a data URI so the viewer HTML stays
|
||||||
|
# self-contained/offline (no external file fetch). "" if the file is missing.
|
||||||
|
_LOGO_URI = None
|
||||||
|
|
||||||
|
|
||||||
|
def logo_data_uri() -> str:
|
||||||
|
"""Return the wired3d logo as a base64 ``data:`` URI (cached).
|
||||||
|
|
||||||
|
Output:
|
||||||
|
``"data:image/avif;base64,<...>"`` read from ``wired3d.avif`` next to
|
||||||
|
this module, or ``""`` if the file cannot be read. Cached after the
|
||||||
|
first call so repeated renders don't re-encode it.
|
||||||
|
"""
|
||||||
|
global _LOGO_URI
|
||||||
|
if _LOGO_URI is None:
|
||||||
|
try:
|
||||||
|
data = (Path(__file__).parent / "assets" / "wired3d.avif").read_bytes()
|
||||||
|
_LOGO_URI = "data:image/avif;base64," + base64.b64encode(data).decode("ascii")
|
||||||
|
except OSError:
|
||||||
|
_LOGO_URI = ""
|
||||||
|
return _LOGO_URI
|
||||||
|
|
||||||
|
|
||||||
# Machine feed rate used for the print-time estimate (mm/min).
|
# Machine feed rate used for the print-time estimate (mm/min).
|
||||||
@@ -40,23 +65,54 @@ FEED_RATE = 800.0
|
|||||||
# controls how chunky the growth looks and the HTML size, not completeness.
|
# controls how chunky the growth looks and the HTML size, not completeness.
|
||||||
ANIM_TARGET_FRAMES = 160
|
ANIM_TARGET_FRAMES = 160
|
||||||
|
|
||||||
# Default 3D camera: ORTHOGRAPHIC projection from an elevated isometric angle.
|
# Default 3D camera: PERSPECTIVE projection from an elevated isometric angle.
|
||||||
# Plotly normalises the scene to a unit cube before placing the camera, so this
|
# Perspective keeps the depth cues that make a tall part like the hemisphere
|
||||||
# single fixed eye fits the whole part regardless of its real size — a "full
|
# read as a real 3D dome — orthographic flattens it into a pancake. Plotly
|
||||||
# view" of any part. Orthographic (no perspective foreshortening) gives a true-
|
# normalises the scene to a unit cube before placing the camera, so this single
|
||||||
# to-scale, CAD-style view; the eye is pulled back so nothing is clipped.
|
# fixed eye fits the whole part regardless of its real size — a "full view" of
|
||||||
# Used as the initial view and the view that Reset snaps back to.
|
# any part. The eye is pulled back so nothing is clipped. (The view stays fixed
|
||||||
|
# and free of zoom-jump because the scene axis ranges are pinned, not because
|
||||||
|
# of the projection.) Used as the initial view and the view Reset snaps back to.
|
||||||
DEFAULT_CAMERA = dict(
|
DEFAULT_CAMERA = dict(
|
||||||
projection=dict(type="orthographic"),
|
projection=dict(type="perspective"),
|
||||||
eye=dict(x=1.6, y=1.6, z=1.2),
|
eye=dict(x=1.6, y=1.6, z=1.2),
|
||||||
center=dict(x=0.0, y=0.0, z=0.0),
|
center=dict(x=0.0, y=0.0, z=0.0),
|
||||||
up=dict(x=0.0, y=0.0, z=1.0),
|
up=dict(x=0.0, y=0.0, z=1.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# JS injected after the plot is built (via plotly's post_script hook, which
|
||||||
|
# substitutes {plot_id}). Overrides the default legend-click on the per-layer
|
||||||
|
# weld traces: clicking a "Layer N" entry highlights that layer (opacity 1.0)
|
||||||
|
# and dims every other layer (opacity 0.1); clicking the same entry again — or
|
||||||
|
# any "All layers" reset — restores them all. Non-layer legend entries (rapid,
|
||||||
|
# trail, angles, …) keep Plotly's normal show/hide toggle. Returning false from
|
||||||
|
# the handler suppresses the default visibility toggle for layer clicks.
|
||||||
|
LEGEND_CLICK_JS = """
|
||||||
|
var gd = document.getElementById('{plot_id}');
|
||||||
|
var weld = [];
|
||||||
|
gd.data.forEach(function(t, i) {
|
||||||
|
if (t.name && t.name.indexOf('Layer ') === 0) weld.push(i);
|
||||||
|
});
|
||||||
|
var active = null;
|
||||||
|
gd.on('plotly_legendclick', function(ev) {
|
||||||
|
var ci = ev.curveNumber;
|
||||||
|
if (weld.indexOf(ci) === -1) return true; // default toggle for non-layers
|
||||||
|
if (active === ci) {
|
||||||
|
Plotly.restyle(gd, {opacity: weld.map(function() { return 1.0; })}, weld);
|
||||||
|
active = null;
|
||||||
|
} else {
|
||||||
|
var op = weld.map(function(idx) { return idx === ci ? 1.0 : 0.1; });
|
||||||
|
Plotly.restyle(gd, {opacity: op}, weld);
|
||||||
|
active = ci;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
"""
|
||||||
|
|
||||||
# Dark theme palette.
|
# Dark theme palette.
|
||||||
SCENE_BG = "#1a1a2e"
|
SCENE_BG = "#1a1a2e"
|
||||||
FIG_BG = "#0f0f1a"
|
FIG_BG = "#0f0f1a"
|
||||||
RAPID_COLOUR = "#888888"
|
RAPID_COLOUR = "#5b647d" # dark slate — visible on the dark scene without glare
|
||||||
ANGLE_A_COLOUR = "#00bfff"
|
ANGLE_A_COLOUR = "#00bfff"
|
||||||
ANGLE_B_COLOUR = "#ff4500"
|
ANGLE_B_COLOUR = "#ff4500"
|
||||||
|
|
||||||
@@ -229,7 +285,9 @@ def build_rapid_trace(moves: list[dict]) -> go.Scatter3d:
|
|||||||
y=[m["y"] for m in rapids],
|
y=[m["y"] for m in rapids],
|
||||||
z=[m["z"] for m in rapids],
|
z=[m["z"] for m in rapids],
|
||||||
mode="lines",
|
mode="lines",
|
||||||
line=dict(color=RAPID_COLOUR, dash="dot", width=1),
|
# Long dashes with tight gaps (a clean "---" look) in a dark slate so
|
||||||
|
# the sparse rapid travel moves read clearly without glare.
|
||||||
|
line=dict(color=RAPID_COLOUR, dash="longdash", width=3),
|
||||||
name="Rapid moves",
|
name="Rapid moves",
|
||||||
customdata=[_customdata_row(m) for m in rapids],
|
customdata=[_customdata_row(m) for m in rapids],
|
||||||
hovertemplate=_HOVER,
|
hovertemplate=_HOVER,
|
||||||
@@ -308,6 +366,11 @@ def build_angle_chart(moves: list[dict]):
|
|||||||
# Two traces forming a filled band: invisible baseline at ymin, then the
|
# Two traces forming a filled band: invisible baseline at ymin, then the
|
||||||
# top edge at ymax filling down to it. connectgaps=False keeps the band
|
# top edge at ymax filling down to it. connectgaps=False keeps the band
|
||||||
# broken across travel (non-welding) gaps.
|
# broken across travel (non-welding) gaps.
|
||||||
|
# These angle-chart traces are kept OUT of the legend (showlegend=False):
|
||||||
|
# they live in the 2D subplot (self-evident from the axis colours and the
|
||||||
|
# green band there), and including them forced Plotly into reversed legend
|
||||||
|
# order, pushing the 3D layer entries off-screen. The legend now shows only
|
||||||
|
# the 3D toolpath traces (rapid + per-layer welds + head + trail).
|
||||||
base = go.Scatter(
|
base = go.Scatter(
|
||||||
x=idx, y=band_low, mode="lines",
|
x=idx, y=band_low, mode="lines",
|
||||||
line=dict(width=0), hoverinfo="skip",
|
line=dict(width=0), hoverinfo="skip",
|
||||||
@@ -318,18 +381,18 @@ def build_angle_chart(moves: list[dict]):
|
|||||||
line=dict(width=0), fill="tonexty",
|
line=dict(width=0), fill="tonexty",
|
||||||
fillcolor="rgba(0,255,128,0.18)",
|
fillcolor="rgba(0,255,128,0.18)",
|
||||||
name="Welding active", hoverinfo="skip",
|
name="Welding active", hoverinfo="skip",
|
||||||
connectgaps=False,
|
showlegend=False, connectgaps=False,
|
||||||
)
|
)
|
||||||
a_trace = go.Scatter(
|
a_trace = go.Scatter(
|
||||||
x=idx, y=a_vals, mode="lines",
|
x=idx, y=a_vals, mode="lines",
|
||||||
line=dict(color=ANGLE_A_COLOUR, width=2),
|
line=dict(color=ANGLE_A_COLOUR, width=2),
|
||||||
name="A axis (°)",
|
name="A axis (°)", showlegend=False,
|
||||||
hovertemplate="move %{x}<br>A: %{y:.2f}°<extra></extra>",
|
hovertemplate="move %{x}<br>A: %{y:.2f}°<extra></extra>",
|
||||||
)
|
)
|
||||||
b_trace = go.Scatter(
|
b_trace = go.Scatter(
|
||||||
x=idx, y=b_vals, mode="lines",
|
x=idx, y=b_vals, mode="lines",
|
||||||
line=dict(color=ANGLE_B_COLOUR, width=2),
|
line=dict(color=ANGLE_B_COLOUR, width=2),
|
||||||
name="B axis (°)",
|
name="B axis (°)", showlegend=False,
|
||||||
hovertemplate="move %{x}<br>B: %{y:.2f}°<extra></extra>",
|
hovertemplate="move %{x}<br>B: %{y:.2f}°<extra></extra>",
|
||||||
)
|
)
|
||||||
# Band first (base then fill) so the angle lines draw on top.
|
# Band first (base then fill) so the angle lines draw on top.
|
||||||
@@ -557,12 +620,12 @@ def build_animation(moves: list[dict], trail_index: int, cursor_index: int,
|
|||||||
move, named by move index) grow the trail up to that move
|
move, named by move index) grow the trail up to that move
|
||||||
and advance the head cursor; the first frame also hides
|
and advance the head cursor; the first frame also hides
|
||||||
every static toolpath trace so only the trail is drawn
|
every static toolpath trace so only the trail is drawn
|
||||||
while playing. A trailing "reset" frame clears the trail,
|
while playing. A trailing "visualize" frame clears the
|
||||||
returns the head to the start, and makes the static
|
trail, returns the head to the start, and shows the FULL
|
||||||
toolpath visible again.
|
sliced part (every static toolpath trace visible).
|
||||||
play_menu -- an ``updatemenus`` dict with Play (~25 fps, restarts from
|
play_menu -- an ``updatemenus`` dict with Play (~25 fps, restarts from
|
||||||
the beginning so the path always re-reveals), Pause
|
the beginning so the path always re-reveals), Pause
|
||||||
(halts immediately), and Reset buttons.
|
(halts immediately), and Visualize buttons.
|
||||||
slider -- a ``sliders`` dict to scrub to any frame by move index.
|
slider -- a ``sliders`` dict to scrub to any frame by move index.
|
||||||
Returns ([], None, None) when there are no moves.
|
Returns ([], None, None) when there are no moves.
|
||||||
|
|
||||||
@@ -589,7 +652,10 @@ def build_animation(moves: list[dict], trail_index: int, cursor_index: int,
|
|||||||
|
|
||||||
# Hiding/showing the static toolpath = a tiny visibility update per static
|
# Hiding/showing the static toolpath = a tiny visibility update per static
|
||||||
# trace (re-used as frame data alongside the trail + cursor updates).
|
# trace (re-used as frame data alongside the trail + cursor updates).
|
||||||
hide_static = [go.Scatter3d(visible=False) for _ in static_indices]
|
# Use "legendonly" (not False) to hide the path during playback: the trace
|
||||||
|
# is removed from the 3D scene but its LEGEND entry stays visible, so the
|
||||||
|
# per-layer legend entries remain while playing. Reset restores visible=True.
|
||||||
|
hide_static = [go.Scatter3d(visible="legendonly") for _ in static_indices]
|
||||||
show_static = [go.Scatter3d(visible=True) for _ in static_indices]
|
show_static = [go.Scatter3d(visible=True) for _ in static_indices]
|
||||||
|
|
||||||
frames = []
|
frames = []
|
||||||
@@ -608,8 +674,8 @@ def build_animation(moves: list[dict], trail_index: int, cursor_index: int,
|
|||||||
|
|
||||||
if k == 0:
|
if k == 0:
|
||||||
# First frame also hides the full static path so playback reveals
|
# First frame also hides the full static path so playback reveals
|
||||||
# it, and flips scene.uirevision to "play" so the next Reset (which
|
# it, and flips scene.uirevision to "play" so the next Visualize
|
||||||
# uses a different value) is guaranteed to re-apply the camera.
|
# (which uses a different value) is guaranteed to re-apply the camera.
|
||||||
data = [trail, cursor, *hide_static]
|
data = [trail, cursor, *hide_static]
|
||||||
traces = [trail_index, cursor_index, *static_indices]
|
traces = [trail_index, cursor_index, *static_indices]
|
||||||
layout = dict(scene=dict(uirevision="play", camera=DEFAULT_CAMERA))
|
layout = dict(scene=dict(uirevision="play", camera=DEFAULT_CAMERA))
|
||||||
@@ -634,20 +700,21 @@ def build_animation(moves: list[dict], trail_index: int, cursor_index: int,
|
|||||||
|
|
||||||
anim_names = [str(i) for i in idxs]
|
anim_names = [str(i) for i in idxs]
|
||||||
|
|
||||||
# Reset frame: empty trail, head stick back at the start, static path
|
# "Visualize" frame: shows the FULL sliced part — every static layer trace
|
||||||
# visible, and the camera snapped back to the default zoomed-out view.
|
# visible, the trail emptied, the head stick back at the start, and the
|
||||||
# Flipping scene.uirevision to "reset" (different from Play's "play") forces
|
# camera snapped back to the default framed view. Flipping scene.uirevision
|
||||||
# Plotly to re-apply DEFAULT_CAMERA even after the user has rotated.
|
# to "visualize" (different from Play's "play") forces Plotly to re-apply
|
||||||
|
# DEFAULT_CAMERA even after the user has rotated.
|
||||||
rx, ry, rz = _stick_points(first, stick_len)
|
rx, ry, rz = _stick_points(first, stick_len)
|
||||||
frames.append(go.Frame(
|
frames.append(go.Frame(
|
||||||
name="reset",
|
name="visualize",
|
||||||
data=[
|
data=[
|
||||||
go.Scatter3d(x=[], y=[], z=[]),
|
go.Scatter3d(x=[], y=[], z=[]),
|
||||||
go.Scatter3d(x=rx, y=ry, z=rz),
|
go.Scatter3d(x=rx, y=ry, z=rz),
|
||||||
*show_static,
|
*show_static,
|
||||||
],
|
],
|
||||||
traces=[trail_index, cursor_index, *static_indices],
|
traces=[trail_index, cursor_index, *static_indices],
|
||||||
layout=dict(scene=dict(uirevision="reset", camera=DEFAULT_CAMERA)),
|
layout=dict(scene=dict(uirevision="visualize", camera=DEFAULT_CAMERA)),
|
||||||
))
|
))
|
||||||
|
|
||||||
play_menu = dict(
|
play_menu = dict(
|
||||||
@@ -683,9 +750,9 @@ def build_animation(moves: list[dict], trail_index: int, cursor_index: int,
|
|||||||
)],
|
)],
|
||||||
),
|
),
|
||||||
dict(
|
dict(
|
||||||
label="⟲ Reset",
|
label="◉ Visualize",
|
||||||
method="animate",
|
method="animate",
|
||||||
args=[["reset"], dict(
|
args=[["visualize"], dict(
|
||||||
mode="immediate",
|
mode="immediate",
|
||||||
frame=dict(duration=0, redraw=True),
|
frame=dict(duration=0, redraw=True),
|
||||||
transition=dict(duration=0),
|
transition=dict(duration=0),
|
||||||
@@ -818,8 +885,20 @@ def build_figure(parsed: dict, source_file: str) -> go.Figure:
|
|||||||
f"| {n_points} points | {Path(source_file).name}"
|
f"| {n_points} points | {Path(source_file).name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# wired3d logo as a layout image (top-left corner, over the empty scene
|
||||||
|
# sky). Embedded as a data URI so the HTML stays self-contained.
|
||||||
|
logo = logo_data_uri()
|
||||||
|
logo_images = [dict(
|
||||||
|
source=logo,
|
||||||
|
xref="paper", yref="paper",
|
||||||
|
x=0.005, y=0.995, sizex=0.15, sizey=0.12,
|
||||||
|
xanchor="left", yanchor="top",
|
||||||
|
sizing="contain", opacity=0.95, layer="above",
|
||||||
|
)] if logo else []
|
||||||
|
|
||||||
fig.update_layout(
|
fig.update_layout(
|
||||||
title=dict(text=title, font=dict(color="white", size=18), x=0.5),
|
title=dict(text=title, font=dict(color="white", size=18), x=0.5),
|
||||||
|
images=logo_images,
|
||||||
autosize=True,
|
autosize=True,
|
||||||
paper_bgcolor=FIG_BG,
|
paper_bgcolor=FIG_BG,
|
||||||
plot_bgcolor=FIG_BG,
|
plot_bgcolor=FIG_BG,
|
||||||
@@ -832,6 +911,10 @@ def build_figure(parsed: dict, source_file: str) -> go.Figure:
|
|||||||
legend=dict(
|
legend=dict(
|
||||||
x=0.62, y=0.99, xanchor="right", yanchor="top",
|
x=0.62, y=0.99, xanchor="right", yanchor="top",
|
||||||
bgcolor="rgba(22,33,62,0.6)", font=dict(color="white", size=10),
|
bgcolor="rgba(22,33,62,0.6)", font=dict(color="white", size=10),
|
||||||
|
# Force normal order (rapid + layers at the top). A filled trace in
|
||||||
|
# the angle chart otherwise flips Plotly to reversed order, which
|
||||||
|
# buried the layer entries below the visible area until a relayout.
|
||||||
|
traceorder="normal",
|
||||||
),
|
),
|
||||||
scene=dict(
|
scene=dict(
|
||||||
bgcolor=SCENE_BG,
|
bgcolor=SCENE_BG,
|
||||||
@@ -878,6 +961,7 @@ def main(nc_path: str) -> None:
|
|||||||
fig, file=out_name, include_plotlyjs="cdn", full_html=True,
|
fig, file=out_name, include_plotlyjs="cdn", full_html=True,
|
||||||
default_width="100%", default_height="100vh",
|
default_width="100%", default_height="100vh",
|
||||||
config={"responsive": True},
|
config={"responsive": True},
|
||||||
|
post_script=LEGEND_CLICK_JS,
|
||||||
)
|
)
|
||||||
print(f"Saved: {out_name}")
|
print(f"Saved: {out_name}")
|
||||||
|
|
||||||
@@ -885,6 +969,6 @@ def main(nc_path: str) -> None:
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: python nc_viewer.py <path_to_nc_file>")
|
print("Usage: python -m wired3d_viewer.viewer <path_to_nc_file>")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
main(sys.argv[1])
|
main(sys.argv[1])
|
||||||
Reference in new issue
Block a user