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:
Richard Sauer 2026-04-08 12:28:24 +10:00
parent d87475558d
commit 748f8d5238

View File

@ -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) {