优化组件选择交互与视图适配时机
This commit is contained in:
1 parent
11093613c5
commit
cf249d6c0d
7 files changed
+473
-18
No files matched your search
+157
-6
@@ -420,11 +420,98 @@ const NODE_SYMBOL_WIDTH = 112;
|
||||
const NODE_SYMBOL_HEIGHT = 84;
|
||||
const SYMBOL_VIEWBOX_WIDTH = 64;
|
||||
const SYMBOL_VIEWBOX_HEIGHT = 48;
|
||||
const CONTACT_SNAP_RADIUS_PX = 24;
|
||||
const SYMBOL_ENVELOPE_PADDING_PX = 2;
|
||||
const CONTACT_SNAP_RADIUS_PX = 12;
|
||||
const CONTACT_DISCONNECT_DISTANCE_PX = 8;
|
||||
const CONTACT_GEOMETRY_TOLERANCE = 0.5;
|
||||
const CONTACT_DELETE_SEPARATION = 16;
|
||||
|
||||
type SymbolEnvelope = {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
const DEFAULT_SYMBOL_ENVELOPE: SymbolEnvelope = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: NODE_SYMBOL_WIDTH,
|
||||
height: NODE_SYMBOL_HEIGHT,
|
||||
};
|
||||
|
||||
function measuredSymbolEnvelope(svg: SVGSVGElement): SymbolEnvelope {
|
||||
const viewBox = svg.viewBox.baseVal;
|
||||
const geometryBounds = svg.getBBox();
|
||||
let bounds: Pick<DOMRect, "x" | "y" | "width" | "height"> = geometryBounds;
|
||||
try {
|
||||
const paintBounds = svg.getBBox({
|
||||
fill: true,
|
||||
stroke: true,
|
||||
markers: true,
|
||||
clipped: false,
|
||||
});
|
||||
const includesPaintOutsideGeometry =
|
||||
paintBounds.x < geometryBounds.x ||
|
||||
paintBounds.y < geometryBounds.y ||
|
||||
paintBounds.x + paintBounds.width >
|
||||
geometryBounds.x + geometryBounds.width ||
|
||||
paintBounds.y + paintBounds.height >
|
||||
geometryBounds.y + geometryBounds.height;
|
||||
if (includesPaintOutsideGeometry) {
|
||||
bounds = paintBounds;
|
||||
}
|
||||
} catch {
|
||||
// Older SVG implementations may not support the getBBox options object.
|
||||
}
|
||||
if (bounds === geometryBounds) {
|
||||
const maximumStrokeWidth = Array.from(svg.querySelectorAll<SVGElement>("*"))
|
||||
.map((element) => getComputedStyle(element))
|
||||
.filter((style) => style.stroke !== "none")
|
||||
.reduce(
|
||||
(maximum, style) =>
|
||||
Math.max(maximum, Number.parseFloat(style.strokeWidth) || 0),
|
||||
0,
|
||||
);
|
||||
if (maximumStrokeWidth > 0) {
|
||||
const strokeRadius = maximumStrokeWidth / 2;
|
||||
bounds = {
|
||||
x: geometryBounds.x - strokeRadius,
|
||||
y: geometryBounds.y - strokeRadius,
|
||||
width: geometryBounds.width + maximumStrokeWidth,
|
||||
height: geometryBounds.height + maximumStrokeWidth,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (
|
||||
viewBox.width <= 0 ||
|
||||
viewBox.height <= 0 ||
|
||||
bounds.width <= 0 ||
|
||||
bounds.height <= 0
|
||||
) {
|
||||
return DEFAULT_SYMBOL_ENVELOPE;
|
||||
}
|
||||
|
||||
const scaleX = NODE_SYMBOL_WIDTH / viewBox.width;
|
||||
const scaleY = NODE_SYMBOL_HEIGHT / viewBox.height;
|
||||
const left = (bounds.x - viewBox.x) * scaleX - SYMBOL_ENVELOPE_PADDING_PX;
|
||||
const top = (bounds.y - viewBox.y) * scaleY - SYMBOL_ENVELOPE_PADDING_PX;
|
||||
const right =
|
||||
(bounds.x + bounds.width - viewBox.x) * scaleX +
|
||||
SYMBOL_ENVELOPE_PADDING_PX;
|
||||
const bottom =
|
||||
(bounds.y + bounds.height - viewBox.y) * scaleY +
|
||||
SYMBOL_ENVELOPE_PADDING_PX;
|
||||
const roundPixel = (value: number) => Math.round(value * 100) / 100;
|
||||
|
||||
return {
|
||||
left: roundPixel(left),
|
||||
top: roundPixel(top),
|
||||
width: roundPixel(Math.max(1, right - left)),
|
||||
height: roundPixel(Math.max(1, bottom - top)),
|
||||
};
|
||||
}
|
||||
|
||||
function defaultModelingPaneLayout(): ModelingPaneLayout {
|
||||
return {
|
||||
paletteWidth: 230,
|
||||
@@ -1396,6 +1483,10 @@ function assignMissingAmesimGasDefinitionIndices(
|
||||
|
||||
function SimulationComponentNode({ id, data, selected }: NodeProps<SimulationNode>) {
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const symbolContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [symbolEnvelope, setSymbolEnvelope] = useState<SymbolEnvelope>(
|
||||
DEFAULT_SYMBOL_ENVELOPE,
|
||||
);
|
||||
const rotation = normalizeNodeRotation(data.rotation);
|
||||
const mirrored = Boolean(data.mirrored);
|
||||
const vertical = rotation === 90 || rotation === 270;
|
||||
@@ -1412,6 +1503,27 @@ function SimulationComponentNode({ id, data, selected }: NodeProps<SimulationNod
|
||||
portAnchors,
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!hasDedicatedSymbol) {
|
||||
return;
|
||||
}
|
||||
const svg = symbolContainerRef.current?.querySelector<SVGSVGElement>(
|
||||
".component-symbol-svg",
|
||||
);
|
||||
if (!svg) {
|
||||
return;
|
||||
}
|
||||
const nextEnvelope = measuredSymbolEnvelope(svg);
|
||||
setSymbolEnvelope((currentEnvelope) =>
|
||||
currentEnvelope.left === nextEnvelope.left &&
|
||||
currentEnvelope.top === nextEnvelope.top &&
|
||||
currentEnvelope.width === nextEnvelope.width &&
|
||||
currentEnvelope.height === nextEnvelope.height
|
||||
? currentEnvelope
|
||||
: nextEnvelope,
|
||||
);
|
||||
}, [hasDedicatedSymbol, symbol]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [mirrored, rotation, id, updateNodeInternals]);
|
||||
@@ -1459,10 +1571,21 @@ function SimulationComponentNode({ id, data, selected }: NodeProps<SimulationNod
|
||||
<div className="node-symbol-anchor" aria-hidden="true">
|
||||
<div
|
||||
className="node-symbol"
|
||||
ref={symbolContainerRef}
|
||||
style={{
|
||||
transform: `rotate(${rotation}deg) scaleX(${mirrored ? -1 : 1})`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="node-symbol-envelope"
|
||||
data-symbol-envelope="true"
|
||||
style={{
|
||||
left: symbolEnvelope.left,
|
||||
top: symbolEnvelope.top,
|
||||
width: symbolEnvelope.width,
|
||||
height: symbolEnvelope.height,
|
||||
}}
|
||||
/>
|
||||
<ComponentSymbol symbol={symbol} surface="canvas" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -3211,6 +3334,9 @@ function FlowWorkbench() {
|
||||
const renderedNodes = useMemo(
|
||||
() =>
|
||||
nodes.map((node) => {
|
||||
const hasDedicatedSymbol = hasComponentSymbol(
|
||||
node.data.symbol ?? node.data.componentType,
|
||||
);
|
||||
const portConnectionStates = Object.fromEntries(
|
||||
node.data.ports.map((port) => {
|
||||
const key = portConnectionKey(node.id, port.name);
|
||||
@@ -3242,11 +3368,18 @@ function FlowWorkbench() {
|
||||
return [port.name, state];
|
||||
}),
|
||||
);
|
||||
const className = [
|
||||
node.className,
|
||||
hasDedicatedSymbol ? "component-icon-node" : "",
|
||||
validationSeverityByNode.has(node.id)
|
||||
? `validation-${validationSeverityByNode.get(node.id)}`
|
||||
: "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return {
|
||||
...node,
|
||||
className: validationSeverityByNode.has(node.id)
|
||||
? `validation-${validationSeverityByNode.get(node.id)}`
|
||||
: node.className,
|
||||
className: className || undefined,
|
||||
data: {
|
||||
...node.data,
|
||||
portConnectionStates,
|
||||
@@ -3791,14 +3924,32 @@ function FlowWorkbench() {
|
||||
id,
|
||||
type: "simulationComponent",
|
||||
position,
|
||||
selected: true,
|
||||
data,
|
||||
};
|
||||
recordHistory();
|
||||
setNodes((currentNodes) => [...currentNodes, node]);
|
||||
setNodes((currentNodes) => [
|
||||
...currentNodes.map((currentNode) => ({
|
||||
...currentNode,
|
||||
selected: false,
|
||||
})),
|
||||
node,
|
||||
]);
|
||||
setEdges((currentEdges) =>
|
||||
currentEdges.map((edge) => ({ ...edge, selected: false })),
|
||||
);
|
||||
setSelectedNodeId(id);
|
||||
setSelectedEdgeId(null);
|
||||
appendConsoleEntry("info", `已添加组件:${id}`);
|
||||
},
|
||||
[appendConsoleEntry, componentDefinitions, reactFlow, recordHistory, setNodes],
|
||||
[
|
||||
appendConsoleEntry,
|
||||
componentDefinitions,
|
||||
reactFlow,
|
||||
recordHistory,
|
||||
setEdges,
|
||||
setNodes,
|
||||
],
|
||||
);
|
||||
|
||||
const updateSelectedNode = (updater: (node: SimulationNode) => SimulationNode) => {
|
||||
|
||||
Reference in new issue
Block a user