Commit be696eb6 authored by fenghen777's avatar fenghen777

feat: line drawing tool, package import for .mo export, simplify paramMap

parent 7dcaa541
...@@ -91,56 +91,63 @@ export default function ComponentEditor() { ...@@ -91,56 +91,63 @@ export default function ComponentEditor() {
} }
}, []); }, []);
// 旋转选中形状(90°)
const handleRotateShape = useCallback(() => {
if (selectedShapeIndex < 0 || !editingTemplate) return;
const shape = editingTemplate.shapes[selectedShapeIndex];
if (!shape) return;
const current = shape.props.transform || '';
const match = current.match(/rotate\(([^)]+)\)/);
const currentAngle = match ? parseFloat(match[1]) : 0;
const newAngle = (currentAngle + 90) % 360;
let cx, cy;
if (shape.type === 'rect') {
cx = shape.props.x + shape.props.width / 2;
cy = shape.props.y + shape.props.height / 2;
} else if (shape.type === 'circle') {
cx = shape.props.cx;
cy = shape.props.cy;
} else if (shape.type === 'ellipse') {
cx = shape.props.cx;
cy = shape.props.cy;
} else if (shape.type === 'triangle' && shape.props.points) {
const pts = shape.props.points;
cx = (pts[0][0] + pts[1][0] + pts[2][0]) / 3;
cy = (pts[0][1] + pts[1][1] + pts[2][1]) / 3;
} else if (shape.type === 'line') {
cx = (shape.props.x1 + shape.props.x2) / 2;
cy = (shape.props.y1 + shape.props.y2) / 2;
} else {
return;
}
const transform = newAngle === 0 ? undefined : `rotate(${newAngle}, ${cx}, ${cy})`;
updateShape(selectedShapeIndex, { props: { ...shape.props, transform } });
}, [selectedShapeIndex, editingTemplate, updateShape]);
// 快捷键:空格旋转 / ESC退回选择 / Ctrl+Z撤销 // 快捷键:空格旋转 / ESC退回选择 / Ctrl+Z撤销
useEffect(() => { useEffect(() => {
function handleKeyDown(e) { function handleKeyDown(e) {
// Ctrl+Z 撤销
if ((e.ctrlKey || e.metaKey) && e.key === 'z') { if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
e.preventDefault(); e.preventDefault();
undo(); undo();
return; return;
} }
// ESC 退回选择工具
if (e.key === 'Escape') { if (e.key === 'Escape') {
setTool(TOOLS.SELECT); setTool(TOOLS.SELECT);
setSelectedShapeIndex(-1); setSelectedShapeIndex(-1);
return; return;
} }
if (e.code === 'Space' && selectedShapeIndex >= 0 && editingTemplate) { if (e.code === 'Space' && selectedShapeIndex >= 0) {
e.preventDefault(); e.preventDefault();
const shape = editingTemplate.shapes[selectedShapeIndex]; handleRotateShape();
if (!shape) return;
// 根据形状类型计算旋转
const current = shape.props.transform || '';
const match = current.match(/rotate\(([^)]+)\)/);
const currentAngle = match ? parseFloat(match[1]) : 0;
const newAngle = (currentAngle + 90) % 360;
// 计算形状中心点
let cx, cy;
if (shape.type === 'rect') {
cx = shape.props.x + shape.props.width / 2;
cy = shape.props.y + shape.props.height / 2;
} else if (shape.type === 'circle') {
cx = shape.props.cx;
cy = shape.props.cy;
} else if (shape.type === 'ellipse') {
cx = shape.props.cx;
cy = shape.props.cy;
} else {
return;
}
const transform = newAngle === 0 ? undefined : `rotate(${newAngle}, ${cx}, ${cy})`;
updateShape(selectedShapeIndex, {
props: { ...shape.props, transform },
});
} }
} }
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [selectedShapeIndex, editingTemplate, updateShape, undo]); }, [selectedShapeIndex, handleRotateShape, undo]);
if (!editingTemplate) return null; if (!editingTemplate) return null;
...@@ -190,6 +197,15 @@ export default function ComponentEditor() { ...@@ -190,6 +197,15 @@ export default function ComponentEditor() {
<span className={styles.toolIcon}>&#9673;</span> <span className={styles.toolIcon}>&#9673;</span>
<span className={styles.toolLabel}>端点</span> <span className={styles.toolLabel}>端点</span>
</button> </button>
<button
className={styles.toolBtn}
onClick={handleRotateShape}
disabled={selectedShapeIndex < 0}
title="旋转选中形状 90°(快捷键:空格)"
>
<span className={styles.toolIcon}>&#8635;</span>
<span className={styles.toolLabel}>旋转</span>
</button>
</div> </div>
<div className={styles.toolSection}> <div className={styles.toolSection}>
...@@ -218,6 +234,22 @@ export default function ComponentEditor() { ...@@ -218,6 +234,22 @@ export default function ComponentEditor() {
<span className={styles.toolIcon}>&#11053;</span> <span className={styles.toolIcon}>&#11053;</span>
<span className={styles.toolLabel}>椭圆</span> <span className={styles.toolLabel}>椭圆</span>
</button> </button>
<button
className={`${styles.toolBtn} ${tool === TOOLS.TRIANGLE ? styles.toolActive : ''}`}
onClick={() => setTool(TOOLS.TRIANGLE)}
title="三角形"
>
<span className={styles.toolIcon}>&#9651;</span>
<span className={styles.toolLabel}>三角形</span>
</button>
<button
className={`${styles.toolBtn} ${tool === TOOLS.LINE ? styles.toolActive : ''}`}
onClick={() => setTool(TOOLS.LINE)}
title="直线"
>
<span className={styles.toolIcon}>&#9585;</span>
<span className={styles.toolLabel}>直线</span>
</button>
</div> </div>
{/* 常用模板 */} {/* 常用模板 */}
...@@ -304,7 +336,7 @@ export default function ComponentEditor() { ...@@ -304,7 +336,7 @@ export default function ComponentEditor() {
<span className={styles.canvasHint}> <span className={styles.canvasHint}>
{tool === TOOLS.SELECT && '点击选择 | 拖拽框选 | 空格旋转 | 中键平移 | 滚轮缩放'} {tool === TOOLS.SELECT && '点击选择 | 拖拽框选 | 空格旋转 | 中键平移 | 滚轮缩放'}
{tool === TOOLS.PORT && '点击组件边缘添加端点'} {tool === TOOLS.PORT && '点击组件边缘添加端点'}
{(tool === TOOLS.RECT || tool === TOOLS.CIRCLE || tool === TOOLS.ELLIPSE) && {(tool === TOOLS.RECT || tool === TOOLS.CIRCLE || tool === TOOLS.ELLIPSE || tool === TOOLS.TRIANGLE || tool === TOOLS.LINE) &&
'拖拽绘制形状'} '拖拽绘制形状'}
</span> </span>
</div> </div>
......
...@@ -72,6 +72,14 @@ export default function NodePreview() { ...@@ -72,6 +72,14 @@ export default function NodePreview() {
case 'rect': return <rect key={i} {...props} style={s} />; case 'rect': return <rect key={i} {...props} style={s} />;
case 'circle': return <circle key={i} {...props} style={s} />; case 'circle': return <circle key={i} {...props} style={s} />;
case 'ellipse': return <ellipse key={i} {...props} style={s} />; case 'ellipse': return <ellipse key={i} {...props} style={s} />;
case 'triangle': {
const pts = props.points;
if (!pts || pts.length < 3) return null;
const pointsStr = pts.map(p => p.join(',')).join(' ');
return <polygon key={i} points={pointsStr} transform={props.transform} style={s} />;
}
case 'line':
return <line key={i} x1={props.x1} y1={props.y1} x2={props.x2} y2={props.y2} transform={props.transform} style={s} />;
default: return null; default: return null;
} }
})} })}
......
...@@ -16,6 +16,8 @@ const TOOLS = { ...@@ -16,6 +16,8 @@ const TOOLS = {
RECT: 'rect', RECT: 'rect',
CIRCLE: 'circle', CIRCLE: 'circle',
ELLIPSE: 'ellipse', ELLIPSE: 'ellipse',
TRIANGLE: 'triangle',
LINE: 'line',
PORT: 'port', PORT: 'port',
}; };
...@@ -44,7 +46,7 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape }) ...@@ -44,7 +46,7 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape })
const handleKeyDown = (e) => { const handleKeyDown = (e) => {
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedShapeIndex >= 0) { if ((e.key === 'Delete' || e.key === 'Backspace') && selectedShapeIndex >= 0) {
// 避免在 input 中误触 // 避免在 input 中误触
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
e.preventDefault(); e.preventDefault();
useComponentLibrary.getState().removeShape(selectedShapeIndex); useComponentLibrary.getState().removeShape(selectedShapeIndex);
onSelectShape(-1); onSelectShape(-1);
...@@ -63,34 +65,27 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape }) ...@@ -63,34 +65,27 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape })
if (!editingTemplate) return null; if (!editingTemplate) return null;
const { shapes, ports, width, height } = editingTemplate; const { shapes, ports, width, height } = editingTemplate;
// ===== SVG 坐标转换 ===== // ===== SVG 坐标转换(使用原生 CTM,准确处理 viewBox + preserveAspectRatio)=====
function toSvg(cx, cy) { function toSvg(cx, cy) {
const svg = svgRef.current; const svg = svgRef.current;
if (!svg) return { x: 0, y: 0 }; if (!svg) return { x: 0, y: 0 };
const r = svg.getBoundingClientRect(); const ctm = svg.getScreenCTM();
const vb = vbRef.current; if (!ctm) return { x: 0, y: 0 };
return { const pt = new DOMPoint(cx, cy).matrixTransform(ctm.inverse());
x: ((cx - r.left) / r.width) * vb.w + vb.x, return { x: pt.x, y: pt.y };
y: ((cy - r.top) / r.height) * vb.h + vb.y,
};
} }
// ===== 滚轮缩放 ===== // ===== 滚轮缩放 =====
function handleWheel(e) { function handleWheel(e) {
e.preventDefault(); e.preventDefault();
const factor = e.deltaY > 0 ? 1.1 : 0.9; const factor = e.deltaY > 0 ? 1.1 : 0.9;
setViewBox(prev => { const mouse = toSvg(e.clientX, e.clientY);
const svg = svgRef.current; setViewBox(prev => ({
const r = svg.getBoundingClientRect(); x: mouse.x - (mouse.x - prev.x) * factor,
const mx = ((e.clientX - r.left) / r.width) * prev.w + prev.x; y: mouse.y - (mouse.y - prev.y) * factor,
const my = ((e.clientY - r.top) / r.height) * prev.h + prev.y; w: prev.w * factor,
return { h: prev.h * factor,
x: mx - (mx - prev.x) * factor, }));
y: my - (my - prev.y) * factor,
w: prev.w * factor,
h: prev.h * factor,
};
});
} }
// ======================================================= // =======================================================
...@@ -152,6 +147,13 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape }) ...@@ -152,6 +147,13 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape })
s.updateShape(shapeIndex, { props: { ...shape.props, cx: snap(origProps.cx + dx), cy: snap(origProps.cy + dy) } }); s.updateShape(shapeIndex, { props: { ...shape.props, cx: snap(origProps.cx + dx), cy: snap(origProps.cy + dy) } });
else if (shape.type === 'ellipse') else if (shape.type === 'ellipse')
s.updateShape(shapeIndex, { props: { ...shape.props, cx: snap(origProps.cx + dx), cy: snap(origProps.cy + dy) } }); s.updateShape(shapeIndex, { props: { ...shape.props, cx: snap(origProps.cx + dx), cy: snap(origProps.cy + dy) } });
else if (shape.type === 'triangle') {
const pts = origProps.points;
const moved = pts.map(([px, py]) => [snap(px + dx), snap(py + dy)]);
s.updateShape(shapeIndex, { props: { ...shape.props, points: moved } });
}
else if (shape.type === 'line')
s.updateShape(shapeIndex, { props: { ...shape.props, x1: snap(origProps.x1 + dx), y1: snap(origProps.y1 + dy), x2: snap(origProps.x2 + dx), y2: snap(origProps.y2 + dy) } });
} }
function onUp() { function onUp() {
...@@ -189,6 +191,10 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape }) ...@@ -189,6 +191,10 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape })
newProps = resizeCircle(origProps, handleDir, dx, dy, MIN_SIZE); newProps = resizeCircle(origProps, handleDir, dx, dy, MIN_SIZE);
} else if (shapeType === 'ellipse') { } else if (shapeType === 'ellipse') {
newProps = resizeEllipse(origProps, handleDir, dx, dy, MIN_SIZE); newProps = resizeEllipse(origProps, handleDir, dx, dy, MIN_SIZE);
} else if (shapeType === 'triangle') {
newProps = resizeTriangle(origProps, handleDir, dx, dy, MIN_SIZE);
} else if (shapeType === 'line') {
newProps = resizeLine(origProps, handleDir, dx, dy);
} }
if (newProps) s.updateShape(shapeIndex, { props: { ...shape.props, ...newProps } }); if (newProps) s.updateShape(shapeIndex, { props: { ...shape.props, ...newProps } });
} }
...@@ -222,6 +228,13 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape }) ...@@ -222,6 +228,13 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape })
if (t === TOOLS.RECT) store.addShape({ type: 'rect', props: { x: snap(x1), y: snap(y1), width: snap(w), height: snap(h), rx: 4 }, style: { fill: 'rgba(255,255,255,0.05)', stroke: color, strokeWidth: 1.5 } }); if (t === TOOLS.RECT) store.addShape({ type: 'rect', props: { x: snap(x1), y: snap(y1), width: snap(w), height: snap(h), rx: 4 }, style: { fill: 'rgba(255,255,255,0.05)', stroke: color, strokeWidth: 1.5 } });
else if (t === TOOLS.CIRCLE) store.addShape({ type: 'circle', props: { cx: snap(x1+w/2), cy: snap(y1+h/2), r: snap(Math.max(w,h)/2) }, style: { fill: 'rgba(255,255,255,0.05)', stroke: color, strokeWidth: 1.5 } }); else if (t === TOOLS.CIRCLE) store.addShape({ type: 'circle', props: { cx: snap(x1+w/2), cy: snap(y1+h/2), r: snap(Math.max(w,h)/2) }, style: { fill: 'rgba(255,255,255,0.05)', stroke: color, strokeWidth: 1.5 } });
else if (t === TOOLS.ELLIPSE) store.addShape({ type: 'ellipse', props: { cx: snap(x1+w/2), cy: snap(y1+h/2), rx: snap(w/2), ry: snap(h/2) }, style: { fill: 'rgba(255,255,255,0.05)', stroke: color, strokeWidth: 1.5 } }); else if (t === TOOLS.ELLIPSE) store.addShape({ type: 'ellipse', props: { cx: snap(x1+w/2), cy: snap(y1+h/2), rx: snap(w/2), ry: snap(h/2) }, style: { fill: 'rgba(255,255,255,0.05)', stroke: color, strokeWidth: 1.5 } });
else if (t === TOOLS.TRIANGLE) {
const sx = snap(x1), sy = snap(y1), sw = snap(w), sh = snap(h);
store.addShape({ type: 'triangle', props: { points: [[sx + sw/2, sy], [sx, sy + sh], [sx + sw, sy + sh]] }, style: { fill: 'rgba(255,255,255,0.05)', stroke: color, strokeWidth: 1.5 } });
}
else if (t === TOOLS.LINE) {
store.addShape({ type: 'line', props: { x1: startPt.x, y1: startPt.y, x2: pt.x, y2: pt.y }, style: { fill: 'none', stroke: color, strokeWidth: 1.5 } });
}
} }
window.addEventListener('mousemove', onMove); window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp); window.addEventListener('mouseup', onUp);
...@@ -322,6 +335,13 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape }) ...@@ -322,6 +335,13 @@ export default function ShapeCanvas({ tool, selectedShapeIndex, onSelectShape })
if (tool === TOOLS.RECT) return <rect x={x1} y={y1} width={w} height={h} rx={4} fill="none" stroke={color} strokeWidth={1.5} strokeDasharray="4 2" opacity={0.6} />; if (tool === TOOLS.RECT) return <rect x={x1} y={y1} width={w} height={h} rx={4} fill="none" stroke={color} strokeWidth={1.5} strokeDasharray="4 2" opacity={0.6} />;
if (tool === TOOLS.CIRCLE) { const r = Math.max(w,h)/2; return <circle cx={x1+w/2} cy={y1+h/2} r={r} fill="none" stroke={color} strokeWidth={1.5} strokeDasharray="4 2" opacity={0.6} />; } if (tool === TOOLS.CIRCLE) { const r = Math.max(w,h)/2; return <circle cx={x1+w/2} cy={y1+h/2} r={r} fill="none" stroke={color} strokeWidth={1.5} strokeDasharray="4 2" opacity={0.6} />; }
if (tool === TOOLS.ELLIPSE) return <ellipse cx={x1+w/2} cy={y1+h/2} rx={w/2} ry={h/2} fill="none" stroke={color} strokeWidth={1.5} strokeDasharray="4 2" opacity={0.6} />; if (tool === TOOLS.ELLIPSE) return <ellipse cx={x1+w/2} cy={y1+h/2} rx={w/2} ry={h/2} fill="none" stroke={color} strokeWidth={1.5} strokeDasharray="4 2" opacity={0.6} />;
if (tool === TOOLS.TRIANGLE) {
const pts = `${x1+w/2},${y1} ${x1},${y1+h} ${x1+w},${y1+h}`;
return <polygon points={pts} fill="none" stroke={color} strokeWidth={1.5} strokeDasharray="4 2" opacity={0.6} />;
}
if (tool === TOOLS.LINE) {
return <line x1={start.x} y1={start.y} x2={current.x} y2={current.y} stroke={color} strokeWidth={1.5} strokeDasharray="4 2" opacity={0.6} />;
}
return null; return null;
} }
...@@ -531,6 +551,13 @@ function getShapeCenter(shape) { ...@@ -531,6 +551,13 @@ function getShapeCenter(shape) {
if (shape.type === 'rect') return { x: shape.props.x + shape.props.width / 2, y: shape.props.y + shape.props.height / 2 }; if (shape.type === 'rect') return { x: shape.props.x + shape.props.width / 2, y: shape.props.y + shape.props.height / 2 };
if (shape.type === 'circle') return { x: shape.props.cx, y: shape.props.cy }; if (shape.type === 'circle') return { x: shape.props.cx, y: shape.props.cy };
if (shape.type === 'ellipse') return { x: shape.props.cx, y: shape.props.cy }; if (shape.type === 'ellipse') return { x: shape.props.cx, y: shape.props.cy };
if (shape.type === 'triangle' && shape.props.points) {
const pts = shape.props.points;
return { x: (pts[0][0] + pts[1][0] + pts[2][0]) / 3, y: (pts[0][1] + pts[1][1] + pts[2][1]) / 3 };
}
if (shape.type === 'line') {
return { x: (shape.props.x1 + shape.props.x2) / 2, y: (shape.props.y1 + shape.props.y2) / 2 };
}
return null; return null;
} }
...@@ -551,6 +578,14 @@ function renderShape(shape) { ...@@ -551,6 +578,14 @@ function renderShape(shape) {
case 'rect': return <rect {...props} style={s} />; case 'rect': return <rect {...props} style={s} />;
case 'circle': return <circle {...props} style={s} />; case 'circle': return <circle {...props} style={s} />;
case 'ellipse': return <ellipse {...props} style={s} />; case 'ellipse': return <ellipse {...props} style={s} />;
case 'triangle': {
const pts = props.points;
if (!pts || pts.length < 3) return null;
const pointsStr = pts.map(p => p.join(',')).join(' ');
return <polygon points={pointsStr} transform={props.transform} style={s} />;
}
case 'line':
return <line x1={props.x1} y1={props.y1} x2={props.x2} y2={props.y2} transform={props.transform} style={s} />;
default: return null; default: return null;
} }
} }
...@@ -574,6 +609,17 @@ function getShapeBounds(shape) { ...@@ -574,6 +609,17 @@ function getShapeBounds(shape) {
h: shape.props.ry * 2, h: shape.props.ry * 2,
}; };
} }
if (shape.type === 'triangle' && shape.props.points) {
const pts = shape.props.points;
const xs = pts.map(p => p[0]), ys = pts.map(p => p[1]);
const minX = Math.min(...xs), minY = Math.min(...ys);
return { x: minX, y: minY, w: Math.max(...xs) - minX, h: Math.max(...ys) - minY };
}
if (shape.type === 'line') {
const { x1, y1, x2, y2 } = shape.props;
const minX = Math.min(x1, x2), minY = Math.min(y1, y2);
return { x: minX, y: minY, w: Math.max(x1, x2) - minX || 1, h: Math.max(y1, y2) - minY || 1 };
}
return null; return null;
} }
...@@ -624,14 +670,72 @@ function resizeEllipse(orig, dir, dx, dy, min) { ...@@ -624,14 +670,72 @@ function resizeEllipse(orig, dir, dx, dy, min) {
return { cx, cy, rx, ry }; return { cx, cy, rx, ry };
} }
/**
* 三角形 resize:按 bounding box 缩放三个顶点
*/
function resizeTriangle(orig, dir, dx, dy, min) {
const pts = orig.points;
const xs = pts.map(p => p[0]), ys = pts.map(p => p[1]);
let minX = Math.min(...xs), minY = Math.min(...ys);
let maxX = Math.max(...xs), maxY = Math.max(...ys);
const origW = maxX - minX || 1, origH = maxY - minY || 1;
if (dir.includes('e')) maxX = snap(maxX + dx);
if (dir.includes('w')) minX = snap(minX + dx);
if (dir.includes('s')) maxY = snap(maxY + dy);
if (dir.includes('n')) minY = snap(minY + dy);
let newW = maxX - minX, newH = maxY - minY;
if (newW < min) newW = min;
if (newH < min) newH = min;
const scaled = pts.map(([px, py]) => [
snap(minX + ((px - Math.min(...xs)) / origW) * newW),
snap(minY + ((py - Math.min(...ys)) / origH) * newH),
]);
return { points: scaled };
}
/**
* 直线 resize:移动端点
*/
function resizeLine(orig, dir, dx, dy) {
let { x1, y1, x2, y2 } = orig;
// 北/西 = 移动起点,南/东 = 移动终点
if (dir.includes('n') || dir.includes('w')) { x1 = snap(orig.x1 + dx); y1 = snap(orig.y1 + dy); }
if (dir.includes('s') || dir.includes('e')) { x2 = snap(orig.x2 + dx); y2 = snap(orig.y2 + dy); }
return { x1, y1, x2, y2 };
}
function hitTestShape(shape, pt) { function hitTestShape(shape, pt) {
if (shape.type === 'rect') { const { x, y, width, height } = shape.props; return pt.x >= x && pt.x <= x+width && pt.y >= y && pt.y <= y+height; } if (shape.type === 'rect') { const { x, y, width, height } = shape.props; return pt.x >= x && pt.x <= x+width && pt.y >= y && pt.y <= y+height; }
if (shape.type === 'circle') { const dx = pt.x-shape.props.cx, dy = pt.y-shape.props.cy; return dx*dx+dy*dy <= shape.props.r*shape.props.r; } if (shape.type === 'circle') { const dx = pt.x-shape.props.cx, dy = pt.y-shape.props.cy; return dx*dx+dy*dy <= shape.props.r*shape.props.r; }
if (shape.type === 'ellipse') { const dx = (pt.x-shape.props.cx)/shape.props.rx, dy = (pt.y-shape.props.cy)/shape.props.ry; return dx*dx+dy*dy <= 1; } if (shape.type === 'ellipse') { const dx = (pt.x-shape.props.cx)/shape.props.rx, dy = (pt.y-shape.props.cy)/shape.props.ry; return dx*dx+dy*dy <= 1; }
if (shape.type === 'triangle' && shape.props.points) { return pointInTriangle(pt, shape.props.points); }
if (shape.type === 'line') {
const { x1, y1, x2, y2 } = shape.props;
const len = Math.sqrt((x2-x1)**2 + (y2-y1)**2);
if (len < 1) return false;
const d = Math.abs((y2-y1)*pt.x - (x2-x1)*pt.y + x2*y1 - y2*x1) / len;
if (d > 6) return false;
const t = ((pt.x-x1)*(x2-x1) + (pt.y-y1)*(y2-y1)) / (len*len);
return t >= -0.05 && t <= 1.05;
}
return false; return false;
} }
/** 点是否在三角形内(重心坐标法) */
function pointInTriangle(pt, tri) {
const [ax, ay] = tri[0], [bx, by] = tri[1], [cx, cy] = tri[2];
const v0x = cx - ax, v0y = cy - ay, v1x = bx - ax, v1y = by - ay, v2x = pt.x - ax, v2y = pt.y - ay;
const d00 = v0x*v0x + v0y*v0y, d01 = v0x*v1x + v0y*v1y, d02 = v0x*v2x + v0y*v2y;
const d11 = v1x*v1x + v1y*v1y, d12 = v1x*v2x + v1y*v2y;
const inv = 1 / (d00*d11 - d01*d01);
const u = (d11*d02 - d01*d12) * inv, v = (d00*d12 - d01*d02) * inv;
return u >= 0 && v >= 0 && (u + v) <= 1;
}
export function getPortPosition(port, width, height) { export function getPortPosition(port, width, height) {
switch (port.side) { switch (port.side) {
case 'top': return { x: width * port.position, y: 0 }; case 'top': return { x: width * port.position, y: 0 };
......
...@@ -90,6 +90,14 @@ function CustomDeviceNode({ data, selected }) { ...@@ -90,6 +90,14 @@ function CustomDeviceNode({ data, selected }) {
case 'rect': return <rect key={i} {...props} style={s} />; case 'rect': return <rect key={i} {...props} style={s} />;
case 'circle': return <circle key={i} {...props} style={s} />; case 'circle': return <circle key={i} {...props} style={s} />;
case 'ellipse': return <ellipse key={i} {...props} style={s} />; case 'ellipse': return <ellipse key={i} {...props} style={s} />;
case 'triangle': {
const pts = props.points;
if (!pts || pts.length < 3) return null;
const pointsStr = pts.map(p => p.join(',')).join(' ');
return <polygon key={i} points={pointsStr} transform={props.transform} style={s} />;
}
case 'line':
return <line key={i} x1={props.x1} y1={props.y1} x2={props.x2} y2={props.y2} transform={props.transform} style={s} />;
default: return null; default: return null;
} }
})} })}
......
...@@ -26,6 +26,20 @@ export default function Sidebar() { ...@@ -26,6 +26,20 @@ export default function Sidebar() {
DEVICE_CATEGORIES.forEach(cat => { init[`__b_${cat.category}`] = true; }); DEVICE_CATEGORIES.forEach(cat => { init[`__b_${cat.category}`] = true; });
return init; return init;
}); });
// 包名覆盖(localStorage 持久化)
const [packageOverrides, setPackageOverrides] = useState(() => {
try { return JSON.parse(localStorage.getItem('eplan_package_overrides') || '{}'); } catch { return {}; }
});
const [editingPkgKey, setEditingPkgKey] = useState(null); // 当前正在编辑包名的 key
/** 保存包名 */
const savePkgName = useCallback((key, value, catObj) => {
const next = { ...packageOverrides, [key]: value };
setPackageOverrides(next);
localStorage.setItem('eplan_package_overrides', JSON.stringify(next));
if (catObj) catObj.packageName = value || undefined;
setEditingPkgKey(null);
}, [packageOverrides]);
const [sidebarWidth, setSidebarWidth] = useState(280); const [sidebarWidth, setSidebarWidth] = useState(280);
const isDragging = useRef(false); const isDragging = useRef(false);
...@@ -351,6 +365,30 @@ export default function Sidebar() { ...@@ -351,6 +365,30 @@ export default function Sidebar() {
{isCollapsed ? '\u25B6' : '\u25BC'} {isCollapsed ? '\u25B6' : '\u25BC'}
</span> </span>
<span className={styles.categoryName}>{cat.category}</span> <span className={styles.categoryName}>{cat.category}</span>
{builtinEditMode && cat.packageName !== undefined && (
editingPkgKey === cat.category ? (
<input
className={styles.pkgInput}
autoFocus
defaultValue={packageOverrides[cat.category] ?? cat.packageName ?? ''}
placeholder="包名"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Enter') savePkgName(cat.category, e.target.value, cat);
if (e.key === 'Escape') setEditingPkgKey(null);
}}
onBlur={(e) => savePkgName(cat.category, e.target.value, cat)}
/>
) : (
<button
className={styles.pkgBtn}
onClick={(e) => { e.stopPropagation(); setEditingPkgKey(cat.category); }}
title={`包名: ${packageOverrides[cat.category] ?? cat.packageName ?? '未设置'}`}
>
{packageOverrides[cat.category] ?? cat.packageName ? '●' : '○'}
</button>
)
)}
<span className={styles.categoryCount}>{cat.items.length}</span> <span className={styles.categoryCount}>{cat.items.length}</span>
</div> </div>
{!isCollapsed && ( {!isCollapsed && (
...@@ -450,6 +488,28 @@ export default function Sidebar() { ...@@ -450,6 +488,28 @@ export default function Sidebar() {
{isCollapsed ? '\u25B6' : '\u25BC'} {isCollapsed ? '\u25B6' : '\u25BC'}
</span> </span>
<span className={styles.categoryName}>{catName}</span> <span className={styles.categoryName}>{catName}</span>
{editingPkgKey === `__custom_${catName}` ? (
<input
className={styles.pkgInput}
autoFocus
defaultValue={packageOverrides[`__custom_${catName}`] || ''}
placeholder="包名"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Enter') savePkgName(`__custom_${catName}`, e.target.value);
if (e.key === 'Escape') setEditingPkgKey(null);
}}
onBlur={(e) => savePkgName(`__custom_${catName}`, e.target.value)}
/>
) : (
<button
className={styles.pkgBtn}
onClick={(e) => { e.stopPropagation(); setEditingPkgKey(`__custom_${catName}`); }}
title={`包名: ${packageOverrides[`__custom_${catName}`] || '未设置'}`}
>
{packageOverrides[`__custom_${catName}`] ? '●' : '○'}
</button>
)}
<span className={styles.categoryCount}>{catTemplates.length}</span> <span className={styles.categoryCount}>{catTemplates.length}</span>
{catName !== '未分类' && ( {catName !== '未分类' && (
<button <button
......
...@@ -345,6 +345,44 @@ ...@@ -345,6 +345,44 @@
border-radius: 8px; border-radius: 8px;
} }
.pkgBtn {
width: 18px;
height: 18px;
border: none;
border-radius: 50%;
background: transparent;
color: #6366f1;
font-size: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.12s;
}
.pkgBtn:hover {
background: rgba(99, 102, 241, 0.2);
color: #818cf8;
}
.pkgInput {
width: 80px;
padding: 2px 6px;
border: 1px solid #6366f1;
border-radius: 4px;
background: #1a1a28;
color: #6366f1;
font-size: 10px;
font-family: 'Consolas', monospace;
outline: none;
flex-shrink: 0;
}
.pkgInput::placeholder {
color: #444;
}
/* ===== Tree hierarchy ===== */ /* ===== Tree hierarchy ===== */
.rootHeader { .rootHeader {
padding: 8px 4px; padding: 8px 4px;
......
...@@ -67,7 +67,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -67,7 +67,7 @@ export const DEVICE_CATEGORIES = [
], ],
}, },
{ {
code: 5007, type: 'logic_switch', label: 'Switch', color: '#7E57C2', icon: '⇋', code: 5007, type: 'logic_switch', label: 'If', color: '#7E57C2', icon: '⇋',
width: 140, height: 100, width: 140, height: 100,
ports: [ ports: [
{ id: 'p-switch', name: 'Switch', side: 'left', position: 0.2, type: 'signal', connector: 'switchCtrl' }, { id: 'p-switch', name: 'Switch', side: 'left', position: 0.2, type: 'signal', connector: 'switchCtrl' },
...@@ -124,6 +124,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -124,6 +124,7 @@ export const DEVICE_CATEGORIES = [
}, },
{ {
category: '基本电子元件', category: '基本电子元件',
packageName: 'Electrical',
items: [ items: [
{ {
code: 1001, type: 'resistor', label: '电阻', color: '#FF9800', icon: 'R', code: 1001, type: 'resistor', label: '电阻', color: '#FF9800', icon: 'R',
...@@ -134,19 +135,17 @@ export const DEVICE_CATEGORIES = [ ...@@ -134,19 +135,17 @@ export const DEVICE_CATEGORIES = [
], ],
params: [ params: [
{ key: 'R', label: '阻值', unit: 'Ω', defaultValue: '10k' }, { key: 'R', label: '阻值', unit: 'Ω', defaultValue: '10k' },
{ key: 'P', label: '功率', unit: 'W', defaultValue: '0.25' },
], ],
}, },
{ {
code: 1002, type: 'capacitor', label: '电容', color: '#2196F3', icon: 'C', code: 1002, type: 'capacitor', label: '电容', color: '#2196F3', icon: 'C',
width: 120, height: 60, width: 120, height: 60,
ports: [ ports: [
{ id: 'p-pos', name: '+', side: 'left', position: 0.5, type: 'power', connector: 'p' }, { id: 'p1', name: '1', side: 'left', position: 0.5, type: 'power', connector: 'p' },
{ id: 'p-neg', name: '-', side: 'right', position: 0.5, type: 'power', connector: 'n' }, { id: 'p2', name: '2', side: 'right', position: 0.5, type: 'power', connector: 'n' },
], ],
params: [ params: [
{ key: 'C', label: '容值', unit: 'F', defaultValue: '100u' }, { key: 'C', label: '容值', unit: 'F', defaultValue: '100u' },
{ key: 'V', label: '耐压', unit: 'V', defaultValue: '50' },
], ],
}, },
{ {
...@@ -176,7 +175,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -176,7 +175,7 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-neg', name: 'V-', side: 'right', position: 0.5, type: 'power', connector: 'n' }, { id: 'p-neg', name: 'V-', side: 'right', position: 0.5, type: 'power', connector: 'n' },
], ],
params: [ params: [
{ key: 'V', label: '电压', unit: 'V', defaultValue: '24' }, { key: 'V0', label: '电压', unit: 'V', defaultValue: '24' },
], ],
}, },
{ {
...@@ -186,10 +185,22 @@ export const DEVICE_CATEGORIES = [ ...@@ -186,10 +185,22 @@ export const DEVICE_CATEGORIES = [
{ id: 'p-gnd', name: 'GND', side: 'left', position: 0.5, type: 'power', connector: 'p' }, { id: 'p-gnd', name: 'GND', side: 'left', position: 0.5, type: 'power', connector: 'p' },
], ],
}, },
{
code: 1007, type: 'ideal_switch', label: '开关', color: '#00BCD4', icon: 'SW',
width: 120, height: 60,
ports: [
{ id: 'p-in', name: 'IN', side: 'left', position: 0.5, type: 'power', connector: 'p' },
{ id: 'p-out', name: 'OUT', side: 'right', position: 0.5, type: 'power', connector: 'n' },
],
params: [
{ key: 'closed', label: '初始状态', unit: '', defaultValue: 'false' },
],
},
], ],
}, },
{ {
category: '电气控制', category: '电气控制',
packageName: 'ElectricalControl',
items: [ items: [
{ {
code: 100, type: 'terminal', label: '端子排', color: '#4CAF50', icon: '⊞', code: 100, type: 'terminal', label: '端子排', color: '#4CAF50', icon: '⊞',
...@@ -288,6 +299,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -288,6 +299,7 @@ export const DEVICE_CATEGORIES = [
}, },
{ {
category: 'PLC', category: 'PLC',
packageName: 'PLC',
items: [ items: [
{ {
code: 2001, type: 'plc_cpu', label: 'PLC CPU', color: '#3F51B5', icon: 'CPU', code: 2001, type: 'plc_cpu', label: 'PLC CPU', color: '#3F51B5', icon: 'CPU',
...@@ -349,6 +361,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -349,6 +361,7 @@ export const DEVICE_CATEGORIES = [
}, },
{ {
category: '水利/液压', category: '水利/液压',
packageName: 'Hydraulic',
items: [ items: [
{ {
code: 3001, type: 'pump', label: '水泵', color: '#00BCD4', icon: 'P', code: 3001, type: 'pump', label: '水泵', color: '#00BCD4', icon: 'P',
...@@ -391,6 +404,7 @@ export const DEVICE_CATEGORIES = [ ...@@ -391,6 +404,7 @@ export const DEVICE_CATEGORIES = [
}, },
{ {
category: '机械/气动', category: '机械/气动',
packageName: 'Mechanical',
items: [ items: [
{ {
code: 4001, type: 'cylinder', label: '气缸', color: '#8BC34A', icon: 'CY', code: 4001, type: 'cylinder', label: '气缸', color: '#8BC34A', icon: 'CY',
......
...@@ -9,178 +9,150 @@ ...@@ -9,178 +9,150 @@
* 模型映射表 * 模型映射表
* type : EPLAN 符号类型(constants.js 中的 type 字段) * type : EPLAN 符号类型(constants.js 中的 type 字段)
* modelName : Modelica 模型名称(用于 .mo 声明) * modelName : Modelica 模型名称(用于 .mo 声明)
* portMap : EPLAN 端口 id → Modelica 端口名 * portMap : EPLAN 端口 id -> Modelica 端口名(connector 字段优先)
* paramMap : EPLAN 参数 key → Modelica 参数名
*/ */
const MODEL_MAP = { const MODEL_MAP = {
// ===== 基本电子元件 ===== // ===== 基本电子元件 =====
resistor: { resistor: {
modelName: 'Resistor', modelName: 'Resistor',
portMap: { 'p1': 'p', 'p2': 'n' }, portMap: { 'p1': 'p', 'p2': 'n' },
paramMap: { R: 'R', P: 'P_nominal' },
}, },
capacitor: { capacitor: {
modelName: 'Capacitor', modelName: 'Capacitor',
portMap: { 'p-pos': 'p', 'p-neg': 'n' }, portMap: { 'p1': 'p', 'p2': 'n' },
paramMap: { C: 'C', V: 'V_nominal' },
}, },
inductor: { inductor: {
modelName: 'Inductor', modelName: 'Inductor',
portMap: { 'p1': 'p', 'p2': 'n' }, portMap: { 'p1': 'p', 'p2': 'n' },
paramMap: { L: 'L' },
}, },
diode: { diode: {
modelName: 'Diode', modelName: 'Diode',
portMap: { 'p-a': 'p', 'p-k': 'n' }, portMap: { 'p-a': 'p', 'p-k': 'n' },
paramMap: {},
}, },
voltage_source: { voltage_source: {
modelName: 'VoltageSource', modelName: 'VoltageSource',
portMap: { 'p-pos': 'p', 'p-neg': 'n' }, portMap: { 'p-pos': 'p', 'p-neg': 'n' },
paramMap: { V: 'V' },
}, },
ground: { ground: {
modelName: 'Ground', modelName: 'Ground',
portMap: { 'p-gnd': 'p' }, portMap: { 'p-gnd': 'p' },
paramMap: {}, },
ideal_switch: {
modelName: 'IdealSwitch',
portMap: { 'p-in': 'p', 'p-out': 'n' },
}, },
// ===== 电气控制 ===== // ===== 电气控制 =====
terminal: { terminal: {
modelName: 'Terminal', modelName: 'Terminal',
portMap: { 'p-in-1': 'p_in', 'p-out-1': 'p_out' }, portMap: { 'p-in-1': 'p_in', 'p-out-1': 'p_out' },
paramMap: {},
}, },
contactor: { contactor: {
modelName: 'Contactor', modelName: 'Contactor',
portMap: { 'p-l1': 'L1', 'p-l2': 'L2', 'p-l3': 'L3', 'p-t1': 'T1', 'p-t2': 'T2', 'p-t3': 'T3', 'p-a1': 'A1', 'p-a2': 'A2' }, portMap: { 'p-l1': 'L1', 'p-l2': 'L2', 'p-l3': 'L3', 'p-t1': 'T1', 'p-t2': 'T2', 'p-t3': 'T3', 'p-a1': 'A1', 'p-a2': 'A2' },
paramMap: {},
}, },
relay: { relay: {
modelName: 'Relay', modelName: 'Relay',
portMap: { 'p-a1': 'A1', 'p-a2': 'A2', 'p-11': 'p11', 'p-14': 'p14' }, portMap: { 'p-a1': 'A1', 'p-a2': 'A2', 'p-11': 'p11', 'p-14': 'p14' },
paramMap: {},
}, },
breaker: { breaker: {
modelName: 'CircuitBreaker', modelName: 'CircuitBreaker',
portMap: { 'p-in': 'p_in', 'p-out': 'p_out' }, portMap: { 'p-in': 'p_in', 'p-out': 'p_out' },
paramMap: { In: 'I_nominal' },
}, },
switch: { switch: {
modelName: 'Switch', modelName: 'Switch',
portMap: { 'p-com': 'COM', 'p-no': 'NO', 'p-nc': 'NC' }, portMap: { 'p-com': 'COM', 'p-no': 'NO', 'p-nc': 'NC' },
paramMap: {},
}, },
motor: { motor: {
modelName: 'Motor', modelName: 'Motor',
portMap: { 'p-u': 'U', 'p-v': 'V', 'p-w': 'W', 'p-pe': 'PE' }, portMap: { 'p-u': 'U', 'p-v': 'V', 'p-w': 'W', 'p-pe': 'PE' },
paramMap: { Pw: 'P_nominal', Rpm: 'n_nominal' },
}, },
transformer: { transformer: {
modelName: 'Transformer', modelName: 'Transformer',
portMap: { 'p-pri1': 'p1', 'p-pri2': 'n1', 'p-sec1': 'p2', 'p-sec2': 'n2' }, portMap: { 'p-pri1': 'p1', 'p-pri2': 'n1', 'p-sec1': 'p2', 'p-sec2': 'n2' },
paramMap: {},
}, },
sensor: { sensor: {
modelName: 'Sensor', modelName: 'Sensor',
portMap: { 'p-vcc': 'VCC', 'p-gnd': 'GND', 'p-sig': 'SIG' }, portMap: { 'p-vcc': 'VCC', 'p-gnd': 'GND', 'p-sig': 'SIG' },
paramMap: {},
}, },
cable: { cable: {
modelName: 'Cable', modelName: 'Cable',
portMap: { 'p-in': 'p_in', 'p-out': 'p_out' }, portMap: { 'p-in': 'p_in', 'p-out': 'p_out' },
paramMap: {},
}, },
// ===== PLC ===== // ===== PLC =====
plc_cpu: { plc_cpu: {
modelName: 'PLC_CPU', modelName: 'PLC_CPU',
portMap: { 'p-24v': 'V24', 'p-0v': 'V0', 'p-di1': 'DI1', 'p-di2': 'DI2', 'p-di3': 'DI3', 'p-do1': 'DO1', 'p-do2': 'DO2', 'p-do3': 'DO3' }, portMap: { 'p-24v': 'V24', 'p-0v': 'V0', 'p-di1': 'DI1', 'p-di2': 'DI2', 'p-di3': 'DI3', 'p-do1': 'DO1', 'p-do2': 'DO2', 'p-do3': 'DO3' },
paramMap: {},
}, },
plc_di: { plc_di: {
modelName: 'PLC_DI', modelName: 'PLC_DI',
portMap: { 'p-di1': 'DI1', 'p-di2': 'DI2', 'p-di3': 'DI3', 'p-di4': 'DI4', 'p-com': 'COM' }, portMap: { 'p-di1': 'DI1', 'p-di2': 'DI2', 'p-di3': 'DI3', 'p-di4': 'DI4', 'p-com': 'COM' },
paramMap: {},
}, },
plc_do: { plc_do: {
modelName: 'PLC_DO', modelName: 'PLC_DO',
portMap: { 'p-do1': 'DO1', 'p-do2': 'DO2', 'p-do3': 'DO3', 'p-do4': 'DO4', 'p-com': 'COM' }, portMap: { 'p-do1': 'DO1', 'p-do2': 'DO2', 'p-do3': 'DO3', 'p-do4': 'DO4', 'p-com': 'COM' },
paramMap: {},
}, },
plc_ai: { plc_ai: {
modelName: 'PLC_AI', modelName: 'PLC_AI',
portMap: { 'p-ai1': 'AI1', 'p-ai2': 'AI2', 'p-com': 'COM' }, portMap: { 'p-ai1': 'AI1', 'p-ai2': 'AI2', 'p-com': 'COM' },
paramMap: {},
}, },
plc_ao: { plc_ao: {
modelName: 'PLC_AO', modelName: 'PLC_AO',
portMap: { 'p-ao1': 'AO1', 'p-ao2': 'AO2', 'p-com': 'COM' }, portMap: { 'p-ao1': 'AO1', 'p-ao2': 'AO2', 'p-com': 'COM' },
paramMap: {},
}, },
// ===== 流程节点 ===== // ===== 流程节点 =====
greater_than: { greater_than: {
modelName: 'GreaterThan', isFlowNode: true, operator: '>', modelName: 'GreaterThan', isFlowNode: true, operator: '>',
portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' }, portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' },
paramMap: {},
}, },
less_than: { less_than: {
modelName: 'LessThan', isFlowNode: true, operator: '<', modelName: 'LessThan', isFlowNode: true, operator: '<',
portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' }, portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' },
paramMap: {},
}, },
equal: { equal: {
modelName: 'Equal', isFlowNode: true, operator: '==', modelName: 'Equal', isFlowNode: true, operator: '==',
portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' }, portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' },
paramMap: {},
}, },
logic_and: { logic_and: {
modelName: 'And', isFlowNode: true, operator: 'and', modelName: 'And', isFlowNode: true, operator: 'and',
portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' }, portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' },
paramMap: {},
}, },
logic_or: { logic_or: {
modelName: 'Or', isFlowNode: true, operator: 'or', modelName: 'Or', isFlowNode: true, operator: 'or',
portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' }, portMap: { 'p-a': 'a', 'p-b': 'b', 'p-out': 'out' },
paramMap: {},
}, },
logic_not: { logic_not: {
modelName: 'Not', isFlowNode: true, operator: 'not', modelName: 'Not', isFlowNode: true, operator: 'not',
portMap: { 'p-in': 'inVal', 'p-out': 'out' }, portMap: { 'p-in': 'inVal', 'p-out': 'out' },
paramMap: {},
}, },
logic_switch: { logic_switch: {
modelName: 'Switch', isFlowNode: true, operator: 'if', modelName: 'Switch', isFlowNode: true, operator: 'if',
portMap: { 'p-switch': 'switchCtrl', 'p-false': 'falseVal', 'p-true': 'trueVal', 'p-out': 'out' }, portMap: { 'p-switch': 'switchCtrl', 'p-false': 'falseVal', 'p-true': 'trueVal', 'p-out': 'out' },
paramMap: {},
}, },
val_integer: { val_integer: {
modelName: 'IntegerValue', isFlowNode: true, valueType: 'Integer', modelName: 'IntegerValue', isFlowNode: true, valueType: 'Integer',
portMap: { 'p-out': 'out' }, portMap: { 'p-out': 'out' },
paramMap: { value: 'value' },
}, },
val_real: { val_real: {
modelName: 'RealValue', isFlowNode: true, valueType: 'Real', modelName: 'RealValue', isFlowNode: true, valueType: 'Real',
portMap: { 'p-out': 'out' }, portMap: { 'p-out': 'out' },
paramMap: { value: 'value' },
}, },
val_string: { val_string: {
modelName: 'StringValue', isFlowNode: true, valueType: 'String', modelName: 'StringValue', isFlowNode: true, valueType: 'String',
portMap: { 'p-out': 'out' }, portMap: { 'p-out': 'out' },
paramMap: { value: 'value' },
}, },
val_boolean: { val_boolean: {
modelName: 'BooleanValue', isFlowNode: true, valueType: 'Boolean', modelName: 'BooleanValue', isFlowNode: true, valueType: 'Boolean',
portMap: { 'p-out': 'out' }, portMap: { 'p-out': 'out' },
paramMap: { value: 'value' },
}, },
}; };
/** /**
* 获取符号类型对应的模型映射 * 获取符号类型对应的模型映射
* @param {string} type - 符号类型 * @param {string} type - 符号类型
* @returns {Object|null} - { modelName, portMap, paramMap } 或 null * @returns {Object|null} - { modelName, portMap } 或 null
*/ */
export function getModelMapping(type) { export function getModelMapping(type) {
return MODEL_MAP[type] || null; return MODEL_MAP[type] || null;
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
* 使用 modelMapping.js 中的映射表进行类型和端口转换。 * 使用 modelMapping.js 中的映射表进行类型和端口转换。
*/ */
import { getModelMapping, resolvePortName } from './modelMapping'; import { getModelMapping, resolvePortName } from './modelMapping';
import { DEVICE_CATEGORIES } from './constants';
// ===== 工程记号解析 ===== // ===== 工程记号解析 =====
const SI_PREFIXES = { const SI_PREFIXES = {
...@@ -57,7 +58,8 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -57,7 +58,8 @@ export function exportToModelica(data, modelName = 'Circuit') {
const warnings = []; const warnings = [];
const errors = []; const errors = [];
const lines = []; const lines = [];
const instanceMap = {}; // nodeId → { instanceName, type, ports, isFlowNode } const instanceMap = {}; // nodeId -> { instanceName, type, ports, isFlowNode }
const instanceCounter = {}; // baseName -> count
// 读取用户自定义的映射覆盖 // 读取用户自定义的映射覆盖
let mappingOverrides = {}; let mappingOverrides = {};
...@@ -68,15 +70,35 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -68,15 +70,35 @@ export function exportToModelica(data, modelName = 'Circuit') {
lines.push(`model ${toMoId(modelName)}`); lines.push(`model ${toMoId(modelName)}`);
lines.push(''); lines.push('');
// 构建 type → packageName 查找表
const typeToPackage = {};
DEVICE_CATEGORIES.forEach(cat => {
if (cat.packageName) {
cat.items.forEach(item => { typeToPackage[item.type] = cat.packageName; });
}
});
const usedPackages = new Set();
// ===== 1. 组件声明(跳过流程节点) ===== // ===== 1. 组件声明(跳过流程节点) =====
nodes.forEach((node, idx) => { nodes.forEach((node, idx) => {
const td = node.data?.templateData; const td = node.data?.templateData;
const type = td?.type; const type = td?.type;
const label = node.data?.label || `comp_${idx}`; const label = node.data?.label || `comp_${idx}`;
const instanceName = toMoId(label) + '_' + (idx + 1);
const mapping = getModelMapping(type); const mapping = getModelMapping(type);
// 实例名:优先用 modelName 首字母小写,无映射则用 label 转换
let baseName;
if (mapping && mapping.modelName) {
baseName = mapping.modelName.charAt(0).toLowerCase() + mapping.modelName.slice(1);
} else {
baseName = toMoId(label);
}
// 按 baseName 分组计数,生成唯一序号
if (!instanceCounter[baseName]) instanceCounter[baseName] = 0;
instanceCounter[baseName]++;
const instanceName = baseName + '_' + instanceCounter[baseName];
instanceMap[node.id] = { instanceMap[node.id] = {
instanceName, instanceName,
type, type,
...@@ -84,6 +106,9 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -84,6 +106,9 @@ export function exportToModelica(data, modelName = 'Circuit') {
isFlowNode: mapping?.isFlowNode || false, isFlowNode: mapping?.isFlowNode || false,
}; };
// 记录使用的包
if (typeToPackage[type]) usedPackages.add(typeToPackage[type]);
// 流程节点不生成组件声明 // 流程节点不生成组件声明
if (mapping?.isFlowNode) return; if (mapping?.isFlowNode) return;
...@@ -109,18 +134,18 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -109,18 +134,18 @@ export function exportToModelica(data, modelName = 'Circuit') {
return; return;
} }
// 有映射:使用模型名称声明 // 有映射:使用模型名称声明,参数直接用 constants.js 中的 key
const paramParts = []; const paramParts = [];
const pv = node.data?.paramValues || {}; const pv = node.data?.paramValues || {};
Object.entries(mapping.paramMap).forEach(([eplanKey, moName]) => { (td?.params || []).forEach(p => {
const rawVal = pv[eplanKey]; const rawVal = pv[p.key];
if (rawVal != null && rawVal !== '') { if (rawVal != null && rawVal !== '') {
const numVal = parseEngValue(rawVal); const numVal = parseEngValue(rawVal);
if (numVal != null) { if (numVal != null) {
paramParts.push(`${moName}=${formatMoValue(numVal)}`); paramParts.push(`${p.key}=${formatMoValue(numVal)}`);
} else { } else {
paramParts.push(`${moName}=${rawVal}`); paramParts.push(`${p.key}=${rawVal}`);
warnings.push(`"${label}".${eplanKey} = "${rawVal}" 无法解析为数值`); warnings.push(`"${label}".${p.key} = "${rawVal}" 无法解析为数值`);
} }
} }
}); });
...@@ -131,6 +156,13 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -131,6 +156,13 @@ export function exportToModelica(data, modelName = 'Circuit') {
lines.push(` ${finalModelName} ${instanceName}${paramStr};`); lines.push(` ${finalModelName} ${instanceName}${paramStr};`);
}); });
// 插入 import 语句(在 model 声明与组件声明之间)
if (usedPackages.size > 0) {
const importLines = [...usedPackages].sort().map(pkg => ` import ${pkg}.*;`);
// lines[0] = 'model XXX', lines[1] = '', 组件声明从 lines[2] 开始
lines.splice(2, 0, ...importLines, '');
}
lines.push(''); lines.push('');
// ===== 2. 构建流程节点端口到连接源的映射 ===== // ===== 2. 构建流程节点端口到连接源的映射 =====
...@@ -217,11 +249,7 @@ export function exportToModelica(data, modelName = 'Circuit') { ...@@ -217,11 +249,7 @@ export function exportToModelica(data, modelName = 'Circuit') {
lines.push(''); lines.push('');
// ===== 4. annotation =====
if (nodes.length > 0) {
lines.push(' annotation(Diagram(coordinateSystem(preserveAspectRatio=false,');
lines.push(' extent={{-200,-200},{800,800}})));');
}
lines.push(`end ${toMoId(modelName)};`); lines.push(`end ${toMoId(modelName)};`);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment