@@ -5,7 +5,7 @@ import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPa
55// @ts -expect-error
66import { CSS2DObject , CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
77import ForceGraph , { ForceGraphMethods , LinkObject , NodeObject } from "react-force-graph-3d" ;
8- import { useEffect , useRef , useState } from "react" ;
8+ import React , { useEffect , useMemo , useRef , useState } from "react" ;
99import { Leva , useControls } from "leva" ;
1010
1111import { buildXGraph , XLink , XNode } from "./XGraph.ts" ;
@@ -53,9 +53,8 @@ const DEFAULT_SETTINGS = {
5353
5454const UNREAL_BLOOM_PASS = new UnrealBloomPass ( )
5555
56- const { xGraph : X_GRAPH , nodes : NODES , fileTree : FILE_TREE } = buildXGraph ( Data . __INLINE_DATA )
57-
58- function App ( ) {
56+ function GraphExplorer ( { graphData } : { graphData : Graph } ) {
57+ const { xGraph, nodes, fileTree } = useMemo ( ( ) => buildXGraph ( graphData ) , [ graphData ] )
5958 const [ highlightNodes , setHighlightNodes ] = useState ( new Set < XNode > ( ) )
6059 const [ highlightLinks , setHighlightLinks ] = useState ( new Set < XLink > ( ) )
6160 const [ selectedNode , setSelectedNode ] = useState < XNode > ( )
@@ -161,10 +160,10 @@ function App () {
161160 if ( link . isDir ) f = settings . DIR_LINK_STRENGTH_FACTOR
162161 if ( link . isPackage ) f = settings . PACKAGE_LINK_STRENGTH_FACTOR
163162 if ( link . ignore ) f = 0
164- return f / Math . min ( NODES [ link . from ] . neighbors ?. length ?? 1 , NODES [ link . to ] . neighbors ?. length ?? 1 ) ;
163+ return f / Math . min ( nodes [ link . from ] . neighbors ?. length ?? 1 , nodes [ link . to ] . neighbors ?. length ?? 1 ) ;
165164 } )
166165 graph . current ?. d3ReheatSimulation ( )
167- } , [ settings . DIR_LINK_STRENGTH_FACTOR , settings . FILE_LINK_STRENGTH_FACTOR , settings . LINK_DISTANCE , settings . PACKAGE_LINK_STRENGTH_FACTOR , updateForced ] )
166+ } , [ nodes , settings . DIR_LINK_STRENGTH_FACTOR , settings . FILE_LINK_STRENGTH_FACTOR , settings . LINK_DISTANCE , settings . PACKAGE_LINK_STRENGTH_FACTOR , updateForced ] )
168167
169168 useEffect ( ( ) => {
170169 graph . current ?. d3Force ( 'charge' )
@@ -187,7 +186,7 @@ function App () {
187186 < ForceGraph
188187 ref = { graph }
189188 extraRenderers = { [ new CSS2DRenderer ( ) ] }
190- graphData = { X_GRAPH }
189+ graphData = { xGraph }
191190 backgroundColor = { '#000003' }
192191 nodeResolution = { settings . NODE_RESOLUTION }
193192 onBackgroundClick = { backgroundClick }
@@ -217,15 +216,97 @@ function App () {
217216 />
218217 < Explorer
219218 className = { 'fixed top-0 left-0 max-h-full bg-transparent' }
220- fileTree = { FILE_TREE }
219+ fileTree = { fileTree }
221220 onSelectNode = { nodeClick }
222221 selected = { selectedNode }
223222 highlighted = { highlightNodes }
224223 onNodesMutated = { forceUpdate }
225224 />
226- < Leva hidden = { ! X_GRAPH . enableGui } />
225+ < Leva hidden = { ! xGraph . enableGui } />
227226 </ >
228227 )
229228}
230229
230+ function DropPlaceholder ( ) {
231+ return (
232+ < div className = "flex flex-col items-center justify-center min-h-screen w-full text-gray-200" >
233+ < h2 className = "text-xl font-semibold mb-2" > Drop Here!</ h2 >
234+ </ div >
235+ )
236+ }
237+
238+ function FilePicker ( { onFilePicked } : { onFilePicked : ( file : File | undefined ) => void } ) {
239+ const handleFileSelected = ( e : React . ChangeEvent < HTMLInputElement > ) => {
240+ onFilePicked ( e . target . files ?. [ 0 ] ) ;
241+ } ;
242+
243+ return (
244+ < >
245+ < h2 className = "text-xl font-semibold mb-2" > Drop Dep-Tree JSON Graph File Here</ h2 >
246+ < p className = "text-gray-400 mb-4" > or</ p >
247+ < input
248+ type = "file"
249+ accept = ".json"
250+ onChange = { handleFileSelected }
251+ className = "bg-gray-800 px-4 py-2 rounded cursor-pointer hover:bg-gray-700"
252+ />
253+ </ >
254+ )
255+ }
256+
257+ function App ( ) {
258+ const [ graphData , setGraphData ] = useState < Graph > ( Data . __INLINE_DATA )
259+ const [ isDragging , setIsDragging ] = useState ( false )
260+ const [ forceRemountKey , setForceRemountKey ] = useState ( 0 )
261+ const [ errorMessage , setErrorMessage ] = useState < string | null > ( null )
262+
263+ async function handleFilePicked ( file : File | undefined ) {
264+ if ( ! file ) return
265+ try {
266+ const data = JSON . parse ( await file . text ( ) )
267+ setGraphData ( data )
268+ setForceRemountKey ( ( x ) => x + 1 )
269+ setErrorMessage ( null )
270+ } catch ( e ) {
271+ console . error ( 'Failed to read file:' , e )
272+ setErrorMessage ( '' + e )
273+ }
274+ }
275+
276+ function handleDrop ( e : React . DragEvent < HTMLDivElement > ) {
277+ e . preventDefault ( )
278+ setIsDragging ( false )
279+ void handleFilePicked ( e . dataTransfer . files [ 0 ] )
280+ }
281+
282+ return (
283+ < div
284+ className = { `fixed top-0 left-0 w-full h-full ${ isDragging ? 'border-2 border-dashed rounded-lg border-gray-500' : '' } ` }
285+ onDrop = { handleDrop }
286+ onDragOver = { ( e ) => {
287+ e . preventDefault ( )
288+ setIsDragging ( true )
289+ } }
290+ onDragLeave = { ( ) => setIsDragging ( false ) } >
291+ { isDragging
292+ ? < DropPlaceholder />
293+ : ( ( ! graphData . nodes ?. length || errorMessage )
294+ ? < div className = "flex flex-col items-center justify-center min-h-screen w-full text-gray-200" >
295+ < FilePicker onFilePicked = { handleFilePicked } />
296+ { errorMessage ? (
297+ < div className = "bg-red-600/20 border border-red-500/50 rounded-lg px-4 py-2 mt-12 max-w-lg text-center" >
298+ < span className = "text-red-500 font-medium" > 🚨 Error</ span >
299+ < p className = "text-red-400/80 text-sm mt-1" > { errorMessage } </ p >
300+ </ div >
301+ ) : (
302+ < div className = "bg-yellow-600/20 border border-yellow-500/50 rounded-lg px-4 py-2 mt-12 max-w-lg text-center" >
303+ < span className = "text-yellow-500 font-medium" > ⚠️ Experimental</ span >
304+ < p className = "text-yellow-400/80 text-sm mt-1" > This feature relies on an internal structure that may break in the future.</ p >
305+ </ div > ) }
306+ </ div >
307+ : < GraphExplorer key = { forceRemountKey } graphData = { graphData } /> ) }
308+ </ div >
309+ )
310+ }
311+
231312export default App
0 commit comments