Visualizer 2
This commit is contained in:
+134
-2
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user