完善AMESim组件界面与仿真求解稳定性
This commit is contained in:
1 parent
e7177ab03e
commit
410ef535e8
34 files changed
+3340
-251
No files matched your search
+316
-47
@@ -89,6 +89,10 @@ import {
|
||||
type ParameterColumnLayout,
|
||||
type ParameterTableRow,
|
||||
} from "./ParameterTable";
|
||||
import {
|
||||
evaluateParameterExpression,
|
||||
isParameterExpressionValue,
|
||||
} from "./parameterExpression";
|
||||
import {
|
||||
WorkspaceViewTabs,
|
||||
type WorkspaceView,
|
||||
@@ -219,7 +223,17 @@ type SimulationNodeData = {
|
||||
type SimulationNode = Node<SimulationNodeData, "simulationComponent">;
|
||||
type SimulationEdge = Edge;
|
||||
|
||||
type SimulationNumericKey = "t_start" | "t_stop" | "step" | "max_step";
|
||||
|
||||
type SimulationConfig = {
|
||||
t_start: ParameterValue;
|
||||
t_stop: ParameterValue;
|
||||
step: ParameterValue;
|
||||
max_step: ParameterValue;
|
||||
method: string;
|
||||
};
|
||||
|
||||
type ResolvedSimulationConfig = {
|
||||
t_start: number;
|
||||
t_stop: number;
|
||||
step: number;
|
||||
@@ -255,6 +269,10 @@ type ReactFlowProjectPayload = {
|
||||
simulation: SimulationConfig;
|
||||
};
|
||||
|
||||
type ExecutableProjectPayload = Omit<ReactFlowProjectPayload, "simulation"> & {
|
||||
simulation: ResolvedSimulationConfig;
|
||||
};
|
||||
|
||||
type SimulationResult = {
|
||||
success: boolean;
|
||||
status: "completed" | "stopped" | "stalled" | "failed";
|
||||
@@ -534,7 +552,7 @@ const unitOptions: Record<UnitQuantity, UnitOption[]> = {
|
||||
],
|
||||
};
|
||||
|
||||
const defaultSimulationConfig: SimulationConfig = {
|
||||
const defaultSimulationConfig: ResolvedSimulationConfig = {
|
||||
t_start: 0,
|
||||
t_stop: 2,
|
||||
step: 0.1,
|
||||
@@ -625,6 +643,12 @@ type SimulationStreamEvent =
|
||||
totalTime?: number;
|
||||
};
|
||||
const SUPPORTED_SOLVER_METHODS = ["BDF", "Radau", "LSODA", "RK45", "RK23", "DOP853"];
|
||||
const simulationConfigLabels: Record<SimulationNumericKey, string> = {
|
||||
t_start: "起始时间",
|
||||
t_stop: "结束时间",
|
||||
step: "采样步长",
|
||||
max_step: "最大积分步长",
|
||||
};
|
||||
|
||||
function physicalPort(
|
||||
name: string,
|
||||
@@ -1527,7 +1551,7 @@ type NormalizedSimulationProgress = {
|
||||
|
||||
function normalizeSimulationProgressEvent(
|
||||
event: Extract<SimulationStreamEvent, { event: "progress" }>,
|
||||
config: SimulationConfig,
|
||||
config: ResolvedSimulationConfig,
|
||||
): NormalizedSimulationProgress {
|
||||
const totalTime = Number.isFinite(event.totalTime)
|
||||
? Number(event.totalTime)
|
||||
@@ -2184,7 +2208,7 @@ function DeferredNumberInput({
|
||||
<input
|
||||
aria-invalid={invalid}
|
||||
aria-label={ariaLabel}
|
||||
inputMode="decimal"
|
||||
inputMode="text"
|
||||
onBlur={() => {
|
||||
editingRef.current = false;
|
||||
if (cancelCommitRef.current) {
|
||||
@@ -3013,10 +3037,11 @@ function FlowWorkbench() {
|
||||
const selectedUnit =
|
||||
selectedNode?.data.parameterUnits[key] ?? definition.unit ?? "";
|
||||
const unit = findUnitOption(definition, selectedUnit);
|
||||
const numericValue = Number(displayValue);
|
||||
const normalizedDisplayValue = displayValue.trim();
|
||||
const numericValue = Number(normalizedDisplayValue);
|
||||
const baseValue =
|
||||
displayValue.trim() === "" || !Number.isFinite(numericValue)
|
||||
? displayValue
|
||||
normalizedDisplayValue === "" || !Number.isFinite(numericValue)
|
||||
? normalizedDisplayValue
|
||||
: unit.toBase(numericValue);
|
||||
|
||||
updateSelectedNode((node) => ({
|
||||
@@ -3216,15 +3241,22 @@ function FlowWorkbench() {
|
||||
]);
|
||||
|
||||
const updateSimulationConfig = (key: keyof SimulationConfig, value: string) => {
|
||||
const normalizedValue = value.trim();
|
||||
const numericValue = Number(normalizedValue);
|
||||
setSimulationConfig((current) => ({
|
||||
...current,
|
||||
[key]: key === "method" ? value : Number(value),
|
||||
[key]:
|
||||
key === "method"
|
||||
? value
|
||||
: normalizedValue !== "" && Number.isFinite(numericValue)
|
||||
? numericValue
|
||||
: normalizedValue,
|
||||
}));
|
||||
};
|
||||
|
||||
const generateXml = () => {
|
||||
try {
|
||||
const xml = buildSystemXml(buildCurrentProject());
|
||||
const xml = buildSystemXml(buildCurrentProject(), componentDefinitions);
|
||||
changeConsoleMode("normal");
|
||||
appendConsoleEntry(
|
||||
"success",
|
||||
@@ -3239,7 +3271,7 @@ function FlowWorkbench() {
|
||||
|
||||
const downloadXml = () => {
|
||||
try {
|
||||
const xml = buildSystemXml(buildCurrentProject());
|
||||
const xml = buildSystemXml(buildCurrentProject(), componentDefinitions);
|
||||
downloadText(`${safeFilename(projectName)}.xml`, xml);
|
||||
appendConsoleEntry("success", `XML 已下载:${safeFilename(projectName)}.xml`);
|
||||
} catch (error) {
|
||||
@@ -3402,7 +3434,7 @@ function FlowWorkbench() {
|
||||
|
||||
const publishSimulationResult = (
|
||||
result: SimulationResult,
|
||||
project: ReactFlowProjectPayload,
|
||||
project: ExecutableProjectPayload,
|
||||
) => {
|
||||
const resultStatus = result.status ?? (result.success ? "completed" : "failed");
|
||||
const sampleCount = Math.min(
|
||||
@@ -3510,34 +3542,48 @@ function FlowWorkbench() {
|
||||
const runSimulation = async () => {
|
||||
const issues = checkModel();
|
||||
const modelErrors = issues.filter((issue) => issue.severity === "error");
|
||||
const simulationResolution = resolveSimulationConfig(
|
||||
simulationConfigRef.current,
|
||||
);
|
||||
const progressConfig = simulationResolution.ok
|
||||
? simulationResolution.value
|
||||
: defaultSimulationConfig;
|
||||
if (modelErrors.length > 0) {
|
||||
changeConsoleMode("normal");
|
||||
setSimulationProgress({
|
||||
state: "error",
|
||||
percent: 0,
|
||||
message: "模型检查未通过",
|
||||
startTime: simulationConfigRef.current.t_start,
|
||||
simulatedTime: simulationConfigRef.current.t_start,
|
||||
totalTime: simulationConfigRef.current.t_stop,
|
||||
startTime: progressConfig.t_start,
|
||||
simulatedTime: progressConfig.t_start,
|
||||
totalTime: progressConfig.t_stop,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!simulationResolution.ok) {
|
||||
changeConsoleMode("normal");
|
||||
appendConsoleEntry("error", simulationResolution.message);
|
||||
return;
|
||||
}
|
||||
const project: ExecutableProjectPayload = {
|
||||
...buildCurrentProject(),
|
||||
simulation: simulationResolution.value,
|
||||
};
|
||||
changeConsoleMode("normal");
|
||||
setSimulationProgress({
|
||||
state: "running",
|
||||
percent: 0,
|
||||
message: "正在提交仿真任务",
|
||||
startTime: simulationConfigRef.current.t_start,
|
||||
simulatedTime: simulationConfigRef.current.t_start,
|
||||
totalTime: simulationConfigRef.current.t_stop,
|
||||
startTime: project.simulation.t_start,
|
||||
simulatedTime: project.simulation.t_start,
|
||||
totalTime: project.simulation.t_stop,
|
||||
});
|
||||
appendConsoleEntry(
|
||||
"info",
|
||||
`开始仿真:${projectNameRef.current || "untitled"},时间范围 ${simulationConfigRef.current.t_start} - ${simulationConfigRef.current.t_stop} s`,
|
||||
`开始仿真:${projectNameRef.current || "untitled"},时间范围 ${project.simulation.t_start} - ${project.simulation.t_stop} s`,
|
||||
);
|
||||
await runBusyAction("正在编译模型并运行仿真...", async () => {
|
||||
const project = buildCurrentProject();
|
||||
const xml = buildSystemXml(project);
|
||||
const xml = buildSystemXml(project, componentDefinitions);
|
||||
const simulationId = createSimulationId();
|
||||
activeSimulationIdRef.current = simulationId;
|
||||
setSimulationStopPending(false);
|
||||
@@ -3988,7 +4034,11 @@ function FlowWorkbench() {
|
||||
const error =
|
||||
gasReference && !gasReferenceAvailable
|
||||
? "引用的介质定义不存在"
|
||||
: parameterValidationMessage(definition, baseValue);
|
||||
: parameterValidationMessage(
|
||||
definition,
|
||||
baseValue,
|
||||
selectedUnit,
|
||||
);
|
||||
let valueControl: ReactNode;
|
||||
if (mediumDefinitionIndex) {
|
||||
valueControl = (
|
||||
@@ -4103,12 +4153,20 @@ function FlowWorkbench() {
|
||||
definition,
|
||||
selectedUnit,
|
||||
),
|
||||
calculatedValue: parameterCalculatedValueMetadata(
|
||||
baseValue,
|
||||
definition,
|
||||
selectedUnit,
|
||||
),
|
||||
};
|
||||
},
|
||||
),
|
||||
]
|
||||
: [];
|
||||
|
||||
const simulationSettingErrors = simulationConfigValidationMessages(
|
||||
simulationConfig,
|
||||
);
|
||||
const simulationSettingRows: ParameterTableRow[] = [
|
||||
{
|
||||
id: "simulation-t-start",
|
||||
@@ -4116,6 +4174,7 @@ function FlowWorkbench() {
|
||||
value: (
|
||||
<DeferredNumberInput
|
||||
ariaLabel="起始时间"
|
||||
invalid={Boolean(simulationSettingErrors.t_start)}
|
||||
value={simulationConfig.t_start}
|
||||
onCommit={(value) => updateSimulationConfig("t_start", value)}
|
||||
onEditEnd={finishHistoryEdit}
|
||||
@@ -4127,6 +4186,11 @@ function FlowWorkbench() {
|
||||
defaultValue: `${defaultSimulationConfig.t_start} s`,
|
||||
range: "有限数值",
|
||||
currentUnit: "s",
|
||||
error: simulationSettingErrors.t_start,
|
||||
calculatedValue: simulationCalculatedValueMetadata(
|
||||
simulationConfig.t_start,
|
||||
"s",
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "simulation-t-stop",
|
||||
@@ -4134,6 +4198,7 @@ function FlowWorkbench() {
|
||||
value: (
|
||||
<DeferredNumberInput
|
||||
ariaLabel="结束时间"
|
||||
invalid={Boolean(simulationSettingErrors.t_stop)}
|
||||
value={simulationConfig.t_stop}
|
||||
onCommit={(value) => updateSimulationConfig("t_stop", value)}
|
||||
onEditEnd={finishHistoryEdit}
|
||||
@@ -4145,6 +4210,11 @@ function FlowWorkbench() {
|
||||
defaultValue: `${defaultSimulationConfig.t_stop} s`,
|
||||
range: "> 起始时间",
|
||||
currentUnit: "s",
|
||||
error: simulationSettingErrors.t_stop,
|
||||
calculatedValue: simulationCalculatedValueMetadata(
|
||||
simulationConfig.t_stop,
|
||||
"s",
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "simulation-step",
|
||||
@@ -4152,6 +4222,7 @@ function FlowWorkbench() {
|
||||
value: (
|
||||
<DeferredNumberInput
|
||||
ariaLabel="采样步长"
|
||||
invalid={Boolean(simulationSettingErrors.step)}
|
||||
value={simulationConfig.step}
|
||||
onCommit={(value) => updateSimulationConfig("step", value)}
|
||||
onEditEnd={finishHistoryEdit}
|
||||
@@ -4163,6 +4234,11 @@ function FlowWorkbench() {
|
||||
defaultValue: `${defaultSimulationConfig.step} s`,
|
||||
range: "> 0",
|
||||
currentUnit: "s",
|
||||
error: simulationSettingErrors.step,
|
||||
calculatedValue: simulationCalculatedValueMetadata(
|
||||
simulationConfig.step,
|
||||
"s",
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "simulation-max-step",
|
||||
@@ -4170,6 +4246,7 @@ function FlowWorkbench() {
|
||||
value: (
|
||||
<DeferredNumberInput
|
||||
ariaLabel="最大积分步长"
|
||||
invalid={Boolean(simulationSettingErrors.max_step)}
|
||||
value={simulationConfig.max_step}
|
||||
onCommit={(value) => updateSimulationConfig("max_step", value)}
|
||||
onEditEnd={finishHistoryEdit}
|
||||
@@ -4181,6 +4258,11 @@ function FlowWorkbench() {
|
||||
defaultValue: `${defaultSimulationConfig.max_step} s`,
|
||||
range: "> 0",
|
||||
currentUnit: "s",
|
||||
error: simulationSettingErrors.max_step,
|
||||
calculatedValue: simulationCalculatedValueMetadata(
|
||||
simulationConfig.max_step,
|
||||
"s",
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "simulation-method",
|
||||
@@ -4838,7 +4920,9 @@ function normalizeLoadedNode(
|
||||
label: node.data.label || definition?.label || node.id,
|
||||
componentType: node.data.componentType || definition?.type || "component",
|
||||
modelType: node.data.modelType || definition?.modelType || node.data.componentType,
|
||||
symbol: node.data.symbol || definition?.symbol || node.data.componentType,
|
||||
// Catalog symbols are versioned presentation metadata. Prefer the current
|
||||
// definition so projects saved with an old generic icon are upgraded on load.
|
||||
symbol: definition?.symbol || node.data.symbol || node.data.componentType,
|
||||
ports: normalizeLoadedPorts(node.data.ports, definition),
|
||||
parameters,
|
||||
parameterUnits,
|
||||
@@ -5122,17 +5206,95 @@ function parameterCurrentUnitMetadata(
|
||||
: undefined;
|
||||
}
|
||||
|
||||
type NumericInputResolution =
|
||||
| { ok: true; value: number; expression: boolean }
|
||||
| { ok: false; message: string; expression: boolean };
|
||||
|
||||
function resolveNumericInput(value: ParameterValue): NumericInputResolution {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value)
|
||||
? { ok: true, value, expression: false }
|
||||
: { ok: false, message: "请输入有限数值", expression: false };
|
||||
}
|
||||
|
||||
const normalizedValue = value.trim();
|
||||
if (!normalizedValue) {
|
||||
return { ok: false, message: "参数不能为空", expression: false };
|
||||
}
|
||||
const numericValue = Number(normalizedValue);
|
||||
if (Number.isFinite(numericValue)) {
|
||||
return { ok: true, value: numericValue, expression: false };
|
||||
}
|
||||
|
||||
const evaluated = evaluateParameterExpression(normalizedValue);
|
||||
return evaluated.ok
|
||||
? { ok: true, value: evaluated.value, expression: true }
|
||||
: {
|
||||
ok: false,
|
||||
message: `等式无法计算:${evaluated.message}`,
|
||||
expression: true,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveParameterValue(
|
||||
value: ParameterValue,
|
||||
definition: ParameterDefinition,
|
||||
selectedUnit: string,
|
||||
): NumericInputResolution {
|
||||
const resolved = resolveNumericInput(value);
|
||||
if (!resolved.ok) {
|
||||
return resolved;
|
||||
}
|
||||
if (resolved.expression && definition.editor) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "该离散参数只能通过下拉栏选择",
|
||||
expression: true,
|
||||
};
|
||||
}
|
||||
if (!resolved.expression) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const baseValue = findUnitOption(definition, selectedUnit).toBase(
|
||||
resolved.value,
|
||||
);
|
||||
return Number.isFinite(baseValue)
|
||||
? { ...resolved, value: baseValue }
|
||||
: {
|
||||
ok: false,
|
||||
message: "等式换算到基础单位后不是有限数值",
|
||||
expression: true,
|
||||
};
|
||||
}
|
||||
|
||||
function parameterCalculatedValueMetadata(
|
||||
value: ParameterValue,
|
||||
definition: ParameterDefinition,
|
||||
selectedUnit: string,
|
||||
) {
|
||||
if (!isParameterExpressionValue(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const resolved = resolveParameterValue(value, definition, selectedUnit);
|
||||
if (!resolved.ok || !resolved.expression) {
|
||||
return undefined;
|
||||
}
|
||||
const unit = findUnitOption(definition, selectedUnit);
|
||||
const displayValue = unit.fromBase(resolved.value);
|
||||
return `${formatEditableNumber(displayValue)}${unit.label ? ` ${unit.label}` : ""}`;
|
||||
}
|
||||
|
||||
function parameterValidationMessage(
|
||||
definition: ParameterDefinition,
|
||||
value: ParameterValue,
|
||||
selectedUnit: string,
|
||||
) {
|
||||
if (typeof value === "string" && value.trim() === "") {
|
||||
return "参数不能为空";
|
||||
}
|
||||
const numericValue = Number(value);
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
return "请输入有效数字";
|
||||
const resolved = resolveParameterValue(value, definition, selectedUnit);
|
||||
if (!resolved.ok) {
|
||||
return resolved.message;
|
||||
}
|
||||
const numericValue = resolved.value;
|
||||
if (
|
||||
definition.options &&
|
||||
!definition.options.some((option) => option.value === numericValue)
|
||||
@@ -5153,6 +5315,85 @@ function parameterValidationMessage(
|
||||
return "";
|
||||
}
|
||||
|
||||
function simulationCalculatedValueMetadata(
|
||||
value: ParameterValue,
|
||||
unit: string,
|
||||
) {
|
||||
if (!isParameterExpressionValue(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const resolved = resolveNumericInput(value);
|
||||
if (!resolved.ok || !resolved.expression) {
|
||||
return undefined;
|
||||
}
|
||||
return `${formatEditableNumber(resolved.value)}${unit ? ` ${unit}` : ""}`;
|
||||
}
|
||||
|
||||
function simulationConfigValidationMessages(
|
||||
simulation: SimulationConfig,
|
||||
): Partial<Record<SimulationNumericKey, string>> {
|
||||
const resolutions = {
|
||||
t_start: resolveNumericInput(simulation.t_start),
|
||||
t_stop: resolveNumericInput(simulation.t_stop),
|
||||
step: resolveNumericInput(simulation.step),
|
||||
max_step: resolveNumericInput(simulation.max_step),
|
||||
} satisfies Record<SimulationNumericKey, NumericInputResolution>;
|
||||
const messages: Partial<Record<SimulationNumericKey, string>> = {};
|
||||
|
||||
(Object.keys(resolutions) as SimulationNumericKey[]).forEach((key) => {
|
||||
const resolved = resolutions[key];
|
||||
if (!resolved.ok) {
|
||||
messages[key] = resolved.message;
|
||||
}
|
||||
});
|
||||
if (
|
||||
resolutions.t_start.ok &&
|
||||
resolutions.t_stop.ok &&
|
||||
resolutions.t_stop.value <= resolutions.t_start.value
|
||||
) {
|
||||
messages.t_stop = "必须大于起始时间";
|
||||
}
|
||||
if (resolutions.step.ok && resolutions.step.value <= 0) {
|
||||
messages.step = "必须大于 0";
|
||||
}
|
||||
if (resolutions.max_step.ok && resolutions.max_step.value <= 0) {
|
||||
messages.max_step = "必须大于 0";
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
type SimulationConfigResolution =
|
||||
| { ok: true; value: ResolvedSimulationConfig }
|
||||
| { ok: false; message: string };
|
||||
|
||||
function resolveSimulationConfig(
|
||||
simulation: SimulationConfig,
|
||||
): SimulationConfigResolution {
|
||||
const messages = simulationConfigValidationMessages(simulation);
|
||||
const firstInvalidKey = (Object.keys(messages) as SimulationNumericKey[])[0];
|
||||
if (firstInvalidKey) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `仿真设置“${simulationConfigLabels[firstInvalidKey]}”:${messages[firstInvalidKey]}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
t_start: (resolveNumericInput(simulation.t_start) as { ok: true; value: number })
|
||||
.value,
|
||||
t_stop: (resolveNumericInput(simulation.t_stop) as { ok: true; value: number })
|
||||
.value,
|
||||
step: (resolveNumericInput(simulation.step) as { ok: true; value: number })
|
||||
.value,
|
||||
max_step: (
|
||||
resolveNumericInput(simulation.max_step) as { ok: true; value: number }
|
||||
).value,
|
||||
method: simulation.method,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function findDuplicateNames(nodes: SimulationNode[]) {
|
||||
const counts = new Map<string, number>();
|
||||
nodes.forEach((node) => {
|
||||
@@ -5236,9 +5477,16 @@ function validateModel(
|
||||
return;
|
||||
}
|
||||
Object.entries(definition.parameters).forEach(([key, parameter]) => {
|
||||
const message = parameterValidationMessage(parameter, node.data.parameters[key] ?? "");
|
||||
const selectedUnit =
|
||||
node.data.parameterUnits[key] ?? parameter.unit ?? "";
|
||||
const message = parameterValidationMessage(
|
||||
parameter,
|
||||
node.data.parameters[key] ?? "",
|
||||
selectedUnit,
|
||||
);
|
||||
if (message) {
|
||||
addIssue("error", `${node.data.label}:${parameter.label}${message}`, {
|
||||
const messageSeparator = message.startsWith("等式") ? ":" : "";
|
||||
addIssue("error", `${node.data.label}:${parameter.label}${messageSeparator}${message}`, {
|
||||
nodeId: node.id,
|
||||
});
|
||||
}
|
||||
@@ -5416,17 +5664,13 @@ function validateModel(
|
||||
});
|
||||
});
|
||||
|
||||
if (!Number.isFinite(simulation.t_start) || !Number.isFinite(simulation.t_stop)) {
|
||||
addIssue("error", "仿真起止时间必须是有效数字");
|
||||
} else if (simulation.t_stop <= simulation.t_start) {
|
||||
addIssue("error", "仿真结束时间必须大于起始时间");
|
||||
}
|
||||
if (!Number.isFinite(simulation.step) || simulation.step <= 0) {
|
||||
addIssue("error", "采样步长必须大于 0");
|
||||
}
|
||||
if (!Number.isFinite(simulation.max_step) || simulation.max_step <= 0) {
|
||||
addIssue("error", "最大积分步长必须大于 0");
|
||||
}
|
||||
const simulationErrors = simulationConfigValidationMessages(simulation);
|
||||
(Object.keys(simulationErrors) as SimulationNumericKey[]).forEach((key) => {
|
||||
const message = simulationErrors[key];
|
||||
if (message) {
|
||||
addIssue("error", `仿真设置:${simulationConfigLabels[key]}:${message}`);
|
||||
}
|
||||
});
|
||||
if (!SUPPORTED_SOLVER_METHODS.includes(simulation.method)) {
|
||||
addIssue("error", "求解器不受支持");
|
||||
}
|
||||
@@ -5548,7 +5792,15 @@ function formatTime(date: Date) {
|
||||
return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function buildSystemXml(project: ReactFlowProjectPayload) {
|
||||
function buildSystemXml(
|
||||
project: ReactFlowProjectPayload,
|
||||
componentDefinitions: ComponentDefinition[],
|
||||
) {
|
||||
const simulationResolution = resolveSimulationConfig(project.simulation);
|
||||
if (!simulationResolution.ok) {
|
||||
throw new Error(simulationResolution.message);
|
||||
}
|
||||
const resolvedSimulation = simulationResolution.value;
|
||||
const xmlDocument = document.implementation.createDocument("", "System", null);
|
||||
const system = xmlDocument.documentElement;
|
||||
system.setAttribute("name", project.name);
|
||||
@@ -5557,16 +5809,22 @@ function buildSystemXml(project: ReactFlowProjectPayload) {
|
||||
system.setAttribute("unitSystem", "SI");
|
||||
|
||||
const simulation = xmlDocument.createElement("Simulation");
|
||||
simulation.setAttribute("tStart", String(project.simulation.t_start));
|
||||
simulation.setAttribute("tStop", String(project.simulation.t_stop));
|
||||
simulation.setAttribute("step", String(project.simulation.step));
|
||||
simulation.setAttribute("maxStep", String(project.simulation.max_step));
|
||||
simulation.setAttribute("method", project.simulation.method);
|
||||
simulation.setAttribute("tStart", String(resolvedSimulation.t_start));
|
||||
simulation.setAttribute("tStop", String(resolvedSimulation.t_stop));
|
||||
simulation.setAttribute("step", String(resolvedSimulation.step));
|
||||
simulation.setAttribute("maxStep", String(resolvedSimulation.max_step));
|
||||
simulation.setAttribute("method", resolvedSimulation.method);
|
||||
const components = xmlDocument.createElement("Components");
|
||||
const connections = xmlDocument.createElement("Connections");
|
||||
system.append(simulation, components, connections);
|
||||
|
||||
project.nodes.forEach((node) => {
|
||||
const definition = componentDefinitions.find(
|
||||
(candidate) => candidate.type === node.data.componentType,
|
||||
);
|
||||
if (!definition) {
|
||||
throw new Error(`组件 ${node.data.label || node.id} 的类型不受支持`);
|
||||
}
|
||||
const component = xmlDocument.createElement("Component");
|
||||
component.setAttribute("id", node.id);
|
||||
component.setAttribute("name", node.data.label);
|
||||
@@ -5590,9 +5848,20 @@ function buildSystemXml(project: ReactFlowProjectPayload) {
|
||||
component.appendChild(port);
|
||||
});
|
||||
Object.entries(node.data.parameters).forEach(([name, value]) => {
|
||||
const parameterDefinition = definition.parameters[name];
|
||||
const selectedUnit =
|
||||
node.data.parameterUnits?.[name] ?? parameterDefinition?.unit ?? "";
|
||||
const resolved = parameterDefinition
|
||||
? resolveParameterValue(value, parameterDefinition, selectedUnit)
|
||||
: resolveNumericInput(value);
|
||||
if (!resolved.ok) {
|
||||
throw new Error(
|
||||
`组件“${node.data.label || node.id}”的参数“${parameterDefinition?.label ?? name}”:${resolved.message}`,
|
||||
);
|
||||
}
|
||||
const parameter = xmlDocument.createElement("Parameter");
|
||||
parameter.setAttribute("name", name);
|
||||
parameter.setAttribute("value", String(value));
|
||||
parameter.setAttribute("value", formatEditableNumber(resolved.value));
|
||||
component.appendChild(parameter);
|
||||
});
|
||||
components.appendChild(component);
|
||||
|
||||
Reference in new issue
Block a user