diff --git a/playground/csharp/Program.cs b/playground/csharp/Program.cs index b07ef27..ad936c4 100644 --- a/playground/csharp/Program.cs +++ b/playground/csharp/Program.cs @@ -46,6 +46,37 @@ int test(int a, int b) Z = 37, }; +var nested = new NestedClass +{ + mat = new Matrix() + { + Row0 = new Vector + { + X = 32, + Y = 64, + Z = 128, + }, + + Row1 = new Vector + { + X = 1, + Y = 2, + Z = 3, + }, + + Row2 = new Vector + { + X = 5, + Y = 6, + Z = 9, + }, + }, + + name = "mat", + age = 33 +}; + + var result = mat.Apply(vec); for (int i = 0; i < 10; ++i) @@ -77,3 +108,11 @@ public Vector Apply(Vector target) }; } } + +class NestedClass +{ + public required Matrix mat { get; init; } + public required string name { get; init; } + public required int age { get; init; } +} + diff --git a/server/index.html b/server/index.html index 6aa221d..d219c58 100644 --- a/server/index.html +++ b/server/index.html @@ -10,7 +10,7 @@ dapviz - +
diff --git a/server/src/App.tsx b/server/src/App.tsx index ac6e8c9..75beec0 100644 --- a/server/src/App.tsx +++ b/server/src/App.tsx @@ -3,6 +3,7 @@ import DapvizProvider, { useDapviz } from "./DapvizProvider"; import Visualizer from "./Visualizer"; import Controls from "./Controls"; import { useState } from "react"; +import { ThemeProvider } from "./ThemeProvider"; const NoConnectionError = () => (

No Connection

@@ -24,11 +25,13 @@ const DapvizApp = () => { } const App = () => ( - } - > - - + + } + > + + + ); export default App; diff --git a/server/src/HeapConnectionsProvider.tsx b/server/src/HeapConnectionsProvider.tsx new file mode 100644 index 0000000..8ddc82a --- /dev/null +++ b/server/src/HeapConnectionsProvider.tsx @@ -0,0 +1,141 @@ +import { Variable } from "./DapvizProvider"; +import { QuadraticBezierLine, QuadraticBezierLineRef } from "@react-three/drei"; +import React, { createContext, useContext, useMemo, useRef } from "react"; +import { useCallback, useState } from "react"; +import * as THREE from "three"; +import { useFrame } from "@react-three/fiber"; +import { useTheme } from "./ThemeProvider"; + +export type HeapConnectionContextType = { + registerNode: (id: number, ref: React.RefObject) => void; + unregisterNode: (id: number) => void; +}; + +export const HeapConnectionContext = createContext(null); + +export const useHeapConnections = () => { + const context = useContext(HeapConnectionContext); + if (!context) { + throw new Error("useHeapConnections called outside of HeapConnectionsProvider"); + } + return context; +} + +interface ConnectionLineProps { + parentRef: React.RefObject; + childRef: React.RefObject; +} + +const ConnectionLine = ({ parentRef, childRef }: ConnectionLineProps) => { + const theme = useTheme(); + + const lineRef = useRef(null); + + const startCircleRef = useRef(null); + const endCircleRef = useRef(null); + + useFrame(() => { + if (!parentRef.current || !childRef.current || !lineRef.current) return; + + const startPos = new THREE.Vector3(); + const endPos = new THREE.Vector3(); + const midPos = new THREE.Vector3(); + const box = new THREE.Box3(); + const size = new THREE.Vector3(); + + parentRef.current.getWorldPosition(startPos); + childRef.current.getWorldPosition(endPos); + + box.setFromObject(parentRef.current); + box.getSize(size); + const startOffset = size.x / 2; + + box.setFromObject(childRef.current); + box.getSize(size); + const endOffset = size.x / 2; + + const padding = 3; + + const finalStart = startPos.clone(); + finalStart.x += startOffset + padding; + + const finalEnd = endPos.clone(); + finalEnd.x -= endOffset + padding; + + midPos.addVectors(finalStart, finalEnd).multiplyScalar(0.5); + midPos.y += 80; + + lineRef.current.setPoints(finalStart, finalEnd, midPos); + startCircleRef.current?.position.copy(finalStart); + endCircleRef.current?.position.copy(finalEnd); + }); + + return ( + <> + + + + + + + + + + + + ); +} + +type VariableWithParent = Variable & { parent: NonNullable }; + +function hasParent(v: Variable): v is VariableWithParent { + return !!v.parent && v.reference > 0; +} + +export const HeapConnectionsProvider = ({ children, allVariables }: { children: React.ReactNode, allVariables: Variable[] }) => { + const [nodeRefs, setNodeRefs] = useState>>(new Map()); + + const registerNode = useCallback((id: number, ref: React.RefObject) => { + setNodeRefs(prev => new Map(prev).set(id, ref)); + }, [setNodeRefs]); + + const unregisterNode = useCallback((id: number) => { + setNodeRefs(prev => { + const newMap = new Map(prev); + newMap.delete(id); + return newMap; + }); + }, [setNodeRefs]); + + const connections = useMemo(() => + allVariables.filter(hasParent), [allVariables] + ); + + return ( + + {children} + + + {connections.map(variable => { + const parentRef = nodeRefs.get(variable.parent); + const childRef = nodeRefs.get(variable.reference); + + if (!parentRef || !childRef) return null; + return ( + + ); + })} + + + ); +} diff --git a/server/src/ThemeProvider.tsx b/server/src/ThemeProvider.tsx new file mode 100644 index 0000000..db24a95 --- /dev/null +++ b/server/src/ThemeProvider.tsx @@ -0,0 +1,63 @@ +import { createContext, ReactNode, useContext } from "react" + +interface AppTheme { + grid: { + cell: string, + section: string, + }, + text: { + primary: string, + secondary: string, + type: string, + }, + node: { + background: string, + backgroundHover: string, + border: string, + divider: string, + }, + connection: { + line: string, + start: string, + end: string, + }, +} + +export const gruvboxTheme: AppTheme = { + grid: { + cell: "#3c3836", + section: "#504945", + }, + text: { + primary: "#ebdbb2", + secondary: "#a89984", + type: "#d65d0e", + }, + node: { + background: "#282828", + backgroundHover: "#504945", + border: "#504945", + divider: "#4a4a5a", + }, + connection: { + line: "#a89984", + start: "#689d6a", + end: "#d79921", + } +} + +const ThemeContext = createContext(gruvboxTheme); + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme called outside of ThemeProvider"); + } + return context; +} + +export const ThemeProvider = ({ children }: { children: ReactNode }) => { + return + {children} + ; +}; diff --git a/server/src/Visualizer.tsx b/server/src/Visualizer.tsx index 0ca7952..c270c21 100644 --- a/server/src/Visualizer.tsx +++ b/server/src/Visualizer.tsx @@ -3,16 +3,21 @@ import { StackFrame, ThreadInfo, Variable } from "./DapvizProvider"; import DebugJsonInfo from "./DebugJsonInfo"; import { Container, DefaultProperties, Root, Text } from "@react-three/uikit"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./components/default/accordion"; +import { HeapConnectionsProvider, useHeapConnections } from "./HeapConnectionsProvider"; +import * as THREE from "three"; +import { useEffect, useMemo, useRef } from "react"; +import { useTheme } from "./ThemeProvider"; const BackgroundGrid = () => { + const theme = useTheme(); const size = 100; const thickness = 1.5; return ( { ); } -const VariableViz = ({ variable }: { variable: Variable }) => { +const StackFrameVariable = ({ variable }: { variable: Variable }) => { + const theme = useTheme(); return ( - {variable.name} - {variable.value} + {variable.name} + {variable.value} ); } -const StackFrameViz = ({ stackFrame }: { stackFrame: StackFrame }) => { +const PrimitiveVariable = ({ variable }: { variable: Variable }) => { + const theme = useTheme(); + return ( + + {variable.type} + {variable.name} + {variable.value} + + ); +} - const allFrameVariables = stackFrame.scopes.flatMap((scope) => scope.variables); - const rootVariables = allFrameVariables.filter((variable) => variable.parent === null); +const StackFrameViz = ({ stackFrame }: { stackFrame: StackFrame }) => { + const rootVariables = useMemo(() => { + const allFrameVariables = stackFrame.scopes.flatMap((scope) => scope.variables); + return allFrameVariables.filter((variable) => variable.parent === null); + }, [stackFrame.scopes]); return ( {rootVariables.map((variable) => ( - @@ -52,13 +70,14 @@ const StackFrameViz = ({ stackFrame }: { stackFrame: StackFrame }) => { } const Stack = ({ thread }: { thread: ThreadInfo }) => { + const theme = useTheme(); return ( {thread.stack_frames.map((stackFrame, i) => ( - + {stackFrame.function} @@ -72,34 +91,81 @@ const Stack = ({ thread }: { thread: ThreadInfo }) => { ); } -const HeapNode = ({ variable, allVariables, initialPosition }: { +const HeapNode = ({ variable, childrenMap, initialPosition }: { variable: Variable; - allVariables: Variable[]; + childrenMap: Map; initialPosition: [number, number, number], }) => { + const theme = useTheme(); + const groupRef = useRef(null); + + const { registerNode, unregisterNode } = useHeapConnections(); + + useEffect(() => { + if (variable.reference > 0 && groupRef.current) { + registerNode(variable.reference, groupRef); + } + + return () => { + if (variable.reference > 0) { + unregisterNode(variable.reference); + } + }; + }, [variable.reference, registerNode, unregisterNode]); + + const { primitiveChildren, referenceChildren } = useMemo(() => { + const children = childrenMap.get(variable.reference) ?? []; + + const primitiveChildren: Variable[] = []; + const referenceChildren: Variable[] = []; + + for (const child of children) { + if (child.reference === 0) { + primitiveChildren.push(child); + } else { + referenceChildren.push(child); + } + } - const primitiveChildren = allVariables.filter(v => v.parent === variable.reference && v.reference === 0); - const referenceChildren = allVariables.filter(v => v.parent === variable.reference && v.reference > 0); + return { primitiveChildren, referenceChildren }; - const xOffset = 150; + }, [childrenMap, variable.reference]); + + const xOffset = 250; const yOffset = 100; return ( <> - + - {variable.name} : [{variable.type}] - {primitiveChildren.map(child => ( - - ))} + + + {variable.name} + + + {variable.type} + + + + + + + {primitiveChildren.map(child => ( + + ))} + @@ -116,14 +182,12 @@ const HeapNode = ({ variable, allVariables, initialPosition }: { ]; return ( - - ); }) } @@ -132,11 +196,25 @@ const HeapNode = ({ variable, allVariables, initialPosition }: { } const Visualizer = ({ thread }: { thread: ThreadInfo }) => { - const allVariables = thread.stack_frames.flatMap(frame => - frame.scopes.flatMap(scope => scope.variables) - ); + const allVariables = useMemo(() => + thread.stack_frames.flatMap(frame => + frame.scopes.flatMap(scope => scope.variables) + ), [thread.stack_frames]); + + const { rootHeapVariables, childrenMap } = useMemo(() => { + const childrenMap = new Map; - const heapVariables = allVariables.filter((variable) => variable.reference > 0 && variable.parent === null); + for (const variable of allVariables) { + const children = childrenMap.get(variable.parent) ?? []; + children.push(variable); + childrenMap.set(variable.parent, children); + } + + const rootVariables = childrenMap.get(null) ?? []; + const rootHeapVariables = rootVariables.filter(v => v.reference > 0); + + return { rootHeapVariables, childrenMap }; + }, [allVariables]); return ( <> @@ -149,14 +227,16 @@ const Visualizer = ({ thread }: { thread: ThreadInfo }) => { - {heapVariables.map((variable, index) => ( - - ))} + + {rootHeapVariables.map((variable, index) => ( + + ))} +