Skip to content

Commit

Permalink
Added networking to the whiteboard
Browse files Browse the repository at this point in the history
Remotes have a list of IPFS URLs that they get from master, and that's kept up to date as more strokes are added. The just send those down in their scene graph.

When a remote experiences a new stroke it sends it to master for processing and then discards it.

When master gets a new stroke, including from a remote, it makes a model out of that stroke and adds it to IPFS. Then it updates its own stroke URL list and all remotes with the new stroke.

Also fixed a failure that happened with strokes that only have one point in them. Those are just spheres now.
  • Loading branch information
JoeLudwig committed Jun 10, 2020
1 parent 5f28d3c commit d336940
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 36 deletions.
61 changes: 42 additions & 19 deletions websrc/whiteboard/src/stroke.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { AvVector } from '@aardvarkxr/aardvark-shared';
import { translateMat } from '@aardvarkxr/aardvark-react';
import { vec2, vec3 } from '@tlaukkan/tsm';
import Color from 'color';
import { exportGLB, Node, GLTFAsset, Material, Mesh as GltfMesh, Vertex, Scene } from "gltf-js-utils";
import { CatmullRomCurve3, ExtrudeGeometry, ExtrudeGeometryOptions, Shape, Vector3 } from 'three';
import { CatmullRomCurve3, ExtrudeGeometry, ExtrudeGeometryOptions, Shape, Vector3, Geometry, SphereGeometry, Matrix4 } from 'three';

export interface Stroke
{
id: number;
thickness: number;
points: vec2[];
points?: vec2[];
networkPoints?: AvVector[];
color: string;
}

Expand Down Expand Up @@ -108,28 +111,48 @@ function makeCubeMesh( center: vec3, dims: vec3 )
export function strokeToGlb( stroke: Stroke )
{
//Create an array of three.js points for the path
let threePoints = stroke.points.map( ( p ) => new Vector3( p.x, p.y, 0 ) );
var curve = new CatmullRomCurve3( threePoints, false, "chordal", 0.1 );

// make a nice circle shape
const k_circleSegments = 16;
var circle = new Shape();
circle.moveTo( 0, stroke.thickness/2 );
for( let theta = Math.PI/ k_circleSegments; theta <= Math.PI * 2; theta += Math.PI/ k_circleSegments )
let threePoints:Vector3[];
if( stroke.points )
{
circle.lineTo(
Math.sin( theta ) * stroke.thickness / 2,
Math.cos( theta ) * stroke.thickness / 2,
);
threePoints = stroke.points.map( ( p ) => new Vector3( p.x, p.y, 0 ) );
}
else
{
threePoints = stroke.networkPoints.map( ( p ) => new Vector3( p.x, p.y, 0 ) );
}

let extrudeSettings:ExtrudeGeometryOptions =
let geometry: Geometry = null;
if( threePoints.length == 1 )
{
steps: stroke.points.length * 2,
extrudePath: curve,
};
geometry = new SphereGeometry( stroke.thickness/2, 10, 10 );
let translateMat = new Matrix4();
translateMat.makeTranslation( threePoints[0].x, threePoints[0].y, threePoints[0].z );
geometry.applyMatrix4( translateMat );
}
else
{
var curve = new CatmullRomCurve3( threePoints, false, "chordal", 0.1 );

// make a nice circle shape
const k_circleSegments = 16;
var circle = new Shape();
circle.moveTo( 0, stroke.thickness/2 );
for( let theta = Math.PI/ k_circleSegments; theta <= Math.PI * 2; theta += Math.PI/ k_circleSegments )
{
circle.lineTo(
Math.sin( theta ) * stroke.thickness / 2,
Math.cos( theta ) * stroke.thickness / 2,
);
}

let geometry = new ExtrudeGeometry( circle, extrudeSettings );
let extrudeSettings:ExtrudeGeometryOptions =
{
steps: threePoints.length * 2,
extrudePath: curve,
};

geometry = new ExtrudeGeometry( circle, extrudeSettings );
}

let asset = new GLTFAsset();
let mesh = new GltfMesh();
Expand Down
131 changes: 114 additions & 17 deletions websrc/whiteboard/src/whiteboard_main.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ActiveInterface, AvComposedEntity, AvGadget, AvInterfaceEntity, AvLine, AvPrimitive, AvStandardGrabbable, AvTransform, MoveableComponent, PrimitiveType, PrimitiveYOrigin, PrimitiveZOrigin, AvModel } from '@aardvarkxr/aardvark-react';
import { AvNodeTransform, AvVolume, endpointAddrToString, EVolumeType, g_builtinModelBox } from '@aardvarkxr/aardvark-shared';
import { ActiveInterface, AvComposedEntity, AvGadget, AvInterfaceEntity, AvLine, AvModel, AvPrimitive, AvStandardGrabbable, AvTransform, MoveableComponent, PrimitiveType, PrimitiveYOrigin, PrimitiveZOrigin } from '@aardvarkxr/aardvark-react';
import { AvNodeTransform, AvVector, AvVolume, endpointAddrToString, EVolumeType, g_builtinModelBox, InitialInterfaceLock } from '@aardvarkxr/aardvark-shared';
import { vec2 } from '@tlaukkan/tsm';
import bind from 'bind-decorator';
import * as IPFS from 'ipfs';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as IPFS from 'ipfs';
import { Stroke, optimizeStroke, strokeToGlb } from './stroke';
import { Stroke, strokeToGlb } from './stroke';

const k_WhiteBoardStateInterface = "whiteboard_state@1";

function vec2FromAvTransformPosition( transform: AvNodeTransform )
{
Expand Down Expand Up @@ -136,11 +137,6 @@ function Marker( props: MarkerProps )
</AvTransform>;
}

interface SurfaceProps
{
addStroke: ( newStroke: Stroke ) => void;
}

let nextStrokeId = 1;

type SurfaceContactDetailsMap = {[endpointAddr: string]: Stroke };
Expand Down Expand Up @@ -183,6 +179,11 @@ function StrokeLines( props: StrokeLineProps )
return <div key={ strokeId }>{ transforms } </div>;
}

interface SurfaceProps
{
addStroke: ( newStroke: Stroke, fromRemote: boolean ) => void;
}

interface SurfaceState
{
strokesInProgress: Stroke[];
Expand Down Expand Up @@ -224,7 +225,7 @@ class Surface extends React.Component<SurfaceProps, SurfaceState>
console.log( `contact from ${ endpointAddrToString( activeContact.peer ) } ended`)
if( stroke && stroke.points.length > 0 )
{
this.props.addStroke( stroke );
this.props.addStroke( stroke, false );
}

if( stroke )
Expand Down Expand Up @@ -302,10 +303,24 @@ interface WhiteboardState
strokes?: string[];
}

interface WhiteboardInterfaceParams
{
strokes?: string[];
}

interface WhiteboardEvent
{
type: "add_stroke" | "stroke_added";
stroke?: Stroke;
strokeUrl?: string;
}


class Whiteboard extends React.Component< {}, WhiteboardState >
{
private nextStrokeId = 0;
private ipfsNode: any = null;
private m_grabbableRef = React.createRef<AvStandardGrabbable>();

constructor( props: any )
{
Expand All @@ -326,30 +341,98 @@ class Whiteboard extends React.Component< {}, WhiteboardState >
}

@bind
private async onAddStroke( newStroke: Stroke )
private onRemoteEvent( event: WhiteboardEvent )
{
// add the stroke to the pending list so we'll keep drawing it while we generate the GLB
this.setState( { pendingStrokes: [...this.state.pendingStrokes, newStroke ] } );
switch( event.type )
{
case "add_stroke":
if( AvGadget.instance().isRemote )
{
console.log( "Received unexpected add_stroke event on remote" );
}
else
{
this.onAddStroke( event.stroke, true );
}
break;

case "stroke_added":
if( !AvGadget.instance().isRemote )
{
console.log( "Received unexpected stroke_added event on master" );
}
else
{
this.setState( { strokes: [ ...this.state.strokes, event.strokeUrl ] } );
}
break;
}
}

@bind
private async onAddStroke( newStroke: Stroke, fromRemote: boolean )
{
if( AvGadget.instance().isRemote )
{
// convert points to AvVector so they'll network
newStroke.networkPoints = newStroke.points.map( ( v: vec2 ) =>
( { x: v.x, y: v.y, z: 0 } as AvVector ) );
delete newStroke.points;
let m: WhiteboardEvent =
{
type: "add_stroke",
stroke: newStroke,
};
this.m_grabbableRef.current.sendRemoteEvent( m, true );
return;
}

if( !fromRemote )
{
// add the stroke to the pending list so we'll keep drawing it while we generate the GLB
this.setState( { pendingStrokes: [...this.state.pendingStrokes, newStroke ] } );
}

let strokeGlb = await strokeToGlb( newStroke );
let ipfsRes = this.ipfsNode.add( new Uint8Array( strokeGlb ) );
for await( let ipfsFile of ipfsRes )
{
console.log( `Stroke added: ${ newStroke.points.length } points `
console.log( `Stroke added: `
+ `${ newStroke.points?.length ?? newStroke.networkPoints?.length } points `
+ `${ strokeGlb.byteLength } bytes. cid=${ipfsFile.cid }` );

let newStrokeUrl = "/ipfs/" + ipfsFile.cid;

let m: WhiteboardEvent =
{
type: "stroke_added",
strokeUrl: newStrokeUrl,
};
this.m_grabbableRef.current.sendRemoteEvent( m, true );

let newPending = [ ...this.state.pendingStrokes ];
newPending.splice( newPending.indexOf( newStroke ), 1 );
if( !fromRemote )
{
newPending.splice( newPending.indexOf( newStroke ), 1 );
}

this.setState(
{
strokes: [...this.state.strokes, "/ipfs/" + ipfsFile.cid ],
strokes: [...this.state.strokes, newStrokeUrl ],
pendingStrokes: newPending,
} );
}
}

public componentDidMount()
{
if( AvGadget.instance().isRemote )
{
let params = AvGadget.instance().findInitialInterface( k_WhiteBoardStateInterface )?.params as
WhiteboardInterfaceParams;

this.setState( { strokes: params.strokes } );
}
}

public componentWillUnmount()
Expand All @@ -368,9 +451,23 @@ class Whiteboard extends React.Component< {}, WhiteboardState >
strokeLines.push( <StrokeLines key={ stroke.id } stroke={ stroke }/> );
}

let remoteInitLocks: InitialInterfaceLock[] = [];
if( !AvGadget.instance().isRemote )
{
remoteInitLocks.push( {
iface: k_WhiteBoardStateInterface,
receiver: null,
params:
{
strokes: this.state.strokes,
}
} );
}

return (
<AvStandardGrabbable modelUri={ g_builtinModelBox } modelScale={ 0.1 }
modelColor="lightblue" useInitialParent={ true } remoteInterfaceLocks={ [] }>
modelColor="lightblue" useInitialParent={ true } remoteInterfaceLocks={ remoteInitLocks }
ref={ this.m_grabbableRef } remoteGadgetCallback={ this.onRemoteEvent } >
<AvTransform translateY={0.2}>
<AvTransform translateZ={ -0.005 }>
<AvPrimitive type={PrimitiveType.Cube} originZ={ PrimitiveZOrigin.Back }
Expand Down

0 comments on commit d336940

Please sign in to comment.