diff --git a/public/app.js b/public/app.js index 8019b31..8b0d292 100644 --- a/public/app.js +++ b/public/app.js @@ -1023,7 +1023,7 @@ const cy = cytoscape({ ], }); -let isDraggingNode = false; +let draggedNodeId = null; // set explicitly in grab/free — not re-derived via ':grabbed' each tick // Convert spatial room units (0..100) to Cytoscape viewport pixels (scaled around center) const SPATIAL_PX_SCALE = 6; @@ -1033,6 +1033,7 @@ function roomToCyPos(x, y) { function cyToRoomPos(pos) { const x = Math.min(ROOM_UNITS, Math.max(0, pos.x / SPATIAL_PX_SCALE + 50)); const y = Math.min(ROOM_UNITS, Math.max(0, pos.y / SPATIAL_PX_SCALE + 50)); + if (!Number.isFinite(x) || !Number.isFinite(y)) return null; // never let NaN/Infinity leak into S.me/peer.x/y return { x, y }; } @@ -1054,19 +1055,33 @@ function zoomToBounds(animate = true) { // Handle dragging nodes cy.on('grab', 'node', (evt) => { - isDraggingNode = true; + cy.stop(); // cancel any in-flight fit animation so it can't fight the drag's pointer->model mapping + draggedNodeId = evt.target.id(); + if (!evt.target.data('isMe')) { + toast(`Repositioning ${evt.target.data('label')} — this only changes how loud/where YOU hear them, not their real position`, 'warn'); + } els.spatial.classList.add('dragging'); }); cy.on('drag', 'node', (evt) => { const node = evt.target; const pos = cyToRoomPos(node.position()); + if (!pos) return; // guard: never propagate a bad (NaN/Infinity) position into shared state + // cyToRoomPos already clamped pos to the room — snap the node's actual on-screen + // position back to that clamp too. Without this, Cytoscape lets the dot follow the + // pointer arbitrarily far past the edge (nothing was clamping node.position() itself, + // only the derived room coordinate), so it visually flew off while the stored + // position silently stayed pinned at the wall — then snapped back the instant you + // let go. This keeps everyone within the room, which given refDistance=4 / + // rolloffFactor=1.4 (see updatePanner) always keeps audio gain above ~18%, i.e. + // nobody can drag themselves or a peer out of hearing range. + node.position(roomToCyPos(pos.x, pos.y)); if (node.data('isMe')) { S.me.x = pos.x; S.me.y = pos.y; sendMyPos(); } else { - // Peer dragged: update local audio position relative to me + // Peer dragged: update local audio position relative to me — never sent to anyone else const peer = S.peers.get(node.id()); if (peer) { peer.x = pos.x; @@ -1078,7 +1093,7 @@ cy.on('drag', 'node', (evt) => { }); cy.on('free', 'node', (evt) => { - isDraggingNode = false; + draggedNodeId = null; els.spatial.classList.remove('dragging'); const node = evt.target; if (node.data('isMe')) { @@ -1143,7 +1158,7 @@ function syncSpatialGraph() { } else { meNode.data('label', `${S.name || 'me'} (you)`); meNode.data('color', colorFor(meId)); - if (!isDraggingNode) { + if (draggedNodeId !== 'me') { meNode.position(roomToCyPos(S.me.x, S.me.y)); } } @@ -1169,7 +1184,7 @@ function syncSpatialGraph() { } else { peerNode.data('label', peer.name); peerNode.data('color', colorFor(peer.id)); - if (!isDraggingNode || cy.$(':grabbed').id() !== peer.id) { + if (draggedNodeId !== peer.id) { peerNode.position(roomToCyPos(peer.x, peer.y)); } } @@ -1302,6 +1317,7 @@ async function mxBoot(session) { refreshToken: session.refreshToken || undefined, userId: session.userId, deviceId: session.deviceId, + fallbackICEServerAllowed: false, }); Mx.client.on(matrixcs.ClientEvent.Sync, (state) => { if (state === 'PREPARED') mxOnReady(); @@ -1508,13 +1524,17 @@ setInterval(() => { } }, 5000); -// spawn me somewhere spread out, not dead-center on top of others +// Spawn me slightly off-center so two people don't land exactly on top of +// each other — small radius on purpose: at refDistance=4/rolloffFactor=1.4, +// the old radius-20 ring put name-hash-unlucky pairs up to 40 units apart +// (diametrically opposite), which is already down to ~7% audio gain before +// anyone's said a word. (() => { let h = 0; for (const c of S.myId || S.name) h = (h * 33 + c.charCodeAt(0)) >>> 0; const a = (h % 360) * (Math.PI / 180); - S.me.x = 50 + 20 * Math.cos(a); - S.me.y = 50 + 20 * Math.sin(a); + S.me.x = 50 + 6 * Math.cos(a); + S.me.y = 50 + 6 * Math.sin(a); })(); window.addEventListener('keydown', (ev) => {