diff --git a/templates/editor.html b/templates/editor.html index a7f45bb..b59d04b 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -183,7 +183,10 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
-
MACHINE SIMULATION
+
+ MACHINE SIMULATION + +
@@ -568,112 +571,191 @@ function exportLayout() { function toggleHelp() { document.getElementById('help-panel').classList.toggle('open'); } // ══════════════════════════════════════════════════════ -// MACHINE SCHEMATIC (SVG) +// MACHINE SCHEMATIC — data-driven, editable // ══════════════════════════════════════════════════════ +const defaultEquipment = [ + {id:'eq_loading', label:'Loading', shape:'rect', x:40, y:160, w:90, h:80, anim:null, link:null}, + {id:'eq_drum', label:'Dryer Drum', shape:'rect', x:200, y:130, w:140, h:140, anim:null, link:null}, + {id:'eq_burner', label:'Burner', shape:'circle', x:200, y:200, r:20, anim:'burner', link:'burner_0'}, + {id:'eq_fan', label:'Fan', shape:'circle', x:340, y:150, r:22, anim:'fan', link:'motor_0'}, + {id:'eq_conveyor', label:'Conveyor', shape:'rect', x:410, y:180, w:130, h:40, anim:'conveyor', link:'motor_1'}, + {id:'eq_agit1', label:'A1', shape:'circle', x:440, y:170, r:12, anim:'agitator', link:'motor_3'}, + {id:'eq_agit2', label:'A2', shape:'circle', x:510, y:170, r:12, anim:'agitator', link:'motor_4'}, + {id:'eq_spinner', label:'Spin', shape:'circle', x:475, y:155, r:14, anim:'fan', link:'motor_2'}, + {id:'eq_discharge',label:'Discharge', shape:'rect', x:580, y:160, w:80, h:60, anim:null, link:null}, + {id:'eq_mill', label:'Mill', shape:'rect', x:580, y:250, w:70, h:50, anim:null, link:'output_4'}, + {id:'eq_shaker', label:'Shaker Sep.',shape:'rect', x:670, y:250, w:90, h:50, anim:null, link:'output_5'}, + {id:'eq_brush', label:'Brush', shape:'rect', x:670, y:160, w:60, h:40, anim:null, link:'output_1'}, +]; + +let simEditMode = false; +let dragEquip = null, dragOffset = {x:0, y:0}; + +function getEquipment() { + return layout.equipment || JSON.parse(JSON.stringify(defaultEquipment)); +} + +function toggleSimEdit() { + simEditMode = !simEditMode; + const btn = document.getElementById('btn-sim-edit'); + btn.textContent = simEditMode ? 'DONE EDITING' : 'EDIT LAYOUT'; + btn.style.background = simEditMode ? 'var(--amber)' : 'var(--card)'; + btn.style.color = simEditMode ? '#000' : 'var(--text2)'; + renderSchematic(); +} + function renderSchematic() { - document.getElementById('sim-container').innerHTML = ` - - - - - - + const equip = getEquipment(); + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('class', 'schematic'); + svg.setAttribute('viewBox', '0 0 800 400'); + svg.id = 'sim-svg'; - - - - - + // Defs for arrows + svg.innerHTML = ``; - - - Loading + // IN/OUT labels + svg.innerHTML += `IN ▶`; + svg.innerHTML += `▶ OUT`; - - - Dryer - Drum + document.getElementById('sim-container').innerHTML = ''; + document.getElementById('sim-container').appendChild(svg); - - - 🔥 - + equip.forEach(eq => { + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.dataset.eqId = eq.id; + g.style.cursor = simEditMode ? 'grab' : 'default'; - - - Fan - - - - - - + if (eq.shape === 'rect') { + const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + r.setAttribute('class', 'equip'); + r.setAttribute('x', eq.x); r.setAttribute('y', eq.y); + r.setAttribute('width', eq.w); r.setAttribute('height', eq.h); + r.setAttribute('rx', '6'); + g.appendChild(r); - - - Conveyor - + const t = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + t.setAttribute('class', 'equip-label'); + t.setAttribute('x', eq.x + eq.w/2); t.setAttribute('y', eq.y + eq.h/2 + 5); + t.textContent = eq.label; + g.appendChild(t); - - - A1 - - - - + // Animation overlay + if (eq.anim === 'conveyor') { + const l = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + l.setAttribute('class', 'anim-overlay conveyor-anim'); + l.id = 'sim-' + eq.id; + l.setAttribute('x1', eq.x+10); l.setAttribute('y1', eq.y+eq.h-5); + l.setAttribute('x2', eq.x+eq.w-10); l.setAttribute('y2', eq.y+eq.h-5); + l.setAttribute('stroke', 'var(--green)'); l.setAttribute('stroke-width', '3'); + l.setAttribute('stroke-dasharray', '10 10'); + g.appendChild(l); + } + } else { // circle + const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + c.setAttribute('class', 'equip'); + c.setAttribute('cx', eq.x); c.setAttribute('cy', eq.y); c.setAttribute('r', eq.r || 20); + g.appendChild(c); - - A2 - - - - + const t = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + t.setAttribute('class', 'equip-label'); + t.setAttribute('x', eq.x); t.setAttribute('y', eq.y + 4); + t.setAttribute('font-size', eq.r < 15 ? '8' : '9'); + t.textContent = eq.label; + g.appendChild(t); - - - Spin - - - - + // Animation overlays + if (eq.anim === 'burner') { + const ac = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + ac.setAttribute('class', 'anim-overlay burner-glow'); + ac.id = 'sim-' + eq.id; + ac.setAttribute('cx', eq.x); ac.setAttribute('cy', eq.y); + ac.setAttribute('r', (eq.r||20)+4); + ac.setAttribute('fill', 'none'); ac.setAttribute('stroke', 'var(--amber)'); ac.setAttribute('stroke-width', '4'); + g.appendChild(ac); + } else if (eq.anim === 'fan' || eq.anim === 'agitator') { + const ag = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + ag.setAttribute('class', `anim-overlay ${eq.anim === 'fan' ? 'fan-anim' : 'agitator-anim'}`); + ag.id = 'sim-' + eq.id; + ag.setAttribute('transform-origin', `${eq.x} ${eq.y}`); + const r = eq.r || 20; + const col = eq.anim === 'fan' ? 'var(--blue)' : 'var(--blue)'; + ag.innerHTML = ` + `; + g.appendChild(ag); + } + } - - - Discharge + // Edit mode: border highlight + if (simEditMode) { + const outline = g.querySelector('.equip'); + if (outline) { outline.setAttribute('stroke', 'var(--amber)'); outline.setAttribute('stroke-width', '2'); outline.setAttribute('stroke-dasharray', '4 4'); } + } - - - Mill + svg.appendChild(g); - - - Shaker Sep. + // Drag in edit mode + if (simEditMode) { + g.addEventListener('mousedown', (e) => startDragEquip(e, eq)); + g.addEventListener('dblclick', () => renameEquip(eq)); + } + }); - - + if (simEditMode) { + svg.addEventListener('mousemove', moveDragEquip); + svg.addEventListener('mouseup', stopDragEquip); + svg.addEventListener('mouseleave', stopDragEquip); + } - - - Brush - + updateSchematicAnimations(); +} - - IN - OUT - `; +function startDragEquip(e, eq) { + if (!simEditMode) return; + dragEquip = eq; + const svg = document.getElementById('sim-svg'); + const pt = svg.createSVGPoint(); + pt.x = e.clientX; pt.y = e.clientY; + const svgPt = pt.matrixTransform(svg.getScreenCTM().inverse()); + dragOffset.x = svgPt.x - eq.x; + dragOffset.y = svgPt.y - eq.y; + e.preventDefault(); +} + +function moveDragEquip(e) { + if (!dragEquip) return; + const svg = document.getElementById('sim-svg'); + const pt = svg.createSVGPoint(); + pt.x = e.clientX; pt.y = e.clientY; + const svgPt = pt.matrixTransform(svg.getScreenCTM().inverse()); + dragEquip.x = Math.round(svgPt.x - dragOffset.x); + dragEquip.y = Math.round(svgPt.y - dragOffset.y); + // Save to layout + layout.equipment = getEquipment(); + const eq = layout.equipment.find(e => e.id === dragEquip.id); + if (eq) { eq.x = dragEquip.x; eq.y = dragEquip.y; } + renderSchematic(); +} + +function stopDragEquip() { dragEquip = null; } + +function renameEquip(eq) { + const name = prompt('Rename equipment:', eq.label); + if (!name) return; + eq.label = name; + if (!layout.equipment) layout.equipment = getEquipment(); + const e = layout.equipment.find(x => x.id === eq.id); + if (e) e.label = name; + renderSchematic(); } function updateSchematicAnimations() { - // Map card IDs to SVG animation elements - const map = { - 'motor_0': 'sim-fan', 'burner_0': 'sim-burner', - 'motor_1': 'sim-conveyor', 'motor_2': 'sim-spinner', - 'motor_3': 'sim-agit1', 'motor_4': 'sim-agit2' - }; - Object.entries(map).forEach(([cardId, svgId]) => { - const el = document.getElementById(svgId); + const equip = getEquipment(); + equip.forEach(eq => { + if (!eq.link || !eq.anim) return; + const el = document.getElementById('sim-' + eq.id); if (!el) return; - const s = simState[cardId]; + const s = simState[eq.link]; el.classList.toggle('active', s && s.on); }); }