diff --git a/web/index.html b/web/index.html
index 572a6f7..5fe1fe2 100644
--- a/web/index.html
+++ b/web/index.html
@@ -500,12 +500,12 @@
Posa target
-
X — mm
-
Y — mm
-
Z — mm
-
R —°
-
P —°
-
W —°
+
+
+
+
+
+
— polso fermo
diff --git a/web/pose_3d.css b/web/pose_3d.css
index e88e22d..4fef9ef 100644
--- a/web/pose_3d.css
+++ b/web/pose_3d.css
@@ -80,7 +80,25 @@
color: #ddd;
margin-bottom: 8px;
}
-#tab-pose-3d .p3-readout span { color: #4da3ff; }
+#tab-pose-3d .p3-readout label {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+#tab-pose-3d .p3-readout input {
+ width: 60px;
+ background: #0d1014;
+ border: 1px solid #2a2f36;
+ color: #4da3ff;
+ padding: 2px 4px;
+ border-radius: 3px;
+ font-size: 12px;
+ font-family: monospace;
+}
+#tab-pose-3d .p3-readout input:focus {
+ outline: 1px solid #4da3ff;
+ border-color: #4da3ff;
+}
#tab-pose-3d .p3-status { font-size: 12px; color: #888; margin-bottom: 8px; }
#tab-pose-3d .p3-status.modified { color: #f0ad4e; }
diff --git a/web/pose_3d.js b/web/pose_3d.js
index 90799e0..4304a49 100644
--- a/web/pose_3d.js
+++ b/web/pose_3d.js
@@ -251,6 +251,21 @@ function quatToRPYdeg(q) {
return [roll * 180 / Math.PI, pitch * 180 / Math.PI, yaw * 180 / Math.PI];
}
+function rpyDegToQuat(roll, pitch, yaw) {
+ const r = roll * Math.PI / 180;
+ const p = pitch * Math.PI / 180;
+ const y = yaw * Math.PI / 180;
+ const cr = Math.cos(r / 2), sr = Math.sin(r / 2);
+ const cp = Math.cos(p / 2), sp = Math.sin(p / 2);
+ const cy = Math.cos(y / 2), sy = Math.sin(y / 2);
+ // ZYX intrinsic (matches quatToRPYdeg)
+ const qx = sr * cp * cy - cr * sp * sy;
+ const qy = cr * sp * cy + sr * cp * sy;
+ const qz = cr * cp * sy - sr * sp * cy;
+ const qw = cr * cp * cy + sr * sp * sy;
+ return [qx, qy, qz, qw];
+}
+
// ----- state apply to scene -----
function updateRealWrists() {
for (const side of ['right', 'left']) {
@@ -294,21 +309,33 @@ function syncGhostsToTargetOrReal() {
}
}
+function setInputIfNotFocused(id, value) {
+ const el = $(id);
+ if (!el) return;
+ if (document.activeElement === el) return; // user is typing, don't stomp
+ el.value = value;
+}
+
function updateReadout() {
const side = state.active_side;
const pose = state.target[side] || state.current[side];
if (!pose) {
- $('p3X').textContent = '—';
+ setInputIfNotFocused('p3X', '');
+ setInputIfNotFocused('p3Y', '');
+ setInputIfNotFocused('p3Z', '');
+ setInputIfNotFocused('p3R', '');
+ setInputIfNotFocused('p3P', '');
+ setInputIfNotFocused('p3W', '');
return;
}
const p = pose.position, q = pose.quaternion;
- $('p3X').textContent = (p[0] * 1000).toFixed(1);
- $('p3Y').textContent = (p[1] * 1000).toFixed(1);
- $('p3Z').textContent = (p[2] * 1000).toFixed(1);
+ setInputIfNotFocused('p3X', (p[0] * 1000).toFixed(1));
+ setInputIfNotFocused('p3Y', (p[1] * 1000).toFixed(1));
+ setInputIfNotFocused('p3Z', (p[2] * 1000).toFixed(1));
const rpy = quatToRPYdeg(q);
- $('p3R').textContent = rpy[0].toFixed(1);
- $('p3P').textContent = rpy[1].toFixed(1);
- $('p3W').textContent = rpy[2].toFixed(1);
+ setInputIfNotFocused('p3R', rpy[0].toFixed(1));
+ setInputIfNotFocused('p3P', rpy[1].toFixed(1));
+ setInputIfNotFocused('p3W', rpy[2].toFixed(1));
$('p3Status').textContent = state.target[side] ? '● modificata' : '— polso fermo';
$('p3Status').className = 'p3-status' + (state.target[side] ? ' modified' : '');
}
@@ -447,6 +474,47 @@ function bindGizmoEvents() {
});
}
+function readReadoutInputs() {
+ const num = (id, dflt) => {
+ const v = parseFloat($(id).value);
+ return Number.isFinite(v) ? v : dflt;
+ };
+ const side = state.active_side;
+ if (!side) return;
+ // fallback values: current pose if any, else SHOULDER position via arm
+ const fallback = state.current[side] || (state.arms[side] ? {
+ position: [state.arms[side].position.x, state.arms[side].position.y, state.arms[side].position.z],
+ quaternion: [0, 0, 0, 1],
+ } : { position: [0, 0, 0], quaternion: [0, 0, 0, 1] });
+ const x_mm = num('p3X', fallback.position[0] * 1000);
+ const y_mm = num('p3Y', fallback.position[1] * 1000);
+ const z_mm = num('p3Z', fallback.position[2] * 1000);
+ const r_deg = num('p3R', 0);
+ const p_deg = num('p3P', 0);
+ const w_deg = num('p3W', 0);
+ const position = [x_mm / 1000, y_mm / 1000, z_mm / 1000];
+ const quaternion = rpyDegToQuat(r_deg, p_deg, w_deg);
+ state.target[side] = { position, quaternion };
+ // Aggiorna ghost in scena
+ const ghost = state.ghosts[side];
+ if (ghost) {
+ ghost.position.set(position[0], position[1], position[2]);
+ ghost.quaternion.set(quaternion[0], quaternion[1], quaternion[2], quaternion[3]);
+ ghost.visible = true;
+ }
+ // Status
+ $('p3Status').textContent = '● modificata (da input)';
+ $('p3Status').className = 'p3-status modified';
+}
+
+function bindReadoutInputs() {
+ for (const id of ['p3X', 'p3Y', 'p3Z', 'p3R', 'p3P', 'p3W']) {
+ const el = $(id);
+ if (!el) continue;
+ el.addEventListener('change', readReadoutInputs); // fire on blur / Enter
+ }
+}
+
// ----- side change listener -----
function bindSideControls() {
for (const r of document.querySelectorAll('input[name=p3Side]')) {
@@ -690,6 +758,7 @@ function ensureInit() {
bindJogAutoApply();
bindPoseButtons();
bindUnlockButton();
+ bindReadoutInputs();
pollState();
pollTimer = setInterval(pollState, POLL_MS);
}