738 lines
28 KiB
JavaScript
738 lines
28 KiB
JavaScript
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 `
|
|
<style>
|
|
.board-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 300px;
|
|
gap: 16px;
|
|
height: 100%;
|
|
align-items: start;
|
|
}
|
|
@media (max-width: 900px) { .board-grid { grid-template-columns: 1fr; } }
|
|
.homing-banner {
|
|
padding: 10px;
|
|
border-radius: var(--radius-sm);
|
|
font-weight: bold;
|
|
text-align: center;
|
|
margin-bottom: 12px;
|
|
transition: all 0.3s;
|
|
}
|
|
.homing-banner.homed {
|
|
background: rgba(0, 230, 118, 0.1);
|
|
color: var(--accent-green);
|
|
border: 1px solid rgba(0, 230, 118, 0.3);
|
|
}
|
|
.homing-banner.not-homed {
|
|
background: rgba(255, 82, 82, 0.1);
|
|
color: var(--accent-red, #ff5252);
|
|
border: 1px solid rgba(255, 82, 82, 0.3);
|
|
}
|
|
.step-box {
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 10px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.step-controls { display: flex; gap: 6px; }
|
|
.btn-icon {
|
|
padding: 4px; border-radius: var(--radius-xs);
|
|
background: transparent; color: var(--text-muted); border: none; cursor: pointer;
|
|
}
|
|
.btn-icon:hover { color: var(--text-primary); background: rgba(255,255,255,0.1); }
|
|
.setting-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
|
|
|
.meta-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 2fr auto;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
align-items: center;
|
|
}
|
|
.step-input-row {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr 1fr;
|
|
gap: 6px;
|
|
margin-bottom: 8px;
|
|
align-items: center;
|
|
}
|
|
.step-input-row.four-cols {
|
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
}
|
|
.mobile-separator { display: none; }
|
|
@media (max-width: 480px) {
|
|
.meta-row { grid-template-columns: 1fr auto; }
|
|
.meta-row .name-input { grid-column: 1 / -1; order: 3; }
|
|
.step-input-row { grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; }
|
|
.step-input-row .full-w-mobile { grid-column: 1 / -1; }
|
|
.step-input-row.four-cols { grid-template-columns: 1fr 1fr; }
|
|
.mobile-separator { display: block; height: 1px; background: rgba(255,255,255,0.05); margin: 16px 0; }
|
|
}
|
|
</style>
|
|
|
|
<div class="section-page fade-in" style="display:flex; flex-direction:column; height: 100%;">
|
|
<div class="section-header">
|
|
<h2>Sequence Editor</h2>
|
|
<p>Build and test custom sequences. Export JSON to save them permanently.</p>
|
|
</div>
|
|
|
|
<div class="board-grid">
|
|
<!-- ── Editor Area ─────────────────────────────────────── -->
|
|
<div style="display:flex; flex-direction:column; gap: 12px;">
|
|
|
|
<div class="card" style="flex:1;">
|
|
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px;">
|
|
<span class="card-title">Sequence Steps</span>
|
|
<div style="display:flex; gap:4px; flex-wrap:wrap;">
|
|
<button id="btn-play-editor" class="btn btn-sm btn-success">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: middle; margin-right: 4px;"><path d="M8 5v14l11-7z"/></svg>PLAY
|
|
</button>
|
|
<button id="btn-stop-editor" class="btn btn-sm btn-danger" style="display:none;">STOP</button>
|
|
<button id="btn-clear-seq" class="btn btn-sm btn-ghost">CLEAR</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sequence Meta Info -->
|
|
<div class="meta-row">
|
|
<input type="text" id="seq-id" class="form-input" placeholder="Sequence ID" value="${sequence.id}" />
|
|
<input type="text" id="seq-name" class="form-input name-input" placeholder="Display Name" value="${sequence.name}" />
|
|
<label style="display:flex; align-items:center; gap:4px; font-size: 0.9rem; cursor:pointer; white-space: nowrap;">
|
|
<input type="checkbox" id="seq-favorite" ${sequence.favorite ? 'checked' : ''} /> Favorite
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Steps List -->
|
|
<div id="steps-list" style="min-height: 150px; margin-bottom: 16px;">
|
|
<!-- Populated dynamically -->
|
|
</div>
|
|
|
|
<!-- Add Step Controls -->
|
|
<div style="background: var(--surface-2); border: 1px solid var(--border); padding: 12px; border-radius: var(--radius-sm);">
|
|
<div style="margin-bottom: 8px; font-weight: bold; font-size: 0.9rem;">Add Step</div>
|
|
|
|
<div class="step-input-row">
|
|
<select id="editor-spot-select" class="form-input full-w-mobile">
|
|
<option value="">-- Select Spot --</option>
|
|
<!-- Populated dynamically -->
|
|
</select>
|
|
<input type="number" id="editor-spot-delay" class="form-input" placeholder="Delay (ms)" step="100" title="Optional delay" />
|
|
<button id="btn-add-spot" class="btn btn-primary">+ Spot</button>
|
|
</div>
|
|
|
|
<div class="mobile-separator"></div>
|
|
|
|
<div class="step-input-row four-cols">
|
|
<input type="number" id="editor-x" class="form-input" placeholder="X" />
|
|
<input type="number" id="editor-y" class="form-input" placeholder="Y" />
|
|
<input type="number" id="editor-xy-delay" class="form-input" placeholder="Delay (ms)" step="100" title="Optional delay" />
|
|
<button id="btn-add-xy" class="btn btn-primary">+ XY</button>
|
|
</div>
|
|
|
|
<div id="text-input-divider" class="mobile-separator" style="display:none;"></div>
|
|
|
|
<div id="text-input-row" class="step-input-row" style="display:none;">
|
|
<input type="text" id="editor-text" class="form-input full-w-mobile" placeholder="Text (e.g. HELLO)" />
|
|
<input type="number" id="editor-text-delay" class="form-input" placeholder="Delay (ms)" step="100" title="Delay between letters" />
|
|
<button id="btn-add-text" class="btn btn-primary">+ Text</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JSON Export -->
|
|
<div class="card">
|
|
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px;">
|
|
<span class="card-title">JSON Export</span>
|
|
<button id="btn-save-browser" class="btn btn-sm btn-primary">SAVE TO BROWSER</button>
|
|
</div>
|
|
<textarea id="json-preview" class="form-input" style="width: 100%; height: 200px; font-family: monospace; font-size: 0.85rem; resize: vertical;" readonly></textarea>
|
|
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 8px;">
|
|
Copy this JSON array into <code>web/assets/backgrounds/${currentBg}/sequences.json</code> to persist it.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Control Panel ──────────────────────────────── -->
|
|
<div style="display:flex; flex-direction:column; gap: 12px;">
|
|
|
|
<!-- Homing Card -->
|
|
<div class="card">
|
|
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
|
|
<span class="card-title">System Status</span>
|
|
<div style="display:flex; gap: 12px;">
|
|
<div style="text-align:right"><div style="font-size:0.6rem; color:var(--text-muted); text-transform:uppercase;">X (mm)</div><div id="pos-x" style="font-weight:bold; font-size:1rem; color:var(--accent-green);">0.0</div></div>
|
|
<div style="text-align:right"><div style="font-size:0.6rem; color:var(--text-muted); text-transform:uppercase;">Y (mm)</div><div id="pos-y" style="font-weight:bold; font-size:1rem; color:var(--accent-green);">0.0</div></div>
|
|
</div>
|
|
</div>
|
|
<div id="homing-banner" class="homing-banner ${isHomed ? 'homed' : 'not-homed'}">
|
|
${isHomed ? '✓ HOMED' : '⚠ NOT HOMED! Movement Locked.'}
|
|
</div>
|
|
<button id="btn-force-home" class="btn btn-danger btn-full" style="padding: 10px; font-weight:bold;">
|
|
${isHomed ? 'FORCE HOME ALL' : 'HOME ALL MOTORS'}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Settings Card -->
|
|
<div class="card">
|
|
<div class="card-header"><span class="card-title">Motion Settings</span></div>
|
|
|
|
<div class="form-group" style="margin-bottom: 12px;">
|
|
<label class="form-label">Background Context</label>
|
|
<select id="bg-select" class="form-input" style="width:100%;">
|
|
<option value="default">Default Spirit Board (Landscape)</option>
|
|
<option value="portrait-right">Right Side Test (Portrait)</option>
|
|
</select>
|
|
<div style="font-size: 0.7rem; color: var(--text-muted); margin-top: 4px;">Provides available spots for the editor.</div>
|
|
</div>
|
|
|
|
<div style="background: var(--surface-2); padding: 10px; border-radius: var(--radius-sm);">
|
|
<div class="setting-row">
|
|
<label class="form-label" style="margin:0;">Speed</label>
|
|
<span id="speed-val" style="font-size: 0.8rem; color: var(--text-muted);">${Settings.speed}</span>
|
|
</div>
|
|
<input type="range" id="slider-speed" min="100" max="2000" step="100" value="${Settings.speed}" style="width:100%; margin-bottom: 8px;">
|
|
|
|
<div class="setting-row">
|
|
<label class="form-label" style="margin:0;">Accel</label>
|
|
<span id="accel-val" style="font-size: 0.8rem; color: var(--text-muted);">${Settings.accel}</span>
|
|
</div>
|
|
<input type="range" id="slider-accel" min="50" max="1000" step="50" value="${Settings.accel}" style="width:100%; margin-bottom: 8px;">
|
|
|
|
<div class="setting-row">
|
|
<label class="form-label" style="margin:0;">Global Delay (ms)</label>
|
|
<span id="delay-val" style="font-size: 0.8rem; color: var(--text-muted);">${Settings.delay}</span>
|
|
</div>
|
|
<input type="range" id="slider-delay" min="500" max="5000" step="100" value="${Settings.delay}" style="width:100%;">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Saved Sequences Card -->
|
|
<div class="card">
|
|
<div class="card-header"><span class="card-title">Saved Sequences</span></div>
|
|
<div id="saved-seq-list" style="display:flex; flex-direction:column; gap:8px;">
|
|
<!-- Populated dynamically -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 = `<div style="text-align: center; color: var(--text-muted); padding: 20px;">No steps added yet.</div>`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = sequence.steps.map((step, idx) => {
|
|
let label = '';
|
|
let details = '';
|
|
if (step.type === 'spot') {
|
|
label = `Spot: <b>${step.id}</b>`;
|
|
} else {
|
|
label = `XY: <b>(${step.x}, ${step.y})</b>`;
|
|
}
|
|
|
|
if (step.delay !== undefined) {
|
|
details = `<span style="font-size: 0.8rem; color: var(--accent-amber); margin-left: 8px;">Delay: ${step.delay}ms</span>`;
|
|
}
|
|
|
|
return `
|
|
<div class="step-box">
|
|
<div>
|
|
<span style="font-family: monospace; font-size: 0.9rem; color: var(--text-muted); margin-right: 8px;">#${idx+1}</span>
|
|
${label} ${details}
|
|
</div>
|
|
<div class="step-controls">
|
|
<button class="btn-icon btn-move-up" data-idx="${idx}" title="Move Up">↑</button>
|
|
<button class="btn-icon btn-move-down" data-idx="${idx}" title="Move Down">↓</button>
|
|
<button class="btn-icon btn-delete-step" data-idx="${idx}" title="Delete" style="color: var(--accent-red);">✕</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).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 = '<option value="">-- Select Spot --</option>' + spots.map(s =>
|
|
`<option value="${s.id}">${s.label || s.id} [${s.x}, ${s.y}]</option>`
|
|
).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 += `<div style="font-size:0.75rem; color:var(--text-muted); text-transform:uppercase; margin-top:4px;">From sequences.json</div>`;
|
|
html += bgSequences.map((s, idx) => `
|
|
<div style="display:flex; justify-content:space-between; align-items:center; background:var(--surface-2); padding:8px; border-radius:var(--radius-sm); border:1px solid var(--border);">
|
|
<span style="font-size:0.9rem;">${s.name}</span>
|
|
<button class="btn btn-sm btn-ghost btn-load-seq" data-source="bg" data-idx="${idx}">Load</button>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
if (savedBrowserSequences.length > 0) {
|
|
html += `<div style="font-size:0.75rem; color:var(--text-muted); text-transform:uppercase; margin-top:8px;">From Browser Storage</div>`;
|
|
html += savedBrowserSequences.map((s, idx) => `
|
|
<div style="display:flex; justify-content:space-between; align-items:center; background:var(--surface-2); padding:8px; border-radius:var(--radius-sm); border:1px solid var(--border);">
|
|
<span style="font-size:0.9rem;">${s.name}</span>
|
|
<div style="display:flex; gap:4px;">
|
|
<button class="btn btn-sm btn-ghost btn-load-seq" data-source="browser" data-idx="${idx}">Load</button>
|
|
<button class="btn btn-sm btn-ghost btn-delete-seq" data-idx="${idx}" style="color:var(--accent-red);" title="Delete from Browser Storage">✕</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
if (!html) {
|
|
html = `<div style="text-align: center; color: var(--text-muted); font-size: 0.85rem; padding: 12px;">No saved sequences</div>`;
|
|
}
|
|
|
|
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();
|
|
|
|
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;
|
|
saveSteps();
|
|
updatePositionReadout();
|
|
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 = [];
|
|
}
|
|
};
|