@@ -68,7 +68,7 @@ export function LearningMap({
6868 const updateNodesStates = useViewerStore ( state => state . updateNodesStates ) ;
6969 const updateNodeState = useViewerStore ( state => state . updateNodeState ) ;
7070
71- const { fitView, getViewport, setViewport } = useReactFlow ( ) ;
71+ const { fitView, getViewport } = useReactFlow ( ) ;
7272
7373 // Use language from settings if available, otherwise use prop
7474 const effectiveLanguage = settings ?. language || language ;
@@ -78,14 +78,40 @@ export function LearningMap({
7878
7979 const parsedRoadmap = parseRoadmapData ( roadmapData ) ;
8080
81+ // Calculate translateExtent to ensure at least one node is always visible
82+ const calculateTranslateExtent = useCallback ( ( ) => {
83+ if ( nodes . length === 0 ) return [ [ - Infinity , - Infinity ] , [ Infinity , Infinity ] ] as [ [ number , number ] , [ number , number ] ] ;
84+
85+ const padding = 200 ; // Add padding so nodes aren't at the very edge
86+ let minX = Infinity , minY = Infinity , maxX = - Infinity , maxY = - Infinity ;
87+
88+ nodes . forEach ( node => {
89+ if ( node . position ) {
90+ // Estimate node size (approximate, could be refined)
91+ const nodeWidth = node . width || 200 ;
92+ const nodeHeight = node . height || 100 ;
93+
94+ minX = Math . min ( minX , node . position . x - padding ) ;
95+ minY = Math . min ( minY , node . position . y - padding ) ;
96+ maxX = Math . max ( maxX , node . position . x + nodeWidth + padding ) ;
97+ maxY = Math . max ( maxY , node . position . y + nodeHeight + padding ) ;
98+ }
99+ } ) ;
100+
101+ return [ [ minX , minY ] , [ maxX , maxY ] ] as [ [ number , number ] , [ number , number ] ] ;
102+ } , [ nodes ] ) ;
103+
81104 useEffect ( ( ) => {
82105 loadRoadmapData ( parsedRoadmap , initialState ) ;
83- setViewport ( {
84- x : initialState ?. x || settings ?. viewport ?. x || 0 ,
85- y : initialState ?. y || settings ?. viewport ?. y || 0 ,
86- zoom : initialState ?. zoom || settings ?. viewport ?. zoom || 1 ,
87- } ) ;
88- } , [ roadmapData , initialState ] ) ;
106+
107+ // Only use fitView if there's no saved state
108+ if ( ! initialState ) {
109+ // Use setTimeout to ensure nodes are rendered before fitView
110+ setTimeout ( ( ) => {
111+ fitView ( { duration : 0 , padding : 0.2 } ) ;
112+ } , 0 ) ;
113+ }
114+ } , [ roadmapData , initialState , loadRoadmapData , fitView ] ) ;
89115
90116 const onNodeClick = useCallback ( ( _ : any , node : Node , focus : boolean = false ) => {
91117 if ( ! isInteractableNode ( node ) ) return ;
@@ -124,7 +150,7 @@ export function LearningMap({
124150 root . dispatchEvent ( new CustomEvent ( "change" , { detail : minimalState } ) ) ;
125151 }
126152 }
127- } , [ nodes , onChange ] ) ;
153+ } , [ nodes , getViewport , getRoadmapState ] ) ;
128154
129155 const defaultEdgeOptions = {
130156 animated : false ,
@@ -135,6 +161,15 @@ export function LearningMap({
135161 type : "default" ,
136162 } ;
137163
164+ // Determine default viewport (only used if no saved state exists)
165+ const defaultViewport = {
166+ x : initialState ?. x || settings ?. viewport ?. x || 0 ,
167+ y : initialState ?. y || settings ?. viewport ?. y || 0 ,
168+ zoom : initialState ?. zoom || settings ?. viewport ?. zoom || 1 ,
169+ } ;
170+
171+ const translateExtent = calculateTranslateExtent ( ) ;
172+
138173 return (
139174 < div
140175 className = "editor-canvas"
@@ -159,7 +194,8 @@ export function LearningMap({
159194 onNodeClick = { onNodeClick }
160195 onNodesChange = { onNodesChange }
161196 nodeTypes = { nodeTypes }
162- fitView
197+ defaultViewport = { defaultViewport }
198+ translateExtent = { translateExtent }
163199 proOptions = { { hideAttribution : true } }
164200 defaultEdgeOptions = { defaultEdgeOptions }
165201 nodesDraggable = { false }
0 commit comments