mirror of
http://10.0.2.1:3031/sauer/bfa-dryer-design.git
synced 2026-06-30 14:26:42 +10:00
Full schematic editor: shapes, animations, labels, arrows, temp probes
- New shape types: label (text), arrow (flow), temp_probe, output_indicator - Right-click context menu: change shape, animation, color, link to HMI control - Toolbar: add rect/circle/label/arrow/temp probe/output, delete selected - IN/OUT now draggable label elements - Temp probes with pulse animation and color coding - Arrows with adjustable length and rotation - Custom colors on any element (fill + stroke) - All positions/sizes/rotations saved to layout JSON Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d87475558d
commit
748f8d5238
@ -116,6 +116,7 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
|||||||
.conveyor-anim{animation:scroll-belt 0.5s linear infinite}
|
.conveyor-anim{animation:scroll-belt 0.5s linear infinite}
|
||||||
.discharge-anim{animation:fall-particles 1s ease-out infinite}
|
.discharge-anim{animation:fall-particles 1s ease-out infinite}
|
||||||
.agitator-anim{animation:spin 2s linear infinite}
|
.agitator-anim{animation:spin 2s linear infinite}
|
||||||
|
.pulse-anim{animation:pulse-glow 1.5s ease-in-out infinite}
|
||||||
|
|
||||||
/* Help panel */
|
/* Help panel */
|
||||||
.help-panel{position:fixed;right:0;top:48px;width:400px;height:calc(100vh - 48px);background:var(--card);border-left:2px solid var(--border);z-index:150;display:none;flex-direction:column;overflow-y:auto;padding:24px}
|
.help-panel{position:fixed;right:0;top:48px;width:400px;height:calc(100vh - 48px);background:var(--card);border-left:2px solid var(--border);z-index:150;display:none;flex-direction:column;overflow-y:auto;padding:24px}
|
||||||
@ -126,6 +127,12 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
|||||||
.help-panel .help-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center}
|
.help-panel .help-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border-radius:50%;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center}
|
||||||
|
|
||||||
/* Inline edit */
|
/* Inline edit */
|
||||||
|
/* Sim toolbar + context menu */
|
||||||
|
.sim-toolbar button{padding:4px 10px;border:1px solid var(--border);border-radius:4px;background:var(--card);color:var(--text2);font-size:11px;cursor:pointer}
|
||||||
|
.sim-toolbar button:hover{background:var(--border);color:var(--text)}
|
||||||
|
.eq-ctx-item{padding:8px 16px;font-size:13px;cursor:pointer;color:var(--text)}
|
||||||
|
.eq-ctx-item:hover{background:var(--border)}
|
||||||
|
|
||||||
.inline-edit{background:transparent;border:none;border-bottom:2px solid var(--blue);color:var(--text);font-size:inherit;font-family:inherit;text-align:center;outline:none;width:100%}
|
.inline-edit{background:transparent;border:none;border-bottom:2px solid var(--blue);color:var(--text);font-size:inherit;font-family:inherit;text-align:center;outline:none;width:100%}
|
||||||
|
|
||||||
/* Popup */
|
/* Popup */
|
||||||
@ -187,6 +194,26 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
|||||||
MACHINE SIMULATION
|
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>
|
<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>
|
||||||
|
<div class="sim-toolbar" id="sim-toolbar" style="display:none;padding:6px 8px;gap:6px;background:#0d1220;border-bottom:1px solid var(--border);flex-wrap:wrap;flex-shrink:0">
|
||||||
|
<button onclick="addEquip('rect')">+ Rectangle</button>
|
||||||
|
<button onclick="addEquip('circle')">+ Circle</button>
|
||||||
|
<button onclick="addEquip('label')">+ Label</button>
|
||||||
|
<button onclick="addEquip('arrow')">+ Arrow</button>
|
||||||
|
<span style="color:var(--dim)">|</span>
|
||||||
|
<button onclick="addEquip('temp_probe')">+ Temp Probe</button>
|
||||||
|
<button onclick="addEquip('output_indicator')">+ Output</button>
|
||||||
|
<span style="color:var(--dim)">|</span>
|
||||||
|
<button onclick="deleteSelectedEquip()" style="color:var(--red)">Delete Selected</button>
|
||||||
|
</div>
|
||||||
|
<!-- Context menu for equipment properties -->
|
||||||
|
<div id="eq-context" style="display:none;position:fixed;z-index:300;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:4px 0;min-width:180px;box-shadow:0 8px 24px rgba(0,0,0,0.5)">
|
||||||
|
<div class="eq-ctx-item" onclick="ctxAction('rename')">Rename</div>
|
||||||
|
<div class="eq-ctx-item" onclick="ctxAction('shape')">Change Shape</div>
|
||||||
|
<div class="eq-ctx-item" onclick="ctxAction('anim')">Change Animation</div>
|
||||||
|
<div class="eq-ctx-item" onclick="ctxAction('link')">Link to HMI Control</div>
|
||||||
|
<div class="eq-ctx-item" onclick="ctxAction('color')">Change Color</div>
|
||||||
|
<div class="eq-ctx-item" onclick="ctxAction('delete')" style="color:var(--red)">Delete</div>
|
||||||
|
</div>
|
||||||
<div class="sim-container" id="sim-container">
|
<div class="sim-container" id="sim-container">
|
||||||
<!-- SVG schematic inserted by JS -->
|
<!-- SVG schematic inserted by JS -->
|
||||||
</div>
|
</div>
|
||||||
@ -586,6 +613,15 @@ const defaultEquipment = [
|
|||||||
{id:'eq_mill', label:'Mill', shape:'rect', x:580, y:250, w:70, h:50, anim:null, link:'output_4'},
|
{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_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'},
|
{id:'eq_brush', label:'Brush', shape:'rect', x:670, y:160, w:60, h:40, anim:null, link:'output_1'},
|
||||||
|
{id:'eq_in', label:'IN \u25B6', shape:'label', x:60, y:148, anim:null, link:null, color:'#4a5670'},
|
||||||
|
{id:'eq_out', label:'\u25B6 OUT', shape:'label', x:740, y:320, anim:null, link:null, color:'#4a5670'},
|
||||||
|
{id:'eq_t_heat', label:'T1', shape:'circle', x:230, y:180, r:10, anim:'pulse', link:'temp_0', color:'#ff4444'},
|
||||||
|
{id:'eq_t_prod1', label:'T2', shape:'circle', x:310, y:200, r:10, anim:'pulse', link:'temp_1', color:'#ff8844'},
|
||||||
|
{id:'eq_t_prod2', label:'T3', shape:'circle', x:460, y:195, r:10, anim:'pulse', link:'temp_2', color:'#ff8844'},
|
||||||
|
{id:'eq_t_exh', label:'T4', shape:'circle', x:350, y:130, r:10, anim:'pulse', link:'temp_3', color:'#44aaff'},
|
||||||
|
{id:'eq_arrow1', label:'', shape:'arrow', x:130, y:200, w:60, anim:null, link:null},
|
||||||
|
{id:'eq_arrow2', label:'', shape:'arrow', x:345, y:200, w:55, anim:null, link:null},
|
||||||
|
{id:'eq_arrow3', label:'', shape:'arrow', x:545, y:190, w:30, anim:null, link:null},
|
||||||
];
|
];
|
||||||
|
|
||||||
let simEditMode = false;
|
let simEditMode = false;
|
||||||
@ -599,10 +635,103 @@ function getEquipment() {
|
|||||||
|
|
||||||
function toggleSimEdit() {
|
function toggleSimEdit() {
|
||||||
simEditMode = !simEditMode;
|
simEditMode = !simEditMode;
|
||||||
|
selectedEquip = null;
|
||||||
const btn = document.getElementById('btn-sim-edit');
|
const btn = document.getElementById('btn-sim-edit');
|
||||||
btn.textContent = simEditMode ? 'DONE EDITING' : 'EDIT LAYOUT';
|
btn.textContent = simEditMode ? 'DONE EDITING' : 'EDIT LAYOUT';
|
||||||
btn.style.background = simEditMode ? 'var(--amber)' : 'var(--card)';
|
btn.style.background = simEditMode ? 'var(--amber)' : 'var(--card)';
|
||||||
btn.style.color = simEditMode ? '#000' : 'var(--text2)';
|
btn.style.color = simEditMode ? '#000' : 'var(--text2)';
|
||||||
|
document.getElementById('sim-toolbar').style.display = simEditMode ? 'flex' : 'none';
|
||||||
|
hideCtx();
|
||||||
|
renderSchematic();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
let ctxEquip = null;
|
||||||
|
function showCtx(e, eq) {
|
||||||
|
e.preventDefault();
|
||||||
|
ctxEquip = eq;
|
||||||
|
const m = document.getElementById('eq-context');
|
||||||
|
m.style.display = 'block';
|
||||||
|
m.style.left = e.clientX + 'px';
|
||||||
|
m.style.top = e.clientY + 'px';
|
||||||
|
}
|
||||||
|
function hideCtx() {
|
||||||
|
document.getElementById('eq-context').style.display = 'none';
|
||||||
|
ctxEquip = null;
|
||||||
|
}
|
||||||
|
document.addEventListener('click', e => { if (!e.target.closest('#eq-context')) hideCtx(); });
|
||||||
|
|
||||||
|
function ctxAction(action) {
|
||||||
|
if (!ctxEquip) return;
|
||||||
|
const eq = ctxEquip;
|
||||||
|
hideCtx();
|
||||||
|
|
||||||
|
if (action === 'rename') {
|
||||||
|
renameEquip(eq);
|
||||||
|
} else if (action === 'shape') {
|
||||||
|
const shapes = ['rect', 'circle', 'label', 'arrow'];
|
||||||
|
const cur = shapes.indexOf(eq.shape);
|
||||||
|
const choice = prompt('Shape — type one of: rect, circle, label, arrow\nCurrent: ' + eq.shape, eq.shape);
|
||||||
|
if (choice && shapes.includes(choice)) {
|
||||||
|
eq.shape = choice;
|
||||||
|
if (choice === 'rect' && !eq.w) { eq.w = 80; eq.h = 60; }
|
||||||
|
if (choice === 'circle' && !eq.r) { eq.r = 20; }
|
||||||
|
saveEquip(eq); renderSchematic();
|
||||||
|
}
|
||||||
|
} else if (action === 'anim') {
|
||||||
|
const anims = ['none', 'fan', 'agitator', 'burner', 'conveyor', 'pulse'];
|
||||||
|
const choice = prompt('Animation — type one of: none, fan, agitator, burner, conveyor, pulse\nCurrent: ' + (eq.anim || 'none'), eq.anim || 'none');
|
||||||
|
if (choice && anims.includes(choice)) {
|
||||||
|
eq.anim = choice === 'none' ? null : choice;
|
||||||
|
saveEquip(eq); renderSchematic();
|
||||||
|
}
|
||||||
|
} else if (action === 'link') {
|
||||||
|
// Show all available HMI card IDs
|
||||||
|
const ids = [];
|
||||||
|
layout.pages.forEach(p => p.cards.forEach(c => ids.push(c.id + ' (' + c.label + ')')));
|
||||||
|
const choice = prompt('Link to HMI control ID:\n\nAvailable:\n' + ids.join('\n') + '\n\nCurrent: ' + (eq.link || 'none') + '\nType ID or "none":', eq.link || '');
|
||||||
|
if (choice !== null) {
|
||||||
|
eq.link = choice === 'none' || choice === '' ? null : choice.split(' ')[0];
|
||||||
|
saveEquip(eq); renderSchematic();
|
||||||
|
}
|
||||||
|
} else if (action === 'color') {
|
||||||
|
const choice = prompt('Color hex (e.g. #ff4444, #44aaff, #00c853):\nCurrent: ' + (eq.color || 'default'), eq.color || '#7a8baa');
|
||||||
|
if (choice) { eq.color = choice; saveEquip(eq); renderSchematic(); }
|
||||||
|
} else if (action === 'delete') {
|
||||||
|
deleteEquipById(eq.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEquip(type) {
|
||||||
|
if (!layout.equipment) layout.equipment = getEquipment();
|
||||||
|
const id = 'eq_' + Date.now();
|
||||||
|
const base = {id, label:'New', rot:0, anim:null, link:null, color:null};
|
||||||
|
|
||||||
|
if (type === 'rect') {
|
||||||
|
layout.equipment.push({...base, shape:'rect', x:350, y:150, w:80, h:60});
|
||||||
|
} else if (type === 'circle') {
|
||||||
|
layout.equipment.push({...base, shape:'circle', x:400, y:200, r:25});
|
||||||
|
} else if (type === 'label') {
|
||||||
|
layout.equipment.push({...base, shape:'label', label:'Label', x:400, y:100});
|
||||||
|
} else if (type === 'arrow') {
|
||||||
|
layout.equipment.push({...base, shape:'arrow', x:300, y:200, w:80, label:''});
|
||||||
|
} else if (type === 'temp_probe') {
|
||||||
|
layout.equipment.push({...base, shape:'circle', label:'T', r:14, x:380, y:180, color:'#ff4444', anim:'pulse', link:null});
|
||||||
|
} else if (type === 'output_indicator') {
|
||||||
|
layout.equipment.push({...base, shape:'rect', label:'Output', x:360, y:160, w:60, h:35, color:'#00c853', link:null});
|
||||||
|
}
|
||||||
|
renderSchematic();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelectedEquip() {
|
||||||
|
if (!selectedEquip) { alert('Click an element first'); return; }
|
||||||
|
deleteEquipById(selectedEquip.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEquipById(id) {
|
||||||
|
if (!layout.equipment) layout.equipment = getEquipment();
|
||||||
|
layout.equipment = layout.equipment.filter(e => e.id !== id);
|
||||||
|
if (selectedEquip && selectedEquip.id === id) selectedEquip = null;
|
||||||
renderSchematic();
|
renderSchematic();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -638,57 +767,92 @@ function renderSchematic() {
|
|||||||
|
|
||||||
const svg = mkSvgEl('svg', {'class':'schematic', viewBox:'0 0 800 400', id:'sim-svg'});
|
const svg = mkSvgEl('svg', {'class':'schematic', viewBox:'0 0 800 400', id:'sim-svg'});
|
||||||
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>`;
|
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>`;
|
||||||
svg.appendChild(mkSvgEl('text', {x:'60',y:'148',fill:'var(--dim)','font-size':'10','text-anchor':'middle','font-family':'sans-serif'})).textContent = 'IN \u25B6';
|
|
||||||
svg.appendChild(mkSvgEl('text', {x:'740',y:'320',fill:'var(--dim)','font-size':'10','text-anchor':'middle','font-family':'sans-serif'})).textContent = '\u25B6 OUT';
|
|
||||||
container.appendChild(svg);
|
container.appendChild(svg);
|
||||||
|
|
||||||
equip.forEach(eq => {
|
equip.forEach(eq => {
|
||||||
if (eq.rot === undefined) eq.rot = 0;
|
if (eq.rot === undefined) eq.rot = 0;
|
||||||
const g = mkSvgEl('g', {'data-eq-id': eq.id});
|
const g = mkSvgEl('g', {'data-eq-id': eq.id});
|
||||||
const isSel = selectedEquip && selectedEquip.id === eq.id;
|
const isSel = selectedEquip && selectedEquip.id === eq.id;
|
||||||
|
const col = eq.color || null;
|
||||||
|
const editStroke = simEditMode ? {stroke: isSel?'var(--blue)':'var(--amber)', 'stroke-width': isSel?'3':'2', 'stroke-dasharray': isSel?'':'4 4'} : {};
|
||||||
|
|
||||||
// Apply rotation around center
|
// Rotation center
|
||||||
let cx, cy;
|
let cx, cy;
|
||||||
if (eq.shape === 'rect') {
|
if (eq.shape === 'rect' || eq.shape === 'arrow') { cx = eq.x+(eq.w||80)/2; cy = eq.y+(eq.h||60)/2; }
|
||||||
cx = eq.x + (eq.w||80)/2; cy = eq.y + (eq.h||60)/2;
|
else { cx = eq.x; cy = eq.y; }
|
||||||
} else {
|
|
||||||
cx = eq.x; cy = eq.y;
|
|
||||||
}
|
|
||||||
if (eq.rot) g.setAttribute('transform', `rotate(${eq.rot} ${cx} ${cy})`);
|
if (eq.rot) g.setAttribute('transform', `rotate(${eq.rot} ${cx} ${cy})`);
|
||||||
g.style.cursor = simEditMode ? 'grab' : 'default';
|
g.style.cursor = simEditMode ? 'grab' : 'default';
|
||||||
|
|
||||||
|
// ── RECT ──
|
||||||
if (eq.shape === 'rect') {
|
if (eq.shape === 'rect') {
|
||||||
g.appendChild(mkSvgEl('rect', {'class':'equip', x:eq.x, y:eq.y, width:eq.w||80, height:eq.h||60, rx:'6',
|
const w=eq.w||80, h=eq.h||60;
|
||||||
...(simEditMode ? {stroke: isSel?'var(--blue)':'var(--amber)', 'stroke-width': isSel?'3':'2', 'stroke-dasharray': isSel?'':'4 4'} : {})}));
|
const rectAttrs = {'class':'equip', x:eq.x, y:eq.y, width:w, height:h, rx:'6', ...editStroke};
|
||||||
const t = mkSvgEl('text', {'class':'equip-label', x:eq.x+(eq.w||80)/2, y:eq.y+(eq.h||60)/2+5});
|
if (col) { rectAttrs.fill = col; rectAttrs['fill-opacity'] = '0.2'; rectAttrs.stroke = col; rectAttrs['stroke-width'] = '2'; rectAttrs['stroke-dasharray'] = ''; }
|
||||||
|
g.appendChild(mkSvgEl('rect', rectAttrs));
|
||||||
|
const t = mkSvgEl('text', {'class':'equip-label', x:eq.x+w/2, y:eq.y+h/2+5, ...(col?{fill:col}:{})});
|
||||||
t.textContent = eq.label; g.appendChild(t);
|
t.textContent = eq.label; g.appendChild(t);
|
||||||
|
|
||||||
if (eq.anim === 'conveyor') {
|
if (eq.anim === 'conveyor') {
|
||||||
g.appendChild(mkSvgEl('line', {'class':'anim-overlay conveyor-anim', id:'sim-'+eq.id,
|
g.appendChild(mkSvgEl('line', {'class':'anim-overlay conveyor-anim', id:'sim-'+eq.id,
|
||||||
x1:eq.x+10, y1:eq.y+(eq.h||60)-5, x2:eq.x+(eq.w||80)-10, y2:eq.y+(eq.h||60)-5,
|
x1:eq.x+10, y1:eq.y+h-5, x2:eq.x+w-10, y2:eq.y+h-5,
|
||||||
stroke:'var(--green)', 'stroke-width':'3', 'stroke-dasharray':'10 10'}));
|
stroke: col||'var(--green)', 'stroke-width':'3', 'stroke-dasharray':'10 10'}));
|
||||||
|
} else if (eq.anim === 'pulse') {
|
||||||
|
g.appendChild(mkSvgEl('rect', {'class':'anim-overlay pulse-anim', id:'sim-'+eq.id,
|
||||||
|
x:eq.x, y:eq.y, width:w, height:h, rx:'6',
|
||||||
|
fill:'none', stroke: col||'var(--green)', 'stroke-width':'3'}));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const r = eq.r || 20;
|
// ── CIRCLE ──
|
||||||
g.appendChild(mkSvgEl('circle', {'class':'equip', cx:eq.x, cy:eq.y, r,
|
} else if (eq.shape === 'circle') {
|
||||||
...(simEditMode ? {stroke: isSel?'var(--blue)':'var(--amber)', 'stroke-width': isSel?'3':'2', 'stroke-dasharray': isSel?'':'4 4'} : {})}));
|
const r = eq.r||20;
|
||||||
const t = mkSvgEl('text', {'class':'equip-label', x:eq.x, y:eq.y+4, 'font-size': r<15?'8':'9'});
|
const circAttrs = {'class':'equip', cx:eq.x, cy:eq.y, r, ...editStroke};
|
||||||
|
if (col) { circAttrs.fill = col; circAttrs['fill-opacity'] = '0.25'; circAttrs.stroke = col; circAttrs['stroke-width'] = '2'; circAttrs['stroke-dasharray'] = ''; }
|
||||||
|
g.appendChild(mkSvgEl('circle', circAttrs));
|
||||||
|
const t = mkSvgEl('text', {'class':'equip-label', x:eq.x, y:eq.y+4, 'font-size':r<15?'8':'10', ...(col?{fill:col}:{})});
|
||||||
t.textContent = eq.label; g.appendChild(t);
|
t.textContent = eq.label; g.appendChild(t);
|
||||||
|
|
||||||
if (eq.anim === 'burner') {
|
if (eq.anim === 'burner') {
|
||||||
g.appendChild(mkSvgEl('circle', {'class':'anim-overlay burner-glow', id:'sim-'+eq.id,
|
g.appendChild(mkSvgEl('circle', {'class':'anim-overlay burner-glow', id:'sim-'+eq.id,
|
||||||
cx:eq.x, cy:eq.y, r:r+4, fill:'none', stroke:'var(--amber)', 'stroke-width':'4'}));
|
cx:eq.x, cy:eq.y, r:r+4, fill:'none', stroke:col||'var(--amber)', 'stroke-width':'4'}));
|
||||||
} else if (eq.anim === 'fan' || eq.anim === 'agitator') {
|
} else if (eq.anim === 'fan' || eq.anim === 'agitator') {
|
||||||
const ag = mkSvgEl('g', {'class':`anim-overlay ${eq.anim==='fan'?'fan-anim':'agitator-anim'}`, id:'sim-'+eq.id, 'transform-origin':`${eq.x} ${eq.y}`});
|
const ag = mkSvgEl('g', {'class':`anim-overlay ${eq.anim==='fan'?'fan-anim':'agitator-anim'}`, id:'sim-'+eq.id, 'transform-origin':`${eq.x} ${eq.y}`});
|
||||||
ag.innerHTML = `<line x1="${eq.x}" y1="${eq.y-r}" x2="${eq.x}" y2="${eq.y+r}" stroke="var(--blue)" stroke-width="2"/><line x1="${eq.x-r}" y1="${eq.y}" x2="${eq.x+r}" y2="${eq.y}" stroke="var(--blue)" stroke-width="2"/>`;
|
ag.innerHTML = `<line x1="${eq.x}" y1="${eq.y-r}" x2="${eq.x}" y2="${eq.y+r}" stroke="${col||'var(--blue)'}" stroke-width="2"/><line x1="${eq.x-r}" y1="${eq.y}" x2="${eq.x+r}" y2="${eq.y}" stroke="${col||'var(--blue)'}" stroke-width="2"/>`;
|
||||||
g.appendChild(ag);
|
g.appendChild(ag);
|
||||||
|
} else if (eq.anim === 'pulse') {
|
||||||
|
g.appendChild(mkSvgEl('circle', {'class':'anim-overlay pulse-anim', id:'sim-'+eq.id,
|
||||||
|
cx:eq.x, cy:eq.y, r:r+3, fill:'none', stroke:col||'var(--green)', 'stroke-width':'2'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LABEL ──
|
||||||
|
} else if (eq.shape === 'label') {
|
||||||
|
const t = mkSvgEl('text', {x:eq.x, y:eq.y, fill:col||'var(--dim)', 'font-size':'12', 'text-anchor':'middle', 'font-family':'sans-serif', 'font-weight':'600'});
|
||||||
|
t.textContent = eq.label; g.appendChild(t);
|
||||||
|
if (simEditMode) {
|
||||||
|
// Invisible hit area for dragging
|
||||||
|
g.appendChild(mkSvgEl('rect', {x:eq.x-30, y:eq.y-12, width:60, height:18, fill:'transparent', ...editStroke}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ARROW ──
|
||||||
|
} else if (eq.shape === 'arrow') {
|
||||||
|
const w = eq.w || 60;
|
||||||
|
g.appendChild(mkSvgEl('line', {'class':'flow-arrow', x1:eq.x, y1:eq.y, x2:eq.x+w, y2:eq.y,
|
||||||
|
fill:'none', stroke:col||'var(--dim)', 'stroke-width':'2', 'marker-end':'url(#arrowhead)'}));
|
||||||
|
if (eq.label) {
|
||||||
|
const t = mkSvgEl('text', {x:eq.x+w/2, y:eq.y-6, fill:col||'var(--dim)', 'font-size':'9', 'text-anchor':'middle', 'font-family':'sans-serif'});
|
||||||
|
t.textContent = eq.label; g.appendChild(t);
|
||||||
|
}
|
||||||
|
if (simEditMode) {
|
||||||
|
g.appendChild(mkSvgEl('rect', {x:eq.x-2, y:eq.y-6, width:w+4, height:12, fill:'transparent', ...editStroke}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.appendChild(g);
|
svg.appendChild(g);
|
||||||
|
|
||||||
|
// Edit mode events
|
||||||
if (simEditMode) {
|
if (simEditMode) {
|
||||||
g.addEventListener('mousedown', e => {
|
g.addEventListener('mousedown', e => {
|
||||||
if (resizeHandle) return;
|
if (resizeHandle) return;
|
||||||
|
hideCtx();
|
||||||
selectedEquip = eq;
|
selectedEquip = eq;
|
||||||
dragEquip = eq;
|
dragEquip = eq;
|
||||||
const p = svgPt(e);
|
const p = svgPt(e);
|
||||||
@ -697,35 +861,36 @@ function renderSchematic() {
|
|||||||
renderSchematic();
|
renderSchematic();
|
||||||
});
|
});
|
||||||
g.addEventListener('dblclick', () => renameEquip(eq));
|
g.addEventListener('dblclick', () => renameEquip(eq));
|
||||||
|
g.addEventListener('contextmenu', e => showCtx(e, eq));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Draw selection handles for selected equipment
|
// Draw selection handles for selected equipment
|
||||||
if (simEditMode && selectedEquip) {
|
if (simEditMode && selectedEquip) {
|
||||||
const eq = selectedEquip;
|
const eq = selectedEquip;
|
||||||
|
const addRotHandle = (hx, hy, rcx, rcy) => {
|
||||||
|
svg.appendChild(mkSvgEl('line', {x1:hx, y1:hy+6, x2:hx, y2:hy-14, stroke:'var(--amber)', 'stroke-width':'1', 'stroke-dasharray':'3 2'}));
|
||||||
|
const rh = mkSvgEl('circle', {cx:hx, cy:hy-20, r:'6', fill:'var(--amber)', stroke:'#fff', 'stroke-width':'1.5', cursor:'crosshair'});
|
||||||
|
rh.addEventListener('mousedown', e => { e.stopPropagation(); resizeHandle='rot'; dragEquip=eq; dragOffset={cx:rcx,cy:rcy}; });
|
||||||
|
svg.appendChild(rh);
|
||||||
|
};
|
||||||
|
|
||||||
if (eq.shape === 'rect') {
|
if (eq.shape === 'rect') {
|
||||||
const w = eq.w||80, h = eq.h||60;
|
const w=eq.w||80, h=eq.h||60;
|
||||||
// Width handle (right middle)
|
|
||||||
handle(svg, eq.x+w, eq.y+h/2, 'w', eq, 'ew-resize');
|
handle(svg, eq.x+w, eq.y+h/2, 'w', eq, 'ew-resize');
|
||||||
// Height handle (bottom middle)
|
|
||||||
handle(svg, eq.x+w/2, eq.y+h, 'h', eq, 'ns-resize');
|
handle(svg, eq.x+w/2, eq.y+h, 'h', eq, 'ns-resize');
|
||||||
// Corner handle (bottom-right for both)
|
|
||||||
handle(svg, eq.x+w, eq.y+h, 'wh', eq, 'nwse-resize');
|
handle(svg, eq.x+w, eq.y+h, 'wh', eq, 'nwse-resize');
|
||||||
// Rotation handle (above top center)
|
addRotHandle(eq.x+w/2, eq.y, eq.x+w/2, eq.y+h/2);
|
||||||
const rotH = mkSvgEl('circle', {cx:eq.x+w/2, cy:eq.y-25, r:'6', fill:'var(--amber)', stroke:'#fff', 'stroke-width':'1.5', cursor:'crosshair', 'data-handle':'rot'});
|
} else if (eq.shape === 'circle') {
|
||||||
svg.appendChild(mkSvgEl('line', {x1:eq.x+w/2, y1:eq.y, x2:eq.x+w/2, y2:eq.y-19, stroke:'var(--amber)', 'stroke-width':'1', 'stroke-dasharray':'3 2'}));
|
const r=eq.r||20;
|
||||||
rotH.addEventListener('mousedown', e => { e.stopPropagation(); resizeHandle = 'rot'; dragEquip = eq; dragOffset = {cx:eq.x+w/2, cy:eq.y+(eq.h||60)/2}; });
|
|
||||||
svg.appendChild(rotH);
|
|
||||||
} else {
|
|
||||||
const r = eq.r || 20;
|
|
||||||
// Radius handle (right edge)
|
|
||||||
handle(svg, eq.x+r, eq.y, 'r', eq, 'ew-resize');
|
handle(svg, eq.x+r, eq.y, 'r', eq, 'ew-resize');
|
||||||
// Rotation handle (above)
|
addRotHandle(eq.x, eq.y-r, eq.x, eq.y);
|
||||||
const rotH = mkSvgEl('circle', {cx:eq.x, cy:eq.y-r-20, r:'6', fill:'var(--amber)', stroke:'#fff', 'stroke-width':'1.5', cursor:'crosshair'});
|
} else if (eq.shape === 'arrow') {
|
||||||
svg.appendChild(mkSvgEl('line', {x1:eq.x, y1:eq.y-r, x2:eq.x, y2:eq.y-r-14, stroke:'var(--amber)', 'stroke-width':'1', 'stroke-dasharray':'3 2'}));
|
// Length handle at end of arrow
|
||||||
rotH.addEventListener('mousedown', e => { e.stopPropagation(); resizeHandle = 'rot'; dragEquip = eq; dragOffset = {cx:eq.x, cy:eq.y}; });
|
handle(svg, eq.x+(eq.w||60), eq.y, 'w', eq, 'ew-resize');
|
||||||
svg.appendChild(rotH);
|
addRotHandle(eq.x+(eq.w||60)/2, eq.y, eq.x+(eq.w||60)/2, eq.y);
|
||||||
}
|
}
|
||||||
|
// Labels: no resize handles, just move + rotate not needed
|
||||||
}
|
}
|
||||||
|
|
||||||
if (simEditMode) {
|
if (simEditMode) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user