完善建模交互与结果曲线功能
支持元件接口接触吸附、旋转后兼容连接及无连线逻辑边。 完善 AMESim 图标显示、参数双栏、结果曲线待定窗口及相关端到端测试。
This commit is contained in:
1 parent
410ef535e8
commit
503394f3ed
10 files changed
+2542
-389
No files matched your search
+1020
-140
File diff suppressed because it is too large.
Load diff
@@ -25,6 +25,7 @@ import { SymbolSvg } from "./componentSymbols/primitives";
|
||||
import { AmesimStep0Symbol, AmesimUd00Symbol } from "./componentSymbols/signals";
|
||||
import { AmesimPnch012Symbol, AmesimPnch023Symbol } from "./componentSymbols/storage";
|
||||
import type {
|
||||
ComponentSymbolPortAnchors,
|
||||
ComponentSymbolPresentation,
|
||||
SymbolDefinition,
|
||||
} from "./componentSymbols/types";
|
||||
@@ -60,7 +61,7 @@ function CylinderSymbol() {
|
||||
function TankSymbol() {
|
||||
return (
|
||||
<SymbolSvg>
|
||||
<path d="M32 2v5M32 41v5" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M2 24h14M32 2v5M32 41v5" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
d="M16 13c0-4 7.2-7 16-7s16 3 16 7v22c0 4-7.2 7-16 7s-16-3-16-7V13Z"
|
||||
fill="#dcfce7"
|
||||
@@ -75,7 +76,7 @@ function TankSymbol() {
|
||||
function PipeSymbol() {
|
||||
return (
|
||||
<SymbolSvg>
|
||||
<path d="M2 19h60M2 29h60" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M2 24h7M55 24h7M9 19h46M9 29h46" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M9 17v14M55 17v14" stroke="currentColor" strokeWidth="2" />
|
||||
</SymbolSvg>
|
||||
);
|
||||
@@ -99,8 +100,18 @@ function OrificeSymbol() {
|
||||
function TeeSymbol() {
|
||||
return (
|
||||
<SymbolSvg>
|
||||
<path d="M2 27h60M32 27V3" stroke="currentColor" strokeWidth="8" />
|
||||
<path d="M2 27h60M32 27V3" stroke="#f8fafc" strokeWidth="4" />
|
||||
<path
|
||||
d="M2 24h28M30 24C42 24 44 12 62 12M30 24c12 0 14 12 32 12"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="7"
|
||||
/>
|
||||
<path
|
||||
d="M2 24h28M30 24C42 24 44 12 62 12M30 24c12 0 14 12 32 12"
|
||||
stroke="#f8fafc"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
</SymbolSvg>
|
||||
);
|
||||
}
|
||||
@@ -195,6 +206,123 @@ const symbolRegistry = {
|
||||
|
||||
type RegisteredComponentSymbol = keyof typeof symbolRegistry;
|
||||
|
||||
const symbolPortAnchors: Partial<
|
||||
Record<RegisteredComponentSymbol, ComponentSymbolPortAnchors>
|
||||
> = {
|
||||
cylinder: {
|
||||
left: [{ x: 3, y: 24 }],
|
||||
right: [{ x: 61, y: 24 }],
|
||||
},
|
||||
tank: { left: [{ x: 2, y: 24 }] },
|
||||
pipe: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
orifice: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
tee: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [
|
||||
{ x: 62, y: 12 },
|
||||
{ x: 62, y: 36 },
|
||||
],
|
||||
},
|
||||
generic: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
signal: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
junction: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
amesim_pnpl01: { left: [{ x: 2, y: 24 }] },
|
||||
amesim_pnch023: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
amesim_pnch012: {
|
||||
left: [
|
||||
{ x: 2, y: 16 },
|
||||
{ x: 2, y: 32 },
|
||||
],
|
||||
right: [
|
||||
{ x: 62, y: 16 },
|
||||
{ x: 62, y: 32 },
|
||||
],
|
||||
},
|
||||
amesim_pnor001: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
amesim_pnvo001_fixed: {
|
||||
left: [{ x: 2, y: 28 }],
|
||||
right: [{ x: 62, y: 28 }],
|
||||
},
|
||||
amesim_pnvo001: {
|
||||
left: [
|
||||
{ x: 2, y: 16 },
|
||||
{ x: 2, y: 32 },
|
||||
],
|
||||
right: [{ x: 62, y: 25 }],
|
||||
},
|
||||
amesim_pnl00r: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
amesim_pnl0001: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
amesim_pnl0002: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
amesim_pnl0003: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
amesim_pn3node2: {
|
||||
left: [{ x: 3, y: 24 }],
|
||||
right: [
|
||||
{ x: 61, y: 12 },
|
||||
{ x: 61, y: 36 },
|
||||
],
|
||||
},
|
||||
amesim_p4node2: {
|
||||
left: [{ x: 3, y: 24 }],
|
||||
right: [
|
||||
{ x: 61, y: 8 },
|
||||
{ x: 61, y: 24 },
|
||||
{ x: 61, y: 40 },
|
||||
],
|
||||
},
|
||||
amesim_step0: { right: [{ x: 60, y: 24 }] },
|
||||
amesim_ud00: { right: [{ x: 60, y: 24 }] },
|
||||
amesim_f000: { right: [{ x: 62, y: 24 }] },
|
||||
amesim_forc: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
amesim_mecmas21: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
amesim_lstp00a: {
|
||||
left: [{ x: 2, y: 24 }],
|
||||
right: [{ x: 62, y: 24 }],
|
||||
},
|
||||
amesim_lmechn1: {
|
||||
left: [7, 12, 17, 22, 27, 32, 37, 42].map((y) => ({ x: 5, y })),
|
||||
right: [{ x: 59, y: 24 }],
|
||||
},
|
||||
};
|
||||
|
||||
function isRegisteredComponentSymbol(symbol: string): symbol is RegisteredComponentSymbol {
|
||||
return Object.prototype.hasOwnProperty.call(symbolRegistry, symbol);
|
||||
}
|
||||
@@ -211,6 +339,14 @@ export function componentSymbolPresentation(
|
||||
: "framed";
|
||||
}
|
||||
|
||||
export function componentSymbolPortAnchors(
|
||||
symbol: string,
|
||||
): ComponentSymbolPortAnchors | undefined {
|
||||
return isRegisteredComponentSymbol(symbol)
|
||||
? symbolPortAnchors[symbol]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function ComponentSymbol({
|
||||
symbol,
|
||||
surface,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
BaseEdge,
|
||||
getSmoothStepPath,
|
||||
type EdgeProps,
|
||||
type EdgeTypes,
|
||||
} from "@xyflow/react";
|
||||
|
||||
export const CONTACT_AWARE_EDGE_TYPE = "contact-aware";
|
||||
// React Flow 在 Loose 模式下会把重叠 Handle 的边端点分别放在圆点两侧,
|
||||
// 因此两个端口中心完全重合时,edge props 仍会相差约一个 Handle 直径。
|
||||
export const CONTACT_EDGE_SCREEN_TOLERANCE = 12;
|
||||
|
||||
export function ContactAwareEdge({
|
||||
id,
|
||||
interactionWidth,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
sourcePosition,
|
||||
sourceX,
|
||||
sourceY,
|
||||
style,
|
||||
targetPosition,
|
||||
targetX,
|
||||
targetY,
|
||||
}: EdgeProps) {
|
||||
const distance = Math.hypot(targetX - sourceX, targetY - sourceY);
|
||||
if (distance <= CONTACT_EDGE_SCREEN_TOLERANCE) {
|
||||
const centerX = (sourceX + targetX) / 2;
|
||||
const centerY = (sourceY + targetY) / 2;
|
||||
return (
|
||||
<>
|
||||
<circle
|
||||
aria-label="接触连接"
|
||||
className="contact-edge-hit"
|
||||
cx={centerX}
|
||||
cy={centerY}
|
||||
data-contact-edge-id={id}
|
||||
r={12}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
/>
|
||||
<circle
|
||||
aria-hidden="true"
|
||||
className="contact-edge-marker"
|
||||
cx={centerX}
|
||||
cy={centerY}
|
||||
r={3.5}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const [edgePath] = getSmoothStepPath({
|
||||
sourcePosition,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetPosition,
|
||||
targetX,
|
||||
targetY,
|
||||
borderRadius: 0,
|
||||
});
|
||||
return (
|
||||
<BaseEdge
|
||||
id={id}
|
||||
interactionWidth={interactionWidth}
|
||||
markerEnd={markerEnd}
|
||||
markerStart={markerStart}
|
||||
path={edgePath}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const contactAwareEdgeTypes: EdgeTypes = {
|
||||
[CONTACT_AWARE_EDGE_TYPE]: ContactAwareEdge,
|
||||
};
|
||||
@@ -34,6 +34,10 @@ import {
|
||||
type WorkspaceView,
|
||||
} from "./WorkspaceViewTabs";
|
||||
import { AutoFitView } from "./AutoFitView";
|
||||
import {
|
||||
CONTACT_AWARE_EDGE_TYPE,
|
||||
contactAwareEdgeTypes,
|
||||
} from "./ContactAwareEdge";
|
||||
|
||||
type ResultPortDefinition = {
|
||||
name: string;
|
||||
@@ -55,6 +59,10 @@ type ResultNodeData = {
|
||||
mirrored: boolean;
|
||||
};
|
||||
|
||||
type ResultRenderedNodeData = ResultNodeData & {
|
||||
portConnectionStates: Record<string, "idle" | "connected">;
|
||||
};
|
||||
|
||||
type ResultProjectNode = {
|
||||
id: string;
|
||||
type: "simulationComponent";
|
||||
@@ -340,8 +348,6 @@ export function SimulationResultsView({
|
||||
const [activePaneResize, setActivePaneResize] =
|
||||
useState<PaneResizeKind | null>(null);
|
||||
const [csvDownloadPending, setCsvDownloadPending] = useState(false);
|
||||
const [multiCreateMenuOpen, setMultiCreateMenuOpen] = useState(false);
|
||||
const [mixedCreateMenuOpen, setMixedCreateMenuOpen] = useState(false);
|
||||
const [activeMultiPickerWindowId, setActiveMultiPickerWindowId] =
|
||||
useState<string | null>(null);
|
||||
const [multiDropTargetWindowId, setMultiDropTargetWindowId] =
|
||||
@@ -428,7 +434,21 @@ export function SimulationResultsView({
|
||||
|
||||
const selectedNode =
|
||||
snapshot.project.nodes.find((node) => node.id === selectedNodeId) ?? null;
|
||||
const readOnlyNodes = useMemo<Node<ResultNodeData, "simulationComponent">[]>(
|
||||
const connectedPortKeys = useMemo(() => {
|
||||
const keys = new Set<string>();
|
||||
snapshot.project.edges.forEach((edge) => {
|
||||
if (edge.sourceHandle) {
|
||||
keys.add(`${edge.source}:${edge.sourceHandle}`);
|
||||
}
|
||||
if (edge.targetHandle) {
|
||||
keys.add(`${edge.target}:${edge.targetHandle}`);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}, [snapshot.project.edges]);
|
||||
const readOnlyNodes = useMemo<
|
||||
Node<ResultRenderedNodeData, "simulationComponent">[]
|
||||
>(
|
||||
() =>
|
||||
snapshot.project.nodes.map((node) => ({
|
||||
...node,
|
||||
@@ -438,8 +458,19 @@ export function SimulationResultsView({
|
||||
focusable: true,
|
||||
selectable: true,
|
||||
selected: node.id === selectedNodeId,
|
||||
data: {
|
||||
...node.data,
|
||||
portConnectionStates: Object.fromEntries(
|
||||
node.data.ports.map((port) => [
|
||||
port.name,
|
||||
connectedPortKeys.has(`${node.id}:${port.name}`)
|
||||
? "connected"
|
||||
: "idle",
|
||||
]),
|
||||
),
|
||||
},
|
||||
})),
|
||||
[selectedNodeId, snapshot.project.nodes],
|
||||
[connectedPortKeys, selectedNodeId, snapshot.project.nodes],
|
||||
);
|
||||
const readOnlyEdges = useMemo<Edge[]>(
|
||||
() =>
|
||||
@@ -447,7 +478,7 @@ export function SimulationResultsView({
|
||||
...edge,
|
||||
focusable: false,
|
||||
selectable: false,
|
||||
type: "step",
|
||||
type: CONTACT_AWARE_EDGE_TYPE,
|
||||
})),
|
||||
[snapshot.project.edges],
|
||||
);
|
||||
@@ -480,23 +511,6 @@ export function SimulationResultsView({
|
||||
),
|
||||
[snapshot.result.series, snapshot.result.variables],
|
||||
);
|
||||
const multiCurveUnitGroups = useMemo(() => {
|
||||
const groups = new Map<string, ResultVariableMetadata[]>();
|
||||
plottableResultVariables.forEach((variable) => {
|
||||
const group = groups.get(variable.unit) ?? [];
|
||||
group.push(variable);
|
||||
groups.set(variable.unit, group);
|
||||
});
|
||||
return Array.from(groups, ([unit, variables]) => ({ unit, variables }))
|
||||
.filter((group) => group.variables.length >= 2)
|
||||
.sort((first, second) =>
|
||||
(first.unit || "无量纲").localeCompare(
|
||||
second.unit || "无量纲",
|
||||
"zh-CN",
|
||||
),
|
||||
);
|
||||
}, [plottableResultVariables]);
|
||||
|
||||
const setPaneSize = (kind: PaneResizeKind, requestedSize: number) => {
|
||||
const bounds = workbenchRef.current?.getBoundingClientRect();
|
||||
if (!bounds) {
|
||||
@@ -659,15 +673,12 @@ export function SimulationResultsView({
|
||||
]);
|
||||
};
|
||||
|
||||
const addMultiChartWindow = (
|
||||
variableKey: string,
|
||||
const addCombinedChartWindow = (
|
||||
kind: "multi" | "mixed",
|
||||
requestedPosition?: { x: number; y: number },
|
||||
) => {
|
||||
const workspace = workspaceRef.current;
|
||||
const variable = snapshot.result.variables.find(
|
||||
(item) => item.key === variableKey,
|
||||
);
|
||||
if (!workspace || !variable) {
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -680,8 +691,8 @@ export function SimulationResultsView({
|
||||
);
|
||||
const index = chartWindows.length;
|
||||
const position = requestedPosition ?? {
|
||||
x: 28 + (index % 3) * 28,
|
||||
y: 28 + (index % 4) * 28,
|
||||
x: (kind === "mixed" ? 38 : 28) + (index % 3) * 28,
|
||||
y: (kind === "mixed" ? 38 : 28) + (index % 4) * 28,
|
||||
};
|
||||
const id = `chart-${nextWindowIdRef.current++}`;
|
||||
const zIndex = ++topZIndexRef.current;
|
||||
@@ -689,68 +700,21 @@ export function SimulationResultsView({
|
||||
...current,
|
||||
{
|
||||
id,
|
||||
kind: "multi",
|
||||
variableKey,
|
||||
variableKeys: [variableKey],
|
||||
kind,
|
||||
variableKey: "",
|
||||
variableKeys: [],
|
||||
hiddenVariableKeys: [],
|
||||
variableUnits: { [variableKey]: variable.unit },
|
||||
variableUnits: {},
|
||||
x: clampWindowCoordinate(position.x, bounds.width, size.width),
|
||||
y: clampWindowCoordinate(position.y, bounds.height, size.height),
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
unit: variable.unit,
|
||||
unit: "",
|
||||
cursorEnabled: false,
|
||||
zIndex,
|
||||
},
|
||||
]);
|
||||
setActiveMultiPickerWindowId(id);
|
||||
};
|
||||
|
||||
const addMixedChartWindow = (
|
||||
variableKey: string,
|
||||
requestedPosition?: { x: number; y: number },
|
||||
) => {
|
||||
const workspace = workspaceRef.current;
|
||||
const variable = snapshot.result.variables.find(
|
||||
(item) => item.key === variableKey,
|
||||
);
|
||||
if (!workspace || !variable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = workspace.getBoundingClientRect();
|
||||
const size = clampChartWindowDimensions(
|
||||
MULTI_CHART_WINDOW_WIDTH,
|
||||
MULTI_CHART_WINDOW_HEIGHT,
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
);
|
||||
const index = chartWindows.length;
|
||||
const position = requestedPosition ?? {
|
||||
x: 38 + (index % 3) * 28,
|
||||
y: 38 + (index % 4) * 28,
|
||||
};
|
||||
const id = `chart-${nextWindowIdRef.current++}`;
|
||||
const zIndex = ++topZIndexRef.current;
|
||||
setChartWindows((current) => [
|
||||
...current,
|
||||
{
|
||||
id,
|
||||
kind: "mixed",
|
||||
variableKey,
|
||||
variableKeys: [variableKey],
|
||||
hiddenVariableKeys: [],
|
||||
variableUnits: { [variableKey]: variable.unit },
|
||||
x: clampWindowCoordinate(position.x, bounds.width, size.width),
|
||||
y: clampWindowCoordinate(position.y, bounds.height, size.height),
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
unit: variable.unit,
|
||||
cursorEnabled: false,
|
||||
zIndex,
|
||||
},
|
||||
]);
|
||||
setActiveMultiPickerWindowId(id);
|
||||
setActiveMultiPickerWindowId(null);
|
||||
};
|
||||
|
||||
const toggleChartCurveVisibility = (
|
||||
@@ -774,41 +738,47 @@ export function SimulationResultsView({
|
||||
};
|
||||
|
||||
const removeChartCurve = (windowId: string, variableKey: string) => {
|
||||
const removesWindow = chartWindows.some(
|
||||
const resetsWindow = chartWindows.some(
|
||||
(window) =>
|
||||
window.id === windowId &&
|
||||
window.kind !== "single" &&
|
||||
window.variableKeys.length === 1,
|
||||
);
|
||||
setChartWindows((current) =>
|
||||
current.flatMap((window) => {
|
||||
current.map((window) => {
|
||||
if (window.id !== windowId || window.kind === "single") {
|
||||
return [window];
|
||||
return window;
|
||||
}
|
||||
if (window.variableKeys.length === 1) {
|
||||
return [];
|
||||
return {
|
||||
...window,
|
||||
variableKey: "",
|
||||
variableKeys: [],
|
||||
hiddenVariableKeys: [],
|
||||
variableUnits: {},
|
||||
unit: "",
|
||||
cursorEnabled: false,
|
||||
};
|
||||
}
|
||||
const variableKeys = window.variableKeys.filter(
|
||||
(key) => key !== variableKey,
|
||||
);
|
||||
return [
|
||||
{
|
||||
...window,
|
||||
variableKey: variableKeys[0],
|
||||
variableKeys,
|
||||
hiddenVariableKeys: window.hiddenVariableKeys.filter(
|
||||
(key) => key !== variableKey,
|
||||
return {
|
||||
...window,
|
||||
variableKey: variableKeys[0],
|
||||
variableKeys,
|
||||
hiddenVariableKeys: window.hiddenVariableKeys.filter(
|
||||
(key) => key !== variableKey,
|
||||
),
|
||||
variableUnits: Object.fromEntries(
|
||||
Object.entries(window.variableUnits).filter(
|
||||
([key]) => key !== variableKey,
|
||||
),
|
||||
variableUnits: Object.fromEntries(
|
||||
Object.entries(window.variableUnits).filter(
|
||||
([key]) => key !== variableKey,
|
||||
),
|
||||
),
|
||||
},
|
||||
];
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (removesWindow) {
|
||||
if (resetsWindow) {
|
||||
setActiveMultiPickerWindowId(null);
|
||||
}
|
||||
};
|
||||
@@ -824,18 +794,23 @@ export function SimulationResultsView({
|
||||
const variable = snapshot.result.variables.find(
|
||||
(item) => item.key === variableKey,
|
||||
);
|
||||
const primaryVariable = snapshot.result.variables.find(
|
||||
(item) => item.key === chartWindow.variableKey,
|
||||
);
|
||||
if (
|
||||
!variableKey ||
|
||||
!variable ||
|
||||
!primaryVariable ||
|
||||
chartWindow.kind === "single"
|
||||
) {
|
||||
const primaryVariable =
|
||||
snapshot.result.variables.find(
|
||||
(item) => item.key === chartWindow.variableKey,
|
||||
) ??
|
||||
chartWindow.variableKeys
|
||||
.map((key) =>
|
||||
snapshot.result.variables.find((item) => item.key === key),
|
||||
)
|
||||
.find((item): item is ResultVariableMetadata => item !== undefined);
|
||||
if (!variableKey || !variable || chartWindow.kind === "single") {
|
||||
return;
|
||||
}
|
||||
if (chartWindow.kind === "multi" && variable.unit !== primaryVariable.unit) {
|
||||
if (
|
||||
chartWindow.kind === "multi" &&
|
||||
primaryVariable &&
|
||||
variable.unit !== primaryVariable.unit
|
||||
) {
|
||||
window.alert(
|
||||
`无法加入曲线:该窗口只接受 SI 单位为 ${primaryVariable.unit || "无量纲"} 的变量。`,
|
||||
);
|
||||
@@ -846,6 +821,16 @@ export function SimulationResultsView({
|
||||
if (window.id !== chartWindow.id || window.kind === "single") {
|
||||
return window;
|
||||
}
|
||||
if (window.variableKeys.length === 0 || !primaryVariable) {
|
||||
return {
|
||||
...window,
|
||||
variableKey,
|
||||
variableKeys: [variableKey],
|
||||
hiddenVariableKeys: [],
|
||||
variableUnits: { [variableKey]: variable.unit },
|
||||
unit: variable.unit,
|
||||
};
|
||||
}
|
||||
if (window.variableKeys.includes(variableKey)) {
|
||||
return {
|
||||
...window,
|
||||
@@ -1143,6 +1128,7 @@ export function SimulationResultsView({
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
deleteKeyCode={null}
|
||||
edges={readOnlyEdges}
|
||||
edgeTypes={contactAwareEdgeTypes}
|
||||
edgesFocusable={false}
|
||||
edgesReconnectable={false}
|
||||
elementsSelectable
|
||||
@@ -1263,32 +1249,11 @@ export function SimulationResultsView({
|
||||
)}{" "}
|
||||
条曲线
|
||||
</span>
|
||||
<div
|
||||
className="results-multi-create"
|
||||
onBlur={(event) => {
|
||||
if (
|
||||
!(event.relatedTarget instanceof HTMLElement) ||
|
||||
!event.currentTarget.contains(event.relatedTarget)
|
||||
) {
|
||||
setMultiCreateMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.stopPropagation();
|
||||
setMultiCreateMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="results-multi-create">
|
||||
<button
|
||||
aria-expanded={multiCreateMenuOpen}
|
||||
aria-label="新建同单位多曲线对比窗口"
|
||||
className={multiCreateMenuOpen ? "active" : ""}
|
||||
disabled={multiCurveUnitGroups.length === 0}
|
||||
onClick={() => {
|
||||
setMixedCreateMenuOpen(false);
|
||||
setMultiCreateMenuOpen((current) => !current);
|
||||
}}
|
||||
disabled={plottableResultVariables.length === 0}
|
||||
onClick={() => addCombinedChartWindow("multi")}
|
||||
title="新建同单位多曲线对比窗口"
|
||||
type="button"
|
||||
>
|
||||
@@ -1298,56 +1263,12 @@ export function SimulationResultsView({
|
||||
strokeWidth={1.9}
|
||||
/>
|
||||
</button>
|
||||
{multiCreateMenuOpen ? (
|
||||
<div
|
||||
aria-label="选择多曲线窗口的 SI 单位"
|
||||
className="results-multi-create-menu"
|
||||
role="menu"
|
||||
>
|
||||
<div className="results-multi-create-title">选择 SI 单位</div>
|
||||
{multiCurveUnitGroups.map((group) => (
|
||||
<button
|
||||
key={group.unit || "dimensionless"}
|
||||
onClick={() => {
|
||||
addMultiChartWindow(group.variables[0].key);
|
||||
setMultiCreateMenuOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
type="button"
|
||||
>
|
||||
<strong>{group.unit || "无量纲"}</strong>
|
||||
<span>{group.variables.length} 条变量</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className="results-multi-create"
|
||||
onBlur={(event) => {
|
||||
if (
|
||||
!(event.relatedTarget instanceof HTMLElement) ||
|
||||
!event.currentTarget.contains(event.relatedTarget)
|
||||
) {
|
||||
setMixedCreateMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.stopPropagation();
|
||||
setMixedCreateMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="results-multi-create">
|
||||
<button
|
||||
aria-expanded={mixedCreateMenuOpen}
|
||||
aria-label="新建异单位上下对比窗口"
|
||||
className={mixedCreateMenuOpen ? "active" : ""}
|
||||
disabled={plottableResultVariables.length === 0}
|
||||
onClick={() => {
|
||||
setMultiCreateMenuOpen(false);
|
||||
setMixedCreateMenuOpen((current) => !current);
|
||||
}}
|
||||
onClick={() => addCombinedChartWindow("mixed")}
|
||||
title="新建异单位上下对比窗口"
|
||||
type="button"
|
||||
>
|
||||
@@ -1357,43 +1278,6 @@ export function SimulationResultsView({
|
||||
strokeWidth={1.9}
|
||||
/>
|
||||
</button>
|
||||
{mixedCreateMenuOpen ? (
|
||||
<div
|
||||
aria-label="选择异单位上下对比窗口的首条曲线"
|
||||
className="results-multi-create-menu variables"
|
||||
role="menu"
|
||||
>
|
||||
<div className="results-multi-create-title">选择首条曲线</div>
|
||||
{plottableResultVariables.map((item) => {
|
||||
const itemComponent = snapshot.project.nodes.find(
|
||||
(node) => node.id === item.componentId,
|
||||
);
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => {
|
||||
addMixedChartWindow(item.key);
|
||||
setMixedCreateMenuOpen(false);
|
||||
}}
|
||||
role="menuitem"
|
||||
title={resultVariableTitle(
|
||||
item,
|
||||
itemComponent?.data.label ?? item.componentId,
|
||||
)}
|
||||
type="button"
|
||||
>
|
||||
<strong>
|
||||
{resultVariableTitle(
|
||||
item,
|
||||
itemComponent?.data.label ?? item.componentId,
|
||||
)}
|
||||
</strong>
|
||||
<span>{item.unit || "无量纲"}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1409,6 +1293,8 @@ export function SimulationResultsView({
|
||||
ref={workspaceRef}
|
||||
>
|
||||
{chartWindows.map((chartWindow) => {
|
||||
const isCombined = chartWindow.kind !== "single";
|
||||
const isMixed = chartWindow.kind === "mixed";
|
||||
const windowVariables = chartWindow.variableKeys
|
||||
.map((key) =>
|
||||
snapshot.result.variables.find((item) => item.key === key),
|
||||
@@ -1421,10 +1307,114 @@ export function SimulationResultsView({
|
||||
(item) => item.key === chartWindow.variableKey,
|
||||
) ?? windowVariables[0];
|
||||
if (!variable) {
|
||||
return null;
|
||||
if (!isCombined) {
|
||||
return null;
|
||||
}
|
||||
const pendingTitle = isMixed
|
||||
? "异单位上下对比"
|
||||
: "同单位多曲线对比";
|
||||
return (
|
||||
<article
|
||||
aria-label={`${pendingTitle}窗口(待定)`}
|
||||
className={`result-chart-window ${chartWindow.kind} pending ${
|
||||
multiDropTargetWindowId === chartWindow.id
|
||||
? "drop-target"
|
||||
: ""
|
||||
}`}
|
||||
data-chart-kind={chartWindow.kind}
|
||||
data-pending="true"
|
||||
key={chartWindow.id}
|
||||
onDragLeave={(event) => {
|
||||
if (
|
||||
event.relatedTarget instanceof Element &&
|
||||
event.currentTarget.contains(event.relatedTarget)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (multiDropTargetWindowId === chartWindow.id) {
|
||||
setMultiDropTargetWindowId(null);
|
||||
}
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
setMultiDropTargetWindowId(chartWindow.id);
|
||||
}}
|
||||
onDrop={(event) =>
|
||||
addDroppedVariableToMultiChart(event, chartWindow)
|
||||
}
|
||||
onPointerDown={() => focusChartWindow(chartWindow.id)}
|
||||
style={{
|
||||
height: chartWindow.height,
|
||||
left: chartWindow.x,
|
||||
top: chartWindow.y,
|
||||
width: chartWindow.width,
|
||||
zIndex: chartWindow.zIndex,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="result-chart-window-header"
|
||||
onPointerCancel={stopWindowDrag}
|
||||
onPointerDown={(event) =>
|
||||
startWindowDrag(event, chartWindow)
|
||||
}
|
||||
onPointerMove={moveChartWindow}
|
||||
onPointerUp={stopWindowDrag}
|
||||
>
|
||||
<div className="result-chart-window-title">
|
||||
<strong>{pendingTitle}</strong>
|
||||
<span>待定 · 请拖入第一条曲线</span>
|
||||
</div>
|
||||
<div className="result-chart-window-actions">
|
||||
<button
|
||||
aria-label={`关闭${pendingTitle}窗口`}
|
||||
className="close"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
removeChartWindow(chartWindow.id);
|
||||
}}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
title="关闭曲线窗口"
|
||||
type="button"
|
||||
>
|
||||
<X aria-hidden="true" size={16} strokeWidth={1.9} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="result-chart-pending">
|
||||
{isMixed ? (
|
||||
<ChartSpline aria-hidden="true" size={34} strokeWidth={1.5} />
|
||||
) : (
|
||||
<ChartNoAxesCombined
|
||||
aria-hidden="true"
|
||||
size={34}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
<strong>等待第一条曲线</strong>
|
||||
<span>
|
||||
拖入变量后,将以第一条曲线初始化单位和显示设置
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
aria-label={`调整${pendingTitle}窗口大小`}
|
||||
className="result-chart-resize-handle"
|
||||
onLostPointerCapture={stopWindowResize}
|
||||
onPointerCancel={stopWindowResize}
|
||||
onPointerDown={(event) =>
|
||||
startWindowResize(event, chartWindow)
|
||||
}
|
||||
onPointerMove={resizeChartWindow}
|
||||
onPointerUp={stopWindowResize}
|
||||
title="调整窗口大小"
|
||||
type="button"
|
||||
>
|
||||
<Scaling aria-hidden="true" size={13} strokeWidth={1.8} />
|
||||
</button>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
const isCombined = chartWindow.kind !== "single";
|
||||
const isMixed = chartWindow.kind === "mixed";
|
||||
const label = resultVariableLabel(variable);
|
||||
const component = snapshot.project.nodes.find(
|
||||
(node) => node.id === variable.componentId,
|
||||
@@ -1476,6 +1466,8 @@ export function SimulationResultsView({
|
||||
? "drop-target"
|
||||
: ""
|
||||
}`}
|
||||
data-chart-kind={chartWindow.kind}
|
||||
data-pending="false"
|
||||
key={chartWindow.id}
|
||||
onDragLeave={(event) => {
|
||||
if (
|
||||
@@ -1525,7 +1517,7 @@ export function SimulationResultsView({
|
||||
{isMixed
|
||||
? "异单位上下对比"
|
||||
: isCombined
|
||||
? "多曲线对比"
|
||||
? "同单位多曲线对比"
|
||||
: label}
|
||||
</strong>
|
||||
<span>
|
||||
@@ -4284,16 +4276,24 @@ function normalizeStoredChartWindow(window: StoredChartWindow): ChartWindow {
|
||||
window.kind === "multi" || window.kind === "mixed"
|
||||
? window.kind
|
||||
: "single";
|
||||
const storedVariableKeys = Array.isArray(window.variableKeys)
|
||||
? window.variableKeys.filter(
|
||||
const rawVariableKeys = window.variableKeys;
|
||||
const hasStoredVariableKeys = Array.isArray(rawVariableKeys);
|
||||
const storedVariableKeys = hasStoredVariableKeys
|
||||
? rawVariableKeys.filter(
|
||||
(key, index, keys) =>
|
||||
typeof key === "string" && key.length > 0 && keys.indexOf(key) === index,
|
||||
)
|
||||
: [];
|
||||
const variableKeys =
|
||||
kind !== "single" && storedVariableKeys.length > 0
|
||||
? storedVariableKeys
|
||||
: [window.variableKey];
|
||||
kind === "single"
|
||||
? window.variableKey
|
||||
? [window.variableKey]
|
||||
: []
|
||||
: hasStoredVariableKeys
|
||||
? storedVariableKeys
|
||||
: window.variableKey
|
||||
? [window.variableKey]
|
||||
: [];
|
||||
const hiddenVariableKeys = Array.isArray(window.hiddenVariableKeys)
|
||||
? window.hiddenVariableKeys.filter(
|
||||
(key, index, keys) =>
|
||||
@@ -4314,7 +4314,7 @@ function normalizeStoredChartWindow(window: StoredChartWindow): ChartWindow {
|
||||
return {
|
||||
...window,
|
||||
kind,
|
||||
variableKey: variableKeys[0],
|
||||
variableKey: variableKeys[0] ?? "",
|
||||
variableKeys,
|
||||
hiddenVariableKeys,
|
||||
variableUnits,
|
||||
|
||||
@@ -2,6 +2,15 @@ import type { ReactNode } from "react";
|
||||
|
||||
export type ComponentSymbolPresentation = "framed" | "bare";
|
||||
|
||||
export type ComponentSymbolPortAnchor = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type ComponentSymbolPortAnchors = Partial<
|
||||
Record<"left" | "right", readonly ComponentSymbolPortAnchor[]>
|
||||
>;
|
||||
|
||||
export type SymbolDefinition = {
|
||||
presentation: ComponentSymbolPresentation;
|
||||
render: () => ReactNode;
|
||||
|
||||
+201
-12
@@ -293,6 +293,7 @@ textarea {
|
||||
.workbench {
|
||||
--palette-width: 230px;
|
||||
--properties-width: 330px;
|
||||
--component-parameters-height: 340px;
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
var(--palette-width) 7px minmax(0, 1fr) 7px
|
||||
@@ -306,6 +307,11 @@ textarea {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.workbench.resizing-component-parameters-height {
|
||||
cursor: row-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.modeling-pane-splitter {
|
||||
position: relative;
|
||||
z-index: 8;
|
||||
@@ -367,6 +373,71 @@ textarea {
|
||||
|
||||
.properties {
|
||||
border-left: 0;
|
||||
display: grid;
|
||||
grid-template-rows:
|
||||
minmax(0, var(--component-parameters-height)) 7px
|
||||
minmax(120px, 1fr);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.properties-subpane {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.properties-subpane-content {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.properties-pane-splitter {
|
||||
position: relative;
|
||||
z-index: 9;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 7px;
|
||||
min-height: 7px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
place-items: center;
|
||||
background: #eef2f6;
|
||||
cursor: row-resize;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.properties-pane-splitter::before {
|
||||
position: absolute;
|
||||
inset: 3px 0 auto;
|
||||
height: 1px;
|
||||
background: #c7d1dc;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.properties-pane-splitter span {
|
||||
position: relative;
|
||||
width: 42px;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background: #9eacbb;
|
||||
opacity: 0;
|
||||
transition: opacity 120ms ease, background 120ms ease;
|
||||
}
|
||||
|
||||
.properties-pane-splitter:hover span,
|
||||
.properties-pane-splitter:focus-visible span,
|
||||
.workbench.resizing-component-parameters-height .properties-pane-splitter span {
|
||||
background: #1d6fb8;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.properties-pane-splitter:focus-visible {
|
||||
outline: 2px solid #1d6fb8;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
@@ -1229,15 +1300,15 @@ textarea {
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 112px;
|
||||
height: 84px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.node-symbol {
|
||||
display: grid;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 112px;
|
||||
height: 84px;
|
||||
place-items: center;
|
||||
transform-origin: center;
|
||||
}
|
||||
@@ -1248,28 +1319,34 @@ textarea {
|
||||
}
|
||||
|
||||
.sim-node.symbol-presentation-bare .sim-node-frame,
|
||||
.sim-node.symbol-presentation-bare.selected .sim-node-frame {
|
||||
.sim-node.symbol-presentation-bare.selected .sim-node-frame,
|
||||
.sim-node.component-icon-only .sim-node-frame,
|
||||
.sim-node.component-icon-only.selected .sim-node-frame {
|
||||
border-width: 0;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sim-node.symbol-presentation-bare .node-label {
|
||||
.sim-node.symbol-presentation-bare .node-label,
|
||||
.sim-node.component-icon-only .node-label {
|
||||
display: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sim-node.symbol-presentation-bare.selected .component-symbol-svg {
|
||||
.sim-node.symbol-presentation-bare.selected .component-symbol-svg,
|
||||
.sim-node.component-icon-only.selected .component-symbol-svg {
|
||||
filter: drop-shadow(0 0 3px rgba(29, 111, 184, 0.62));
|
||||
}
|
||||
|
||||
.react-flow__node.validation-error
|
||||
.sim-node.symbol-presentation-bare
|
||||
.sim-node.component-icon-only
|
||||
.component-symbol-svg {
|
||||
filter: drop-shadow(0 0 3px rgba(198, 59, 50, 0.68));
|
||||
}
|
||||
|
||||
.react-flow__node.validation-warning
|
||||
.sim-node.symbol-presentation-bare
|
||||
.sim-node.component-icon-only
|
||||
.component-symbol-svg {
|
||||
filter: drop-shadow(0 0 3px rgba(192, 138, 22, 0.68));
|
||||
}
|
||||
@@ -1310,10 +1387,49 @@ textarea {
|
||||
|
||||
.port-handle {
|
||||
z-index: 3;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid #374151;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
box-sizing: border-box;
|
||||
border: 1.5px solid var(--port-handle-color, #475569);
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.82);
|
||||
opacity: 1;
|
||||
transition: opacity 100ms ease, border-color 100ms ease,
|
||||
background 100ms ease, box-shadow 100ms ease;
|
||||
}
|
||||
|
||||
.port-handle-physical {
|
||||
--port-handle-color: #a12b70;
|
||||
}
|
||||
|
||||
.port-handle-signal {
|
||||
--port-handle-color: #dc2626;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.port-handle.port-connection-connected {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.port-handle.port-connection-origin {
|
||||
border-color: #d97706;
|
||||
background: #fff7d6;
|
||||
box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.24),
|
||||
0 0 8px rgba(217, 119, 6, 0.42);
|
||||
}
|
||||
|
||||
.port-handle.port-connection-compatible {
|
||||
border-color: #15803d;
|
||||
background: #dcfce7;
|
||||
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.24),
|
||||
0 0 9px rgba(21, 128, 61, 0.48);
|
||||
}
|
||||
|
||||
.port-handle.port-connection-incompatible {
|
||||
pointer-events: none;
|
||||
opacity: 0.18;
|
||||
}
|
||||
|
||||
.flow-canvas .react-flow__edge-path,
|
||||
@@ -1342,6 +1458,38 @@ textarea {
|
||||
filter: drop-shadow(0 0 2px rgba(29, 111, 184, 0.42));
|
||||
}
|
||||
|
||||
.contact-edge-hit {
|
||||
cursor: pointer;
|
||||
fill: transparent;
|
||||
pointer-events: all;
|
||||
stroke: transparent;
|
||||
}
|
||||
|
||||
.contact-edge-marker {
|
||||
fill: #ffffff;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
stroke: #64748b;
|
||||
stroke-width: 1.5;
|
||||
transition: opacity 120ms ease-out, fill 120ms ease-out, stroke 120ms ease-out;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.react-flow__edge.editor-edge-contact:hover .contact-edge-marker,
|
||||
.react-flow__edge.editor-edge-contact.selected .contact-edge-marker,
|
||||
.react-flow__edge.editor-edge-contact.editor-edge-selected .contact-edge-marker,
|
||||
.contact-edge-hit:focus-visible + .contact-edge-marker {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.react-flow__edge.editor-edge-contact.selected .contact-edge-marker,
|
||||
.react-flow__edge.editor-edge-contact.editor-edge-selected .contact-edge-marker,
|
||||
.contact-edge-hit:focus-visible + .contact-edge-marker {
|
||||
fill: #dbeafe;
|
||||
stroke: #1d6fb8;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -1791,6 +1939,15 @@ textarea {
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.results-system-canvas .contact-edge-hit {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.results-system-canvas .contact-edge-marker {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.results-chart-workspace {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
@@ -1834,6 +1991,38 @@ textarea {
|
||||
0 7px 20px rgba(30, 41, 59, 0.18);
|
||||
}
|
||||
|
||||
.result-chart-pending {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
gap: 7px;
|
||||
margin: 16px;
|
||||
padding: 24px;
|
||||
border: 1px dashed #9aaaba;
|
||||
border-radius: 5px;
|
||||
color: #718096;
|
||||
text-align: center;
|
||||
background: #f8fafc;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.result-chart-pending strong {
|
||||
color: #43556a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.result-chart-pending span {
|
||||
max-width: 300px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.result-chart-window.pending.drop-target .result-chart-pending {
|
||||
border-color: #1d6fb8;
|
||||
color: #1d6fb8;
|
||||
background: #edf6fd;
|
||||
}
|
||||
|
||||
.result-chart-window-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in new issue
Block a user