Examples - IRobot1/three-flow-ts GitHub Wiki

Loader

The loader example demonstrates loading diagrams from a JSON file. There are three example JSON diagrams in a drop down list. See How To Load a Diagram from a Server for code to load diagrams

image

This example also uses Three.js Text Geometry for displaying labels. See How To Register a Font for using by Three Flow Diagram to learn how to use custom fonts.

Process

The process example demonstrates creating custom node and edge geometry to create a basic visual for a process flow diagram. The nodes and edges are programmatically added using addNode and addEdge. FlowInteraction is used so the shapes are draggable.

image

A custom ProcessFlowDiagram class extends FlowDiagram to override material, label, node and edge creation.

class ProcessFlowDiagram extends FlowDiagram {

  constructor(options?: FlowDiagramOptions) { super(options) }

  override createMeshMaterial(purpose: string, parameters: MeshBasicMaterialParameters): Material {
    parameters.side = DoubleSide
    return new MeshStandardMaterial(parameters);
  }

  override createLabel(label: FlowLabelParameters): FlowLabel {
    return new TroikaFlowLabel(this, label)
  }

  override createNode(node: ProcessShape): FlowNode {
    return new ProcessNode(this, node)
  }

  override createEdge(edge: FlowEdgeParameters): FlowEdge {
    return new ProcessEdge(this, edge)
  }
}

The ProcessEdge class overrides createGeometry to use Three.js TubeGeometry to render lines and forces the color to orange. Tube Geometry renders lines better when its input curve is a curve path of line curves.

class ProcessEdge extends FlowEdge {
  constructor(diagram: ProcessFlowDiagram, edge: FlowEdgeParameters) {
    edge.material = {color : 'orange'}
    super(diagram, edge)
  }

  override createGeometry(curvepoints: Array<Vector3>, thickness: number): BufferGeometry | undefined {
    const curve = new CurvePath<Vector3>()
    for (let i = 0; i < curvepoints.length - 1; i++) {
      curve.add(new LineCurve3(curvepoints[i], curvepoints[i + 1]))
    }
    return new TubeGeometry(curve, 64, thickness)
  }
}

The Process Node class overrides createGeometry to return specific shapes depending on the Process Shape parameters. It also uses Flow Diagram to get a shared material with its purpose being border to add a border behind the nodes shape.

type ProcessShapeType = 'circle' | 'rhombus' | 'rect' | 'parallel'
interface ProcessShape extends FlowNodeParameters {
  shape: ProcessShapeType
}

class ProcessNode extends FlowNode {
  constructor(diagram: ProcessFlowDiagram, parameters: ProcessShape) {
    parameters.material = {color : '#018083'}
    super(diagram, parameters)

    const geometry = this.makeGeometry(parameters.shape, parameters.width! + 0.05, parameters.height! + 0.05)
    const mesh = new Mesh(geometry, diagram.getMaterial('geometry', 'border', 'white'))
    mesh.position.z = -0.001
    mesh.castShadow = true
    this.add(mesh)
  }

  override createGeometry(parameters: ProcessShape): BufferGeometry {
    return this.makeGeometry(parameters.shape, parameters.width!, parameters.height!)
  }

  private makeGeometry(shape: ProcessShapeType, width: number, height: number): BufferGeometry {
    let geometry: BufferGeometry
    switch (shape) {
      case 'circle':
        geometry = new CircleGeometry(width / 2)
        break;
      case 'rect':
        geometry = new ShapeGeometry(this.rectangularShape(width, height, 0.1))
        break;
      case 'rhombus':
        geometry = new ShapeGeometry(this.rhombusShape(width, height))
        break;
      case 'parallel':
        geometry = new ShapeGeometry(this.parallelogramShape(width, height))
        break;
    }
    return geometry
  }

 // see example code for methods to generate specific shapes
}

With shadows and a spot light, the result looks pretty good.

Pop Out

The popout example demonstrates using custom nodes and a mix of flat and extruded geometry to create a basic organizational diagram. The nodes and edges are programmatically added using addNode and addEdge. FlowConnectors is used to organize multiple edges connecting to summary nodes. FlowInteraction is used so the shapes are draggable.

image

The flow diagram is a child of a background plane and set slightly forward to avoid z-fighting. The default edge line style is set to split to allow breaks for edges that are not aligned.

const background = new Mesh(new PlaneGeometry(20, 10), new MeshStandardMaterial())
background.receiveShadow = background.castShadow = true
scene.add(background)

const flow = new PopoutFlowDiagram({ linestyle: 'split', gridsize: 0.1 })
background.add(flow);
flow.position.z = 0.1

The custom PopoutFlowDiagram class creates different nodes depending on the shape. Popout shape includes additional parameters for controlling the extrude geometry settings.

type PopoutShapeType = 'circle' | 'stadium'

interface PopoutShape extends FlowNodeParameters {
  shape: PopoutShapeType
  extruderadius: number
  extrudedepth: number
  extrudecolor: string
  icon: string
}

class PopoutFlowDiagram extends FlowDiagram {
  constructor(options?: FlowDiagramOptions) { super(options) }

  override createNode(node: PopoutShape): FlowNode {
    if (node.shape == 'circle')
      return new PopoutCircleNode(this, node)
    else
      return new PopoutStadiumNode(this, node)
  }
}

PopoutCircleNode renders a flat circle, an extruded circle on top and an icon using Troika label

class PopoutCircleNode extends FlowNode {
  constructor(diagram: PopoutFlowDiagram, parameters: PopoutShape) {
    super(diagram, parameters)

    const mesh = new Mesh(this.createCircle(parameters), diagram.getMaterial('geometry', 'border', parameters.extrudecolor))
    mesh.position.z = 0.001
    mesh.castShadow = true
    this.add(mesh)

    if (parameters.icon) {
      const iconparams = <FlowLabelParameters>{ text: parameters.icon, isicon: true, size: 0.3, material: {color: 'white'} }
      const icon = diagram.createLabel(iconparams)
      icon.position.set(0, 0.15, 0.051)
      icon.updateLabel()

      this.add(icon)
    }
  }

  override createGeometry(parameters: PopoutShape): BufferGeometry {
    return new CircleGeometry(this.width / 2, 64)
  }

  createCircle(parameters: PopoutShape): BufferGeometry {
    const circleShape = new Shape();
    const radius = parameters.extruderadius; // radius of the circle
    circleShape.absarc(0, 0, radius, 0, Math.PI * 2, false);

    // Define extrusion settings
    const extrudeSettings = <ExtrudeGeometryOptions>{
      curveSegments: 64,
      depth: parameters.extrudedepth, // extrusion depth
      bevelEnabled: false // no bevel
    };

    return new ExtrudeGeometry(circleShape, extrudeSettings);
  }
}

Frames

The frames example demonstrates using custom nodes with transparent backgrounds, textures and a 3D animated model.

image

The frame around each node is created by drawing a rounded rectangle shape, then using a slightly smaller copy to create a hole inside it.

private addBorder(): BufferGeometry {
  // add a border around node
  const shape = this.rectangularShape(this.width, this.height, 0.1)

  const points = shape.getPoints();
  points.forEach(item => item.multiplyScalar(0.95))

  // draw the hole
  const holePath = new Shape(points.reverse())

  // add hole to shape
  shape.holes.push(holePath);
  return new ShapeGeometry(shape);
}

Since a frame has no geometry in the center, it can only be dragged or selected when over the frame. To work around this, so clicking anywhere inside support selecting and dragging, the basic plane mesh created by the library is made completely transparent.

    const material = this.material as MeshBasicMaterial
    material.transparent = true
    material.opacity = 0

The model node is a basic torus knot geometry with a timer to rotate it.

class MeshFrameNode extends FramesNode {
  constructor(diagram: FramesFlowDiagram, parameters: FrameShape) {
    super(diagram, parameters)

    const geometry = new TorusKnotGeometry(10, 3, 100, 16);
    const material = new MeshBasicMaterial({ color: 'purple' });
    const torusKnot = new Mesh(geometry, material);
    torusKnot.scale.setScalar(0.02)
    this.add(torusKnot);

    setInterval(() => {
      torusKnot.rotation.y += 0.03
    }, 1000 / 30)
  }
}

Podium Timeline

The podium timeline example demonstrates using custom nodes with labels inside semi-transparent geometry.

image

The transparent sphere sets depthWrite to false in the material to allow the inside to be visible at all angles.

// partial transparent sphere
const sphere = new Mesh()
sphere.material = diagram.getMaterial('geometry', 'sphere',
  <MeshBasicMaterialParameters>{
    color: '#FFD301', transparent: true, opacity: 0.3, depthWrite: false
  })
sphere.position.set(0, 0, 0.55)
sphere.castShadow = true
this.add(sphere)

Banner

The banner example demonstrates using custom nodes that create a child node and edge. For this to work, the node must finish being created before the child node and edge can be added. A simple way to delay is to create inside an requestAnimationFrame. This results in a single frame (1/60th of a second) delay.

image

requestAnimationFrame(() => {

  const from = parameters.connectors![0].id
  const pin = from + '-pin'

  const nodeparams = <FlowNodeParameters>{
    id: pin, x: parameters.x, y: -this.height / 2 - 0.5,
    width: 0.3, height: 0.3, lockaspectratio: true,
    connectors: [
      { id: pin + '-top', anchor: 'top', hidden: true },
    ]
  }
  const node = diagram.addNode(nodeparams)
  node.material = bannermaterial
  node.material.side = DoubleSide

  const edgeparams = <FlowEdgeParameters>{
    from: parameters.id, to: pin,
    fromconnector: from, toconnector: pin + '-top',
    material: { color: 'black' }
  }

diagram.addEdge(edgeparams)
})

The resizeGeometry override is also used to update the geometry and positions of child meshes when the nodes width or height changes

this.resizeGeometry = () => {
  mesh1.geometry = new PlaneGeometry(this.width, titleborderheight)
  mesh1.position.set(0, (this.height - titleborderheight) / 2, 0.001)

  mesh2.geometry = this.bannerGeometry(bannerheight, bannerdentsize)
  mesh2.position.set(0, (-this.height + bannerheight) / 2, 0.001)

  if (subtitle.wrapwidth != this.width - 0.2) {
    subtitle.wrapwidth = this.width - 0.2
    subtitle.updateLabel()
  }
}

Live Data

The live data example shows a diagram of hardware resources connected in a network and their current status. It demonstrates updating data inside each node based on external data changing.

image

A simple timer is used to simulate random data changes within ranges reasonable for each metric. A message is dispatched to each node to notify new data is available.

setInterval(() => {
  flow.allNodes.forEach(node => {
    const parameters = node.node as ComputerParameters

    // Randomly decide if each attribute should be updated
    if (parameters.cpu_usage != undefined && Math.random() > 0.5) {
      parameters.cpu_usage = MathUtils.clamp(parameters.cpu_usage! + Math.floor(-10 + Math.random() * 20), 0, 100)
    }
    if (parameters.memory_usage != undefined && Math.random() > 0.5) {
      parameters.memory_usage = MathUtils.clamp(parameters.memory_usage! + Math.floor(-20 + Math.random() * 40), 1, 8192)
    }
    if (parameters.disk_usage != undefined && Math.random() > 0.5) {
      parameters.disk_usage = MathUtils.clamp(parameters.disk_usage! + Math.floor(-0.1 + Math.random() * 0.2), 0, 128)
    }

    node.dispatchEvent<any>({ type: 'update-data' })
  })
}, 5000);

Each node listens for this message and updates the text when its displayed and has changed

this.addEventListener('update-data', () => {
  if (cpu_label && parameters.cpu_usage != last_cpu_usage) {
    cpu_label.text = `CPU: ${parameters.cpu_usage}%`
    last_cpu_usage = parameters.cpu_usage
  }
  if (memory_label && parameters.memory_usage != last_memory_usage) {
    memory_label.text = `Memory: ${parameters.memory_usage} MB`;
    last_memory_usage = parameters.memory_usage
  }
  if (disk_label && parameters.disk_usage != last_disk_usage) {
    disk_label.text = `Disk: ${parameters.disk_usage} GB`;
    last_disk_usage = parameters.disk_usage
  }
})
⚠️ **GitHub.com Fallback** ⚠️