Skip to content
Snippets Groups Projects
renderer.tsx 12.4 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";
    import Graph, { Coordinate, LinkData, NodeData } from "./graph";
    
    // import { graph } from "../editor/js/editor";
    
    export interface GraphNode extends NodeData {
        x: number;
        y: number;
        z: number;
        vx: number;
        vy: number;
        vz: number;
        fx: number;
        fy: number;
        fz: number;
        color: string;
        __threeObj: THREE.Group;
    }
    
    export interface GraphLink extends LinkData {
        __lineObj?: Line2;
    }
    
    
    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>;
    
        highlightedNodes: Set<GraphNode>;
        highlightedLinks: Set<GraphLink>;
        hoverNode: GraphNode;
    
    
        static propTypes = {
    
            graph: PropTypes.instanceOf(Graph).isRequired,
    
            loadingFinishedCallback: PropTypes.func,
            onNodeClicked: PropTypes.func,
    
            isFullscreen: PropTypes.bool,
    
        static stateTypes = {};
    
    
        constructor(props: InferType<typeof GraphRenderer.propTypes>) {
            super(props);
    
    
            this.highlightedNodes = new Set();
            this.highlightedLinks = new Set();
            this.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 group = new THREE.Group();
    
            const text = new SpriteText(node.name);
            text.color = "white";
            text.backgroundColor = "black"; // TODO: Set this dynamically based on the node type
            text.textHeight = 5;
            // text.padding = 2;
            text.borderRadius = 5;
            text.borderWidth = 3;
            text.borderColor = "black";
            text.translateY(12);
            text.material.opacity = 0.85;
    
            text.renderOrder = 999;
    
            group.add(text);
    
            // Draw node circle image
            const textureLoader = new THREE.TextureLoader();
            textureLoader.setCrossOrigin("anonymous");
            const imageAlpha = textureLoader.load(
                Config.PLUGIN_PATH + "datasets/images/alpha.png"
            );
    
            const material = new THREE.SpriteMaterial({
                //map: imageTexture,
                color: this.nodeColors.get(node.type),
                alphaMap: imageAlpha,
                transparent: true,
                alphaTest: 0.2,
                depthWrite: false,
                depthTest: false,
            });
    
            const sprite = new THREE.Sprite(material);
    
            //sprite.renderOrder = 999; // This may not be optimal. But it allows us to render the sprite on top of everything else.
    
            sprite.scale.set(8, 8, 8);
    
            group.add(sprite);
    
            return group;
    
        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.highlightedNodes.size) ||
                (node && this.hoverNode === node)
    
            const highlightedNodes = new Set<GraphNode>();
            const highlightedLinks = new Set<GraphLink>();
    
                highlightedNodes.add(node);
                node.neighbors.forEach((neighbor) =>
                    highlightedNodes.add(neighbor as GraphNode)
                );
                node.links.forEach((link) =>
                    highlightedLinks.add(link as GraphLink)
                );
    
            this.hoverNode = node || null;
            this.updateHighlight(highlightedNodes, highlightedLinks);
    
        onLinkHover(link: GraphLink) {
            const highlightedNodes = new Set<GraphNode>();
            const highlightedLinks = new Set<GraphLink>();
    
            if (link && link.__lineObj) {
                highlightedLinks.add(link);
                highlightedNodes.add(link.source as GraphNode);
                highlightedNodes.add(link.target as GraphNode);
    
            this.updateHighlight(highlightedNodes, highlightedLinks);
        }
    
        updateHighlight(
            highlightedNodes: Set<GraphNode>,
            highlightedLinks: Set<GraphLink>
        ) {
            // Update Links
            this.highlightedLinks.forEach(
                (link) => (link.__lineObj.material.linewidth = Config.LINK_WIDTH)
            );
            this.highlightedLinks = highlightedLinks;
            this.highlightedLinks.forEach(
                (link) =>
                    (link.__lineObj.material.linewidth = Config.HOVER_LINK_WIDTH)
            );
    
            // Update Nodes
            this.highlightedNodes.forEach((node) => {
                node.__threeObj.children[1].scale.set(8, 8, 8);
            });
            this.highlightedNodes = highlightedNodes;
            this.highlightedNodes.forEach((node) => {
                node.__threeObj.children[1].scale.set(12, 12, 12);
    
            });
        }
    
        onNodeClick(node: GraphNode) {
            this.focusOnNode(node);
    
            if (MODE === "default" && this.props.onNodeClicked) {
    
                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;
            }
    
            // console.log("Updating links");
    
            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
    
            // line.geometry.setFromPoints()
    
    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 downstream? This could be a prop
    
                    height={Helpers.getHeight()}
                    graphData={this.props.graph}
                    rendererConfig={{ antialias: true }}
    
                    // nodeLabel={"hidden"}
    
                    nodeThreeObject={(node: GraphNode) => this.drawNode(node)}
                    linkThreeObject={(link: LinkData) => this.drawLink(link)}
                    onNodeClick={(node: GraphNode) => this.onNodeClick(node)}
    
                    //d3AlphaDecay={0.1}
                    warmupTicks={150}
                    cooldownTime={1000} // TODO: Do we want the simulation to unfreeze on node drag?
    
                    onNodeHover={(node: GraphNode) => this.onNodeHover(node)}
                    onLinkHover={(link: GraphLink) => this.onLinkHover(link)}
    
                    linkPositionUpdate={(
                        line: Line2,
                        coords: { start: Coordinate; end: Coordinate }
                    ) => this.updateLinkPosition(line, coords.start, coords.end)}
    
                    onNodeDragEnd={(node: GraphNode, translate: Coordinate) =>
                        this.onNodeDragEnd(node, translate)
                    }