forked from Rockachopa/the-matrix
Clicking/tapping an agent in the 3D world now slides in a panel showing that agent's current Gitea issue, active branch, open PR link, and last 5 commits. - js/agent-panel.js (new): fetches Gitea API on demand; AbortController cancels stale requests; graceful fallback when gitea URL / gitLogin are unset - js/interaction.js: adds initClickDetection() using THREE.Raycaster; distinguishes clicks from orbit-control drags (< 6px pointer travel) - js/agents.js: sets group.userData.agentId for raycasting; exports getAgentGroups() - js/agent-defs.js: adds optional gitLogin field per agent - js/config.js: adds giteaUrl / giteaToken / giteaRepo config fields (VITE_GITEA_URL, VITE_GITEA_TOKEN, VITE_GITEA_REPO env vars) - index.html: #agent-panel slides in from right edge; Esc / ✕ closes it Fixes #8 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
2.7 KiB
JavaScript
96 lines
2.7 KiB
JavaScript
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
let controls;
|
|
|
|
export function initInteraction(camera, renderer) {
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.screenSpacePanning = false;
|
|
controls.minDistance = 5;
|
|
controls.maxDistance = 80;
|
|
controls.maxPolarAngle = Math.PI / 2.1;
|
|
controls.target.set(0, 0, 0);
|
|
controls.update();
|
|
|
|
renderer.domElement.addEventListener('contextmenu', e => e.preventDefault());
|
|
}
|
|
|
|
export function updateControls() {
|
|
if (controls) controls.update();
|
|
}
|
|
|
|
/**
|
|
* Dispose orbit controls (used on world teardown).
|
|
*/
|
|
export function disposeInteraction() {
|
|
if (controls) {
|
|
controls.dispose();
|
|
controls = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up pointer-based click detection for agent meshes.
|
|
* Distinguishes clicks from orbit-control drags (pointer must move < 6px).
|
|
*
|
|
* @param {THREE.Camera} camera
|
|
* @param {THREE.WebGLRenderer} renderer
|
|
* @param {THREE.Object3D[]} agentGroups — array of agent group objects to raycast against
|
|
* @param {function} onAgentClick — called with agentId string on hit
|
|
* @returns {function} cleanup — call to remove event listeners
|
|
*/
|
|
export function initClickDetection(camera, renderer, agentGroups, onAgentClick) {
|
|
const raycaster = new THREE.Raycaster();
|
|
const pointer = new THREE.Vector2();
|
|
let downX = 0, downY = 0;
|
|
|
|
function toNDC(clientX, clientY) {
|
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
return {
|
|
x: ((clientX - rect.left) / rect.width) * 2 - 1,
|
|
y: -((clientY - rect.top) / rect.height) * 2 + 1,
|
|
};
|
|
}
|
|
|
|
function onPointerDown(e) {
|
|
downX = e.clientX;
|
|
downY = e.clientY;
|
|
}
|
|
|
|
function onPointerUp(e) {
|
|
const dx = e.clientX - downX;
|
|
const dy = e.clientY - downY;
|
|
if (dx * dx + dy * dy > 36) return; // drag threshold 6px
|
|
|
|
const ndc = toNDC(e.clientX, e.clientY);
|
|
pointer.set(ndc.x, ndc.y);
|
|
raycaster.setFromCamera(pointer, camera);
|
|
|
|
const hits = raycaster.intersectObjects(agentGroups, true);
|
|
if (hits.length > 0) {
|
|
const agentId = findAgentId(hits[0].object);
|
|
if (agentId) onAgentClick(agentId);
|
|
}
|
|
}
|
|
|
|
const el = renderer.domElement;
|
|
el.addEventListener('pointerdown', onPointerDown);
|
|
el.addEventListener('pointerup', onPointerUp);
|
|
|
|
return () => {
|
|
el.removeEventListener('pointerdown', onPointerDown);
|
|
el.removeEventListener('pointerup', onPointerUp);
|
|
};
|
|
}
|
|
|
|
function findAgentId(object) {
|
|
let o = object;
|
|
while (o) {
|
|
if (o.userData?.agentId) return o.userData.agentId;
|
|
o = o.parent;
|
|
}
|
|
return null;
|
|
}
|