Visualizer 2

This commit is contained in:
Vignesh Suresh
2026-06-06 20:43:01 +02:00
parent a4b9a311dd
commit 29dfbf99a5
7 changed files with 1063 additions and 10 deletions
+134 -2
View File
@@ -107,6 +107,42 @@ gd.on('plotly_legendclick', function(ev) {
}
return false;
});
// --- Projectile toggle (custom HTML button, reliable single-click) ---------
// Replaces the old Plotly updatemenu toggle, whose `active` default left the
// button highlighted while the overlay was hidden (inverted/2-click state).
// This button is the SOLE control for the projectile line + arrowheads; its
// label and colour always reflect the real visibility.
(function() {
var lineIdx = -1, arrowIdx = -1;
gd.data.forEach(function(t, i) {
if (t.name === 'Projectile (tool direction)') lineIdx = i;
if (t.name === 'Projectile arrowheads') arrowIdx = i;
});
if (lineIdx === -1) return; // no projectile in this figure
var idxs = arrowIdx === -1 ? [lineIdx] : [lineIdx, arrowIdx];
var btn = document.createElement('button');
btn.type = 'button';
// Sits just BELOW the layer dropdown (top-left) so the two never overlap.
btn.style.cssText = 'position:fixed;top:52px;left:14px;z-index:1000;' +
'padding:7px 13px;font-size:13px;border-radius:7px;cursor:pointer;' +
'border:1px solid #ff9d00;font-family:system-ui,-apple-system,sans-serif;';
function isOn() { return gd.data[lineIdx].visible === true; }
function refresh() {
var on = isOn();
btn.textContent = on ? '🎯 Projectile: ON' : '🎯 Projectile: OFF';
btn.style.background = on ? '#ff9d00' : '#16213e';
btn.style.color = on ? '#1a1a2e' : '#ffffff';
}
btn.addEventListener('click', function() {
Plotly.restyle(gd, {visible: isOn() ? 'legendonly' : true}, idxs)
.then(refresh);
});
document.body.appendChild(btn);
refresh();
})();
"""
# Dark theme palette.
@@ -560,6 +596,85 @@ def build_cursor_trace(moves: list[dict]) -> go.Scatter3d:
)
def build_projectile_trace(moves: list[dict], length: float) -> go.Scatter3d:
"""Build the per-point 'projectile' overlay: the tool direction at every move.
Inputs:
moves -- list of move dicts (mm coords, A/B degrees).
length -- length (mm) of each direction stick.
Output:
a single ``go.Scatter3d`` line trace containing one short segment per
move — from the move's coordinate along its tool vector
``compute_tool_vector(a, b)`` (straight up at A=B=0, tilted as the head
inclines). Segments are separated by ``None`` so the whole field of
directions is one lightweight trace. Hidden by default
(``visible="legendonly"``, no legend entry) and revealed by the custom
"Projectile" toggle button injected via the post-script. This is the
projectile/direction-of-every-coordinate view.
"""
xs: list = []
ys: list = []
zs: list = []
for m in moves:
u, v, w = compute_tool_vector(m["a"], m["b"])
xs += [m["x"], m["x"] + u * length, None]
ys += [m["y"], m["y"] + v * length, None]
zs += [m["z"], m["z"] + w * length, None]
return go.Scatter3d(
x=xs, y=ys, z=zs,
mode="lines",
line=dict(color="#ff9d00", width=2),
name="Projectile (tool direction)",
visible="legendonly",
# Toggled only by the custom HTML "Projectile" button (no legend entry),
# so the line and its arrowheads never get out of sync.
showlegend=False,
hoverinfo="skip",
)
def build_projectile_arrows(moves: list[dict], length: float) -> go.Cone:
"""Build arrowheads (cones) at the tip of every projectile vector.
Inputs:
moves -- list of move dicts (mm coords, A/B degrees).
length -- length (mm) of each projectile stick (the cone sits at its end).
Output:
a ``go.Cone`` trace with one small cone per move, placed at the stick's
TIP (``coord + length × tool_vector``) and pointing along the tool
vector, so each projectile line reads as a directional arrow. Hidden by
default (``visible="legendonly"``) and toggled together with the line by
the Projectile button. ``go.Cone`` colours by vector norm, so the
colorscale is pinned to the single projectile amber.
"""
xs: list = []
ys: list = []
zs: list = []
us: list = []
vs: list = []
ws: list = []
for m in moves:
u, v, w = compute_tool_vector(m["a"], m["b"])
xs.append(m["x"] + u * length)
ys.append(m["y"] + v * length)
zs.append(m["z"] + w * length)
us.append(u)
vs.append(v)
ws.append(w)
return go.Cone(
x=xs, y=ys, z=zs, u=us, v=vs, w=ws,
anchor="tip", # cone point sits at the stick tip
sizemode="absolute",
sizeref=length * 0.45,
colorscale=[[0, "#ff9d00"], [1, "#ff9d00"]],
showscale=False,
name="Projectile arrowheads",
showlegend=False, # one legend entry (the line) is enough
visible="legendonly",
hoverinfo="skip",
)
def build_trail_trace() -> go.Scatter3d:
"""Build the (initially empty) trail that is drawn progressively on Play.
@@ -864,13 +979,30 @@ def build_figure(parsed: dict, source_file: str) -> go.Figure:
# --- stats table (row 2, col 2) ---
fig.add_trace(build_stats_table(stats), row=2, col=2)
# --- projectile overlay (3D scene) ---
# Appended LAST so it never shifts the indices the frames/dropdown reference
# (rapid/weld/cursor/trail/angles/table). It is a scene trace despite being
# added after the table — add_trace order is just append order. Hidden until
# the custom "Projectile" HTML button (post-script) toggles it on.
stick = _stick_length(moves)
projectile_index = len(fig.data)
fig.add_trace(build_projectile_trace(moves, stick), row=1, col=1)
arrow_index = len(fig.data)
fig.add_trace(build_projectile_arrows(moves, stick), row=1, col=1)
projectile_traces = [projectile_index, arrow_index]
# Total trace count (for dropdown bookkeeping): rapid + welds + cursor +
# trail + 4 angle traces + table.
total_traces = 1 + len(weld_traces) + 2 + 4 + 1
# trail + 4 angle traces + table + projectile line + arrowheads.
total_traces = 1 + len(weld_traces) + 2 + 4 + 1 + 2
dropdown = build_layer_dropdown(
weld_traces, n_static_before=1, total_traces=total_traces,
)
# The projectile overlay is toggled by a custom HTML button injected via the
# post-script (LEGEND_CLICK_JS), not a Plotly updatemenu — the updatemenu
# toggle's default `active` left the button visually inverted/2-click. The
# JS finds the two projectile traces by name, so no indices are passed here.
_ = projectile_traces # indices kept for clarity; JS locates traces by name
# --- animation: frames + play/pause/reset + slider ---
frames, play_menu, slider = build_animation(