From cf249d6c0d4d20d09fa1aa15c1165576f3d88953 Mon Sep 17 00:00:00 2001 From: ljz <425868052@qq.com> Date: Mon, 3 Aug 2026 11:55:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BB=84=E4=BB=B6=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E4=BA=A4=E4=BA=92=E4=B8=8E=E8=A7=86=E5=9B=BE=E9=80=82?= =?UTF-8?q?=E9=85=8D=E6=97=B6=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 163 ++++++++++++++++++- frontend/src/AutoFitView.tsx | 34 +++- frontend/src/SimulationResultsView.tsx | 1 + frontend/src/styles.css | 56 ++++++- frontend/tests/e2e/component-symbols.spec.ts | 86 +++++++++- frontend/tests/e2e/fit-view.spec.ts | 39 +++++ frontend/tests/e2e/modeling-actions.spec.ts | 112 +++++++++++++ 7 files changed, 473 insertions(+), 18 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 043853d..bb25286 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 = 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("*")) + .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) { const updateNodeInternals = useUpdateNodeInternals(); + const symbolContainerRef = useRef(null); + const [symbolEnvelope, setSymbolEnvelope] = useState( + 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 { + if (!hasDedicatedSymbol) { + return; + } + const svg = symbolContainerRef.current?.querySelector( + ".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