完善MECMAS21参数配置与分组交互

This commit is contained in:
ljz committed 2026-08-03 23:51:27 +08:00
1 parent 32f21f08ff
commit be54070855
28 files changed
+3418 -202

No files matched your search

+361 -72
View File
@@ -95,6 +95,7 @@ import {
loadParameterColumnLayout,
saveParameterColumnLayout,
type ParameterColumnLayout,
type ParameterTableGroup,
type ParameterTableRow,
} from "./ParameterTable";
import {
@@ -174,8 +175,9 @@ type ParameterDefinition = {
min?: number;
max?: number;
minExclusive?: boolean;
editor?: "amesimGasReference" | "amesimGasPropertyModel";
editor?: "choice" | "amesimGasReference" | "amesimGasPropertyModel";
options?: ParameterOption[];
visibleWhen?: ParameterVisibilityCondition[];
};
type ParameterOption = {
@@ -183,6 +185,19 @@ type ParameterOption = {
label: string;
};
type ParameterVisibilityCondition = {
parameter: string;
values: number[];
};
type ParameterGroupDefinition = {
id: string;
label: string;
order: number;
parameters: string[];
defaultExpanded: boolean;
};
type ComponentDefinition = {
type: string;
label: string;
@@ -198,6 +213,7 @@ type ComponentDefinition = {
role?: "amesimGasMediumDefinition";
ports: PortDefinition[];
parameters: Record<string, ParameterDefinition>;
parameterGroups?: ParameterGroupDefinition[];
};
type ComponentLibraryDefinition = {
@@ -220,8 +236,9 @@ type ComponentCatalogParameterPayload = {
minimum?: number;
maximum?: number;
minimumExclusive: boolean;
editor?: "amesimGasReference" | "amesimGasPropertyModel";
editor?: "choice" | "amesimGasReference" | "amesimGasPropertyModel";
options?: ParameterOption[];
visibleWhen?: ParameterVisibilityCondition[];
};
type ComponentCatalogComponentPayload = {
@@ -239,6 +256,13 @@ type ComponentCatalogComponentPayload = {
role?: "amesimGasMediumDefinition";
ports: Array<PortDefinition & { order: number }>;
parameters: ComponentCatalogParameterPayload[];
parameterGroups?: Array<{
id: string;
label: string;
order: number;
parameters: string[];
defaultExpanded?: boolean;
}>;
};
type ComponentCatalogResponse = {
@@ -313,7 +337,8 @@ type ProjectEdgePayload = {
type ReactFlowProjectPayload = {
name: string;
mediumReferenceVersion?: 1;
mediumReferenceVersion?: 1 | null;
amesimParameterEncodingVersion?: 1 | null;
nodes: ProjectNodePayload[];
edges: ProjectEdgePayload[];
simulation: SimulationConfig;
@@ -1055,6 +1080,58 @@ function normalizeComponentCatalog(payload: unknown): ComponentLibraryDefinition
}
componentTypes.add(component.type);
const parameters = Object.fromEntries(
component.parameters.map((parameter) => {
const visibleWhen = normalizeParameterVisibilityConditions(
parameter.visibleWhen,
);
return [
parameter.name,
{
label: parameter.label || parameter.name,
...(typeof parameter.description === "string" &&
parameter.description.trim()
? { description: parameter.description.trim() }
: {}),
...(parameter.unit ? { unit: parameter.unit } : {}),
...(parameter.quantity && parameter.quantity !== "dimensionless"
? { quantity: parameter.quantity }
: {}),
default: parameter.default,
...(parameter.minimum !== undefined
? { min: parameter.minimum }
: {}),
...(parameter.maximum !== undefined
? { max: parameter.maximum }
: {}),
...(parameter.minimumExclusive
? { minExclusive: true }
: {}),
...(parameter.editor === "choice" ||
parameter.editor === "amesimGasReference" ||
parameter.editor === "amesimGasPropertyModel"
? { editor: parameter.editor }
: {}),
...(Array.isArray(parameter.options)
? {
options: parameter.options.flatMap((option) =>
Number.isFinite(Number(option?.value)) &&
typeof option?.label === "string"
? [{ value: Number(option.value), label: option.label }]
: [],
),
}
: {}),
...(visibleWhen ? { visibleWhen } : {}),
},
] as const;
}),
);
const parameterGroups = normalizeParameterGroups(
component.parameterGroups,
new Set(Object.keys(parameters)),
);
return {
type: component.type,
label: component.label,
@@ -1080,46 +1157,8 @@ function normalizeComponentCatalog(payload: unknown): ComponentLibraryDefinition
: {}),
side: port.side,
})),
parameters: Object.fromEntries(
component.parameters.map((parameter) => [
parameter.name,
{
label: parameter.label || parameter.name,
...(typeof parameter.description === "string" &&
parameter.description.trim()
? { description: parameter.description.trim() }
: {}),
...(parameter.unit ? { unit: parameter.unit } : {}),
...(parameter.quantity && parameter.quantity !== "dimensionless"
? { quantity: parameter.quantity }
: {}),
default: parameter.default,
...(parameter.minimum !== undefined
? { min: parameter.minimum }
: {}),
...(parameter.maximum !== undefined
? { max: parameter.maximum }
: {}),
...(parameter.minimumExclusive
? { minExclusive: true }
: {}),
...(parameter.editor === "amesimGasReference" ||
parameter.editor === "amesimGasPropertyModel"
? { editor: parameter.editor }
: {}),
...(Array.isArray(parameter.options)
? {
options: parameter.options.flatMap((option) =>
Number.isFinite(Number(option?.value)) &&
typeof option?.label === "string"
? [{ value: Number(option.value), label: option.label }]
: [],
),
}
: {}),
},
]),
),
parameters,
...(parameterGroups.length > 0 ? { parameterGroups } : {}),
} satisfies ComponentDefinition;
});
@@ -1193,11 +1232,44 @@ function defaultParameterUnits(definition: ComponentDefinition) {
);
}
function orderedParameterEntries(definition: ComponentDefinition) {
return Object.entries(definition.parameters).sort(
([firstKey], [secondKey]) =>
Number(secondKey === "gi") - Number(firstKey === "gi"),
function ungroupedParameterPriority(
key: string,
definition: ParameterDefinition,
) {
if (key === "gi") {
return 0;
}
return definition.editor ? 1 : 2;
}
function orderedParameterEntries(
definition: ComponentDefinition,
): Array<[string, ParameterDefinition]> {
const entries = Object.entries(definition.parameters);
const entryByName = new Map(entries);
const groupedNames = new Set(
definition.parameterGroups?.flatMap((group) => group.parameters) ?? [],
);
const ungroupedEntries = entries
.map((entry, index) => ({ entry, index }))
.filter(({ entry: [key] }) => !groupedNames.has(key))
.sort(
(first, second) =>
ungroupedParameterPriority(first.entry[0], first.entry[1]) -
ungroupedParameterPriority(second.entry[0], second.entry[1]) ||
first.index - second.index,
)
.map(({ entry }) => entry);
const groupedEntries =
definition.parameterGroups?.flatMap((group) =>
group.parameters.flatMap((name) => {
const parameter = entryByName.get(name);
return parameter
? ([[name, parameter]] as Array<[string, ParameterDefinition]>)
: [];
}),
) ?? [];
return [...ungroupedEntries, ...groupedEntries];
}
function buildNodeData(definition: ComponentDefinition): SimulationNodeData {
@@ -1392,9 +1464,9 @@ function repairAmesimGasReferences(
function migrateLegacyDefaultAmesimGasReference(
nodes: SimulationNode[],
componentDefinitions: ComponentDefinition[],
mediumReferenceVersion: 1 | undefined,
mediumReferenceVersion: 1 | null | undefined,
): AmesimGasReferenceRepair {
const hasVersionMarker = mediumReferenceVersion !== undefined;
const hasVersionMarker = mediumReferenceVersion === 1;
const hasMediumDefinition = nodes.some((node) =>
isAmesimGasMediumDefinition(
componentDefinitionForNode(node, componentDefinitions),
@@ -4260,7 +4332,11 @@ function FlowWorkbench() {
recordHistory();
}
const normalizedNodes = project.nodes.map((node) =>
normalizeLoadedNode(node, componentDefinitions),
normalizeLoadedNode(
node,
componentDefinitions,
project.amesimParameterEncodingVersion,
),
);
const indexedMedia = assignMissingAmesimGasDefinitionIndices(
normalizedNodes,
@@ -4993,8 +5069,11 @@ function FlowWorkbench() {
readOnly: true,
description: "系统为画布节点分配的稳定标识,用于工程数据和连接关系。",
},
...orderedParameterEntries(selectedDefinition).map(
([key, definition]): ParameterTableRow => {
...orderedParameterEntries(selectedDefinition)
.filter(([, definition]) =>
parameterIsVisible(definition, selectedNode.data.parameters),
)
.map(([key, definition]): ParameterTableRow => {
const selectedUnit =
selectedNode.data.parameterUnits[key] ?? definition.unit ?? "";
const units = parameterUnitOptions(definition);
@@ -5012,6 +5091,7 @@ function FlowWorkbench() {
definition.editor === "amesimGasReference";
const gasPropertyModel =
definition.editor === "amesimGasPropertyModel";
const genericChoice = definition.editor === "choice";
const parameterLabel = mediumDefinitionIndex
? "介质定义索引"
: gasReference
@@ -5025,21 +5105,20 @@ function FlowWorkbench() {
amesimGasMediumOptions.some(
(option) => option.value === gasReferenceIndex,
);
const propertyModelValue = Number(baseValue);
const propertyModelAvailable =
gasPropertyModel &&
Number.isFinite(propertyModelValue) &&
const optionValue = numericChoiceValue(baseValue);
const optionAvailable =
(gasPropertyModel || genericChoice) &&
optionValue !== null &&
(definition.options ?? []).some(
(option) => option.value === propertyModelValue,
(option) => option.value === optionValue,
);
const error =
gasReference && !gasReferenceAvailable
? "引用的介质定义不存在"
: parameterValidationMessage(
definition,
baseValue,
selectedUnit,
);
componentParameterValidationMessage(
definition,
baseValue,
selectedUnit,
gasReference ? amesimGasMediumOptions : undefined,
);
let valueControl: ReactNode;
if (mediumDefinitionIndex) {
valueControl = (
@@ -5076,7 +5155,7 @@ function FlowWorkbench() {
))}
</select>
);
} else if (gasPropertyModel) {
} else if (gasPropertyModel || genericChoice) {
valueControl = (
<select
aria-invalid={Boolean(error)}
@@ -5087,12 +5166,14 @@ function FlowWorkbench() {
}
onFocus={recordHistory}
value={
propertyModelAvailable ? String(propertyModelValue) : ""
optionAvailable ? String(optionValue) : ""
}
>
{!propertyModelAvailable ? (
{!optionAvailable ? (
<option disabled value="">
当前模型无效,请重新选择
{gasPropertyModel
? "当前模型无效,请重新选择"
: "当前选项无效,请重新选择"}
</option>
) : null}
{(definition.options ?? []).map((option) => (
@@ -5160,10 +5241,61 @@ function FlowWorkbench() {
selectedUnit,
),
};
},
),
}),
]
: [];
const componentParameterGroups: ParameterTableGroup[] =
selectedNode && selectedDefinition
? (selectedDefinition.parameterGroups ?? []).flatMap((group) => {
const rowIds = group.parameters.flatMap((name) => {
const definition = selectedDefinition.parameters[name];
return definition &&
parameterIsVisible(definition, selectedNode.data.parameters)
? [`component-parameter-${name}`]
: [];
});
const invalidParameters = group.parameters.filter((name) => {
const definition = selectedDefinition.parameters[name];
if (!definition) {
return false;
}
const selectedUnit =
selectedNode.data.parameterUnits[name] ?? definition.unit ?? "";
const mediumDefinitionIndex =
isAmesimGasMediumDefinition(selectedDefinition) && name === "gi";
const gasReference =
!mediumDefinitionIndex &&
definition.editor === "amesimGasReference";
return Boolean(
componentParameterValidationMessage(
definition,
selectedNode.data.parameters[name] ?? "",
selectedUnit,
gasReference ? amesimGasMediumOptions : undefined,
),
);
});
const hiddenErrorCount = invalidParameters.filter((name) => {
const definition = selectedDefinition.parameters[name];
return (
definition &&
!parameterIsVisible(definition, selectedNode.data.parameters)
);
}).length;
return rowIds.length > 0 || invalidParameters.length > 0
? [
{
id: `${selectedNode.id}:${group.id}`,
label: group.label,
rowIds,
defaultExpanded: group.defaultExpanded,
errorCount: invalidParameters.length,
hiddenErrorCount,
},
]
: [];
})
: [];
const simulationSettingErrors = simulationConfigValidationMessages(
simulationConfig,
@@ -5825,6 +5957,7 @@ function FlowWorkbench() {
ariaLabel="组件参数"
availableWidth={parameterTableAvailableWidth}
columnLayout={parameterColumnLayout}
groups={componentParameterGroups}
onColumnLayoutChange={setParameterColumnLayout}
rows={componentParameterRows}
/>
@@ -5929,6 +6062,7 @@ function buildProjectPayload(
return {
name: name.trim() || "untitled",
mediumReferenceVersion: 1,
amesimParameterEncodingVersion: 1,
nodes: nodes.map((node) => ({
id: node.id,
type: "simulationComponent",
@@ -5961,6 +6095,110 @@ function buildProjectPayload(
};
}
function numericChoiceValue(value: ParameterValue | undefined) {
if (typeof value === "string" && value.trim() === "") {
return null;
}
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : null;
}
function parameterIsVisible(
definition: ParameterDefinition,
parameters: Record<string, ParameterValue>,
) {
return (
!definition.visibleWhen?.length ||
definition.visibleWhen.every((condition) => {
const currentValue = numericChoiceValue(parameters[condition.parameter]);
return currentValue !== null && condition.values.includes(currentValue);
})
);
}
function normalizeParameterGroups(
value: unknown,
parameterNames: ReadonlySet<string>,
): ParameterGroupDefinition[] {
if (!Array.isArray(value)) {
return [];
}
const groupIds = new Set<string>();
const groupedParameters = new Set<string>();
const groups = value.flatMap((entry) => {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
return [];
}
const candidate = entry as Record<string, unknown>;
const id = typeof candidate.id === "string" ? candidate.id.trim() : "";
const label =
typeof candidate.label === "string" ? candidate.label.trim() : "";
if (!id || !label || groupIds.has(id) || !Array.isArray(candidate.parameters)) {
return [];
}
const parameters = candidate.parameters.flatMap((parameter) => {
if (
typeof parameter !== "string" ||
!parameterNames.has(parameter) ||
groupedParameters.has(parameter)
) {
return [];
}
groupedParameters.add(parameter);
return [parameter];
});
if (parameters.length === 0) {
return [];
}
groupIds.add(id);
const numericOrder = Number(candidate.order);
return [
{
id,
label,
order: Number.isInteger(numericOrder) ? numericOrder : 0,
parameters,
defaultExpanded: candidate.defaultExpanded === true,
},
];
});
return groups.sort(
(first, second) => first.order - second.order || first.id.localeCompare(second.id),
);
}
function normalizeParameterVisibilityConditions(
value: unknown,
): ParameterVisibilityCondition[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const conditions = value.flatMap((entry) => {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
return [];
}
const candidate = entry as Record<string, unknown>;
if (typeof candidate.parameter !== "string" || !candidate.parameter.trim()) {
return [];
}
if (!Array.isArray(candidate.values)) {
return [];
}
const values = [
...new Set(
candidate.values.flatMap((item) => {
const numericValue = Number(item);
return Number.isFinite(numericValue) ? [numericValue] : [];
}),
),
];
return values.length > 0
? [{ parameter: candidate.parameter.trim(), values }]
: [];
});
return conditions.length > 0 ? conditions : undefined;
}
function normalizeParameterScientificNotation(
value: unknown,
): Record<string, ParameterScientificNotation> {
@@ -5991,14 +6229,22 @@ function normalizeParameterScientificNotation(
function normalizeLoadedNode(
node: ProjectNodePayload,
componentDefinitions: ComponentDefinition[],
amesimParameterEncodingVersion?: 1 | null,
): SimulationNode {
const definition =
componentDefinitions.find((item) => item.type === node.data.componentType) ??
componentDefinitions.find((item) => item.modelType === node.data.modelType);
const effectiveModelType =
definition?.modelType || node.data.modelType || node.data.componentType;
const rawParameters = node.data.parameters ?? {};
const parameters = definition
const mergedParameters = definition
? { ...defaultParameters(definition), ...rawParameters }
: { ...rawParameters };
const parameters = migrateLegacyAmesimParameterEncoding(
effectiveModelType,
mergedParameters,
amesimParameterEncodingVersion,
);
if (
isAmesimGasMediumDefinition(definition) &&
!Object.prototype.hasOwnProperty.call(rawParameters, "gi")
@@ -6288,6 +6534,26 @@ function displayParameterValue(
return formatEditableNumber(displayValue);
}
function migrateLegacyAmesimParameterEncoding(
modelType: string,
parameters: Record<string, ParameterValue>,
version?: 1 | null,
) {
if (version === 1 || modelType !== "amesim_mecmas21") {
return { ...parameters };
}
const migrated = { ...parameters };
(["useFriction", "strib"] as const).forEach((name) => {
const value = numericChoiceValue(migrated[name]);
if (value === 0) {
migrated[name] = 1;
} else if (value === 1) {
migrated[name] = 2;
}
});
return migrated;
}
function formatEditableNumber(value: number) {
if (!Number.isFinite(value)) {
return "";
@@ -6496,6 +6762,24 @@ function parameterValidationMessage(
return "";
}
function componentParameterValidationMessage(
definition: ParameterDefinition,
value: ParameterValue,
selectedUnit: string,
gasMediumOptions?: readonly AmesimGasMediumOption[],
) {
if (gasMediumOptions !== undefined) {
const gasReferenceIndex = normalizeAmesimGasIndex(value);
if (
gasReferenceIndex === null ||
!gasMediumOptions.some((option) => option.value === gasReferenceIndex)
) {
return "引用的介质定义不存在";
}
}
return parameterValidationMessage(definition, value, selectedUnit);
}
function simulationCalculatedValueMetadata(
value: ParameterValue,
unit: string,
@@ -6951,7 +7235,11 @@ function parseProjectPayload(raw: string | null): ReactFlowProjectPayload | null
!project.simulation ||
typeof project.simulation !== "object" ||
(project.mediumReferenceVersion !== undefined &&
project.mediumReferenceVersion !== 1)
project.mediumReferenceVersion !== null &&
project.mediumReferenceVersion !== 1) ||
(project.amesimParameterEncodingVersion !== undefined &&
project.amesimParameterEncodingVersion !== null &&
project.amesimParameterEncodingVersion !== 1)
) {
return null;
}
@@ -6987,6 +7275,7 @@ function buildSystemXml(
system.setAttribute("name", project.name);
system.setAttribute("schemaVersion", "2");
system.setAttribute("mediumReferenceVersion", "1");
system.setAttribute("amesimParameterEncodingVersion", "1");
system.setAttribute("unitSystem", "SI");
const simulation = xmlDocument.createElement("Simulation");