import BLE from '../ble.js'; import UI from '../ui.js'; import IK from '../kinematics.js'; import Settings from '../settings.js'; import Motion from '../motion.js'; import { buildQuickSequencesHTML, initQuickSequences } from '../quick-sequences.js'; import { IdleEffects } from '../idle-effects.js'; let eventCleanup = []; let isHomed = false; let spots = []; let bounds = { xMin: -150, xMax: 150, yMin: -30, yMax: 145 }; let currentBg = localStorage.getItem('wiji_bg') || 'default'; let steps1 = 0; let steps2 = 0; let isManualPickMode = false; let isFullscreen = false; function syncStateFromStorage() { isHomed = localStorage.getItem('wiji_homed') === 'true'; steps1 = isHomed ? parseInt(localStorage.getItem('wiji_steps1') || IK.ARM.HOME_STEPS.m1) : IK.ARM.HOME_STEPS.m1; steps2 = isHomed ? parseInt(localStorage.getItem('wiji_steps2') || IK.ARM.HOME_STEPS.m2) : IK.ARM.HOME_STEPS.m2; currentBg = localStorage.getItem('wiji_bg') || 'default'; Motion.setSteps(steps1, steps2); } function saveSteps() { if (isHomed) { localStorage.setItem('wiji_steps1', steps1); localStorage.setItem('wiji_steps2', steps2); } } function buildHTML() { return `

Board Control

Click on the map or select from the list to move the pointer.

Interactive Map
System Status
X (mm)
0.0
Y (mm)
0.0
${isHomed ? '✓ HOMED' : '⚠ NOT HOMED! Movement Locked.'}
Navigation
${Settings.speed}
${Settings.accel}
${buildQuickSequencesHTML()}
`; } function updatePosMarker() { const marker = document.getElementById('pos-marker'); if (!marker) return; const t1 = IK.stepsToRad(steps1); const t2 = IK.stepsToRad(steps2); const { endX, endY, valid } = IK.forward(t1, t2); if (valid && isFinite(endX) && isFinite(endY)) { const px = document.getElementById('pos-x'); const py = document.getElementById('pos-y'); if (px) px.textContent = endX.toFixed(1); if (py) py.textContent = endY.toFixed(1); const w = bounds.xMax - bounds.xMin; const h = bounds.yMax - bounds.yMin; // Bounds check to avoid marker flying off screen entirely if (endX >= bounds.xMin && endX <= bounds.xMax && endY >= bounds.yMin && endY <= bounds.yMax) { const xPct = ((endX - bounds.xMin) / w) * 100; const yPct = ((bounds.yMax - endY) / h) * 100; marker.style.left = `${xPct}%`; marker.style.top = `${yPct}%`; marker.style.display = 'block'; } else { marker.style.display = 'none'; // Out of bounds visually } } else { marker.style.display = 'none'; } } function updateHomingUI() { const banner = document.getElementById('homing-banner'); const btn = document.getElementById('btn-force-home'); if (!banner || !btn) return; if (isHomed) { banner.className = 'homing-banner homed'; banner.textContent = '✓ HOMED'; btn.textContent = 'FORCE HOME'; } else { banner.className = 'homing-banner not-homed'; banner.textContent = '⚠ NOT HOMED! Movement Locked.'; btn.textContent = 'HOME ALL'; } } async function loadSpots() { try { const res = await fetch(`web/assets/backgrounds/${currentBg}/spots.json?v=${Date.now()}`); const data = await res.json(); let parsedSpots = []; let parkPosition = { x: 0, y: 80 }; if (Array.isArray(data)) { parsedSpots = data; } else { parsedSpots = data.spots || []; if (data.bounds) bounds = data.bounds; if (data.parkPosition) parkPosition = data.parkPosition; } if (!parsedSpots.find(s => s.id === 'PARK')) { parsedSpots.push({ id: 'PARK', label: 'Park Position', x: parkPosition.x, y: parkPosition.y }); } IdleEffects.setParkPosition(parkPosition.x, parkPosition.y); spots = parsedSpots; document.getElementById('bg-image').src = `web/assets/backgrounds/${currentBg}/bg.svg?v=${Date.now()}`; renderSpots(); updatePosMarker(); } catch(e) { UI.log('Failed to load spots.json', 'error'); } } async function sendSettings() { if (!BLE.isConnected()) return; try { await BLE.write(`SPD:${Settings.speed}`); await BLE.write(`ACC:${Settings.accel}`); } catch (e) { console.error('Failed to send settings', e); } } function renderSpots() { const listEl = document.getElementById('spot-list'); const markersEl = document.getElementById('markers-layer'); const mapContainer = document.getElementById('map-container'); if (!listEl || !markersEl || !mapContainer) return; const w = bounds.xMax - bounds.xMin; const h = bounds.yMax - bounds.yMin; mapContainer.style.setProperty('--w', w); mapContainer.style.setProperty('--h', h); listEl.innerHTML = ''; markersEl.innerHTML = ''; spots.forEach(spot => { // List item const opt = document.createElement('option'); opt.value = spot.id; opt.textContent = `${spot.label.padEnd(10, ' ')} [${spot.x}, ${spot.y}]`; listEl.appendChild(opt); // Map marker const marker = document.createElement('div'); const xPct = ((spot.x - bounds.xMin) / w) * 100; const yPct = ((bounds.yMax - spot.y) / h) * 100; marker.className = 'spot-marker'; marker.style = `position: absolute; left: ${xPct}%; top: ${yPct}%; transform: translate(-50%, -50%); width: 18px; height: 18px; border-radius: 50%; background: rgba(0, 230, 118, 0.4); border: 2px solid var(--accent-green); cursor: pointer; transition: all 0.2s;`; marker.title = spot.label; marker.addEventListener('click', () => { listEl.value = spot.id; document.getElementById('btn-go').disabled = false; executeMove(spot); }); markersEl.appendChild(marker); }); } async function executeMove(spot) { if (!isHomed) { const msg = 'Cannot move: System is not homed! Please click FORCE HOME in the control panel first.'; UI.log(msg, 'error'); alert(msg); return; } const mode = document.getElementById('mode-select').value; UI.log(`Moving to ${spot.label} (${spot.x}, ${spot.y}) [${mode}]`, 'success'); await Motion.goto(spot.x, spot.y, mode); } export default { mount(container) { syncStateFromStorage(); container.innerHTML = buildHTML(); updateHomingUI(); loadSpots(); // ── Idle Effects Selection ────────────────────────────────── const idleSelect = document.getElementById('bc-idle-mode-select'); if (idleSelect) { idleSelect.value = IdleEffects.getMode(); idleSelect.addEventListener('change', () => { IdleEffects.setMode(idleSelect.value); }); } // ── Background Selection ────────────────────────────────── const bgSelect = document.getElementById('bg-select'); if (bgSelect) { bgSelect.value = currentBg; bgSelect.addEventListener('change', () => { currentBg = bgSelect.value; localStorage.setItem('wiji_bg', currentBg); loadSpots(); initQuickSequences( currentBg, () => spots, () => ({ steps1, steps2 }), (s1, s2) => { steps1 = s1; steps2 = s2; updatePosMarker(); } ); }); } // ── Settings Sliders ────────────────────────────────────── const sSpeed = document.getElementById('bc-slider-speed'); const sAccel = document.getElementById('bc-slider-accel'); if (sSpeed) { sSpeed.addEventListener('input', (e) => { Settings.speed = parseInt(e.target.value); document.getElementById('speed-val').textContent = Settings.speed; sendSettings(); }); } if (sAccel) { sAccel.addEventListener('input', (e) => { Settings.accel = parseInt(e.target.value); document.getElementById('accel-val').textContent = Settings.accel; sendSettings(); }); } // ── Homing Logic ────────────────────────────────────────── document.getElementById('btn-force-home').addEventListener('click', async () => { isHomed = true; localStorage.setItem('wiji_homed', 'true'); updateHomingUI(); UI.log('Homing all motors...', 'info'); if (BLE.isConnected()) { try { await BLE.write('HOMEALL'); } catch (e) { UI.log(e.message, 'error'); } } else { steps1 = IK.ARM.HOME_STEPS.m1; steps2 = IK.ARM.HOME_STEPS.m2; Motion.setSteps(steps1, steps2); saveSteps(); updatePosMarker(); UI.log('[sim] HOMEALL', 'info'); } }); document.getElementById('btn-park').addEventListener('click', () => { const parkSpot = spots.find(s => s.id === 'PARK'); if (parkSpot) executeMove(parkSpot); }); // ── List Selection ──────────────────────────────────────── const listEl = document.getElementById('spot-list'); const goBtn = document.getElementById('btn-go'); listEl.addEventListener('change', () => { goBtn.disabled = !listEl.value; }); goBtn.addEventListener('click', () => { const selectedId = listEl.value; const spot = spots.find(s => s.id === selectedId); if (spot) { executeMove(spot); } }); // ── Manual Pick Mode ────────────────────────────────────── const btnManualPick = document.getElementById('btn-manual-pick'); const mapContainer = document.getElementById('map-container'); if (btnManualPick && mapContainer) { btnManualPick.addEventListener('click', () => { isManualPickMode = !isManualPickMode; if (isManualPickMode) { btnManualPick.classList.add('btn-success'); btnManualPick.classList.remove('btn-secondary'); mapContainer.style.cursor = 'crosshair'; } else { btnManualPick.classList.remove('btn-success'); btnManualPick.classList.add('btn-secondary'); mapContainer.style.cursor = 'default'; } }); mapContainer.addEventListener('click', (e) => { if (!isManualPickMode) return; // Ignore clicks on existing markers if (e.target.classList.contains('spot-marker')) return; const rect = mapContainer.getBoundingClientRect(); const xClick = e.clientX - rect.left; const yClick = e.clientY - rect.top; const w = bounds.xMax - bounds.xMin; const h = bounds.yMax - bounds.yMin; const x = bounds.xMin + (xClick / rect.width) * w; const y = bounds.yMax - (yClick / rect.height) * h; const check = IK.checkWorkspace(x, y); if (!check.ok) { UI.log(check.reason, 'warning'); return; } // Execute move if (listEl) listEl.value = ''; // Deselect predefined spots goBtn.disabled = true; executeMove({ id: 'manual', label: `Manual [${x.toFixed(1)}, ${y.toFixed(1)}]`, x, y }); // Turn off mode automatically (one-shot) isManualPickMode = false; btnManualPick.classList.remove('btn-success'); btnManualPick.classList.add('btn-secondary'); mapContainer.style.cursor = 'default'; }); } // ── Full Screen Mode ────────────────────────────────────── const btnFullscreen = document.getElementById('btn-fullscreen'); const mapCard = document.getElementById('map-card'); const fullscreenIcon = document.getElementById('fullscreen-icon'); const fullscreenText = document.getElementById('fullscreen-text'); if (btnFullscreen && mapCard) { btnFullscreen.addEventListener('click', () => { isFullscreen = !isFullscreen; if (isFullscreen) { document.body.classList.add('board-fullscreen'); mapCard.classList.add('fullscreen-card'); fullscreenIcon.textContent = '✖'; fullscreenText.textContent = 'Exit'; } else { document.body.classList.remove('board-fullscreen'); mapCard.classList.remove('fullscreen-card'); fullscreenIcon.textContent = '⛶'; fullscreenText.textContent = 'Full Screen'; } }); } // ── Sync Steps from BLE ─────────────────────────────────── const onBLEStatus = (e) => { const msg = e.detail; if (msg.startsWith('P:')) { const [s1, s2] = msg.slice(2).split(',').map(Number); steps1 = s1; steps2 = s2; saveSteps(); updatePosMarker(); } }; document.addEventListener('ble:status', onBLEStatus); // Sync position on mount if connected if (BLE.isConnected()) { BLE.write('POS').catch(() => {}); sendSettings(); } else { updatePosMarker(); } initQuickSequences( currentBg, () => spots, () => ({ steps1, steps2 }), (s1, s2) => { steps1 = s1; steps2 = s2; updatePosMarker(); } ); eventCleanup.push(() => document.removeEventListener('ble:status', onBLEStatus)); }, unmount() { document.body.classList.remove('board-fullscreen'); eventCleanup.forEach(fn => fn()); eventCleanup = []; } };