diff --git a/templates/editor.html b/templates/editor.html
index b59d04b..993ead5 100644
--- a/templates/editor.html
+++ b/templates/editor.html
@@ -590,6 +590,8 @@ const defaultEquipment = [
let simEditMode = false;
let dragEquip = null, dragOffset = {x:0, y:0};
+let selectedEquip = null;
+let resizeHandle = null; // 'w','h','r','rot'
function getEquipment() {
return layout.equipment || JSON.parse(JSON.stringify(defaultEquipment));
@@ -604,148 +606,178 @@ function toggleSimEdit() {
renderSchematic();
}
+function svgPt(e) {
+ const svg = document.getElementById('sim-svg');
+ const pt = svg.createSVGPoint();
+ pt.x = e.clientX; pt.y = e.clientY;
+ return pt.matrixTransform(svg.getScreenCTM().inverse());
+}
+
+function saveEquip(eq) {
+ if (!layout.equipment) layout.equipment = getEquipment();
+ const e = layout.equipment.find(x => x.id === eq.id);
+ if (e) Object.assign(e, {x:eq.x, y:eq.y, w:eq.w, h:eq.h, r:eq.r, rot:eq.rot, label:eq.label});
+}
+
+function mkSvgEl(tag, attrs) {
+ const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
+ for (const [k,v] of Object.entries(attrs)) el.setAttribute(k, v);
+ return el;
+}
+
+function handle(svg, cx, cy, type, eq, cursor) {
+ const h = mkSvgEl('circle', {cx, cy, r:'6', fill:'var(--blue)', stroke:'#fff', 'stroke-width':'1.5', cursor, 'data-handle':type});
+ h.addEventListener('mousedown', e => { e.stopPropagation(); resizeHandle = type; dragEquip = eq; dragOffset = svgPt(e); });
+ svg.appendChild(h);
+}
+
function renderSchematic() {
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';
+ const container = document.getElementById('sim-container');
+ container.innerHTML = '';
- // Defs for arrows
+ const svg = mkSvgEl('svg', {'class':'schematic', viewBox:'0 0 800 400', id:'sim-svg'});
svg.innerHTML = ``;
-
- // IN/OUT labels
- svg.innerHTML += `IN ▶`;
- svg.innerHTML += `▶ OUT`;
-
- document.getElementById('sim-container').innerHTML = '';
- document.getElementById('sim-container').appendChild(svg);
+ 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);
equip.forEach(eq => {
- const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
- g.dataset.eqId = eq.id;
+ if (eq.rot === undefined) eq.rot = 0;
+ const g = mkSvgEl('g', {'data-eq-id': eq.id});
+ const isSel = selectedEquip && selectedEquip.id === eq.id;
+
+ // Apply rotation around center
+ let cx, cy;
+ if (eq.shape === 'rect') {
+ cx = eq.x + (eq.w||80)/2; cy = eq.y + (eq.h||60)/2;
+ } else {
+ cx = eq.x; cy = eq.y;
+ }
+ if (eq.rot) g.setAttribute('transform', `rotate(${eq.rot} ${cx} ${cy})`);
g.style.cursor = simEditMode ? 'grab' : 'default';
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);
+ g.appendChild(mkSvgEl('rect', {'class':'equip', x:eq.x, y:eq.y, width:eq.w||80, height:eq.h||60, rx:'6',
+ ...(simEditMode ? {stroke: isSel?'var(--blue)':'var(--amber)', 'stroke-width': isSel?'3':'2', 'stroke-dasharray': isSel?'':'4 4'} : {})}));
+ const t = mkSvgEl('text', {'class':'equip-label', x:eq.x+(eq.w||80)/2, y:eq.y+(eq.h||60)/2+5});
+ t.textContent = eq.label; g.appendChild(t);
- 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);
-
- // 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);
+ 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,
+ stroke:'var(--green)', 'stroke-width':'3', 'stroke-dasharray':'10 10'}));
}
- } 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);
+ } else {
+ const r = eq.r || 20;
+ g.appendChild(mkSvgEl('circle', {'class':'equip', cx:eq.x, cy:eq.y, r,
+ ...(simEditMode ? {stroke: isSel?'var(--blue)':'var(--amber)', 'stroke-width': isSel?'3':'2', 'stroke-dasharray': isSel?'':'4 4'} : {})}));
+ const t = mkSvgEl('text', {'class':'equip-label', x:eq.x, y:eq.y+4, 'font-size': r<15?'8':'9'});
+ t.textContent = eq.label; g.appendChild(t);
- 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);
-
- // 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);
+ 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'}));
} 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 = `
- `;
+ 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 = ``;
g.appendChild(ag);
}
}
-
- // 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'); }
- }
-
svg.appendChild(g);
- // Drag in edit mode
if (simEditMode) {
- g.addEventListener('mousedown', (e) => startDragEquip(e, eq));
+ g.addEventListener('mousedown', e => {
+ if (resizeHandle) return;
+ selectedEquip = eq;
+ dragEquip = eq;
+ const p = svgPt(e);
+ dragOffset = {x: p.x - eq.x, y: p.y - eq.y};
+ e.preventDefault();
+ renderSchematic();
+ });
g.addEventListener('dblclick', () => renameEquip(eq));
}
});
- if (simEditMode) {
- svg.addEventListener('mousemove', moveDragEquip);
- svg.addEventListener('mouseup', stopDragEquip);
- svg.addEventListener('mouseleave', stopDragEquip);
+ // Draw selection handles for selected equipment
+ if (simEditMode && selectedEquip) {
+ const eq = selectedEquip;
+ if (eq.shape === 'rect') {
+ 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');
+ // Height handle (bottom middle)
+ 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');
+ // Rotation handle (above top center)
+ 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'});
+ 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'}));
+ 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');
+ // Rotation handle (above)
+ 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'});
+ 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'}));
+ rotH.addEventListener('mousedown', e => { e.stopPropagation(); resizeHandle = 'rot'; dragEquip = eq; dragOffset = {cx:eq.x, cy:eq.y}; });
+ svg.appendChild(rotH);
+ }
}
+ if (simEditMode) {
+ svg.addEventListener('mousemove', onSvgMouseMove);
+ svg.addEventListener('mouseup', onSvgMouseUp);
+ svg.addEventListener('mouseleave', onSvgMouseUp);
+ // Click on empty space deselects
+ svg.addEventListener('click', e => { if (e.target === svg) { selectedEquip = null; renderSchematic(); } });
+ }
updateSchematicAnimations();
}
-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) {
+function onSvgMouseMove(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();
+ const p = svgPt(e);
+
+ if (resizeHandle === 'rot') {
+ // Rotation: angle from center to mouse
+ const cx = dragOffset.cx, cy = dragOffset.cy;
+ const angle = Math.round(Math.atan2(p.x - cx, -(p.y - cy)) * 180 / Math.PI);
+ dragEquip.rot = angle;
+ saveEquip(dragEquip);
+ renderSchematic();
+ } else if (resizeHandle === 'w') {
+ dragEquip.w = Math.max(30, Math.round(p.x - dragEquip.x));
+ saveEquip(dragEquip); renderSchematic();
+ } else if (resizeHandle === 'h') {
+ dragEquip.h = Math.max(20, Math.round(p.y - dragEquip.y));
+ saveEquip(dragEquip); renderSchematic();
+ } else if (resizeHandle === 'wh') {
+ dragEquip.w = Math.max(30, Math.round(p.x - dragEquip.x));
+ dragEquip.h = Math.max(20, Math.round(p.y - dragEquip.y));
+ saveEquip(dragEquip); renderSchematic();
+ } else if (resizeHandle === 'r') {
+ dragEquip.r = Math.max(8, Math.round(Math.sqrt((p.x-dragEquip.x)**2 + (p.y-dragEquip.y)**2)));
+ saveEquip(dragEquip); renderSchematic();
+ } else if (dragEquip && !resizeHandle) {
+ // Move
+ dragEquip.x = Math.round(p.x - dragOffset.x);
+ dragEquip.y = Math.round(p.y - dragOffset.y);
+ saveEquip(dragEquip);
+ renderSchematic();
+ }
}
-function stopDragEquip() { dragEquip = null; }
+function onSvgMouseUp() { dragEquip = null; resizeHandle = 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;
+ saveEquip(eq);
renderSchematic();
}