import BLE from '../ble.js'; import UI from '../ui.js'; import IK from '../kinematics.js'; import Settings from '../settings.js'; import SequenceRunner from '../sequence-runner.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 hasAlphabet = false; let savedBrowserSequences = []; let bgSequences = []; let sequence = { id: "my_custom_sequence", name: "My Sequence", favorite: false, steps: [] }; 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; const savedSeq = localStorage.getItem('wiji_sequence_draft'); if (savedSeq) { try { sequence = JSON.parse(savedSeq); } catch(e) {} } const savedSeqs = localStorage.getItem('wiji_saved_sequences'); if (savedSeqs) { try { savedBrowserSequences = JSON.parse(savedSeqs); } catch(e) {} } } function saveSteps() { if (isHomed) { localStorage.setItem('wiji_steps1', steps1); localStorage.setItem('wiji_steps2', steps2); } } function saveSequenceDraft() { localStorage.setItem('wiji_sequence_draft', JSON.stringify(sequence)); updateJSONPreview(); } function updateJSONPreview() { const el = document.getElementById('json-preview'); if (el) { el.value = JSON.stringify([sequence], null, 2); } } function saveToBrowser() { const existingIdx = savedBrowserSequences.findIndex(s => s.id === sequence.id); if (existingIdx >= 0) { savedBrowserSequences[existingIdx] = JSON.parse(JSON.stringify(sequence)); } else { savedBrowserSequences.push(JSON.parse(JSON.stringify(sequence))); } localStorage.setItem('wiji_saved_sequences', JSON.stringify(savedBrowserSequences)); renderSavedSequences(); UI.log(`Saved '${sequence.name}' to browser storage`, 'success'); } function buildHTML() { return `

Sequence Editor

Build and test custom sequences. Export JSON to save them permanently.

Sequence Steps
Add Step
JSON Export
Copy this JSON array into web/assets/backgrounds/${currentBg}/sequences.json to persist it.
System Status
X (mm)
0.0
Y (mm)
0.0
${isHomed ? '✓ HOMED' : '⚠ NOT HOMED! Movement Locked.'}
Motion Settings
Provides available spots for the editor.
${Settings.speed}
${Settings.accel}
${Settings.delay}
Saved Sequences
`; } function updatePositionReadout() { const t1 = IK.stepsToRad(steps1); const t2 = IK.stepsToRad(steps2); const { endX, endY } = IK.forward(t1, t2); const px = document.getElementById('pos-x'); const py = document.getElementById('pos-y'); if (px) px.textContent = isFinite(endX) ? endX.toFixed(1) : '---'; if (py) py.textContent = isFinite(endY) ? endY.toFixed(1) : '---'; } 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 ALL'; } else { banner.className = 'homing-banner not-homed'; banner.textContent = '⚠ NOT HOMED! Movement Locked.'; btn.textContent = 'HOME ALL MOTORS'; } } function renderStepsList() { const container = document.getElementById('steps-list'); if (!container) return; if (sequence.steps.length === 0) { container.innerHTML = `
No steps added yet.
`; return; } container.innerHTML = sequence.steps.map((step, idx) => { let label = ''; let details = ''; if (step.type === 'spot') { label = `Spot: ${step.id}`; } else { label = `XY: (${step.x}, ${step.y})`; } if (step.delay !== undefined) { details = `Delay: ${step.delay}ms`; } return `
#${idx+1} ${label} ${details}
`; }).join(''); // Attach listeners container.querySelectorAll('.btn-move-up').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.idx); if (idx > 0) { [sequence.steps[idx - 1], sequence.steps[idx]] = [sequence.steps[idx], sequence.steps[idx - 1]]; saveSequenceDraft(); renderStepsList(); } }); }); container.querySelectorAll('.btn-move-down').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.idx); if (idx < sequence.steps.length - 1) { [sequence.steps[idx + 1], sequence.steps[idx]] = [sequence.steps[idx], sequence.steps[idx + 1]]; saveSequenceDraft(); renderStepsList(); } }); }); container.querySelectorAll('.btn-delete-step').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.idx); sequence.steps.splice(idx, 1); saveSequenceDraft(); renderStepsList(); }); }); } async function loadSpots() { try { const res = await fetch(`web/assets/backgrounds/${currentBg}/spots.json?v=${Date.now()}`); const data = await res.json(); if (Array.isArray(data)) { spots = data; hasAlphabet = false; } else { spots = data.spots || []; if (data.bounds) bounds = data.bounds; hasAlphabet = !!data.hasAlphabet; } document.getElementById('text-input-row').style.display = hasAlphabet ? 'grid' : 'none'; document.getElementById('text-input-divider').style.display = hasAlphabet ? 'block' : 'none'; // Populate spot dropdown const select = document.getElementById('editor-spot-select'); if (select) { select.innerHTML = '' + spots.map(s => `` ).join(''); } updateJSONPreview(); await loadBgSequences(); } catch(e) { UI.log('Failed to load spots.json', 'error'); } } async function loadBgSequences() { try { const res = await fetch(`web/assets/backgrounds/${currentBg}/sequences.json?v=${Date.now()}`); if (res.ok) { bgSequences = await res.json(); } else { bgSequences = []; } } catch (e) { bgSequences = []; } renderSavedSequences(); } function renderSavedSequences() { const container = document.getElementById('saved-seq-list'); if (!container) return; let html = ''; if (bgSequences.length > 0) { html += `
From sequences.json
`; html += bgSequences.map((s, idx) => `
${s.name}
`).join(''); } if (savedBrowserSequences.length > 0) { html += `
From Browser Storage
`; html += savedBrowserSequences.map((s, idx) => `
${s.name}
`).join(''); } if (!html) { html = `
No saved sequences
`; } container.innerHTML = html; container.querySelectorAll('.btn-load-seq').forEach(btn => { btn.addEventListener('click', () => { const source = btn.dataset.source; const idx = parseInt(btn.dataset.idx); const srcObj = source === 'bg' ? bgSequences[idx] : savedBrowserSequences[idx]; if (srcObj) { sequence = JSON.parse(JSON.stringify(srcObj)); document.getElementById('seq-id').value = sequence.id; document.getElementById('seq-name').value = sequence.name; const favCheckbox = document.getElementById('seq-favorite'); if (favCheckbox) favCheckbox.checked = !!sequence.favorite; saveSequenceDraft(); renderStepsList(); UI.log(`Loaded sequence '${sequence.name}'`, 'info'); } }); }); container.querySelectorAll('.btn-delete-seq').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.idx); if (confirm('Delete this sequence from browser storage?')) { savedBrowserSequences.splice(idx, 1); localStorage.setItem('wiji_saved_sequences', JSON.stringify(savedBrowserSequences)); renderSavedSequences(); } }); }); } 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); } } export default { mount(container) { syncStateFromStorage(); container.innerHTML = buildHTML(); updateHomingUI(); loadSpots(); renderStepsList(); // ── Input bindings for metadata ────────────────────────────── document.getElementById('seq-id').addEventListener('input', (e) => { sequence.id = e.target.value; saveSequenceDraft(); }); document.getElementById('seq-name').addEventListener('input', (e) => { sequence.name = e.target.value; saveSequenceDraft(); }); const favCheckbox = document.getElementById('seq-favorite'); if (favCheckbox) { favCheckbox.addEventListener('change', (e) => { sequence.favorite = e.target.checked; saveSequenceDraft(); }); } // ── 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(); }); } // ── 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; }); // ── Add Steps Logic ─────────────────────────────────────── document.getElementById('btn-add-spot').addEventListener('click', () => { const select = document.getElementById('editor-spot-select'); const delayInput = document.getElementById('editor-spot-delay'); const spotId = select.value; const delay = parseInt(delayInput.value); if (spotId) { const step = { type: 'spot', id: spotId }; if (!isNaN(delay) && delay >= 0) step.delay = delay; sequence.steps.push(step); saveSequenceDraft(); renderStepsList(); select.value = ""; delayInput.value = ""; } }); document.getElementById('btn-add-xy').addEventListener('click', () => { const xInput = document.getElementById('editor-x'); const yInput = document.getElementById('editor-y'); const delayInput = document.getElementById('editor-xy-delay'); const x = parseFloat(xInput.value); const y = parseFloat(yInput.value); const delay = parseInt(delayInput.value); if (!isNaN(x) && !isNaN(y)) { const step = { type: 'xy', x, y }; if (!isNaN(delay) && delay >= 0) { step.delay = delay; } sequence.steps.push(step); saveSequenceDraft(); renderStepsList(); xInput.value = ""; yInput.value = ""; delayInput.value = ""; } }); document.getElementById('btn-add-text').addEventListener('click', () => { const textInput = document.getElementById('editor-text'); const delayInput = document.getElementById('editor-text-delay'); const text = textInput.value.trim().toUpperCase(); const delay = parseInt(delayInput.value); if (text) { const chars = Array.from(text); let addedCount = 0; for (const char of chars) { const spot = spots.find(s => (s.label && s.label.toUpperCase() === char) || (s.id && s.id.toUpperCase() === char) ); if (spot) { const step = { type: 'spot', id: spot.id }; if (!isNaN(delay) && delay >= 0) step.delay = delay; sequence.steps.push(step); addedCount++; } } if (addedCount > 0) { saveSequenceDraft(); renderStepsList(); UI.log(`Added ${addedCount} letter spots`, 'success'); } else { UI.log('No spots found for entered text', 'warn'); } textInput.value = ""; delayInput.value = ""; } }); document.getElementById('btn-clear-seq').addEventListener('click', () => { if (confirm('Are you sure you want to clear this sequence?')) { sequence.steps = []; saveSequenceDraft(); renderStepsList(); } }); document.getElementById('btn-save-browser').addEventListener('click', () => { saveToBrowser(); }); // ── Sequence Execution ──────────────────────────────────── const btnPlay = document.getElementById('btn-play-editor'); const btnStop = document.getElementById('btn-stop-editor'); btnPlay.addEventListener('click', async () => { if (!isHomed) { UI.log('Cannot move: System is not homed!', 'error'); return; } if (sequence.steps.length === 0) return; btnPlay.style.display = 'none'; btnStop.style.display = 'inline-block'; await sendSettings(); await SequenceRunner.run(sequence, spots, steps1, steps2, (s1, s2) => { steps1 = s1; steps2 = s2; updatePositionReadout(); }); btnPlay.style.display = 'inline-block'; btnStop.style.display = 'none'; }); btnStop.addEventListener('click', () => { SequenceRunner.stop(); }); // ── Homing Logic ────────────────────────────────────────── document.getElementById('btn-force-home').addEventListener('click', async () => { isHomed = true; localStorage.setItem('wiji_homed', 'true'); updateHomingUI(); steps1 = IK.ARM.HOME_STEPS.m1; steps2 = IK.ARM.HOME_STEPS.m2; saveSteps(); updatePositionReadout(); UI.log('Homing all motors...', 'info'); if (BLE.isConnected()) { try { await BLE.write('HOMEALL'); } catch (e) { UI.log(e.message, 'error'); } } else { UI.log('[sim] HOMEALL', '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(); updatePositionReadout(); } }; document.addEventListener('ble:status', onBLEStatus); if (BLE.isConnected()) { BLE.write('POS').catch(() => {}); sendSettings(); } else { updatePositionReadout(); } eventCleanup.push(() => document.removeEventListener('ble:status', onBLEStatus)); }, unmount() { SequenceRunner.stop(); eventCleanup.forEach(fn => fn()); eventCleanup = []; } };