mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Merge pull request #486 from Hawksight-AI/fix/graph-motion
Fix explorer zooming and Loading Flicker
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState }
|
||||
import { batchMergeEdges, batchMergeNodes, clearGraph, graph, type EdgeAttributes, type NodeAttributes } from "../../store/graphStore";
|
||||
import { SigmaSceneAdapter } from "./SigmaSceneAdapter";
|
||||
import { createGraphLoadProgress } from "./graphLoading";
|
||||
import { resolveDisplayGraph } from "./graphSceneState";
|
||||
import {
|
||||
chooseColorAccessor,
|
||||
colorForNodeKey,
|
||||
@@ -41,6 +42,7 @@ const STAGE_EFFECTS_STATE: GraphEffectsState = {
|
||||
lensMode: "neighborhood",
|
||||
effectQuality: "bounded",
|
||||
};
|
||||
const EMPTY_PATH: string[] = [];
|
||||
|
||||
const socketProtocol = () => (window.location.protocol === "https:" ? "wss:" : "ws:");
|
||||
|
||||
@@ -117,6 +119,10 @@ export const GraphRuntimeStage = forwardRef<GraphStageHandle, GraphRuntimeStageP
|
||||
const prevActiveIdsRef = useRef<Set<string>>(new Set());
|
||||
const [graphVersion, setGraphVersion] = useState(0);
|
||||
const [runtimeLayoutSource, setRuntimeLayoutSource] = useState<GraphLayoutSource>(snapshot?.summary.layoutSource ?? "runtime");
|
||||
const displayResult = useMemo(
|
||||
() => resolveDisplayGraph(selectedNodeId, activePath, EMPTY_PATH, viewMode, { aggregationEnabled: true }),
|
||||
[activePath, graphVersion, selectedNodeId, viewMode],
|
||||
);
|
||||
|
||||
const stageSignature = useMemo(() => (snapshot ? `${snapshot.fetchedAt}:${snapshot.summary.nodeCount}:${snapshot.summary.edgeCount}` : null), [snapshot]);
|
||||
|
||||
@@ -451,9 +457,15 @@ export const GraphRuntimeStage = forwardRef<GraphStageHandle, GraphRuntimeStageP
|
||||
<SigmaSceneAdapter
|
||||
ref={sceneRef}
|
||||
onNodeSelect={onNodeSelect}
|
||||
graphVersion={graphVersion}
|
||||
graphReady={Boolean(snapshot)}
|
||||
displayGraph={displayResult.graph}
|
||||
displayMeta={displayResult.meta}
|
||||
displayState={displayResult.state}
|
||||
selectedEdgeId=""
|
||||
selectedNodeId={selectedNodeId}
|
||||
activePath={activePath}
|
||||
activePathEdgeIds={EMPTY_PATH}
|
||||
effectsState={STAGE_EFFECTS_STATE}
|
||||
isLayoutRunning={isLayoutRunning}
|
||||
onLayoutRunningChange={onLayoutRunningChange}
|
||||
|
||||
@@ -12,7 +12,8 @@ import { useLoadGraph, useReloadGraph } from "./useLoadGraph";
|
||||
import { GraphLoadingOverlay } from "./GraphLoadingOverlay";
|
||||
import { createGraphLoadProgress, getGraphLoadTitle } from "./graphLoading";
|
||||
import { GRAPH_THEME, withAlpha } from "./graphTheme";
|
||||
import { resolveDisplayGraph } from "./graphSceneState";
|
||||
import { resolveDisplayGraph, resolveDisplayStateSnapshot } from "./graphSceneState";
|
||||
import { computeGraphAnalyticsBase } from "./graphAnalytics";
|
||||
import {
|
||||
type GraphPlugin,
|
||||
type GraphPluginActionRequest,
|
||||
@@ -108,6 +109,15 @@ const LazyGraphInspectorPanel = lazy(() => import("./GraphInspectorPanel").then(
|
||||
const loadExplorationEffectsPlugin = () => import("./plugins/explorationEffectsPluginPhaseC").then((module) => module.explorationEffectsPluginPhaseC);
|
||||
const loadNeighborhoodPanelPlugin = () => import("./plugins/neighborhoodPanelPlugin").then((module) => module.neighborhoodPanelPlugin);
|
||||
const loadTemporalOverlayPlugin = () => import("./plugins/temporalOverlayPlugin").then((module) => module.temporalOverlayPlugin);
|
||||
const EMPTY_PATH: string[] = [];
|
||||
const DEBUG_GRAPH_WORKSPACE = import.meta.env.DEV;
|
||||
|
||||
function debugGraphWorkspace(message: string, payload?: Record<string, unknown>) {
|
||||
if (!DEBUG_GRAPH_WORKSPACE) {
|
||||
return;
|
||||
}
|
||||
console.debug(`[GraphWorkspace] ${message}`, payload ?? {});
|
||||
}
|
||||
|
||||
function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
@@ -670,6 +680,8 @@ export function GraphWorkspace() {
|
||||
const [selectedNodeId, setSelectedNodeId] = useState("");
|
||||
const [selectedEdgeId, setSelectedEdgeId] = useState("");
|
||||
const [isLayoutRunning, setIsLayoutRunning] = useState(false);
|
||||
const [graphReady, setGraphReady] = useState(false);
|
||||
const [graphVersion, setGraphVersion] = useState(0);
|
||||
const [viewMode, setViewMode] = useState<GraphViewMode>("full");
|
||||
const [aggregationEnabled] = useState(true);
|
||||
const [collapsedNeighborhoodNodeIds, setCollapsedNeighborhoodNodeIds] = useState<string[]>([]);
|
||||
@@ -715,9 +727,18 @@ export function GraphWorkspace() {
|
||||
});
|
||||
const reload = useReloadGraph();
|
||||
|
||||
const handleLoadProgress = useCallback((progress: GraphLoadProgress) => {
|
||||
setLoadingProgress(progress);
|
||||
if (progress.phase !== "ready" && progress.phase !== "stabilizing_layout") {
|
||||
setGraphReady(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { data: summary, isLoading, isFetching } = useLoadGraph({
|
||||
enabled: true,
|
||||
onGraphReady: (graphSummary) => {
|
||||
setGraphReady(true);
|
||||
setGraphVersion((current) => current + 1);
|
||||
setIsLayoutRunning(!graphSummary.layoutReady);
|
||||
if (settlingOverlayTimeoutRef.current !== null) {
|
||||
window.clearTimeout(settlingOverlayTimeoutRef.current);
|
||||
@@ -746,7 +767,7 @@ export function GraphWorkspace() {
|
||||
settlingOverlayTimeoutRef.current = null;
|
||||
}, 900);
|
||||
},
|
||||
onProgress: setLoadingProgress,
|
||||
onProgress: handleLoadProgress,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -808,6 +829,7 @@ export function GraphWorkspace() {
|
||||
});
|
||||
prevActiveIdsRef.current = nextActiveIds;
|
||||
setActiveNodeCount(data.active_node_count);
|
||||
setGraphVersion((current) => current + 1);
|
||||
sceneRef.current?.getRuntime()?.requestRender();
|
||||
});
|
||||
} catch (fetchError) {
|
||||
@@ -877,8 +899,10 @@ export function GraphWorkspace() {
|
||||
setPathResult(null);
|
||||
setSearchResults([]);
|
||||
setSearchError("");
|
||||
setIsLayoutRunning(false);
|
||||
}, []);
|
||||
if (viewMode === "focused") {
|
||||
setIsLayoutRunning(false);
|
||||
}
|
||||
}, [viewMode]);
|
||||
|
||||
const handleEdgeSelect = useCallback((edgeId: string) => {
|
||||
setSelectedEdgeId(edgeId);
|
||||
@@ -997,6 +1021,7 @@ export function GraphWorkspace() {
|
||||
},
|
||||
]);
|
||||
logEvent("add-node", `Added node ${payload.label ?? payload.id}${payload.nodeType ? ` (${payload.nodeType})` : ""} via realtime ws`, { nodeId: payload.id, nodeType: payload.nodeType });
|
||||
setGraphVersion((current) => current + 1);
|
||||
sceneRef.current?.getRuntime()?.requestRender();
|
||||
}
|
||||
if (eventType === "ADD_EDGE") {
|
||||
@@ -1010,6 +1035,7 @@ export function GraphWorkspace() {
|
||||
},
|
||||
]);
|
||||
logEvent("add-edge", `Added edge ${payload.edgeType ?? payload.id} (${payload.source_id} → ${payload.target_id}) via realtime ws`, { edgeId: payload.id, edgeType: payload.edgeType, source: payload.source_id, target: payload.target_id });
|
||||
setGraphVersion((current) => current + 1);
|
||||
sceneRef.current?.getRuntime()?.requestRender();
|
||||
}
|
||||
} catch (socketError) {
|
||||
@@ -1026,18 +1052,77 @@ export function GraphWorkspace() {
|
||||
setCollapsedNeighborhoodNodeIds([]);
|
||||
}, [summary?.edgeCount, summary?.nodeCount]);
|
||||
|
||||
const showLoadingOverlay = isLoading || isFetching || loadingProgress?.phase === "stabilizing_layout";
|
||||
const showLoadingOverlay = !graphReady && (isLoading || isFetching || Boolean(loadingProgress));
|
||||
const showSettlingStatus = graphReady && loadingProgress?.phase === "stabilizing_layout";
|
||||
const hasGraphContent = Boolean(summary?.nodeCount);
|
||||
const activePath = pathResult?.path ?? [];
|
||||
const activePathEdgeIds = pathResult?.edge_ids ?? [];
|
||||
const activePath = pathResult?.path ?? EMPTY_PATH;
|
||||
const activePathEdgeIds = pathResult?.edge_ids ?? EMPTY_PATH;
|
||||
const structuralSelectedNodeId = useMemo(() => {
|
||||
if (!selectedNodeId || !graph.hasNode(selectedNodeId)) {
|
||||
return "";
|
||||
}
|
||||
if (viewMode === "focused") {
|
||||
return selectedNodeId;
|
||||
}
|
||||
return collapsedNeighborhoodNodeIds.includes(selectedNodeId) ? selectedNodeId : "";
|
||||
}, [collapsedNeighborhoodNodeIds, selectedNodeId, viewMode]);
|
||||
const structuralActivePath = structuralSelectedNodeId ? activePath : EMPTY_PATH;
|
||||
const structuralActivePathEdgeIds = structuralSelectedNodeId ? activePathEdgeIds : EMPTY_PATH;
|
||||
const groupedViewAvailable = useMemo(
|
||||
() => computeGraphAnalyticsBase(graph, { computeCommunities: true, computeCentrality: false }).communitiesByNode.size > 0,
|
||||
[graphVersion],
|
||||
);
|
||||
const displayResult = useMemo(
|
||||
() => resolveDisplayGraph(selectedNodeId, activePath, activePathEdgeIds, viewMode, {
|
||||
() => resolveDisplayGraph(structuralSelectedNodeId, structuralActivePath, structuralActivePathEdgeIds, viewMode, {
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
groupedViewAvailable,
|
||||
}),
|
||||
[activePath, activePathEdgeIds, aggregationEnabled, collapsedNeighborhoodNodeIds, selectedNodeId, viewMode],
|
||||
[
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
graphVersion,
|
||||
groupedViewAvailable,
|
||||
structuralActivePath,
|
||||
structuralActivePathEdgeIds,
|
||||
structuralSelectedNodeId,
|
||||
viewMode,
|
||||
],
|
||||
);
|
||||
const displayState = displayResult.state;
|
||||
const displayState = useMemo(
|
||||
() => resolveDisplayStateSnapshot(selectedNodeId, activePath, viewMode, {
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
groupedViewAvailable,
|
||||
}),
|
||||
[activePath, aggregationEnabled, collapsedNeighborhoodNodeIds, groupedViewAvailable, selectedNodeId, viewMode],
|
||||
);
|
||||
const displayMeta = displayResult.meta;
|
||||
const previousDisplayGraphRef = useRef(displayResult.graph);
|
||||
const previousDisplayStateRef = useRef(displayState);
|
||||
useEffect(() => {
|
||||
const graphRebuilt = previousDisplayGraphRef.current !== displayResult.graph;
|
||||
const displayStateChanged = previousDisplayStateRef.current !== displayState;
|
||||
debugGraphWorkspace("display-state-derived", {
|
||||
selectedNodeId,
|
||||
structuralSelectedNodeId,
|
||||
viewMode,
|
||||
graphRebuilt,
|
||||
displayStateChanged,
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodActive: Boolean(structuralSelectedNodeId && collapsedNeighborhoodNodeIds.includes(structuralSelectedNodeId)),
|
||||
});
|
||||
previousDisplayGraphRef.current = displayResult.graph;
|
||||
previousDisplayStateRef.current = displayState;
|
||||
}, [
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
displayResult.graph,
|
||||
displayState,
|
||||
selectedNodeId,
|
||||
structuralSelectedNodeId,
|
||||
viewMode,
|
||||
]);
|
||||
const focusedSummary = useMemo(() => {
|
||||
if (!selectedNodeId || !graph.hasNode(selectedNodeId)) {
|
||||
if (viewMode === "grouped") {
|
||||
@@ -1473,25 +1558,13 @@ export function GraphWorkspace() {
|
||||
id: "zoom-in",
|
||||
label: "+ Zoom In",
|
||||
title: "Zoom in (or scroll up on the canvas)",
|
||||
onClick: () => {
|
||||
const runtime = sceneRef.current?.getRuntime();
|
||||
if (runtime?.renderer === "sigma") {
|
||||
const camera = (runtime.scene as import("sigma").default).getCamera();
|
||||
camera.animatedZoom({ duration: 200 });
|
||||
}
|
||||
},
|
||||
onClick: () => sceneRef.current?.zoomIn(),
|
||||
},
|
||||
{
|
||||
id: "zoom-out",
|
||||
label: "- Zoom Out",
|
||||
title: "Zoom out (or scroll down on the canvas)",
|
||||
onClick: () => {
|
||||
const runtime = sceneRef.current?.getRuntime();
|
||||
if (runtime?.renderer === "sigma") {
|
||||
const camera = (runtime.scene as import("sigma").default).getCamera();
|
||||
camera.animatedUnzoom({ duration: 200 });
|
||||
}
|
||||
},
|
||||
onClick: () => sceneRef.current?.zoomOut(),
|
||||
},
|
||||
{
|
||||
id: "fit-view",
|
||||
@@ -1550,6 +1623,11 @@ export function GraphWorkspace() {
|
||||
const sceneAdapterProps = {
|
||||
onNodeSelect: focusNode,
|
||||
onEdgeSelect: handleEdgeSelect,
|
||||
graphVersion,
|
||||
graphReady,
|
||||
displayGraph: displayResult.graph,
|
||||
displayMeta,
|
||||
displayState,
|
||||
selectedNodeId,
|
||||
selectedEdgeId,
|
||||
activePath,
|
||||
@@ -1557,9 +1635,8 @@ export function GraphWorkspace() {
|
||||
effectsState,
|
||||
temporalState,
|
||||
isLayoutRunning,
|
||||
layoutSource: graphSummary?.layoutSource,
|
||||
viewMode,
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
showFitViewButton: false,
|
||||
pluginOverlays: pluginOverlays.map((overlay) => overlay.element),
|
||||
onRuntimeChange: handleSceneRuntimeChange,
|
||||
@@ -1580,7 +1657,7 @@ export function GraphWorkspace() {
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<div className="explore-toolbar">
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
|
||||
{showLoadingOverlay && loadingProgress ? (
|
||||
{(showLoadingOverlay || showSettlingStatus) && loadingProgress ? (
|
||||
<MetricChip>{getGraphLoadTitle(loadingProgress.phase)}</MetricChip>
|
||||
) : null}
|
||||
{summary ? (
|
||||
|
||||
@@ -24,6 +24,8 @@ export const SigmaSceneAdapter = forwardRef<GraphSceneHandle, GraphSceneProps>(
|
||||
useImperativeHandle(ref, () => ({
|
||||
fitView: () => canvasRef.current?.fitView(),
|
||||
focusNode: (nodeId: string) => canvasRef.current?.focusNode(nodeId),
|
||||
zoomIn: () => canvasRef.current?.zoomIn(),
|
||||
zoomOut: () => canvasRef.current?.zoomOut(),
|
||||
getRuntime: () => runtimeRef.current,
|
||||
setLayoutRunning: onLayoutRunningChange
|
||||
? (running: boolean) => {
|
||||
|
||||
@@ -5,11 +5,16 @@ export const focusCameraBehavior: GraphBehavior = {
|
||||
attach: () => {},
|
||||
detach: () => {},
|
||||
performAction: (context, action) => {
|
||||
if (action.type !== "focusNode") {
|
||||
return false;
|
||||
if (action.type === "focusNode") {
|
||||
context.focusNodeInView(action.nodeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
context.focusNodeInView(action.nodeId);
|
||||
return true;
|
||||
if (action.type === "centerSelection") {
|
||||
context.centerSelectionInView(action.nodeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { GraphBehavior } from "./types";
|
||||
|
||||
export function createSearchFocusBehavior(): GraphBehavior {
|
||||
let lastFocusedNodeId = "";
|
||||
let lastSelectedNodeId = "";
|
||||
|
||||
return {
|
||||
id: "search-focus",
|
||||
attach: () => {},
|
||||
detach: () => {
|
||||
lastFocusedNodeId = "";
|
||||
lastSelectedNodeId = "";
|
||||
},
|
||||
onStateChange: (context, interactionState) => {
|
||||
const nextFocusedNodeId = interactionState.focusedNodeId;
|
||||
if (!nextFocusedNodeId || nextFocusedNodeId === lastFocusedNodeId) {
|
||||
lastFocusedNodeId = nextFocusedNodeId;
|
||||
const nextSelectedNodeId = interactionState.selectedNodeId;
|
||||
if (!nextSelectedNodeId || nextSelectedNodeId === lastSelectedNodeId) {
|
||||
lastSelectedNodeId = nextSelectedNodeId;
|
||||
return;
|
||||
}
|
||||
|
||||
lastFocusedNodeId = nextFocusedNodeId;
|
||||
context.dispatchAction({ type: "focusNode", nodeId: nextFocusedNodeId });
|
||||
lastSelectedNodeId = nextSelectedNodeId;
|
||||
context.dispatchAction({ type: "centerSelection", nodeId: nextSelectedNodeId });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import type { GraphCameraState, GraphInteractionState } from "../types";
|
||||
|
||||
export type GraphBehaviorActionRequest =
|
||||
| { type: "fitView" }
|
||||
| { type: "focusNode"; nodeId: string };
|
||||
| { type: "focusNode"; nodeId: string }
|
||||
| { type: "centerSelection"; nodeId: string };
|
||||
|
||||
export interface GraphBehaviorContext {
|
||||
sigma: Sigma;
|
||||
@@ -17,6 +18,7 @@ export interface GraphBehaviorContext {
|
||||
onNodeSelectionChange: (nodeId: string) => void;
|
||||
onEdgeSelectionChange: (edgeId: string) => void;
|
||||
focusNodeInView: (nodeId: string) => void;
|
||||
centerSelectionInView: (nodeId: string) => void;
|
||||
fitCurrentView: () => void;
|
||||
dispatchAction: (action: GraphBehaviorActionRequest) => void;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@ export function createViewModeSwitchBehavior(): GraphBehavior {
|
||||
}
|
||||
|
||||
lastViewMode = interactionState.viewMode;
|
||||
const nextSelectedNodeId = interactionState.selectedNodeId;
|
||||
|
||||
if (interactionState.focusedNodeId) {
|
||||
context.dispatchAction({ type: "focusNode", nodeId: interactionState.focusedNodeId });
|
||||
if (interactionState.viewMode === "focused" && nextSelectedNodeId) {
|
||||
context.dispatchAction({ type: "focusNode", nodeId: nextSelectedNodeId });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
zoomTierAtLeast,
|
||||
} from "./graphTheme";
|
||||
import { computeGraphAnalyticsBase } from "./graphAnalytics";
|
||||
import type { GraphDisplayStateSnapshot, GraphInteractionState, GraphViewMode } from "./types";
|
||||
import type { GraphDisplayMeta, GraphDisplayStateSnapshot, GraphInteractionState, GraphViewMode } from "./types";
|
||||
|
||||
const MAX_FOCUS_NEIGHBORS = GRAPH_THEME.focus.maxNeighbors;
|
||||
const FOCUS_RING_CAPACITY = GRAPH_THEME.focus.ringCapacity;
|
||||
@@ -34,6 +34,28 @@ type GraphRef = typeof graph | Graph<NodeAttributes, EdgeAttributes>;
|
||||
export type GraphDisplayResult = {
|
||||
graph: GraphRef;
|
||||
state: GraphDisplayStateSnapshot;
|
||||
meta: GraphDisplayMeta;
|
||||
};
|
||||
|
||||
const BASE_DISPLAY_META: GraphDisplayMeta = {
|
||||
layoutMode: "base",
|
||||
positionSource: "store",
|
||||
tracksStoreNodePositions: true,
|
||||
hasSyntheticNodes: false,
|
||||
};
|
||||
|
||||
const MIRRORED_DISPLAY_META: GraphDisplayMeta = {
|
||||
layoutMode: "mirrored",
|
||||
positionSource: "store",
|
||||
tracksStoreNodePositions: true,
|
||||
hasSyntheticNodes: false,
|
||||
};
|
||||
|
||||
const OWNED_DISPLAY_META: GraphDisplayMeta = {
|
||||
layoutMode: "owned",
|
||||
positionSource: "display",
|
||||
tracksStoreNodePositions: false,
|
||||
hasSyntheticNodes: true,
|
||||
};
|
||||
|
||||
function getOverviewPresenceBoost(cameraRatio: number) {
|
||||
@@ -1068,7 +1090,7 @@ function buildCommunityGroupedGraph(): GraphDisplayResult {
|
||||
const state = createEmptyDisplayState("", true);
|
||||
state.groupedViewAvailable = base.communitiesByNode.size > 0;
|
||||
if (base.communitiesByNode.size === 0) {
|
||||
return { graph: aggregateDisplayGraph(graph), state };
|
||||
return { graph: aggregateDisplayGraph(graph), state, meta: MIRRORED_DISPLAY_META };
|
||||
}
|
||||
|
||||
const grouped = new Graph<NodeAttributes, EdgeAttributes>({
|
||||
@@ -1209,9 +1231,51 @@ function buildCommunityGroupedGraph(): GraphDisplayResult {
|
||||
selectedVisibleNeighborIds: [],
|
||||
selectedCollapsedNeighborIds: [],
|
||||
},
|
||||
meta: OWNED_DISPLAY_META,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveDisplayStateSnapshot(
|
||||
selectedNodeId: string,
|
||||
activePath: string[],
|
||||
viewMode: GraphViewMode,
|
||||
options?: {
|
||||
aggregationEnabled?: boolean;
|
||||
collapsedNeighborhoodNodeIds?: Iterable<string>;
|
||||
groupedViewAvailable?: boolean;
|
||||
},
|
||||
): GraphDisplayStateSnapshot {
|
||||
const aggregationEnabled = options?.aggregationEnabled ?? true;
|
||||
const collapsedNeighborhoodNodeIds = new Set(
|
||||
Array.from(options?.collapsedNeighborhoodNodeIds ?? []).filter((nodeId) => typeof nodeId === "string"),
|
||||
);
|
||||
const displayState = createEmptyDisplayState(selectedNodeId, aggregationEnabled);
|
||||
displayState.groupedViewAvailable = options?.groupedViewAvailable ?? computeGraphAnalyticsBase(graph, {
|
||||
computeCommunities: true,
|
||||
computeCentrality: false,
|
||||
}).communitiesByNode.size > 0;
|
||||
|
||||
if (!selectedNodeId || !graph.hasNode(selectedNodeId)) {
|
||||
return displayState;
|
||||
}
|
||||
|
||||
const shouldCollapseNeighborhood = Boolean(
|
||||
viewMode !== "grouped"
|
||||
&& collapsedNeighborhoodNodeIds.has(selectedNodeId),
|
||||
);
|
||||
const collapsedState = shouldCollapseNeighborhood
|
||||
? buildCollapsedNeighborhoodState(selectedNodeId, activePath)
|
||||
: {
|
||||
selectedVisibleNeighborIds: rankNeighbors(selectedNodeId),
|
||||
selectedCollapsedNeighborIds: [],
|
||||
};
|
||||
|
||||
displayState.selectedRootNodeId = selectedNodeId;
|
||||
displayState.selectedVisibleNeighborIds = collapsedState.selectedVisibleNeighborIds;
|
||||
displayState.selectedCollapsedNeighborIds = collapsedState.selectedCollapsedNeighborIds;
|
||||
return displayState;
|
||||
}
|
||||
|
||||
export function createFocusedGraph(
|
||||
nodeId: string,
|
||||
activePath: string[],
|
||||
@@ -1247,8 +1311,8 @@ export function createFocusedGraph(
|
||||
const selectedState = resolveNodeElementStyle(GRAPH_THEME, "inspection", "selected", selectedAttrs, selectedAttrs.label);
|
||||
addNode(nodeId, {
|
||||
...selectedAttrs,
|
||||
x: 0,
|
||||
y: 0,
|
||||
x: Number.isFinite(selectedAttrs.x) ? selectedAttrs.x : 0,
|
||||
y: Number.isFinite(selectedAttrs.y) ? selectedAttrs.y : 0,
|
||||
color: selectedState.color,
|
||||
size: Math.max(selectedState.size, 22),
|
||||
baseColor: selectedState.color,
|
||||
@@ -1286,8 +1350,8 @@ export function createFocusedGraph(
|
||||
|
||||
addNode(neighborId, {
|
||||
...baseAttrs,
|
||||
x: Math.cos(angle) * radius,
|
||||
y: Math.sin(angle) * radius,
|
||||
x: Number.isFinite(baseAttrs.x) ? baseAttrs.x : Math.cos(angle) * radius,
|
||||
y: Number.isFinite(baseAttrs.y) ? baseAttrs.y : Math.sin(angle) * radius,
|
||||
color: style.color,
|
||||
size: Math.max(style.size, 8.5),
|
||||
baseColor: style.color,
|
||||
@@ -1332,33 +1396,22 @@ export function resolveDisplayGraph(
|
||||
options?: {
|
||||
aggregationEnabled?: boolean;
|
||||
collapsedNeighborhoodNodeIds?: Iterable<string>;
|
||||
groupedViewAvailable?: boolean;
|
||||
},
|
||||
): GraphDisplayResult {
|
||||
const aggregationEnabled = options?.aggregationEnabled ?? true;
|
||||
const collapsedNeighborhoodNodeIds = new Set(
|
||||
Array.from(options?.collapsedNeighborhoodNodeIds ?? []).filter((nodeId) => typeof nodeId === "string"),
|
||||
);
|
||||
const displayState = createEmptyDisplayState(selectedNodeId, aggregationEnabled);
|
||||
displayState.groupedViewAvailable = computeGraphAnalyticsBase(graph, {
|
||||
computeCommunities: true,
|
||||
computeCentrality: false,
|
||||
}).communitiesByNode.size > 0;
|
||||
const displayState = resolveDisplayStateSnapshot(selectedNodeId, activePath, viewMode, {
|
||||
aggregationEnabled,
|
||||
collapsedNeighborhoodNodeIds,
|
||||
groupedViewAvailable: options?.groupedViewAvailable,
|
||||
});
|
||||
const isFocusedView = viewMode === "focused" && Boolean(selectedNodeId) && graph.hasNode(selectedNodeId);
|
||||
const isGroupedView = viewMode === "grouped";
|
||||
const shouldCollapseNeighborhood = Boolean(selectedNodeId && collapsedNeighborhoodNodeIds.has(selectedNodeId));
|
||||
|
||||
if (selectedNodeId && graph.hasNode(selectedNodeId)) {
|
||||
const collapsedState = shouldCollapseNeighborhood
|
||||
? buildCollapsedNeighborhoodState(selectedNodeId, activePath)
|
||||
: {
|
||||
selectedVisibleNeighborIds: rankNeighbors(selectedNodeId),
|
||||
selectedCollapsedNeighborIds: [],
|
||||
};
|
||||
displayState.selectedRootNodeId = selectedNodeId;
|
||||
displayState.selectedVisibleNeighborIds = collapsedState.selectedVisibleNeighborIds;
|
||||
displayState.selectedCollapsedNeighborIds = collapsedState.selectedCollapsedNeighborIds;
|
||||
}
|
||||
|
||||
if (isGroupedView) {
|
||||
const grouped = buildCommunityGroupedGraph();
|
||||
return {
|
||||
@@ -1369,6 +1422,7 @@ export function resolveDisplayGraph(
|
||||
selectedVisibleNeighborIds: displayState.selectedVisibleNeighborIds,
|
||||
selectedCollapsedNeighborIds: displayState.selectedCollapsedNeighborIds,
|
||||
},
|
||||
meta: grouped.meta,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1376,6 +1430,7 @@ export function resolveDisplayGraph(
|
||||
return {
|
||||
graph: createFocusedGraph(selectedNodeId, activePath, activePathEdgeIds, shouldCollapseNeighborhood),
|
||||
state: displayState,
|
||||
meta: MIRRORED_DISPLAY_META,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1386,6 +1441,11 @@ export function resolveDisplayGraph(
|
||||
return {
|
||||
graph: aggregationEnabled ? aggregateDisplayGraph(baseGraph) : baseGraph,
|
||||
state: displayState,
|
||||
meta: aggregationEnabled
|
||||
? MIRRORED_DISPLAY_META
|
||||
: baseGraph === graph
|
||||
? BASE_DISPLAY_META
|
||||
: MIRRORED_DISPLAY_META,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { graph, type EdgeAttributes, type NodeAttributes } from "../../store/gra
|
||||
import type {
|
||||
GraphAnalyticsSnapshot,
|
||||
GraphCameraState,
|
||||
GraphDisplayMeta,
|
||||
GraphDisplayStateSnapshot,
|
||||
GraphDiagnosticsSnapshot,
|
||||
GraphEffectsState,
|
||||
GraphInteractionState,
|
||||
@@ -22,6 +24,8 @@ export interface GraphSceneRuntime {
|
||||
scene: unknown;
|
||||
graph: GraphSceneGraph;
|
||||
displayGraph: GraphSceneGraph;
|
||||
graphVersion: number;
|
||||
layoutMode?: GraphDisplayMeta["layoutMode"];
|
||||
requestRender: () => void;
|
||||
getCameraState: () => GraphCameraState | null;
|
||||
}
|
||||
@@ -37,6 +41,11 @@ export interface GraphSceneEventMap {
|
||||
}
|
||||
|
||||
export interface GraphSceneProps extends GraphSceneEventMap {
|
||||
graphVersion: number;
|
||||
graphReady: boolean;
|
||||
displayGraph: GraphSceneGraph;
|
||||
displayMeta: GraphDisplayMeta;
|
||||
displayState?: GraphDisplayStateSnapshot;
|
||||
selectedNodeId: string;
|
||||
selectedEdgeId: string;
|
||||
activePath?: string[];
|
||||
@@ -48,8 +57,6 @@ export interface GraphSceneProps extends GraphSceneEventMap {
|
||||
layoutSource?: GraphLayoutSource;
|
||||
onLayoutStatusChange?: (status: GraphLayoutStatus) => void;
|
||||
viewMode: GraphViewMode;
|
||||
aggregationEnabled?: boolean;
|
||||
collapsedNeighborhoodNodeIds?: string[];
|
||||
className?: string;
|
||||
showFitViewButton?: boolean;
|
||||
pluginOverlays?: ReactNode[];
|
||||
@@ -58,6 +65,8 @@ export interface GraphSceneProps extends GraphSceneEventMap {
|
||||
export interface GraphSceneHandle {
|
||||
fitView: () => void;
|
||||
focusNode: (nodeId: string) => void;
|
||||
zoomIn: () => void;
|
||||
zoomOut: () => void;
|
||||
getRuntime: () => GraphSceneRuntime | null;
|
||||
setLayoutRunning?: (running: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,15 @@ export interface GraphDisplayStateSnapshot {
|
||||
selectedCollapsedNeighborIds: string[];
|
||||
}
|
||||
|
||||
export type GraphDisplayLayoutMode = "base" | "mirrored" | "owned";
|
||||
|
||||
export interface GraphDisplayMeta {
|
||||
layoutMode: GraphDisplayLayoutMode;
|
||||
positionSource: "store" | "display";
|
||||
tracksStoreNodePositions: boolean;
|
||||
hasSyntheticNodes: boolean;
|
||||
}
|
||||
|
||||
export type GraphEffectToggle =
|
||||
| "pathPulseEnabled"
|
||||
| "pathFlowEnabled"
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
"extraction",
|
||||
"mcp"
|
||||
],
|
||||
"skills": "./skills"
|
||||
"skills": "./skills",
|
||||
"agents": "./agents"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user