diff --git a/src/components/tools/drawing-manager.js b/src/components/tools/drawing-manager.js
new file mode 100644
index 0000000000000000000000000000000000000000..cfc1754a75cb2aae5f7754247dd5ae163eeae6c4
--- /dev/null
+++ b/src/components/tools/drawing-manager.js
@@ -0,0 +1,20 @@
+/**
+ * Drawing Manager
+ * @component drawing-manager
+ */
+AFRAME.registerComponent("drawing-manager", {
+  schema: {
+  },
+
+  init() {
+  	this.points = [];
+  },
+
+  play() {
+
+  },
+
+  pause() {
+
+  }
+}
\ No newline at end of file
diff --git a/src/components/tools/networked-drawing.js b/src/components/tools/networked-drawing.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ff84444dde720d2bd6831ef031621e012b28478
--- /dev/null
+++ b/src/components/tools/networked-drawing.js
@@ -0,0 +1,57 @@
+/* global THREE */
+/**
+ * Networked Drawing
+ * @component networked-drawing
+ */
+AFRAME.registerComponent("networked-drawing", {
+  schema: {},
+
+  init() {
+    this.points = [];
+
+    var sampleClosedSpline = new THREE.CatmullRomCurve3([
+      new THREE.Vector3(0, -40, -40),
+      new THREE.Vector3(0, 40, -40),
+      new THREE.Vector3(0, 140, -40),
+      new THREE.Vector3(0, 40, 40),
+      new THREE.Vector3(0, -40, 40)
+    ]);
+
+    var params = {
+      scale: 0.02,
+      extrusionSegments: 100,
+      radiusSegments: 3,
+      closed: true,
+      animationView: false,
+      lookAhead: false,
+      cameraHelper: false
+    };
+
+    var geometry = new THREE.TubeBufferGeometry(
+      sampleClosedSpline,
+      params.extrusionSegments,
+      2,
+      params.radiusSegments,
+      params.closed
+    );
+
+    var wireframeMaterial = new THREE.MeshBasicMaterial({
+      color: 0x000000,
+      opacity: 0.3,
+      wireframe: true,
+      transparent: true
+    });
+    var material = new THREE.MeshLambertMaterial({ color: 0xff00ff });
+    var mesh = new THREE.Mesh(geometry, material);
+
+    var wireframe = new THREE.Mesh(geometry, wireframeMaterial);
+    mesh.add(wireframe);
+    mesh.scale.set(params.scale, params.scale, params.scale);
+
+    this.el.object3D.add(mesh);
+  },
+
+  play() {},
+
+  pause() {}
+});
diff --git a/src/components/tools/pen.js b/src/components/tools/pen.js
new file mode 100644
index 0000000000000000000000000000000000000000..52b698edd2adfc83d9fb2753272c55897e842b25
--- /dev/null
+++ b/src/components/tools/pen.js
@@ -0,0 +1,186 @@
+/**
+ * Pen tool
+ * @component pen
+ */
+
+import SharedBufferGeometryManager from "../../vendor/sharedbuffergeometrymanager";
+
+AFRAME.registerComponent("pen", {
+  schema: {
+    drawFrequency: { default: 1000 },
+    drawPoints: { default: [] },
+    minDistanceBetweenPoints: { default: 0.05 },
+    segments: { default: 3 },
+    radius: { default: 0.05 },
+    debug: { default: true }
+  },
+
+  init() {
+    this.onMouseDown = this.onMouseDown.bind(this);
+    this.onMouseUp = this.onMouseUp.bind(this);
+
+    let material = new THREE.MeshBasicMaterial({ side: THREE.DoubleSide, color: 0xff0000 });
+
+    this.sharedBufferGeometryManager = new SharedBufferGeometryManager();
+    this.sharedBufferGeometryManager.addSharedBuffer(0, material, THREE.TriangleStripDrawMode);
+
+    this.isDrawing = false;
+    this.timeSinceLastDraw = 0;
+
+    this.lastPosition = new THREE.Vector3();
+    this.lastPositionSet = false;
+    this.lastSegmentsSet = false;
+    this.firstVertex = true;
+    this.lastSegments = [];
+    this.currentSegments = [];
+    for (var x = 0; x < this.data.segments; x++) {
+      this.lastSegments[x] = new THREE.Vector3();
+      this.currentSegments[x] = new THREE.Vector3();
+    }
+
+    this.sharedBuffer = this.sharedBufferGeometryManager.getSharedBuffer(0);
+    this.drawing = this.sharedBuffer.getDrawing();
+    let sceneEl = document.querySelector("a-scene");
+    this.scene = sceneEl.object3D;
+    this.scene.add(this.drawing);
+
+    if (this.data.debug) {
+      this.debugGeometry = new THREE.SphereGeometry(0.005, 32, 32);
+      this.debugMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
+    }
+  },
+
+  play() {
+    document.addEventListener("mousedown", this.onMouseDown);
+    document.addEventListener("mouseup", this.onMouseUp);
+  },
+
+  pause() {
+    document.removeEventListener("mousedown", this.onMouseDown);
+    document.removeEventListener("mouseup", this.onMouseUp);
+  },
+
+  tick: (() => {
+    return function(t, dt) {
+      if (this.isDrawing && this.timeSinceLastDraw + dt >= this.data.drawFrequency) {
+        this.addPoint(this.el.object3D.position);
+        this.sharedBuffer.update();
+      }
+
+      this.timeSinceLastDraw = (this.timeSinceLastDraw + dt) % this.data.drawFrequency;
+    };
+  })(),
+
+  onMouseDown(e) {
+    if (e.button === 0) {
+      this.isDrawing = true;
+      this.restart();
+    }
+  },
+
+  onMouseUp(e) {
+    if (e.button === 0) {
+      this.isDrawing = false;
+    }
+  },
+
+  restart() {
+    this.sharedBuffer.restartPrimitive();
+
+    //restart the draw (readd the last vertex) if this drawing has already been started
+    if (!this.firstVertex) {
+      this.restartDraw = true;
+    }
+    this.lastPositionSet = false;
+    this.lastSegmentsSet = false;
+  },
+
+  addSegments(segmentsList, point, forward, up) {
+    const angleIncrement = Math.PI * 2 / this.data.segments;
+    for (let i = 0; i < this.data.segments; i++) {
+      const segment = segmentsList[i];
+
+      this.rotatePointAroundAxis(segment, point, forward, up, angleIncrement * i, this.data.radius);
+
+      if (this.data.debug) {
+        const sphere = new THREE.Mesh(this.debugGeometry, this.debugMaterial);
+        this.scene.add(sphere);
+        sphere.position.copy(segment);
+      }
+    }
+  },
+
+  addVertex(point) {
+    this.firstVertex = false;
+    this.sharedBuffer.addVertex(point.x, point.y, point.z);
+  },
+
+  addPoint: (() => {
+    const forward = new THREE.Vector3();
+    return function(position) {
+      if (this.lastPositionSet) {
+        //don't draw if distance from last point is not far enough
+        const distance = position.distanceTo(this.lastPosition);
+        if (distance >= this.data.minDistanceBetweenPoints) {
+          //calculate forward only if I have lastPositionSet
+          forward.subVectors(position, this.lastPosition).normalize();
+
+          //if I don't have the lastSegments yet, add them now
+          if (!this.lastSegmentsSet) {
+            this.addSegments(this.lastSegments, this.lastPosition, forward, THREE.Object3D.DefaultUp);
+            this.lastSegmentsSet = true;
+          }
+
+          //add currentSegments
+          this.addSegments(this.currentSegments, position, forward, THREE.Object3D.DefaultUp);
+
+          //add the first vertex of the currentSegment if this is a restartDraw
+          if (this.restartDraw) {
+            this.addVertex(this.lastSegments[0]);
+            this.restartDraw = false;
+          }
+
+          //draw the triangle strip
+          // this.printSegments();
+          for (var j = 0; j <= this.data.segments; j++) {
+            this.addVertex(this.lastSegments[j % this.data.segments]);
+            this.addVertex(this.currentSegments[j % this.data.segments]);
+          }
+
+          //copy the currentSegments to lastSegments
+          for (var j = 0; j < this.data.segments; j++) {
+            this.lastSegments[j].copy(this.currentSegments[j]);
+          }
+
+          this.lastPosition.copy(position);
+        }
+      } else {
+        this.lastPosition.copy(position);
+        this.lastPositionSet = true;
+      }
+    };
+  })(),
+
+  rotatePointAroundAxis: (() => {
+    const calculatedDirection = new THREE.Vector3();
+    return function(out, point, axis, up, angle, radius) {
+      calculatedDirection.copy(up);
+      calculatedDirection.applyAxisAngle(axis, angle);
+      out.copy(point).add(calculatedDirection.normalize().multiplyScalar(radius));
+    };
+  })(),
+
+  printSegments() {
+    console.group("lastSegments");
+    for (var i = 0; i < this.data.segments; i++) {
+      console.log(this.lastSegments[i].x, this.lastSegments[i].y, this.lastSegments[i].z);
+    }
+    console.groupEnd();
+
+    console.group("currentSegments");
+    for (var j = 0; j < this.data.segments; j++) {
+      console.log(this.currentSegments[j].x, this.currentSegments[j].y, this.currentSegments[j].z);
+    }
+    console.groupEnd();
+  }
+});
diff --git a/src/hub.html b/src/hub.html
index d54fac0939d964a1f6fe9ceca816eebb4ba8b7cc..bf421fc71224821f5e63e7f348580853463c207a 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -231,6 +231,7 @@
             segments-height="9"
             segments-width="9"
             event-repeater="events: raycaster-intersection, raycaster-intersection-cleared; eventSource: #cursor-controller"
+            pen
         ></a-sphere>
 
         <!-- Player Rig -->
@@ -370,6 +371,11 @@
             nav-mesh-helper
             static-body="shape: none;"
         ></a-entity>
+
+<!--         <a-entity
+            position="0 2 0"
+            networked-drawing
+        ></a-entity> -->
     </a-scene>
 
     <div id="ui-root"></div>
diff --git a/src/hub.js b/src/hub.js
index aeb047d7a74e4325aebf1d113d4c2df866923432..4a3cf673e67524cdb3bb9ea2ab4620f63933fa56 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -127,6 +127,9 @@ import registerTelemetry from "./telemetry";
 import { getAvailableVREntryTypes, VR_DEVICE_AVAILABILITY } from "./utils/vr-caps-detect.js";
 import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js";
 
+import "./components/tools/pen";
+// import "./components/tools/networked-drawing";
+
 function qsTruthy(param) {
   const val = qs[param];
   // if the param exists but is not set (e.g. "?foo&bar"), its value is null.
diff --git a/src/vendor/sharedbuffergeometry.js b/src/vendor/sharedbuffergeometry.js
new file mode 100644
index 0000000000000000000000000000000000000000..343a77b078aa03dab2ae4154408dbc42fd1167bd
--- /dev/null
+++ b/src/vendor/sharedbuffergeometry.js
@@ -0,0 +1,154 @@
+export default class SharedBufferGeometry {
+
+  constructor(material, primitiveMode) {
+    this.material = material;
+    this.primitiveMode = primitiveMode;
+
+    this.maxBufferSize = 1000000;
+    this.geometries = [];
+    this.current = null;
+    this.drawing = new THREE.Object3D();
+    this.addBuffer(false);
+  }
+
+  getDrawing () {
+    return this.drawing;
+  }
+
+  restartPrimitive () {
+    if (this.idx.position >= this.current.attributes.position.count) {
+      this.addBuffer(false);
+    } else if (this.idx.position !== 0) {
+      let prev = (this.idx.position - 1) * 3;
+      const position = this.current.attributes.position.array;
+      this.addVertex(position[prev++], position[prev++], position[prev++]);
+
+      this.idx.color++;
+      this.idx.normal++;
+      this.idx.uv++;
+    }
+  }
+
+  remove (prevIdx, idx) {
+    const pos = this.current.attributes.position.array;
+
+    // Loop through all the attributes: position, color, uv, normal,...
+    if (this.idx.position > idx.position) {
+      for (let key in this.idx) {
+        const componentSize = key === 'uv' ? 2 : 3;
+        let pos = (prevIdx[key]) * componentSize;
+        const start = (idx[key] + 1) * componentSize;
+        const end = this.idx[key] * componentSize;
+        for (let i = start; i < end; i++) {
+          this.current.attributes[key].array[pos++] = this.current.attributes[key].array[i];
+        }
+      }
+    }
+
+    for (key in this.idx) {
+      const diff = (idx[key] - prevIdx[key]);
+      this.idx[key] -= diff;
+    }
+
+    this.update();
+  }
+
+  undo (prevIdx) {
+    this.idx = prevIdx;
+    this.update();
+  }
+
+  addBuffer (copyLast) {
+    const geometry = new THREE.BufferGeometry();
+
+    const vertices = new Float32Array(this.maxBufferSize * 3);
+    const normals = new Float32Array(this.maxBufferSize * 3);
+    const uvs = new Float32Array(this.maxBufferSize * 2);
+    const colors = new Float32Array(this.maxBufferSize * 3);
+
+    const mesh = new THREE.Mesh(geometry, this.material);
+
+    mesh.drawMode = this.primitiveMode;
+
+    mesh.frustumCulled = false;
+    mesh.vertices = vertices;
+
+    this.object3D = new THREE.Object3D();
+    this.drawing.add(this.object3D);
+    this.object3D.add(mesh);
+
+    geometry.setDrawRange(0, 0);
+    geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3).setDynamic(true));
+    geometry.addAttribute('uv', new THREE.BufferAttribute(uvs, 2).setDynamic(true));
+    geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3).setDynamic(true));
+    geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3).setDynamic(true));
+
+
+    this.previous = null;
+    if (this.geometries.length > 0) {
+      this.previous = this.current;
+    }
+
+    this.idx = {
+      position: 0,
+      uv: 0,
+      normal: 0,
+      color: 0
+    };
+
+    this.geometries.push(geometry);
+    this.current = geometry;
+
+    if (this.previous && copyLast) {
+      let prev = (this.maxBufferSize - 2) * 3;
+      let col = (this.maxBufferSize - 2) * 3;
+      const uv = (this.maxBufferSize - 2) * 2;
+      let norm = (this.maxBufferSize - 2) * 3;
+
+      const position = this.previous.attributes.position.array;
+      this.addVertex(position[prev++], position[prev++], position[prev++]);
+      this.addVertex(position[prev++], position[prev++], position[prev++]);
+
+      const normal = this.previous.attributes.normal.array;
+      this.addNormal(normal[norm++], normal[norm++], normal[norm++]);
+      this.addNormal(normal[norm++], normal[norm++], normal[norm++]);
+
+      const color = this.previous.attributes.color.array;
+      this.addColor(color[col++], color[col++], color[col++]);
+      this.addColor(color[col++], color[col++], color[col++]);
+
+      const uvs = this.previous.attributes.uv.array;
+
+    }
+  }
+
+  addColor (r, g, b) {
+    this.current.attributes.color.setXYZ(this.idx.color++, r, g, b);
+  }
+
+  addNormal (x, y, z) {
+    this.current.attributes.normal.setXYZ(this.idx.normal++, x, y, z);
+  }
+
+  addVertex (x, y, z) {
+    let buffer = this.current.attributes.position;
+    if (this.idx.position === buffer.count) {
+      this.addBuffer(true);
+      buffer = this.current.attributes.position;
+    }
+    buffer.setXYZ(this.idx.position++, x, y, z);
+  }
+
+  addUV (u, v) {
+    this.current.attributes.uv.setXY(this.idx.uv++, u, v);
+  }
+
+  update () {
+    this.current.setDrawRange(0, this.idx.position);
+
+    this.current.attributes.color.needsUpdate = true;
+    this.current.attributes.normal.needsUpdate = true;
+    this.current.attributes.position.needsUpdate = true;
+    this.current.attributes.uv.needsUpdate = true;
+  }
+};
diff --git a/src/vendor/sharedbuffergeometrymanager.js b/src/vendor/sharedbuffergeometrymanager.js
new file mode 100644
index 0000000000000000000000000000000000000000..555906ac85a2f5c980e141a61d11e49aeb98a1dc
--- /dev/null
+++ b/src/vendor/sharedbuffergeometrymanager.js
@@ -0,0 +1,16 @@
+import SharedBufferGeometry from "./sharedbuffergeometry";
+
+export default class SharedBufferGeometryManager {
+  constructor() {
+  	this.sharedBuffers = {};
+  }
+
+  addSharedBuffer(name, material, primitiveMode) {
+    this.sharedBuffers[name]  = new SharedBufferGeometry(material, primitiveMode);
+  }
+
+  getSharedBuffer(name) {
+    return this.sharedBuffers[name];
+  }
+}
+