mirror of
http://10.0.2.1:3031/sauer/bfa-dryer-design.git
synced 2026-06-30 10:06:42 +10:00
Editable machine simulation: drag equipment, rename labels, data-driven SVG
This commit is contained in:
parent
5c33a29bc7
commit
5f8e7c0ffd
@ -183,7 +183,10 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
||||
|
||||
<!-- RIGHT: MACHINE SIM -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">MACHINE SIMULATION</div>
|
||||
<div class="panel-header">
|
||||
MACHINE SIMULATION
|
||||
<button id="btn-sim-edit" onclick="toggleSimEdit()" style="margin-left:auto;padding:4px 12px;border:1px solid var(--border);border-radius:4px;background:var(--card);color:var(--text2);font-size:11px;cursor:pointer">EDIT LAYOUT</button>
|
||||
</div>
|
||||
<div class="sim-container" id="sim-container">
|
||||
<!-- SVG schematic inserted by JS -->
|
||||
</div>
|
||||
@ -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 = `
|
||||
<svg class="schematic" viewBox="0 0 800 400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="var(--dim)"/>
|
||||
</marker>
|
||||
</defs>
|
||||
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';
|
||||
|
||||
<!-- Flow arrows -->
|
||||
<line class="flow-arrow" x1="130" y1="200" x2="195" y2="200"/>
|
||||
<line class="flow-arrow" x1="340" y1="200" x2="405" y2="200"/>
|
||||
<line class="flow-arrow" x1="540" y1="200" x2="575" y2="200"/>
|
||||
<line class="flow-arrow" x1="540" y1="280" x2="575" y2="280"/>
|
||||
// Defs for arrows
|
||||
svg.innerHTML = `<defs><marker id="arrowhead" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="var(--dim)"/></marker></defs>`;
|
||||
|
||||
<!-- Loading Hopper -->
|
||||
<rect class="equip" x="40" y="160" width="90" height="80" rx="6"/>
|
||||
<text class="equip-label" x="85" y="205">Loading</text>
|
||||
// IN/OUT labels
|
||||
svg.innerHTML += `<text x="60" y="148" fill="var(--dim)" font-size="10" text-anchor="middle" font-family="sans-serif">IN ▶</text>`;
|
||||
svg.innerHTML += `<text x="740" y="320" fill="var(--dim)" font-size="10" text-anchor="middle" font-family="sans-serif">▶ OUT</text>`;
|
||||
|
||||
<!-- Dryer Drum -->
|
||||
<rect class="equip" x="200" y="130" width="140" height="140" rx="12"/>
|
||||
<text class="equip-label" x="270" y="195">Dryer</text>
|
||||
<text class="equip-label" x="270" y="215" font-size="10" fill="var(--dim)">Drum</text>
|
||||
document.getElementById('sim-container').innerHTML = '';
|
||||
document.getElementById('sim-container').appendChild(svg);
|
||||
|
||||
<!-- Burner (on drum) -->
|
||||
<circle class="equip" cx="200" cy="200" r="20"/>
|
||||
<text class="equip-label" x="200" y="204" font-size="9">🔥</text>
|
||||
<circle class="anim-overlay burner-glow" id="sim-burner" cx="200" cy="200" r="24" fill="none" stroke="var(--amber)" stroke-width="4"/>
|
||||
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 (on drum) -->
|
||||
<circle class="equip" cx="340" cy="150" r="22"/>
|
||||
<text class="equip-label" x="340" y="154" font-size="9">Fan</text>
|
||||
<g class="anim-overlay fan-anim" id="sim-fan" transform-origin="340 150">
|
||||
<line x1="340" y1="130" x2="340" y2="170" stroke="var(--blue)" stroke-width="3"/>
|
||||
<line x1="320" y1="150" x2="360" y2="150" stroke="var(--blue)" stroke-width="3"/>
|
||||
<line x1="326" y1="136" x2="354" y2="164" stroke="var(--blue)" stroke-width="3"/>
|
||||
<line x1="354" y1="136" x2="326" y2="164" stroke="var(--blue)" stroke-width="3"/>
|
||||
</g>
|
||||
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 -->
|
||||
<rect class="equip" x="410" y="180" width="130" height="40" rx="4"/>
|
||||
<text class="equip-label" x="475" y="205">Conveyor</text>
|
||||
<line class="anim-overlay conveyor-anim" id="sim-conveyor" x1="420" y1="210" x2="530" y2="210" stroke="var(--green)" stroke-width="3" stroke-dasharray="10 10"/>
|
||||
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);
|
||||
|
||||
<!-- Agitators (on conveyor) -->
|
||||
<circle class="equip" cx="440" cy="170" r="12"/>
|
||||
<text class="equip-label" x="440" y="174" font-size="8">A1</text>
|
||||
<g class="anim-overlay agitator-anim" id="sim-agit1" transform-origin="440 170">
|
||||
<line x1="440" y1="160" x2="440" y2="180" stroke="var(--blue)" stroke-width="2"/>
|
||||
<line x1="430" y1="170" x2="450" y2="170" stroke="var(--blue)" stroke-width="2"/>
|
||||
</g>
|
||||
// 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);
|
||||
|
||||
<circle class="equip" cx="510" cy="170" r="12"/>
|
||||
<text class="equip-label" x="510" y="174" font-size="8">A2</text>
|
||||
<g class="anim-overlay agitator-anim" id="sim-agit2" transform-origin="510 170">
|
||||
<line x1="510" y1="160" x2="510" y2="180" stroke="var(--blue)" stroke-width="2"/>
|
||||
<line x1="500" y1="170" x2="520" y2="170" stroke="var(--blue)" stroke-width="2"/>
|
||||
</g>
|
||||
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);
|
||||
|
||||
<!-- Spinner -->
|
||||
<circle class="equip" cx="475" cy="155" r="14"/>
|
||||
<text class="equip-label" x="475" y="159" font-size="8">Spin</text>
|
||||
<g class="anim-overlay fan-anim" id="sim-spinner" transform-origin="475 155" style="animation-duration:0.7s">
|
||||
<line x1="475" y1="143" x2="475" y2="167" stroke="var(--green)" stroke-width="2"/>
|
||||
<line x1="463" y1="155" x2="487" y2="155" stroke="var(--green)" stroke-width="2"/>
|
||||
</g>
|
||||
// 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 = `<line x1="${eq.x}" y1="${eq.y-r}" x2="${eq.x}" y2="${eq.y+r}" stroke="${col}" stroke-width="2"/>
|
||||
<line x1="${eq.x-r}" y1="${eq.y}" x2="${eq.x+r}" y2="${eq.y}" stroke="${col}" stroke-width="2"/>`;
|
||||
g.appendChild(ag);
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Discharge -->
|
||||
<rect class="equip" x="580" y="160" width="80" height="60" rx="6"/>
|
||||
<text class="equip-label" x="620" y="195">Discharge</text>
|
||||
// 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 -->
|
||||
<rect class="equip" x="580" y="250" width="70" height="50" rx="6"/>
|
||||
<text class="equip-label" x="615" y="280">Mill</text>
|
||||
svg.appendChild(g);
|
||||
|
||||
<!-- Shaker -->
|
||||
<rect class="equip" x="670" y="250" width="90" height="50" rx="6"/>
|
||||
<text class="equip-label" x="715" y="280">Shaker Sep.</text>
|
||||
// Drag in edit mode
|
||||
if (simEditMode) {
|
||||
g.addEventListener('mousedown', (e) => startDragEquip(e, eq));
|
||||
g.addEventListener('dblclick', () => renameEquip(eq));
|
||||
}
|
||||
});
|
||||
|
||||
<line class="flow-arrow" x1="650" y1="280" x2="665" y2="280"/>
|
||||
<line class="flow-arrow" x1="620" y1="220" x2="620" y2="245"/>
|
||||
if (simEditMode) {
|
||||
svg.addEventListener('mousemove', moveDragEquip);
|
||||
svg.addEventListener('mouseup', stopDragEquip);
|
||||
svg.addEventListener('mouseleave', stopDragEquip);
|
||||
}
|
||||
|
||||
<!-- Brush -->
|
||||
<rect class="equip" x="670" y="160" width="60" height="40" rx="4"/>
|
||||
<text class="equip-label" x="700" y="184" font-size="9">Brush</text>
|
||||
<line class="flow-arrow" x1="660" y1="190" x2="665" y2="190"/>
|
||||
updateSchematicAnimations();
|
||||
}
|
||||
|
||||
<!-- Labels -->
|
||||
<text x="85" y="150" fill="var(--dim)" font-size="10" text-anchor="middle" font-family="sans-serif">IN</text>
|
||||
<text x="715" y="320" fill="var(--dim)" font-size="10" text-anchor="middle" font-family="sans-serif">OUT</text>
|
||||
</svg>`;
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user