mirror of
http://10.0.2.1:3031/sauer/bfa-dryer-design.git
synced 2026-06-30 12:56:42 +10:00
Resize + rotate equipment blocks in schematic editor
This commit is contained in:
parent
5f8e7c0ffd
commit
d87475558d
@ -590,6 +590,8 @@ const defaultEquipment = [
|
|||||||
|
|
||||||
let simEditMode = false;
|
let simEditMode = false;
|
||||||
let dragEquip = null, dragOffset = {x:0, y:0};
|
let dragEquip = null, dragOffset = {x:0, y:0};
|
||||||
|
let selectedEquip = null;
|
||||||
|
let resizeHandle = null; // 'w','h','r','rot'
|
||||||
|
|
||||||
function getEquipment() {
|
function getEquipment() {
|
||||||
return layout.equipment || JSON.parse(JSON.stringify(defaultEquipment));
|
return layout.equipment || JSON.parse(JSON.stringify(defaultEquipment));
|
||||||
@ -604,148 +606,178 @@ function toggleSimEdit() {
|
|||||||
renderSchematic();
|
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() {
|
function renderSchematic() {
|
||||||
const equip = getEquipment();
|
const equip = getEquipment();
|
||||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
const container = document.getElementById('sim-container');
|
||||||
svg.setAttribute('class', 'schematic');
|
container.innerHTML = '';
|
||||||
svg.setAttribute('viewBox', '0 0 800 400');
|
|
||||||
svg.id = 'sim-svg';
|
|
||||||
|
|
||||||
// Defs for arrows
|
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';
|
||||||
// IN/OUT labels
|
svg.appendChild(mkSvgEl('text', {x:'740',y:'320',fill:'var(--dim)','font-size':'10','text-anchor':'middle','font-family':'sans-serif'})).textContent = '\u25B6 OUT';
|
||||||
svg.innerHTML += `<text x="60" y="148" fill="var(--dim)" font-size="10" text-anchor="middle" font-family="sans-serif">IN ▶</text>`;
|
container.appendChild(svg);
|
||||||
svg.innerHTML += `<text x="740" y="320" fill="var(--dim)" font-size="10" text-anchor="middle" font-family="sans-serif">▶ OUT</text>`;
|
|
||||||
|
|
||||||
document.getElementById('sim-container').innerHTML = '';
|
|
||||||
document.getElementById('sim-container').appendChild(svg);
|
|
||||||
|
|
||||||
equip.forEach(eq => {
|
equip.forEach(eq => {
|
||||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
if (eq.rot === undefined) eq.rot = 0;
|
||||||
g.dataset.eqId = eq.id;
|
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';
|
g.style.cursor = simEditMode ? 'grab' : 'default';
|
||||||
|
|
||||||
if (eq.shape === 'rect') {
|
if (eq.shape === 'rect') {
|
||||||
const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
g.appendChild(mkSvgEl('rect', {'class':'equip', x:eq.x, y:eq.y, width:eq.w||80, height:eq.h||60, rx:'6',
|
||||||
r.setAttribute('class', 'equip');
|
...(simEditMode ? {stroke: isSel?'var(--blue)':'var(--amber)', 'stroke-width': isSel?'3':'2', 'stroke-dasharray': isSel?'':'4 4'} : {})}));
|
||||||
r.setAttribute('x', eq.x); r.setAttribute('y', eq.y);
|
const t = mkSvgEl('text', {'class':'equip-label', x:eq.x+(eq.w||80)/2, y:eq.y+(eq.h||60)/2+5});
|
||||||
r.setAttribute('width', eq.w); r.setAttribute('height', eq.h);
|
t.textContent = eq.label; g.appendChild(t);
|
||||||
r.setAttribute('rx', '6');
|
|
||||||
g.appendChild(r);
|
|
||||||
|
|
||||||
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') {
|
if (eq.anim === 'conveyor') {
|
||||||
const l = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
g.appendChild(mkSvgEl('line', {'class':'anim-overlay conveyor-anim', id:'sim-'+eq.id,
|
||||||
l.setAttribute('class', 'anim-overlay conveyor-anim');
|
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,
|
||||||
l.id = 'sim-' + eq.id;
|
stroke:'var(--green)', 'stroke-width':'3', 'stroke-dasharray':'10 10'}));
|
||||||
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
|
} else {
|
||||||
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
const r = eq.r || 20;
|
||||||
c.setAttribute('class', 'equip');
|
g.appendChild(mkSvgEl('circle', {'class':'equip', cx:eq.x, cy:eq.y, r,
|
||||||
c.setAttribute('cx', eq.x); c.setAttribute('cy', eq.y); c.setAttribute('r', eq.r || 20);
|
...(simEditMode ? {stroke: isSel?'var(--blue)':'var(--amber)', 'stroke-width': isSel?'3':'2', 'stroke-dasharray': isSel?'':'4 4'} : {})}));
|
||||||
g.appendChild(c);
|
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') {
|
if (eq.anim === 'burner') {
|
||||||
const ac = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
g.appendChild(mkSvgEl('circle', {'class':'anim-overlay burner-glow', id:'sim-'+eq.id,
|
||||||
ac.setAttribute('class', 'anim-overlay burner-glow');
|
cx:eq.x, cy:eq.y, r:r+4, fill:'none', stroke:'var(--amber)', 'stroke-width':'4'}));
|
||||||
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') {
|
} else if (eq.anim === 'fan' || eq.anim === 'agitator') {
|
||||||
const ag = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
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.setAttribute('class', `anim-overlay ${eq.anim === 'fan' ? 'fan-anim' : 'agitator-anim'}`);
|
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.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);
|
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);
|
svg.appendChild(g);
|
||||||
|
|
||||||
// Drag in edit mode
|
|
||||||
if (simEditMode) {
|
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));
|
g.addEventListener('dblclick', () => renameEquip(eq));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (simEditMode) {
|
// Draw selection handles for selected equipment
|
||||||
svg.addEventListener('mousemove', moveDragEquip);
|
if (simEditMode && selectedEquip) {
|
||||||
svg.addEventListener('mouseup', stopDragEquip);
|
const eq = selectedEquip;
|
||||||
svg.addEventListener('mouseleave', stopDragEquip);
|
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();
|
updateSchematicAnimations();
|
||||||
}
|
}
|
||||||
|
|
||||||
function startDragEquip(e, eq) {
|
function onSvgMouseMove(e) {
|
||||||
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;
|
if (!dragEquip) return;
|
||||||
const svg = document.getElementById('sim-svg');
|
const p = svgPt(e);
|
||||||
const pt = svg.createSVGPoint();
|
|
||||||
pt.x = e.clientX; pt.y = e.clientY;
|
if (resizeHandle === 'rot') {
|
||||||
const svgPt = pt.matrixTransform(svg.getScreenCTM().inverse());
|
// Rotation: angle from center to mouse
|
||||||
dragEquip.x = Math.round(svgPt.x - dragOffset.x);
|
const cx = dragOffset.cx, cy = dragOffset.cy;
|
||||||
dragEquip.y = Math.round(svgPt.y - dragOffset.y);
|
const angle = Math.round(Math.atan2(p.x - cx, -(p.y - cy)) * 180 / Math.PI);
|
||||||
// Save to layout
|
dragEquip.rot = angle;
|
||||||
layout.equipment = getEquipment();
|
saveEquip(dragEquip);
|
||||||
const eq = layout.equipment.find(e => e.id === dragEquip.id);
|
renderSchematic();
|
||||||
if (eq) { eq.x = dragEquip.x; eq.y = dragEquip.y; }
|
} else if (resizeHandle === 'w') {
|
||||||
renderSchematic();
|
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) {
|
function renameEquip(eq) {
|
||||||
const name = prompt('Rename equipment:', eq.label);
|
const name = prompt('Rename equipment:', eq.label);
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
eq.label = name;
|
eq.label = name;
|
||||||
if (!layout.equipment) layout.equipment = getEquipment();
|
saveEquip(eq);
|
||||||
const e = layout.equipment.find(x => x.id === eq.id);
|
|
||||||
if (e) e.label = name;
|
|
||||||
renderSchematic();
|
renderSchematic();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user