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 isSpelling = false;
let currentResolve = null;
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();
if (!data.hasAlphabet) {
UI.log(`Background '${currentBg}' does not support an alphabet.`, 'warn');
}
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()}`;
const mapContainer = document.getElementById('map-container');
if (mapContainer) {
const w = bounds.xMax - bounds.xMin;
const h = bounds.yMax - bounds.yMin;
// mapContainer.style.aspectRatio = `${w}/${h}`; // Let natural image aspect ratio dictate container height
}
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 delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function executeMove(spot, forceDirect = false) {
UI.log(`Spelling: ${spot.label}`, 'info');
const modeEl = document.getElementById('text-mode-select');
const mode = forceDirect ? 'direct' : (modeEl ? modeEl.value : 'direct');
return await Motion.goto(spot.x, spot.y, mode);
}
async function spellText(text) {
const btnSpell = document.getElementById('btn-spell');
const btnStop = document.getElementById('btn-stop');
const inputEl = document.getElementById('text-input');
if (!isHomed) {
UI.log('Cannot move: System is not homed! Please click HOME ALL.', 'error');
return;
}
text = text.toUpperCase().replace(/\s+/g, '');
if (!text) return;
isSpelling = true;
btnSpell.style.display = 'none';
btnStop.style.display = 'block';
inputEl.disabled = true;
// Send speed/accel right before moving just in case
await sendSettings();
for (let i = 0; i < text.length; i++) {
if (!isSpelling) break;
const char = text[i];
// Find spot by ID (assuming ID is the uppercase letter)
const spot = spots.find(s => s.id.toUpperCase() === char);
if (spot) {
await executeMove(spot, i === 0);
// Wait for delay
await delay(Settings.delay);
} else {
UI.log(`Character '${char}' not found on this board.`, 'warn');
}
}
isSpelling = false;
btnSpell.style.display = 'block';
btnStop.style.display = 'none';
inputEl.disabled = false;
UI.log('Finished spelling text', 'success');
}
export default {
mount(container) {
syncStateFromStorage();
container.innerHTML = buildHTML();
updateHomingUI();
loadSpots();
// ── Idle Effects Selection ──────────────────────────────────
const idleSelect = document.getElementById('it-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('slider-speed');
const sAccel = document.getElementById('slider-accel');
const sDelay = document.getElementById('slider-delay');
sSpeed.addEventListener('input', (e) => {
Settings.speed = parseInt(e.target.value);
document.getElementById('speed-val').textContent = Settings.speed;
sendSettings();
});
sAccel.addEventListener('input', (e) => {
Settings.accel = parseInt(e.target.value);
document.getElementById('accel-val').textContent = Settings.accel;
sendSettings();
});
sDelay.addEventListener('input', (e) => {
Settings.delay = parseInt(e.target.value);
document.getElementById('delay-val').textContent = Settings.delay;
});
// ── 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);
});
// ── Spelling Logic ────────────────────────────────────────
const btnSpell = document.getElementById('btn-spell');
const btnStop = document.getElementById('btn-stop');
const inputEl = document.getElementById('text-input');
btnSpell.addEventListener('click', () => {
spellText(inputEl.value);
});
inputEl.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
spellText(inputEl.value);
}
});
const btnClearText = document.getElementById('btn-clear-text');
if (btnClearText) {
btnClearText.addEventListener('click', () => {
inputEl.value = '';
inputEl.focus();
});
}
btnStop.addEventListener('click', () => {
isSpelling = false;
UI.log('Spelling stopped', 'info');
});
// ── 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() {
isSpelling = false;
eventCleanup.forEach(fn => fn());
eventCleanup = [];
}
};