From 5bc38bd7f07869f3392a8babbda49b75a63cc773 Mon Sep 17 00:00:00 2001
From: LaShonda Hopper <lashonda@1stplayable.com>
Date: Fri, 7 Jul 2017 11:15:42 -0400
Subject: [PATCH] [WL21389] Collision Shapes need to be updated (details
 below).

Revised approach involves creating a helper function within ShapeFactory to aid
in devising the ShapeType to be used by an ShapeEntityItem for collision.  The
ShapeFactory is currently doing this for creating the actual Bullet Library
collision shapes.

ShapeEntityItem overrides its virtually inherited computeShapeInfo which
in turn calls the new ShapeFactory helper function.

ShapeEntityItem has a new memvar _collisionShapeType to cache its actual
ShapeType used by the physics system.  This memvar is returned via the getShapeType
accessor which is expected to return an object's ShapeType.

Note(s):
    This is similar to the original approach save translation between entity::Shape and ShapeType
    isn't tied to the EntityItemProperties shapeTypeNames or shapeType. This approach more
    directly solves the issue of getting the actual ShapeType used by the time it's needed
    to determine the bullet collision object type created when initializing the physic information.

    Translation of the ShapeEntityItem's entity::Shape to its ShapeType is handled by
    ShapeFactory which handles creating the bullet collision objects when setting up
    physics on the ShapeEntityItems.

Known Issue(s):
    This doesn't compile.  It appears that the Entity Library needs to know about
    the Physics Library.  The naive attempt at providing that link failed to resolve
    all compilation issues.

    Current Error:
    C1083: Cannot open include file: btBulletDynamicsCommon.h:
    No such file or directory (C:\projects\cusack\libraries\entities\src\ShapeEntityItem.cpp)
       C:\projects\cusack\libraries\physics\src\ShapeFactory.h	15	1	entities

	modified:   libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
	modified:   libraries/entities/CMakeLists.txt
	modified:   libraries/entities/src/ShapeEntityItem.cpp
	modified:   libraries/entities/src/ShapeEntityItem.h
	modified:   libraries/physics/src/ShapeFactory.cpp
	modified:   libraries/physics/src/ShapeFactory.h
	modified:   libraries/physics/src/ShapeInfo.cpp
	modified:   scripts/developer/tests/basicEntityTest/entitySpawner.js
	new file:   scripts/developer/tests/basicEntityTest/shapeSpawner.js
---
 .../src/RenderableShapeEntityItem.cpp         |  10 +-
 libraries/entities/CMakeLists.txt             |   3 +-
 libraries/entities/src/ShapeEntityItem.cpp    |  54 ++++-
 libraries/entities/src/ShapeEntityItem.h      |   4 +-
 libraries/physics/src/ShapeFactory.cpp        | 203 ++++++++++++++++--
 libraries/physics/src/ShapeFactory.h          |   3 +
 libraries/shared/src/ShapeInfo.cpp            |   5 +-
 .../tests/basicEntityTest/entitySpawner.js    |   4 +-
 .../tests/basicEntityTest/shapeSpawner.js     |  33 +++
 9 files changed, 286 insertions(+), 33 deletions(-)
 create mode 100644 scripts/developer/tests/basicEntityTest/shapeSpawner.js

diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
index 62ab3377a8..b197e0b9e6 100644
--- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
@@ -48,18 +48,22 @@ RenderableShapeEntityItem::Pointer RenderableShapeEntityItem::baseFactory(const
 }
 
 EntityItemPointer RenderableShapeEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) {
-    return baseFactory(entityID, properties);
+	auto result = baseFactory(entityID, properties);
+
+	qCDebug(entities) << "Creating RenderableShapeEntityItem( " << result->_name << " ): " << result.get() << " ID: " << result->_id;
+
+	return result;
 }
 
 EntityItemPointer RenderableShapeEntityItem::boxFactory(const EntityItemID& entityID, const EntityItemProperties& properties) {
     auto result = baseFactory(entityID, properties);
-    result->setShape(entity::Cube);
+    result->setShape(entity::Shape::Cube);
     return result;
 }
 
 EntityItemPointer RenderableShapeEntityItem::sphereFactory(const EntityItemID& entityID, const EntityItemProperties& properties) {
     auto result = baseFactory(entityID, properties);
-    result->setShape(entity::Sphere);
+    result->setShape(entity::Shape::Sphere);
     return result;
 }
 
diff --git a/libraries/entities/CMakeLists.txt b/libraries/entities/CMakeLists.txt
index 19341ec3e2..90de43b5f8 100644
--- a/libraries/entities/CMakeLists.txt
+++ b/libraries/entities/CMakeLists.txt
@@ -1,3 +1,4 @@
 set(TARGET_NAME entities)
 setup_hifi_library(Network Script)
-link_hifi_libraries(shared networking octree avatars)
+link_hifi_libraries(shared networking octree avatars physics)
+
diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp
index 018d8c568a..d9c48f8593 100644
--- a/libraries/entities/src/ShapeEntityItem.cpp
+++ b/libraries/entities/src/ShapeEntityItem.cpp
@@ -12,6 +12,7 @@
 #include <QtCore/QDebug>
 
 #include <GeometryUtil.h>
+#include <ShapeFactory.h>
 
 #include "EntitiesLogging.h"
 #include "EntityItemProperties.h"
@@ -58,7 +59,11 @@ ShapeEntityItem::Pointer ShapeEntityItem::baseFactory(const EntityItemID& entity
 }
 
 EntityItemPointer ShapeEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) {
-    return baseFactory(entityID, properties);
+	auto result = baseFactory(entityID, properties);
+
+	qCDebug(entities) << "Creating ShapeEntityItem( " << result->_name << " ): " << result.get() << " ID: " << result->_id;
+
+	return result;
 }
 
 EntityItemPointer ShapeEntityItem::boxFactory(const EntityItemID& entityID, const EntityItemProperties& properties) {
@@ -101,6 +106,11 @@ void ShapeEntityItem::setShape(const entity::Shape& shape) {
     }
 }
 
+//TODO_CUSACK: Move back to header prior to PN
+void ShapeEntityItem::setShape( const QString &shape ) {
+	setShape(entity::shapeFromString(shape));
+}
+
 bool ShapeEntityItem::setProperties(const EntityItemProperties& properties) {
     bool somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class
 
@@ -160,10 +170,46 @@ void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit
     APPEND_ENTITY_PROPERTY(PROP_ALPHA, getAlpha());
 }
 
+void ShapeEntityItem::computeShapeInfo(ShapeInfo& info) {
+
+	if ( _collisionShapeType == ShapeType::SHAPE_TYPE_NONE ) {
+		if (_shape == entity::Shape::NUM_SHAPES)
+		{
+			EntityItem::computeShapeInfo(info);
+
+			//--EARLY EXIT--( allow default handling to process )
+			return;
+		}
+
+		_collisionShapeType = ShapeFactory::computeShapeType(getShape(), getDimensions());
+	}
+
+	return EntityItem::computeShapeInfo(info);
+}
+
 // This value specifes how the shape should be treated by physics calculations.  
 // For now, all polys will act as spheres
 ShapeType ShapeEntityItem::getShapeType() const {
-    return (_shape == entity::Shape::Cube) ? SHAPE_TYPE_BOX : SHAPE_TYPE_ELLIPSOID;
+	//TODO_CUSACK: This needs to be retrieved from properties if possible
+	//		    or stored within a new member and set during parsing of
+	//			the properties like setShape via set/get/readEntityProperties.
+	//          Perhaps if the _actual_ collisionShapeType is needed (the version that's in use
+	//			based on analysis of the shape's halfExtents when BulletLibrary collision shape was
+	//			created as opposed to the desired ShapeType is it possible to retrieve that information)?
+	//if (_shape == entity::Shape::Cylinder) {
+	//	return SHAPE_TYPE_CYLINDER_Y;
+	//}
+
+	//// Original functionality:  Everything not a cube, is treated like an ellipsoid/sphere
+	//return (_shape == entity::Shape::Cube) ? SHAPE_TYPE_BOX : SHAPE_TYPE_ELLIPSOID;
+
+	if (_collisionShapeType == ShapeType::SHAPE_TYPE_NONE)
+	{
+		//--EARLY EXIT--( Maintain previous behavior of treating invalid as Ellipsoid/Sphere )
+		return SHAPE_TYPE_ELLIPSOID;
+	}
+
+	return _collisionShapeType;
 }
 
 void ShapeEntityItem::setColor(const rgbColor& value) {
@@ -223,10 +269,12 @@ bool ShapeEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const
 void ShapeEntityItem::debugDump() const {
     quint64 now = usecTimestampNow();
     qCDebug(entities) << "SHAPE EntityItem id:" << getEntityItemID() << "---------------------------------------------";
-    qCDebug(entities) << "              shape:" << stringFromShape(_shape);
+	qCDebug(entities) << "               name:" << _name;
+    qCDebug(entities) << "              shape:" << stringFromShape(_shape) << " (EnumId: " << _shape << " )";
     qCDebug(entities) << "              color:" << _color[0] << "," << _color[1] << "," << _color[2];
     qCDebug(entities) << "           position:" << debugTreeVector(getPosition());
     qCDebug(entities) << "         dimensions:" << debugTreeVector(getDimensions());
     qCDebug(entities) << "      getLastEdited:" << debugTime(getLastEdited(), now);
+	qCDebug(entities) << "SHAPE EntityItem Ptr:" << this;
 }
 
diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h
index 96f69deb0c..f021fcf957 100644
--- a/libraries/entities/src/ShapeEntityItem.h
+++ b/libraries/entities/src/ShapeEntityItem.h
@@ -70,7 +70,7 @@ public:
 
     entity::Shape getShape() const { return _shape; }
     void setShape(const entity::Shape& shape);
-    void setShape(const QString& shape) { setShape(entity::shapeFromString(shape)); }
+	void setShape(const QString& shape);
 
     float getAlpha() const { return _alpha; };
     void setAlpha(float alpha) { _alpha = alpha; }
@@ -84,6 +84,7 @@ public:
     QColor getQColor() const;
     void setColor(const QColor& value);
 
+	void computeShapeInfo(ShapeInfo& info);
     ShapeType getShapeType() const override;
     bool shouldBePhysical() const override { return !isDead(); }
     
@@ -100,6 +101,7 @@ protected:
     float _alpha { 1 };
     rgbColor _color;
     entity::Shape _shape { entity::Shape::Sphere };
+	ShapeType _collisionShapeType{ ShapeType::SHAPE_TYPE_NONE };
 };
 
 #endif // hifi_ShapeEntityItem_h
diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp
index d209667966..a9bc8b2e05 100644
--- a/libraries/physics/src/ShapeFactory.cpp
+++ b/libraries/physics/src/ShapeFactory.cpp
@@ -17,7 +17,7 @@
 #include "BulletUtil.h"
 
 // These are the same normalized directions used by the btShapeHull class.
-// 12 points for the face centers of a duodecohedron plus another 30 points
+// 12 points for the face centers of a dodecahedron plus another 30 points
 // for the midpoints the edges, for a total of 42.
 const uint32_t NUM_UNIT_SPHERE_DIRECTIONS = 42;
 static const btVector3 _unitSphereDirections[NUM_UNIT_SPHERE_DIRECTIONS] = {
@@ -247,6 +247,124 @@ void deleteStaticMeshArray(btTriangleIndexVertexArray* dataArray) {
     delete dataArray;
 }
 
+ShapeType validateShapeType(ShapeType type, const glm::vec3 &halfExtents, btCollisionShape *outCollisionShape = nullptr)
+{
+	if ((type == SHAPE_TYPE_SPHERE) || (type == SHAPE_TYPE_ELLIPSOID))
+	{
+		float radius = halfExtents.x;
+		const float MIN_RADIUS = 0.001f;
+		const float MIN_RELATIVE_SPHERICAL_ERROR = 0.001f;
+		if (radius > MIN_RADIUS
+			&& fabsf(radius - halfExtents.y) / radius < MIN_RELATIVE_SPHERICAL_ERROR
+			&& fabsf(radius - halfExtents.z) / radius < MIN_RELATIVE_SPHERICAL_ERROR) {
+			// close enough to true sphere
+			if (outCollisionShape) {
+				outCollisionShape = new btSphereShape(radius);
+			}
+
+			return SHAPE_TYPE_SPHERE;
+		}
+		else {
+			ShapeInfo::PointList points;
+			points.reserve(NUM_UNIT_SPHERE_DIRECTIONS);
+			for (uint32_t i = 0; i < NUM_UNIT_SPHERE_DIRECTIONS; ++i) {
+				points.push_back(bulletToGLM(_unitSphereDirections[i]) * halfExtents);
+			}
+			if (outCollisionShape) {
+				outCollisionShape = createConvexHull(points);
+			}
+
+			return SHAPE_TYPE_ELLIPSOID;
+		}
+	}
+	else if ((type == SHAPE_TYPE_CYLINDER_X) || (type == SHAPE_TYPE_CYLINDER_Y) || (type == SHAPE_TYPE_CYLINDER_Z))
+	{
+		const btVector3 btHalfExtents(halfExtents.x, halfExtents.y, halfExtents.z);
+		if ((halfExtents.y > halfExtents.x) && (halfExtents.y > halfExtents.z)) {
+			if (outCollisionShape) {
+				outCollisionShape = new btCylinderShape(btHalfExtents);
+			}
+
+			return SHAPE_TYPE_CYLINDER_Y;
+		}
+		else if (halfExtents.x > halfExtents.z) {
+			if (outCollisionShape) {
+				outCollisionShape = new btCylinderShapeX(btHalfExtents);
+			}
+
+			return SHAPE_TYPE_CYLINDER_X;
+		}
+		else if (halfExtents.z > halfExtents.x) {
+			if (outCollisionShape) {
+				outCollisionShape = new btCylinderShapeZ(btHalfExtents);
+			}
+
+			return SHAPE_TYPE_CYLINDER_Z;
+		}
+		else //...there was no major axis, treat as a sphere
+		{
+			ShapeType cylinderFallback = validateShapeType(SHAPE_TYPE_ELLIPSOID, halfExtents, outCollisionShape);
+			return cylinderFallback;
+		}
+	}
+
+	//Got here, then you are what you are along with outCollisionShape
+	return type;
+}
+
+ShapeType ShapeFactory::computeShapeType(entity::Shape shape, const glm::vec3 &entityDimensions) {
+	if ( shape == entity::Shape::NUM_SHAPES ) {
+		//--EARLY EXIT--
+		return SHAPE_TYPE_NONE;
+	}
+
+	const glm::vec3 halfExtents = entityDimensions * 0.5f;
+	switch (shape){
+	case entity::Shape::Triangle: {
+		//TODO_CUSACK: Implement this
+		return validateShapeType(SHAPE_TYPE_ELLIPSOID, halfExtents);
+	}
+
+	//Note: Intentional Fallthrough from Quad to Cube
+	case entity::Shape::Quad:
+	case entity::Shape::Cube: {
+		return SHAPE_TYPE_BOX;
+	}
+
+	//Note: Intentional Fallthrough from Hexagon to Sphere
+	case entity::Shape::Hexagon:
+	case entity::Shape::Octagon:
+	case entity::Shape::Circle:
+	case entity::Shape::Sphere: {
+		return validateShapeType(SHAPE_TYPE_SPHERE, halfExtents);
+	}
+
+	case entity::Shape::Cylinder: {
+		return validateShapeType(SHAPE_TYPE_CYLINDER_Y, halfExtents);
+	}
+
+	//Note: Intentional Fallthrough from Tetrahedron to Icosahedron
+	case entity::Shape::Tetrahedron:
+	case entity::Shape::Octahedron:
+	case entity::Shape::Dodecahedron:
+	case entity::Shape::Icosahedron: {
+
+		//TODO_CUSACK: Implement the hedrons
+		return validateShapeType( SHAPE_TYPE_ELLIPSOID, halfExtents );
+	}
+
+	//Note:  Intentional Fallthrough from Torus to default.
+	case entity::Shape::Torus:
+	case entity::Shape::Cone: {
+
+		// These types are currently unsupported
+	}
+	default:
+		return SHAPE_TYPE_NONE;
+	}
+
+}
+
 const btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) {
     btCollisionShape* shape = NULL;
     int type = info.getType();
@@ -255,30 +373,33 @@ const btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info)
             shape = new btBoxShape(glmToBullet(info.getHalfExtents()));
         }
         break;
-        case SHAPE_TYPE_SPHERE: {
-            glm::vec3 halfExtents = info.getHalfExtents();
-            float radius = glm::max(halfExtents.x, glm::max(halfExtents.y, halfExtents.z));
-            shape = new btSphereShape(radius);
-        }
-        break;
+        //case SHAPE_TYPE_SPHERE: {
+        //    glm::vec3 halfExtents = info.getHalfExtents();
+        //    float radius = glm::max(halfExtents.x, glm::max(halfExtents.y, halfExtents.z));
+        //    shape = new btSphereShape(radius);
+        //}
+        //break;
+		case SHAPE_TYPE_SPHERE:
         case SHAPE_TYPE_ELLIPSOID: {
             glm::vec3 halfExtents = info.getHalfExtents();
-            float radius = halfExtents.x;
-            const float MIN_RADIUS = 0.001f;
-            const float MIN_RELATIVE_SPHERICAL_ERROR = 0.001f;
-            if (radius > MIN_RADIUS
-                    && fabsf(radius - halfExtents.y) / radius < MIN_RELATIVE_SPHERICAL_ERROR
-                    && fabsf(radius - halfExtents.z) / radius < MIN_RELATIVE_SPHERICAL_ERROR) {
-                // close enough to true sphere
-                shape = new btSphereShape(radius);
-            } else {
-                ShapeInfo::PointList points;
-                points.reserve(NUM_UNIT_SPHERE_DIRECTIONS);
-                for (uint32_t i = 0; i < NUM_UNIT_SPHERE_DIRECTIONS; ++i) {
-                    points.push_back(bulletToGLM(_unitSphereDirections[i]) * halfExtents);
-                }
-                shape = createConvexHull(points);
-            }
+            //float radius = halfExtents.x;
+            //const float MIN_RADIUS = 0.001f;
+            //const float MIN_RELATIVE_SPHERICAL_ERROR = 0.001f;
+            //if (radius > MIN_RADIUS
+            //        && fabsf(radius - halfExtents.y) / radius < MIN_RELATIVE_SPHERICAL_ERROR
+            //        && fabsf(radius - halfExtents.z) / radius < MIN_RELATIVE_SPHERICAL_ERROR) {
+            //    // close enough to true sphere
+            //    shape = new btSphereShape(radius);
+            //} else {
+            //    ShapeInfo::PointList points;
+            //    points.reserve(NUM_UNIT_SPHERE_DIRECTIONS);
+            //    for (uint32_t i = 0; i < NUM_UNIT_SPHERE_DIRECTIONS; ++i) {
+            //        points.push_back(bulletToGLM(_unitSphereDirections[i]) * halfExtents);
+            //    }
+            //    shape = createConvexHull(points);
+            //}
+
+			validateShapeType(SHAPE_TYPE_ELLIPSOID, halfExtents, shape);
         }
         break;
         case SHAPE_TYPE_CAPSULE_Y: {
@@ -288,6 +409,42 @@ const btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info)
             shape = new btCapsuleShape(radius, height);
         }
         break;
+		case SHAPE_TYPE_CAPSULE_X: {
+			glm::vec3 halfExtents = info.getHalfExtents();
+			float radius = halfExtents.y;
+			float height = 2.0f * halfExtents.x;
+			shape = new btCapsuleShapeX(radius, height);
+		}
+		break;
+		case SHAPE_TYPE_CAPSULE_Z: {
+			glm::vec3 halfExtents = info.getHalfExtents();
+			float radius = halfExtents.x;
+			float height = 2.0f * halfExtents.z;
+			shape = new btCapsuleShapeZ(radius, height);
+		}
+		break;
+		case SHAPE_TYPE_CYLINDER_X:
+		case SHAPE_TYPE_CYLINDER_Z:
+		case SHAPE_TYPE_CYLINDER_Y: {
+			// TODO_CUSACK: Should allow for minor variance along axes.
+			const glm::vec3 halfExtents = info.getHalfExtents();
+			//const btVector3 btHalfExtents(halfExtents.x, halfExtents.y, halfExtents.z);
+			//if ((halfExtents.y > halfExtents.x) && (halfExtents.y > halfExtents.z)) {
+			//	shape = new btCylinderShape(btHalfExtents);
+			//}
+			//else if (halfExtents.x > halfExtents.z) {
+			//	shape = new btCylinderShapeX(btHalfExtents);
+			//}
+			//else if (halfExtents.z > halfExtents.x) {
+			//	shape = new btCylinderShapeZ(btHalfExtents);
+			//}
+			//else //...there was no major axis, treat as a sphere
+			//{
+			//	//TODO_CUSACK: Shunt to ELLIPSOID handling
+			//}
+			validateShapeType(SHAPE_TYPE_CYLINDER_Y, halfExtents, shape);
+		}
+		break;
         case SHAPE_TYPE_COMPOUND:
         case SHAPE_TYPE_SIMPLE_HULL: {
             const ShapeInfo::PointCollection& pointCollection = info.getPointCollection();
diff --git a/libraries/physics/src/ShapeFactory.h b/libraries/physics/src/ShapeFactory.h
index 2bf79f390c..52b448ee1d 100644
--- a/libraries/physics/src/ShapeFactory.h
+++ b/libraries/physics/src/ShapeFactory.h
@@ -15,11 +15,14 @@
 #include <btBulletDynamicsCommon.h>
 #include <glm/glm.hpp>
 
+#include <ShapeEntityItem.h> //< Needed for entity::Shape
 #include <ShapeInfo.h>
 
 // translates between ShapeInfo and btShape
 
 namespace ShapeFactory {
+
+	ShapeType computeShapeType( entity::Shape shape, const glm::vec3 &entityDimensions);
     const btCollisionShape* createShapeFromInfo(const ShapeInfo& info);
     void deleteShape(const btCollisionShape* shape);
 
diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp
index 496e94f8bd..e40d91379c 100644
--- a/libraries/shared/src/ShapeInfo.cpp
+++ b/libraries/shared/src/ShapeInfo.cpp
@@ -136,7 +136,10 @@ float ShapeInfo::computeVolume() const {
         }
         case SHAPE_TYPE_CAPSULE_Y: {
             float radius = _halfExtents.x;
-            volume = PI * radius * radius * (2.0f * (_halfExtents.y - _halfExtents.x) + 4.0f * radius / 3.0f);
+            // Need to offset halfExtents.y by x to account for the system treating
+            // the y extent of the capsule as the cylindrical height + spherical radius.
+            float cylinderHeight = 2.0f * (_halfExtents.y - _halfExtents.x);
+            volume = PI * radius * radius * (cylinderHeight + 4.0f * radius / 3.0f);
             break;
         }
         default:
diff --git a/scripts/developer/tests/basicEntityTest/entitySpawner.js b/scripts/developer/tests/basicEntityTest/entitySpawner.js
index 538e9145f5..fa5c9291cb 100644
--- a/scripts/developer/tests/basicEntityTest/entitySpawner.js
+++ b/scripts/developer/tests/basicEntityTest/entitySpawner.js
@@ -8,7 +8,9 @@
   var SCRIPT_URL = Script.resolvePath("myEntityScript.js")
 
   var myEntity = Entities.addEntity({
-      type: "Sphere",
+      name: "Cusack_Testing",
+      type: "Shape",
+      shapeType: "Cylinder",
       color: {
           red: 200,
           green: 10,
diff --git a/scripts/developer/tests/basicEntityTest/shapeSpawner.js b/scripts/developer/tests/basicEntityTest/shapeSpawner.js
new file mode 100644
index 0000000000..f93072a582
--- /dev/null
+++ b/scripts/developer/tests/basicEntityTest/shapeSpawner.js
@@ -0,0 +1,33 @@
+  var orientation = Camera.getOrientation();
+  orientation = Quat.safeEulerAngles(orientation);
+  orientation.x = 0;
+  orientation = Quat.fromVec3Degrees(orientation);
+  var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation)));
+
+  // Math.random ensures no caching of script
+  var SCRIPT_URL = Script.resolvePath("myEntityScript.js")
+
+  var myEntity = Entities.addEntity({
+      name: "ShapeSpawnTest",
+      type: "Shape",
+      shape: "Cylinder",
+      color: {
+          red: 200,
+          green: 10,
+          blue: 200
+      },
+      position: center,
+      dimensions: {
+          x: 1,
+          y: 1,
+          z: 1
+      },
+      script: SCRIPT_URL
+  })
+
+
+  function cleanup() {
+      // Entities.deleteEntity(myEntity);
+  }
+
+  Script.scriptEnding.connect(cleanup);