Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
E
EPlanVisualizer
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
嵇洲
EPlanVisualizer
Commits
be696eb6
Commit
be696eb6
authored
Mar 16, 2026
by
fenghen777
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: line drawing tool, package import for .mo export, simplify paramMap
parent
7dcaa541
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
371 additions
and
107 deletions
+371
-107
ComponentEditor.jsx
src/components/ComponentEditor/ComponentEditor.jsx
+65
-33
NodePreview.jsx
src/components/ComponentEditor/NodePreview.jsx
+8
-0
ShapeCanvas.jsx
src/components/ComponentEditor/ShapeCanvas.jsx
+124
-20
CustomDeviceNode.jsx
src/components/Nodes/CustomDeviceNode.jsx
+8
-0
Sidebar.jsx
src/components/Sidebar/Sidebar.jsx
+60
-0
Sidebar.module.css
src/components/Sidebar/Sidebar.module.css
+38
-0
constants.js
src/utils/constants.js
+20
-6
modelMapping.js
src/utils/modelMapping.js
+7
-35
modelicaExporter.js
src/utils/modelicaExporter.js
+41
-13
No files found.
src/components/ComponentEditor/ComponentEditor.jsx
View file @
be696eb6
...
@@ -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
,
upd
ateShape
,
undo
]);
},
[
selectedShapeIndex
,
handleRot
ateShape
,
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
}
>
◉
</
span
>
<
span
className=
{
styles
.
toolIcon
}
>
◉
</
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
}
>
↻
</
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
}
>
⬭
</
span
>
<
span
className=
{
styles
.
toolIcon
}
>
⬭
</
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
}
>
△
</
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
}
>
╱
</
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
>
...
...
src/components/ComponentEditor/NodePreview.jsx
View file @
be696eb6
...
@@ -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
;
}
}
})
}
})
}
...
...
src/components/ComponentEditor/ShapeCanvas.jsx
View file @
be696eb6
...
@@ -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
};
...
...
src/components/Nodes/CustomDeviceNode.jsx
View file @
be696eb6
...
@@ -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
;
}
}
})
}
})
}
...
...
src/components/Sidebar/Sidebar.jsx
View file @
be696eb6
...
@@ -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
...
...
src/components/Sidebar/Sidebar.module.css
View file @
be696eb6
...
@@ -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
;
...
...
src/utils/constants.js
View file @
be696eb6
...
@@ -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
:
'p
1'
,
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
:
'p
2'
,
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
:
'V
0
'
,
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'
,
...
...
src/utils/modelMapping.js
View file @
be696eb6
...
@@ -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
;
...
...
src/utils/modelicaExporter.js
View file @
be696eb6
...
@@ -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
[
eplanK
ey
];
const
rawVal
=
pv
[
p
.
k
ey
];
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
}
".
${
eplanK
ey
}
= "
${
rawVal
}
" 无法解析为数值`
);
warnings
.
push
(
`"
${
label
}
".
${
p
.
k
ey
}
= "
${
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
)}
;`
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment