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 `
${isHomed ? '✓ HOMED' : '⚠ NOT HOMED! Movement Locked.'}
${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 = [];
}
};