Examples - IRobot1/three-flow-ts GitHub Wiki
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
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.
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.
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.
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.
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);
}
}
The frames example demonstrates using custom nodes with transparent backgrounds, textures and a 3D animated model.
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)
}
}
The podium timeline example demonstrates using custom nodes with labels inside semi-transparent geometry.
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)
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.
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()
}
}
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.
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
}
})