Skip to content
Snippets Groups Projects
renderer.tsx 10.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • import * as Config from "../config";
    import * as Helpers from "./helpers";
    
    // import { loadGraphJson } from "../datasets";
    
    import { ForceGraph3D } from "react-force-graph";
    
    // import screenfull from "screenfull";
    
    // import {
    //     CSS3DRenderer,
    //     CSS3DSprite,
    // } from "three/examples/jsm/renderers/CSS3DRenderer.js";
    
    import { MODE, DRAG_THRESHOLD_3D } from "../config";
    
    // import background from "./background.jpg";
    
    //import { Line2, LineGeometry, LineMaterial } from "three-fatline";
    
    import { Line2, LineGeometry, LineMaterial } from "three-fatline";
    
    import React from "react";
    import PropTypes, { InferType } from "prop-types";
    import SpriteText from "three-spritetext";
    import { Object3D } from "three";
    
        Coordinate,
        GraphLink,
        GraphNode,
        LinkData,
        NodeData,
    } from "./graph";
    
    // import { graph } from "../editor/js/editor";
    
    
    export class GraphRenderer extends React.Component<
        InferType<typeof GraphRenderer.propTypes>,
        InferType<typeof GraphRenderer.stateTypes>
    > {
        props: InferType<typeof GraphRenderer.propTypes>;
        state: InferType<typeof GraphRenderer.stateTypes>;
        forceGraph: React.RefObject<any>; // using typeof ForceGraph3d produces an error here...
        edgeColors: Map<string, string>;
    
        nodeColors: Map<string, string>;
    
    
        static propTypes = {
    
            graph: PropTypes.instanceOf(Graph).isRequired,
    
            loadingFinishedCallback: PropTypes.func,
            onNodeClicked: PropTypes.func,
    
            isFullscreen: PropTypes.bool,
    
        };
    
        static stateTypes = {
            highlightedNodes: PropTypes.array,
            highlightedLinks: PropTypes.array,
            hoverNode: PropTypes.object,
        };
    
        constructor(props: InferType<typeof GraphRenderer.propTypes>) {
            super(props);
            this.state = {
                highlightedNodes: [],
                highlightedLinks: [],
                hoverNode: null,
            };
            this.forceGraph = React.createRef();
    
            this.edgeColors = new Map<string, string>();
            this.nodeColors = new Map<string, string>();
    
            this.mapLinkColors();
            this.mapNodeColors();
    
            // TODO: NodeVisibility, linkVisibility, graphLoading has to be moved to parent component
        }
    
        // componentDidMount() {
        //     loadGraphJson(this.props.spaceId).then((json) =>
        //         this.updateGraph(json)
        //     );
        // }
    
        // updateGraph(json: { nodes: NodeData[]; links: LinkData[] }) {
        //     const graph = new Graph(json.nodes, json.links);
        //     this.setState({ graph: graph });
        //
        //     this.edgeVisibility = new Map<string, boolean>(
        //         graph.getLinkClasses().map((cls) => [cls, true])
        //     );
        //     this.nodeVisibility = new Map<string, boolean>(
        //         graph.getNodeClasses().map((cls) => [cls, true])
        //     );
        // }
    
        drawNode(node: GraphNode): Object3D {
    
            const sprite = new SpriteText(node.name);
            sprite.color = "white";
            sprite.backgroundColor = "black"; // TODO: Set this dynamically based on the node type
            sprite.textHeight = 5;
            sprite.padding = 2;
            sprite.borderRadius = 5;
            sprite.borderWidth = 3;
            sprite.borderColor = "black";
    
    
        drawLink(link: LinkData) {
            const colors = new Float32Array(
                [].concat(
                    ...[link.target, link.source]
                        .map((node) => this.nodeColors.get(node.type))
                        .map((color) => color.replace(/[^\d,]/g, "").split(",")) // Extract rgb() color components
                        .map((rgb) => rgb.map((v) => parseInt(v) / 255))
                )
            );
    
            const geometry = new LineGeometry();
            geometry.setPositions([0, 0, 0, 1, 1, 1]);
            geometry.setColors(colors);
    
            const material = new LineMaterial({
                color: 0xffffff,
                linewidth: Config.LINK_WIDTH, // in world units with size attenuation, pixels otherwise
                vertexColors: true,
    
                resolution: new THREE.Vector2(
                    window.screen.width,
                    window.screen.height
                ), // Set the resolution to the maximum width and height of the screen.
                dashed: false,
                alphaToCoverage: true,
            });
    
            const line = new Line2(geometry, material);
            line.computeLineDistances();
            line.scale.set(1, 1, 1);
            return line;
        }
    
    
        onNodeHover(node: GraphNode) {
            // no state change
            if (
                (!node && !this.state.highlightNodes.size) ||
                (node && this.state.hoverNode === node)
            )
                return;
    
            const highlightNodes: Set<NodeData> = new Set<NodeData>();
            const highlightLinks: Set<LinkData> = new Set<LinkData>();
    
            if (node) {
                highlightNodes.add(node);
                node.neighbors.forEach((neighbor) => highlightNodes.add(neighbor));
                node.links.forEach((link) => highlightLinks.add(link));
            }
    
            this.setState({
                highlightedNodes: highlightNodes,
                highlightedLinks: highlightLinks,
                hoverNode: node || null,
            });
        }
    
        onLinkHover(link: GraphLink, previousLink: GraphLink) {
            const highlightNodes: Set<NodeData> = new Set<NodeData>();
            const highlightLinks: Set<LinkData> = new Set<LinkData>();
    
            if (previousLink && previousLink.material) {
                // A bit hacky, but the alternative would require additional data structures
                previousLink.material.linewidth = Config.LINK_WIDTH;
            }
    
            if (link && link.material) {
                link.material.linewidth = Config.HOVER_LINK_WIDTH;
    
                highlightLinks.add(link);
                highlightNodes.add(link.source);
                highlightNodes.add(link.target);
            }
    
            this.setState({
                highlightedNodes: highlightNodes,
                highlightedLinks: highlightLinks,
            });
        }
    
        onNodeClick(node: GraphNode) {
            this.focusOnNode(node);
            if (MODE === "default") {
                this.props.onNodeClicked(node);
            }
        }
    
        onNodeDragEnd(node: GraphNode, translate: Coordinate) {
            // NodeDrag is handled like NodeClick if distance is very short
            if (
                Math.sqrt(
                    Math.pow(translate.x, 2) +
                        Math.pow(translate.y, 2) +
                        Math.pow(translate.z, 2)
                ) < DRAG_THRESHOLD_3D
            ) {
                this.onNodeClick(node);
            }
        }
    
        focusOnNode(node: GraphNode) {
            // Aim at node from outside it
    
            const distance = 400;
    
            const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
    
            this.forceGraph.current.cameraPosition(
                {
                    x: node.x * distRatio,
                    y: node.y * distRatio,
                    z: node.z * distRatio,
                }, // new position
                node, // lookAt ({ x, y, z })
                1000 // ms transition duration
            );
        }
    
        getLinkColor(link: LinkData) {
            if ("type" in link) {
                return this.edgeColors.get(link.type);
            }
            return "rgb(255, 255, 255)";
        }
    
    
        getLinkWidth(link: LinkData) {
            return this.state.highlightLinks.has(link) ? 2 : 0.8;
    
        }
    
        /**
         * Maps the colors of the color palette to the different edge types
         */
        mapLinkColors() {
    
            // TODO: Move this to the graph data structure - access is also needed in the other menues?
    
            const linkClasses = this.props.graph.getLinkClasses();
            for (let i = 0; i < linkClasses.length; i++) {
                this.edgeColors.set(
                    linkClasses[i],
                    Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length]
                );
            }
        }
    
    
        /**
         * Maps the colors of the color palette to the different edge types
         */
    
            // TODO: Move this to the graph data structure - access is also needed in the other menues?
            const nodeClasses = this.props.graph.getNodeClasses();
    
            for (let i = 0; i < nodeClasses.length; i++) {
    
                this.nodeColors.set(
                    nodeClasses[i],
                    Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length]
                );
    
        resize() {
            // TODO
            // if (screenfull.isFullscreen) {
            //     this.forceGraph.height(screen.height);
            //     this.forceGraph.width(screen.width);
            // } else {
            //     this.forceGraph.height(window.innerHeight - 200);
            //     this.forceGraph.width(Helpers.getWidth());
            // }
    
        updateLinkPosition(line: Line2, start: Coordinate, end: Coordinate) {
    
            if (!(line instanceof Line2)) {
                return false;
            }
    
    
            const startR = 4;
            const endR = 4;
            const lineLen = Math.sqrt(
    
                Math.pow(end.x - start.x, 2) +
                    Math.pow(end.y - start.y, 2) +
                    Math.pow(end.z - start.z, 2)
    
    Matthias Konitzny's avatar
    Matthias Konitzny committed
            const positions = [startR / lineLen, 1 - endR / lineLen]
    
                        (dim) =>
                            start[dim as keyof typeof start] +
                            (end[dim as keyof typeof end] -
                                start[dim as keyof typeof start]) *
                                t
    
    Matthias Konitzny's avatar
    Matthias Konitzny committed
            line.geometry.setPositions(positions);
    
            // line.geometry.getAttribute("position").needsUpdate = true;
            // line.computeLineDistances();
    
        render() {
            return (
                <ForceGraph3D
                    ref={this.forceGraph}
                    width={Helpers.getWidth()} // TODO: Replace Helpers?
                    height={Helpers.getHeight()}
                    //                extraRenderers={[new CSS3DRenderer()]}
                    graphData={this.props.graph}
                    rendererConfig={{ antialias: true }}
                    nodeLabel={"hidden"}
                    // nodeThreeObjectExtend={false}
                    nodeThreeObject={(node: GraphNode) => this.drawNode(node)}
                    linkThreeObject={(link: LinkData) => this.drawLink(link)}
                    onNodeClick={(node: GraphNode) => this.onNodeClick(node)}
                    // onNodeHover={(node: GraphNode) => this.onNodeHover(node)}
                    // onLinkHover={(link: GraphLink, previousLink: GraphLink) =>
                    //     this.onLinkHover(link, previousLink)
                    // }
                    linkPositionUpdate={(
                        line: Line2,
                        coords: { start: Coordinate; end: Coordinate }
                    ) => this.updateLinkPosition(line, coords.start, coords.end)}
                    // onNodeDragEnd={(node: GraphNode, translate: Coordinate) =>
                    //     this.onNodeDragEnd(node, translate)
                    // }
                />