diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..5171c540
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+npm-debug.log
\ No newline at end of file
diff --git a/README.md b/README.md
index fad423fa..936da3fa 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,8 @@
-
-# Project 4: Shape Grammar
-
-For this assignment you'll be building directly off of Project 3. To make things easier to keep track of, please fork and clone this repository [https://github.com/CIS700-Procedural-Graphics/Project4-Shape-Grammar](https://github.com/CIS700-Procedural-Graphics/Project4-Shape-Grammar) and copy your Project 3 code to start.
-
-**Goal:** to model an urban environment using a shape grammar.
-
-**Note:** We’re well aware that a nice-looking procedural city is a lot of work for a single week. Focus on designing a nice building grammar. The city layout strategies outlined in class (the extended l-systems) are complex and not expected. We will be satisfied with something reasonably simple, just not a uniform grid!
-
-## Symbol Node (5 points)
-Modify your symbol node class to include attributes necessary for rendering, such as
-- Associated geometry instance
-- Position
-- Scale
-- Anything else you may need
-
-## Grammar design (55 points)
-- Design at least five shape grammar rules for producing procedural buildings. Your buildings should vary in geometry and decorative features (beyond just differently-scaled cubes!). At least some of your rules should create child geometry that is in some way dependent on its parent’s state. (20 points)
- - Eg. A building may be subdivided along the x, y, or z axis into two smaller buildings
- - Some of your rules must be designed to use some property about its location. (10 points)
- - Your grammar should have some element of variation so your buildings are non-deterministic. Eg. your buildings sometimes subdivide along the x axis, and sometimes the y. (10 points)
-- Write a renderer that will interpret the results of your shape grammar parser and adds the appropriate geometry to your scene for each symbol in your set. (10 points)
-
-## Create a city (30 points)
-- Add a ground plane or some other base terrain to your scene (0 points, come on now)
-- Using any strategy you’d like, procedurally generate features that demarcate your city into different areas in an interesting and plausible way (Just a uniform grid is neither interesting nor plausible). (20 points)
- - Suggestions: roads, rivers, lakes, parks, high-population density
- - Note, these features don’t have to be directly visible, like high-population density, but they should somehow be visible in the appearance or arrangement of your buildings. Eg. High population density is more likely to generate taller buildings
-- Generate buildings throughout your city, using information about your city’s features. Color your buildings with a method that uses some aspect of its state. Eg. Color buildings by height, by population density, by number of rules used to generate it. (5 points)
-- Document your grammar rules and general approach in the readme. (5 points)
-- ???
-- Profit.
-
-## Make it interesting (10)
-Experiment! Make your city a work of art.
-
-
-## Warnings:
-You can very easily blow up three.js with this assignment. With a very simple grammar, our medium quality machine was able to handle 100 buildings with 6 generations each, but be careful if you’re doing this all CPU-side.
-
-## Suggestions for the overachievers:
-Go for a very high level of decorative detail!
-Place buildings with a strategy such that buildings have doors and windows that are always accessible.
-Generate buildings with coherent interiors
-If dividing your city into lots, generate odd-shaped lots and create building meshes that match their shape ie. rather than working with cubes, extrude upwards from the building footprints you find to generate a starting mesh to subdivide rather than starting with platonic geometry.
+# Shape Grammars
+
+Disclaimer: this is not finished as expected, because of the unexpected complexities of convex hull operations.
+However it does have a procedural city with different rules for mass volumes (lot shape and profile), as described in the original paper.
+
+It does not (for now) use any per-component rule (such as windows); although it is implemented, more time is needed to build the obj library.
+
+
diff --git a/deploy.js b/deploy.js
new file mode 100644
index 00000000..9defe7c3
--- /dev/null
+++ b/deploy.js
@@ -0,0 +1,38 @@
+var colors = require('colors');
+var path = require('path');
+var git = require('simple-git')(__dirname);
+var deploy = require('gh-pages-deploy');
+var packageJSON = require('require-module')('./package.json');
+
+var success = 1;
+git.fetch('origin', 'master', function(err) {
+ if (err) throw err;
+ git.status(function(err, status) {
+ if (err) throw err;
+ if (!status.isClean()) {
+ success = 0;
+ console.error('Error: You have uncommitted changes! Please commit them first'.red);
+ }
+
+ if (status.current !== 'master') {
+ success = 0;
+ console.warn('Warning: Please deploy from the master branch!'.yellow)
+ }
+
+ git.diffSummary(['origin/master'], function(err, diff) {
+ if (err) throw err;
+
+ if (diff.files.length || diff.insertions || diff.deletions) {
+ success = 0;
+ console.error('Error: Current branch is different from origin/master! Please push all changes first'.red)
+ }
+
+ if (success) {
+ var cfg = packageJSON['gh-pages-deploy'] || {};
+ var buildCmd = deploy.getFullCmd(cfg);
+ deploy.displayCmds(deploy.getFullCmd(cfg));
+ deploy.execBuild(buildCmd, cfg);
+ }
+ })
+ })
+})
\ No newline at end of file
diff --git a/images/hull_intersection.png b/images/hull_intersection.png
new file mode 100644
index 00000000..754c7eba
Binary files /dev/null and b/images/hull_intersection.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..c8e72fdc
--- /dev/null
+++ b/index.html
@@ -0,0 +1,19 @@
+
+
+
+ Shape Grammars
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..be683fcb
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "scripts": {
+ "start": "webpack-dev-server --hot --inline",
+ "build": "webpack",
+ "deploy": "node deploy.js"
+ },
+ "gh-pages-deploy": {
+ "prep": [
+ "build"
+ ],
+ "noprompt": true
+ },
+ "dependencies": {
+ "dat-gui": "^0.5.0",
+ "gl-matrix": "^2.3.2",
+ "stats-js": "^1.0.0-alpha1",
+ "three": "^0.82.1",
+ "three-orbit-controls": "^82.1.0"
+ },
+ "devDependencies": {
+ "babel-core": "^6.18.2",
+ "babel-loader": "^6.2.8",
+ "babel-preset-es2015": "^6.18.0",
+ "colors": "^1.1.2",
+ "gh-pages-deploy": "^0.4.2",
+ "simple-git": "^1.65.0",
+ "webpack": "^1.13.3",
+ "webpack-dev-server": "^1.16.2",
+ "webpack-glsl-loader": "^1.0.1"
+ }
+}
diff --git a/src/building.js b/src/building.js
new file mode 100644
index 00000000..a566c9af
--- /dev/null
+++ b/src/building.js
@@ -0,0 +1,444 @@
+const THREE = require('three');
+
+class Shape
+{
+ constructor(size)
+ {
+ this.children = [];
+ this.active = true;
+ this.size = size;
+ this.hasComponents = false;
+ }
+}
+
+class BuildingFactory
+{
+ constructor()
+ {
+ this.lots = [];
+ this.profiles = [];
+
+ this.build();
+ }
+
+ build()
+ {
+ this.lots.push(this.buildHLot());
+ this.lots.push(this.buildTLot());
+ this.lots.push(this.buildLLot());
+ this.lots.push(this.buildCLot());
+ this.lots.push(this.buildSquareLot());
+
+ for(var i = 0; i < this.lots.length; i++)
+ this.lots[i].buildNormals();
+
+ this.profiles.push(this.buildSimpleProfile());
+ this.profiles.push(this.buildExtremeProfile());
+ }
+
+ getProfileForShape(random, faceLength, depth, height)
+ {
+ if(height > .75 || random.real(0,1) < .05)
+ {
+ var floors = random.integer(1, 7 * height);
+ var profile = new Profile();
+
+ profile.addPoint(1.05, 0.0);
+ profile.addPoint(1.05, 0.05);
+ profile.addPoint(1.0, 0.05);
+
+ for(var i = 0; i < floors; i++)
+ {
+ var height = (i / floors);
+
+ // Floor separator
+ profile.addPoint(1.0, height - .025);
+ profile.addPoint(1.1, height - .025);
+ profile.addPoint(1.1, height + .025);
+ profile.addPoint(1.0, height + .025);
+ }
+
+ profile.addPoint(1.0, 1.0);
+
+ profile.addPoint(.9, 1.0);
+ profile.addPoint(.9, 1.1);
+ profile.addPoint(.8, 1.1);
+ profile.addPoint(.8, 1.0);
+
+ profile.addPoint(0.7, 1.0);
+
+ return profile;
+ }
+
+ // Almost always simple case
+ if(random.real(0,1) > .15)
+ return this.profiles[0];
+
+ // Extremee
+ return this.profiles[1];
+ }
+
+ buildSimpleProfile()
+ {
+ var profile = new Profile();
+
+ profile.addPoint(1.05, 0.0);
+ profile.addPoint(1.05, 0.05);
+ profile.addPoint(1.0, 0.05);
+ profile.addPoint(1.0, 1.0);
+
+ profile.addPoint(.9, 1.0);
+ profile.addPoint(.9, 1.1);
+ profile.addPoint(.8, 1.1);
+ profile.addPoint(.8, 1.0);
+
+ profile.addPoint(0.7, 1.0);
+
+ return profile;
+ }
+
+ buildExtremeProfile()
+ {
+ var profile = new Profile();
+ profile.addPoint(.5, 0.0);
+ profile.addPoint(.5, .2);
+ profile.addPoint(.2, .2);
+ profile.addPoint(.2, 1.0);
+
+ profile.addPoint(.9, 1.0);
+ profile.addPoint(.9, 1.1);
+ profile.addPoint(.8, 1.1);
+ profile.addPoint(.8, 1.0);
+
+ profile.addPoint(0.7, 1.0);
+
+ return profile;
+ }
+
+ getLotForShape(random, faceLength, depth, height)
+ {
+ if(random.real(0,1) < .2 || (height > 2 && random.real(0,1) < .6))
+ {
+ // Circular
+ var subdivs = random.integer(4, 15);
+ var displ = random.real(0, 1);
+
+ var lot = new BuildingLot();
+
+ for(var i = 0; i < subdivs; i++)
+ {
+ var a = i * Math.PI * 2 / subdivs;
+ var r = 1.0 - Math.pow(Math.sin(a * 10) * .5 + .5, 5.0) * displ;
+ lot.addPoint(Math.cos(a) * r, Math.sin(a) * r);
+ }
+
+ return lot;
+ }
+
+ return this.lots[random.integer(0, this.lots.length - 1)];
+ }
+
+ buildSquareLot()
+ {
+ var lot = new BuildingLot();
+
+ lot.addPoint(-1, -1);
+ lot.addPoint(1, -1);
+ lot.addPoint(1, 1);
+ lot.addPoint(-1, 1);
+
+ return lot;
+ }
+
+ buildCLot()
+ {
+ var lot = new BuildingLot();
+
+ lot.addPoint(-1, -1);
+ lot.addPoint(1, -1);
+ lot.addPoint(1, -.5);
+ lot.addPoint(0, -.5);
+ lot.addPoint(0, .5);
+ lot.addPoint(1, .5);
+ lot.addPoint(1, 1);
+ lot.addPoint(-1, 1);
+
+ return lot;
+ }
+
+ buildLLot()
+ {
+ var lot = new BuildingLot();
+
+ lot.addPoint(-1, -1);
+ lot.addPoint(0, -1);
+ lot.addPoint(0, 0);
+ lot.addPoint(1, 0);
+ lot.addPoint(1, 1);
+ lot.addPoint(-1, 1);
+
+ return lot;
+ }
+
+ buildTLot()
+ {
+ var lot = new BuildingLot();
+
+ lot.addPoint(-1, -1);
+ lot.addPoint(0, -1);
+ lot.addPoint(0, -.5);
+ lot.addPoint(1, -.5);
+ lot.addPoint(1, .5);
+ lot.addPoint(0, .5);
+ lot.addPoint(0, 1);
+ lot.addPoint(-1, 1);
+
+ return lot;
+ }
+
+ buildHLot()
+ {
+ var lot = new BuildingLot();
+
+ lot.addPoint(-1, -1);
+ lot.addPoint(-.5, -1);
+ lot.addPoint(-.5, -.5);
+ lot.addPoint(.5, -.5);
+ lot.addPoint(.5, -1);
+ lot.addPoint(1, -1);
+
+ lot.addPoint(1, 1);
+ lot.addPoint(.5, 1);
+ lot.addPoint(.5, .5);
+ lot.addPoint(-.5, .5);
+ lot.addPoint(-.5, 1);
+
+ return lot;
+ }
+}
+
+// The object that defines the boundaries of the mass shape
+class BuildingLot
+{
+ constructor()
+ {
+ this.points = [];
+ this.normals = [];
+ this.hasCap = true;
+ this.center = THREE.Vector2(0,0);
+ }
+
+ addPoint(x, y)
+ {
+ this.points.push(new THREE.Vector2(x, y));
+ }
+
+ buildNormals()
+ {
+ var l = this.points.length;
+ this.center = new THREE.Vector2(0,0);
+
+ for(var i = 0; i < l; i++)
+ {
+ var prev = ((i == 0) ? l - 1 : i - 1);
+ var next = (i+1) % l;
+
+ var p = this.points[i];
+ var prevP = this.points[prev];
+ var nextP = this.points[next];
+
+ var t1 = prevP.clone().sub(p).normalize();
+ var t2 = p.clone().sub(nextP).normalize();
+
+ var n1 = new THREE.Vector2(-t1.y, t1.x);
+ var n2 = new THREE.Vector2(-t2.y, t2.x);
+
+ var n = n1.add(n2).multiplyScalar(.5);
+ this.normals[i] = n.normalize();
+
+ this.center.add(p);
+ }
+
+ this.center.multiplyScalar(1.0 / l);
+ }
+}
+
+// The profile of an extrusion
+class Profile
+{
+ constructor()
+ {
+ this.points = [];
+ this.scale = 1.0; // Specifically, height
+ }
+
+ addPoint(x, y)
+ {
+ this.points.push(new THREE.Vector2(x, y));
+ }
+}
+
+class MassShape
+{
+ constructor(lot, profile)
+ {
+ this.lot = lot;
+ this.profile = profile;
+ }
+
+ generateMesh()
+ {
+ this.lot.buildNormals();
+
+ var material = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 });
+ var geometry = new THREE.Geometry();
+
+ var boundaryVertexCount = this.lot.points.length;
+ var offset = 0;
+
+ for(var i = 0; i < this.profile.points.length; i++)
+ {
+ var profilePoint = this.profile.points[i];
+ var profileDiff = (i == 0) ? new THREE.Vector2(0,0) : (profilePoint.clone().sub(this.profile.points[i-1]));
+
+ for(var j = 0; j < boundaryVertexCount; j++)
+ {
+ var boundaryPoint = this.lot.points[j];
+ var boundaryNormal = this.lot.normals[j];
+
+ var vertex = new THREE.Vector3(boundaryPoint.x, profilePoint.y, boundaryPoint.y);
+
+ var displ = boundaryNormal.clone().multiplyScalar(profilePoint.x - 1.0);
+ vertex.add(new THREE.Vector3(displ.x, 0.0, displ.y));
+
+ geometry.vertices.push(vertex);
+ }
+
+ // Ignore first row of vertices
+ if(i > 0)
+ {
+ for(var v = 0; v < boundaryVertexCount; v++)
+ {
+ var v1 = offset + v;
+ var v2 = offset + ((v + 1) % boundaryVertexCount);
+ var v3 = offset + boundaryVertexCount + ((v + 1) % boundaryVertexCount);
+
+ var v4 = offset + v;
+ var v5 = offset + boundaryVertexCount + ((v + 1) % boundaryVertexCount);
+ var v6 = offset + boundaryVertexCount + v;
+
+ geometry.faces.push(new THREE.Face3(v3, v2, v1));
+ geometry.faces.push(new THREE.Face3(v6, v5, v4));
+
+ }
+
+ offset += boundaryVertexCount;
+ }
+ }
+
+ // End the last row of vertices into a cap
+ if(this.lot.hasCap)
+ {
+ var center = this.lot.center;
+ var height = this.profile.points[this.profile.points.length - 1].y;
+ var vertex = new THREE.Vector3(center.x, height, center.y);
+ geometry.vertices.push(vertex);
+
+ for(var v = 0; v < boundaryVertexCount; v++)
+ {
+
+ var v1 = offset + v;
+ var v2 = offset + ((v + 1) % boundaryVertexCount);
+ var v3 = geometry.vertices.length - 1;
+
+ geometry.faces.push(new THREE.Face3(v1, v2, v3));
+ }
+ }
+
+ geometry.mergeVertices();
+ geometry.computeFlatVertexNormals();
+
+ material.side = THREE.DoubleSide;
+
+ var mesh = new THREE.Mesh(geometry, material);
+
+ this.geometry = geometry;
+ this.mesh = mesh;
+ return mesh;
+ }
+}
+
+class Rule
+{
+ constructor()
+ {
+ this.componentWise = false;
+ }
+
+ evaluateOverall(shape)
+ {
+ }
+
+ evaluateComponent(shape, scope)
+ {
+ }
+
+ evaluate(shape, scene)
+ {
+ if(this.componentWise)
+ {
+ var material = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 });
+ var geometry = new THREE.BoxGeometry( 1, 1, 1 );
+ for(var f = 0; f < shape.geometry.faces.length; f += 2)
+ {
+ var face = shape.geometry.faces[f];
+
+ var v1 = shape.geometry.vertices[face.b];
+ var v2 = shape.geometry.vertices[face.a];
+ var v3 = shape.geometry.vertices[face.c];
+
+ var height = v2.clone().sub(v1).length();
+ var width = v3.clone().sub(v1).length();
+
+ var u = v2.clone().sub(v1).normalize();
+ var v = v3.clone().sub(v1).normalize();
+
+ var repetitionsU = 4;
+ var repetitionsV = 4;
+
+ if(face.normal.y > .1 || height < .5)
+ continue;
+
+ for(var r = 0; r < repetitionsU; r++)
+ {
+ for(var rV = 0; rV < repetitionsV; rV++)
+ {
+ var cube = new THREE.Mesh( geometry, material );
+ var t = r / repetitionsU;
+ var tV = rV / repetitionsV;
+ cube.position.copy(u.clone().multiplyScalar(t * height).add(v.clone().multiplyScalar(tV * width).add(v1)));
+ cube.scale.set(.1 * width, .1 * height, .1);
+ cube.lookAt(cube.position.clone().add(face.normal));
+ scene.add( cube );
+ }
+ }
+ }
+ }
+ else
+ {
+ evaluateOverall(shape);
+ }
+ }
+}
+
+class ShapeBuilder
+{
+ constructor()
+ {
+ this.shapes = [];
+ this.iterations = 5;
+ }
+
+}
+
+export {Shape, Rule, BuildingLot, Profile, MassShape, BuildingFactory}
\ No newline at end of file
diff --git a/src/city.js b/src/city.js
new file mode 100644
index 00000000..21432479
--- /dev/null
+++ b/src/city.js
@@ -0,0 +1,664 @@
+const THREE = require('three');
+const Random = require("random-js");
+
+import * as Common from './common.js'
+import * as Building from './building.js'
+
+class Voronoi
+{
+ constructor()
+ {
+
+ }
+}
+
+// 2D only!
+class ConvexHull
+{
+ constructor()
+ {
+ this.bounds = null;
+ this.segments = [];
+ this.vertices = [];
+ this.midpoint = new THREE.Vector2(0,0);
+ this.area = 0;
+ }
+
+ getCenter()
+ {
+ var c = new THREE.Vector2(0,0);
+
+ for(var p = 0; p < this.points.length; p++)
+ c.add(this.points[p]);
+
+ c.multiplyScalar(1.0 / this.points.length);
+
+ return c;
+ }
+
+ addSegment(normal, direction, midpoint, min, max)
+ {
+ this.segments.push({ valid : false, normal: normal.clone(), dir: direction.clone(), midpoint: midpoint.clone(), min : 1000, max : -1000});
+ }
+
+ calculateArea()
+ {
+ var area = 0;
+ for(var i = 0; i < this.vertices.length; i++)
+ {
+ var nextIndex = (i+1) % this.vertices.length;
+
+ var v = this.vertices[i];
+ var next = this.vertices[nextIndex];
+
+ area += v.x * next.y - v.y * next.x;
+ }
+
+ this.area = Math.abs(area) * .5;
+ }
+
+ // Sorts the vertices with the gift wrapping algorithm
+ sortVertices()
+ {
+ var unorderedVertices = this.vertices;
+ this.vertices = [];
+
+ if(unorderedVertices.length == 0)
+ return;
+
+ var leftmostPoint = unorderedVertices[0];
+
+ for(var i = 1; i < unorderedVertices.length; i++)
+ if(unorderedVertices[i].x < leftmostPoint.x)
+ leftmostPoint = unorderedVertices[i];
+
+ function isLeft(a, b, c)
+ {
+ return ((b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x)) > 0;
+ }
+
+ var currentPoint = leftmostPoint;
+ var endpoint = unorderedVertices[0];
+ var i = 0;
+ do
+ {
+ this.vertices.push(currentPoint);
+ endpoint = unorderedVertices[0];
+
+ for(var j = 1; j < unorderedVertices.length; j++)
+ {
+ if((endpoint === currentPoint) || isLeft(currentPoint, endpoint, unorderedVertices[j]))
+ endpoint = unorderedVertices[j];
+ }
+
+ currentPoint = endpoint;
+ i++;
+ }
+ while(i < unorderedVertices.length * 2 && !(endpoint === this.vertices[0]));
+
+ if(unorderedVertices.length != this.vertices.length)
+ console.log("Convex hull vertex sort did not work: " + unorderedVertices.length + " original points, result: " + this.vertices.length)
+
+ this.calculateArea();
+ }
+
+ calculateVertices()
+ {
+ this.vertices = [];
+ this.midpoint = new THREE.Vector2(0,0);
+
+ function addVertex(v, vertices, midpoint)
+ {
+ for(var i = 0; i < vertices.length; i++)
+ if(v.distanceTo(vertices[i]) < .01)
+ return;
+
+ vertices.push(v);
+ midpoint.add(v);
+ }
+
+ for(var s = 0; s < this.segments.length; s++)
+ {
+ var segment = this.segments[s];
+
+ if(!segment.valid)
+ continue;
+
+ var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min));
+ var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max));
+
+ // Cache these points for future reference...
+ segment.from = from;
+ segment.to = to;
+
+ addVertex(from, this.vertices, this.midpoint);
+ addVertex(to, this.vertices, this.midpoint);
+ }
+
+ if(this.vertices.length > 0)
+ this.midpoint.multiplyScalar(1.0 / this.vertices.length);
+ }
+
+ updateBounds()
+ {
+ this.bounds = null;
+
+ for(var h = 0; h < this.segments.length; h++)
+ {
+ if(this.segments[h].valid)
+ {
+ var segment = this.segments[h];
+ var from = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.min));
+ var to = segment.midpoint.clone().add(segment.dir.clone().multiplyScalar(segment.max));
+
+ if(this.bounds == null)
+ this.bounds = new Common.Bounds(from, from);
+
+ this.bounds.encapsulate(from);
+ this.bounds.encapsulate(to);
+ }
+ }
+ }
+
+ calculateSegments()
+ {
+ for(var i = 0; i < this.segments.length; i++)
+ {
+ for(var j = 0; j < this.segments.length; j++)
+ {
+ if(i == j)
+ continue;
+
+ var s1 = this.segments[i];
+ var s2 = this.segments[j];
+
+ // Parallel
+ if(Math.abs(s1.dir.dot(s2.normal)) < .001)
+ continue;
+
+ var diff = s2.midpoint.clone().sub(s1.midpoint);
+ var det = s2.dir.x * s1.dir.y - s2.dir.y * s1.dir.x;
+
+ var u = (diff.y * s2.dir.x - diff.x * s2.dir.y) / det;
+ var v = (diff.y * s1.dir.x - diff.x * s1.dir.y) / det;
+
+ var newPoint = s1.midpoint.clone().add(s1.dir.clone().multiplyScalar(u));
+ var insideHull = true;
+
+ // This is the naive, N^3 intersection test method
+ for(var k = 0; k < this.segments.length; k++)
+ {
+ if(k != j && k != i)
+ {
+ var dP = newPoint.clone().sub(this.segments[k].midpoint);
+
+ // Remember normals are inverted, this is why it is > 0 and not <= 0
+ if(this.segments[k].normal.clone().dot(dP) > 0)
+ insideHull = false;
+ }
+ }
+
+ if(!insideHull)
+ continue;
+
+ s1.min = Math.min(s1.min, u);
+ s1.max = Math.max(s1.max, u);
+ s1.valid = true;
+ }
+ }
+
+ this.updateBounds();
+ }
+
+ isValid()
+ {
+ var valid = 0;
+
+ for(var h = 0; h < this.segments.length; h++)
+ {
+ if(this.segments[h].valid)
+ valid++;
+ }
+
+ return valid > 2 && this.segments.length > 2;
+ }
+
+ // Returns two copies
+ splitComplex(axis, splitDistance)
+ {
+ var h1 = new ConvexHull();
+ var h2 = new ConvexHull();
+
+ var tangent = new THREE.Vector2( -axis.y, axis.x);
+ var offset = axis.clone().multiplyScalar(splitDistance);
+
+ var oldPoints = this.points;
+
+ h1.addSegment(axis, tangent, offset);
+ h2.addSegment(axis.clone().negate(), tangent.clone().negate(), offset);
+
+ for(var h = 0; h < this.segments.length; h++)
+ {
+ var segment = this.segments[h];
+
+ // Some pruning
+ if(segment.valid)
+ {
+ h1.addSegment(segment.normal, segment.dir, segment.midpoint);
+ h2.addSegment(segment.normal, segment.dir, segment.midpoint);
+ }
+ }
+
+ h1.calculateSegments();
+ h2.calculateSegments();
+
+ return [h1, h2];
+ }
+}
+
+class Generator
+{
+ constructor()
+ {
+ }
+
+ sliceHullSet(hulls, axis, subdivisions, scale, intersectionFunction)
+ {
+ var newHulls = [];
+
+ for(var h = 0; h < hulls.length; h++)
+ {
+ var hull = hulls[h];
+ var intersected = false;
+
+ for(var x = 0; x < subdivisions + 1 && !intersected; x++)
+ {
+ var t = x / subdivisions;
+ var sliceOffset = t * scale;
+
+ if(intersectionFunction(hull, sliceOffset))
+ {
+ var split = hull.splitComplex(axis, sliceOffset);
+
+ if(split[0].isValid())
+ newHulls.push(split[0]);
+
+ if(split[1].isValid())
+ newHulls.push(split[1]);
+
+ intersected = true;
+ }
+ }
+
+ if(!intersected)
+ newHulls.push(hull);
+ }
+
+ return newHulls;
+ }
+
+ // Algorithm overview:
+ // Build voronoi with half plane intersection tests, based on a jittered grid
+ // (super important each point is inside each cell)
+ // Generate convex hulls
+ // Subdivide convex hulls in X and Y, 9 times
+ // Extract sections from this subdivision
+ // Ignore border sections, as I have no time to deal with edge cases (no pun intended)
+ buildHulls(scene, random)
+ {
+ // count * count final points
+ var count = 30;
+ var scale = 2;
+
+ var geometry = new THREE.Geometry();
+ var pointsGeo = new THREE.Geometry();
+ var points = [];
+ var xAxis = new THREE.Vector2( 1, 0 );
+ var yAxis = new THREE.Vector2( 0, 1 );
+
+ // From center
+ var randomAmplitude = .499;
+
+ // Distribute points
+ for(var x = 0; x < count; x++)
+ {
+ points.push(new Array());
+
+ for(var y = 0; y < count; y++)
+ {
+ var r1 = random.real(-1, 1) * randomAmplitude * scale;
+ var r2 = random.real(-1, 1) * randomAmplitude * scale;
+
+ var p = new THREE.Vector2( x * scale + .5 + r1, y * scale + .5 + r2);
+ points[x].push(p);
+ }
+ }
+
+ // Build half planes, and finding their convex hulls
+ var hulls = [];
+
+ for(var x = 0; x < count; x++)
+ {
+ for(var y = 0; y < count; y++)
+ {
+ var p = points[x][y];
+ var hull = new ConvexHull();
+
+ // Find all planes for all close neighbors within 3 cells
+ for(var i = -2; i < 3; i++)
+ {
+ for(var j = -2; j < 3; j++)
+ {
+ if(x+i >= 0 && y+j >= 0 && x+i < count && y+j < count)
+ {
+ var neighbor = points[x+i][y+j];
+
+ var normal = neighbor.clone().sub(p).normalize();
+ var midpoint = neighbor.clone().add(p).multiplyScalar(.5);
+ var tangent = new THREE.Vector2( -normal.y, normal.x );
+ hull.addSegment(normal, tangent, midpoint);
+ }
+ }
+ }
+
+ hull.calculateSegments();
+ hulls.push(hull);
+ }
+ }
+
+ // Subdivide everything 11 times in X and Y
+ // Then we take the inner 9x9 grids to build a cube
+ hulls = this.sliceHullSet(hulls, xAxis, 11, count * scale, function(hull, sliceOffset){ return hull.bounds.intersectsX(sliceOffset); });
+ hulls = this.sliceHullSet(hulls, yAxis, 11, count * scale, function(hull, sliceOffset){ return hull.bounds.intersectsY(sliceOffset); });
+ console.log("Hulls: " + hulls.length);
+
+ var cellScale = count * scale / 11;
+ var cellBounds = this.getSubcellRemapping(cellScale);
+ var boundedHulls = new Array();
+
+ for(var i = 0; i < cellBounds.length; i++)
+ boundedHulls.push(new Array());
+
+ // Save geo for display
+ for(var h = 0; h < hulls.length; h++)
+ {
+ var hull = hulls[h];
+
+ if(!hull.isValid())
+ continue;
+
+ hull.calculateVertices();
+
+ var bounded = false;
+ for(var b = 0; b < cellBounds.length; b++)
+ {
+ if(cellBounds[b].contains(hull.midpoint))
+ {
+ bounded = true;
+ hull.cellIndex = b;
+ boundedHulls[b].push(hull);
+ break;
+ }
+ }
+
+ if(!bounded)
+ continue;
+
+ hull.sortVertices();
+
+ // var height = 0;
+ // pointsGeo.vertices.push(new THREE.Vector3( hull.midpoint.x, height, hull.midpoint.y));
+
+ // for(var i = 0; i < hull.vertices.length; i++)
+ // {
+ // var i1 = (i+1) % hull.vertices.length;
+ // var v = hull.vertices[i];
+ // var v1 = hull.vertices[i1];
+
+ // geometry.vertices.push(new THREE.Vector3( v.x, height, v.y ));
+ // geometry.vertices.push(new THREE.Vector3( v1.x, height, v1.y ));
+
+ // pointsGeo.vertices.push(new THREE.Vector3( v.x, height, v.y ));
+ // }
+ }
+
+ // console.log(geometry.vertices.length);
+
+ // var lineMaterial = new THREE.LineBasicMaterial( {color: 0xffffff} );
+ // var line = new THREE.LineSegments(geometry, lineMaterial);
+ // scene.add(line);
+
+ // var pointsMaterial = new THREE.PointsMaterial( { color: 0xffffff } )
+ // pointsMaterial.size = .1;
+ // var pointsMesh = new THREE.Points( pointsGeo, pointsMaterial );
+ // scene.add( pointsMesh );
+
+ return { hulls: boundedHulls, cells : cellBounds };
+ }
+
+ getSubcellRemapping(cellScale)
+ {
+ var cellBounds = [];
+
+ function addSubcells()
+ {
+ for(var x = 0; x < 3; x++)
+ {
+ for(var y = 0; y < 3; y++)
+ {
+ var from = new THREE.Vector2( (offset.x + x) * cellScale, (offset.y + y) * cellScale );
+ var to = from.clone().add(new THREE.Vector2(cellScale, cellScale));
+
+ cellBounds.push(new Common.Bounds(from, to));
+ }
+ }
+ }
+
+ // Top cell
+ var offset = new THREE.Vector2( 4, 1 );
+ addSubcells();
+
+ // Down cell
+ offset = new THREE.Vector2( 4, 7 );
+ addSubcells();
+
+ // // Horizontal cells
+ offset = new THREE.Vector2( 1, 4 );
+ addSubcells();
+
+ offset = new THREE.Vector2( 4, 4 );
+ addSubcells();
+
+ offset = new THREE.Vector2( 7, 4 );
+ addSubcells();
+
+ // // Last cell
+ offset = new THREE.Vector2( 7, 7 );
+ addSubcells();
+
+ return cellBounds;
+ }
+
+ buildLots(hullContainer, random)
+ {
+ var lotContainer = [];
+
+ for(var i = 0; i < hullContainer.length; i++)
+ {
+ lotContainer.push(new Array());
+ var hulls = hullContainer[i];
+
+ for(var h = 0; h < hulls.length; h++)
+ {
+ var hull = hulls[h];
+
+ // If it is too small, no lot
+ // If it is medium sized, it can be ignored with a probability
+ // if(hull.area > .5 && (random.real(0,1) > .1 || hull.area > 2))
+ {
+ var lot = new Building.BuildingLot();
+
+ // Yes, directly reuse them ;)
+ lot.points = hull.vertices;
+ lot.buildNormals();
+ lot.hasCap = true;
+ lot.hull = hull;
+ lot.park = (hull.area < .55 || (random.real(0,1) < .1 && hull.area < 2));
+
+ lotContainer[i].push(lot);
+ }
+ }
+ }
+
+ return lotContainer;
+ }
+
+ getMassShapeLot(position, faceLength, depth, height)
+ {
+ var lot = new Building.BuildingLot();
+ var subdivs = 8;
+
+ for(var i = 0; i < subdivs; i++)
+ {
+ var a = i * Math.PI * 2 / subdivs;
+ var r = 1.0 - Math.pow(Math.sin(a * 10) * .5 + .5, 5.0) * .5;
+ lot.addPoint(Math.cos(a) * r * .5, Math.sin(a) * r * .5);
+ }
+
+ lot.hasCap = true;
+
+ return lot;
+ }
+
+ generateMassShapesForLot(lot, random, factory)
+ {
+ if(lot.park)
+ return [];
+
+ var hull = lot.hull;
+ var shapes = [];
+
+ var material = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 });
+ var geometry = new THREE.BoxGeometry( 1, 1, 1 );
+
+ for(var s = 0; s < hull.segments.length; s++)
+ {
+ var segment = hull.segments[s];
+
+ if(!segment.valid)
+ continue;
+
+ var from = segment.from;
+ var to = segment.to;
+ var segmentLength = from.distanceTo(to);
+
+ if(segmentLength < .5)
+ continue;
+
+ var count = Math.floor(Math.pow(random.real(0, 1), 2.0) * 3 * segmentLength) + 1;
+
+ for(var i = 0; i < count; i++)
+ {
+ // We dont want to overlap with other segments
+ var t = (i / (count + 1));
+ var p = from.clone().lerp(to, t * .8 + .1);
+ p.add(segment.normal.clone().multiplyScalar(-.25));
+ p.add(segment.dir.clone().multiplyScalar(.5*segmentLength/count));
+
+ var normal = new THREE.Vector3( segment.normal.x, 0, segment.normal.y );
+
+ // Facing street
+ var faceLength = random.real(.7, .99) * .6 * segmentLength / count;
+ var depth = random.real(.4, .7);
+ var height = random.real(random.real(.1, .2), THREE.Math.clamp(2.0 * depth * faceLength, .4, 4)); // Height is dependent on depth+length
+
+ p.add(segment.normal.clone().multiplyScalar(depth*-.5));
+
+ var position = new THREE.Vector3( p.x, 0, p.y );
+ var massLot = factory.getLotForShape(random, faceLength, depth, height);// this.getMassShapeLot(position, faceLength, depth, height);
+ var massProfile = factory.getProfileForShape(random, faceLength, depth, height);
+
+ var shape = new Building.MassShape(massLot, massProfile)
+ var shapeMesh = shape.generateMesh();
+
+ // var cube = new THREE.Mesh( geometry, material );
+ shapeMesh.scale.set(faceLength * .5, height, depth * .5);
+ shapeMesh.position.copy(position);
+ shapeMesh.lookAt(shapeMesh.position.clone().add(normal));
+
+ var intersects = false;
+ for(var j = 0; j < shapes.length; j++)
+ {
+ if(shapes[j].position.distanceTo(shapeMesh.position) < .5 * faceLength)
+ {
+ intersects = true;
+ break;
+ }
+ }
+
+ if(!intersects)
+ shapes.push(shapeMesh);
+ }
+ }
+
+ return shapes;
+ }
+
+ build(scene)
+ {
+ var random = new Random(Random.engines.mt19937().autoSeed());
+
+
+ var factory = new Building.BuildingFactory();
+ var hullData = this.buildHulls(scene, random);
+ var hulls = hullData.hulls;
+ var cellBounds = hullData.cells;
+ var lots = this.buildLots(hulls, random);
+
+ var baseLotProfile = new Building.Profile();
+ baseLotProfile.addPoint(1.15, 0.0);
+ baseLotProfile.addPoint(1.15, .025);
+ baseLotProfile.addPoint(1.2, .025);
+ baseLotProfile.addPoint(1.2, .035);
+ baseLotProfile.addPoint(1.35, .035);
+
+ var cityBlocks = [];
+
+ for(var i = 0; i < lots.length; i++)
+ {
+ var group = new THREE.Group();
+
+ var geometryBatch = new THREE.Geometry();
+
+ for(var j = 0; j < lots[i].length; j++)
+ {
+ // The blocks profiles are also mass shapes
+ var shape = new Building.MassShape(lots[i][j], baseLotProfile);
+ var mesh = shape.generateMesh();
+ geometryBatch.mergeMesh(mesh)
+
+ var massShapes = this.generateMassShapesForLot(lots[i][j], random, factory);
+
+ for(var s = 0; s < massShapes.length; s++)
+ geometryBatch.mergeMesh(massShapes[s])
+ }
+
+ var batchMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, emissive: 0x333333 });
+ batchMaterial.side = THREE.DoubleSide;
+ var batchedMesh = new THREE.Mesh(geometryBatch, batchMaterial);
+ group.add(batchedMesh);
+
+ var center = cellBounds[i].min.clone().add(cellBounds[i].max).multiplyScalar(.5);
+ group.position.set(-center.x, 0, -center.y);
+
+ var g2 = new THREE.Group();
+ g2.add(group);
+ g2.userData = { index: i, offset: center }
+ cityBlocks.push(g2);
+ }
+
+ return cityBlocks;
+ }
+}
+
+export {Generator}
\ No newline at end of file
diff --git a/src/common.js b/src/common.js
new file mode 100644
index 00000000..f5da52ab
--- /dev/null
+++ b/src/common.js
@@ -0,0 +1,38 @@
+const THREE = require('three');
+
+class Bounds
+{
+ constructor(min, max)
+ {
+ this.min = min.clone();
+ this.max = max.clone();
+ }
+
+ encapsulate(p)
+ {
+ this.min.min(p);
+ this.max.max(p);
+ }
+
+ intersectsX(x)
+ {
+ return this.min.x < x && this.max.x > x;
+ }
+
+ intersectsY(y)
+ {
+ return this.min.y < y && this.max.y > y;
+ }
+
+ intersectsZ(Z)
+ {
+ return this.min.z < z && this.max.z > z;
+ }
+
+ contains(p)
+ {
+ return this.min.x < p.x && this.min.y < p.y && this.max.x > p.x && this.max.y > p.y;
+ }
+}
+
+export {Bounds}
\ No newline at end of file
diff --git a/src/framework.js b/src/framework.js
new file mode 100644
index 00000000..76f901a5
--- /dev/null
+++ b/src/framework.js
@@ -0,0 +1,72 @@
+
+const THREE = require('three');
+const OrbitControls = require('three-orbit-controls')(THREE)
+import Stats from 'stats-js'
+import DAT from 'dat-gui'
+
+// when the scene is done initializing, the function passed as `callback` will be executed
+// then, every frame, the function passed as `update` will be executed
+function init(callback, update) {
+ var stats = new Stats();
+ stats.setMode(1);
+ stats.domElement.style.position = 'absolute';
+ stats.domElement.style.left = '0px';
+ stats.domElement.style.top = '0px';
+ document.body.appendChild(stats.domElement);
+
+ var gui = new DAT.GUI();
+
+ var framework = {
+ gui: gui,
+ stats: stats
+ };
+
+ // run this function after the window loads
+ window.addEventListener('load', function() {
+
+ var scene = new THREE.Scene();
+ var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
+ var renderer = new THREE.WebGLRenderer( { antialias: true } );
+ renderer.setPixelRatio(window.devicePixelRatio);
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setClearColor(0x020202, 0);
+
+ var controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ controls.enableZoom = true;
+ controls.target.set(0, 0, 0);
+ controls.rotateSpeed = 0.3;
+ controls.zoomSpeed = 1.0;
+ controls.panSpeed = 2.0;
+
+ document.body.appendChild(renderer.domElement);
+
+ // resize the canvas when the window changes
+ window.addEventListener('resize', function() {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ }, false);
+
+ // assign THREE.js objects to the object we will return
+ framework.scene = scene;
+ framework.camera = camera;
+ framework.renderer = renderer;
+
+ // begin the animation loop
+ (function tick() {
+ stats.begin();
+ update(framework); // perform any requested updates
+ renderer.render(scene, camera); // render the scene
+ stats.end();
+ requestAnimationFrame(tick); // register to call this again when the browser renders a new frame
+ })();
+
+ // we will pass the scene, gui, renderer, camera, etc... to the callback function
+ return callback(framework);
+ });
+}
+
+export default {
+ init: init
+}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 00000000..323a22b0
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,134 @@
+const THREE = require('three');
+const Random = require("random-js");
+
+import Framework from './framework'
+import * as Building from './building.js'
+import * as Rubik from './rubik.js'
+import * as City from './city.js'
+
+var UserSettings =
+{
+ iterations : 5
+}
+
+var Engine =
+{
+ initialized : false,
+ time : 0.0,
+ deltaTime : 0.0,
+ clock : null,
+
+ music : null,
+
+ loadingBlocker : null,
+ materials : [],
+
+ rubik : null,
+ rubikTime : 0,
+}
+
+function onLoad(framework)
+{
+ var scene = framework.scene;
+ var camera = framework.camera;
+ var renderer = framework.renderer;
+ var gui = framework.gui;
+ var stats = framework.stats;
+
+ renderer.setClearColor(new THREE.Color(.4, .75, .95), 1);
+
+ // initialize a simple box and material
+ var directionalLight = new THREE.DirectionalLight( 0xffffff, 1 );
+ directionalLight.color = new THREE.Color(.9, .9, 1 );
+ directionalLight.position.set(-10, 10, 10);
+ scene.add(directionalLight);
+
+ // initialize a simple box and material
+ var directionalLight2 = new THREE.DirectionalLight( 0xffffff, 1 );
+ directionalLight2.color = new THREE.Color(.4, .4, .7);
+ directionalLight2.position.set(-1, -3, 2);
+ directionalLight2.position.multiplyScalar(10);
+ scene.add(directionalLight2);
+
+ // set camera position
+ camera.position.set(40, 40, 40);
+ camera.lookAt(new THREE.Vector3(0,0,0));
+ camera.fov = 5;
+ camera.updateProjectionMatrix();
+
+ var profile = new Building.Profile();
+ profile.addPoint(1.0, 0.0);
+ profile.addPoint(1.0, 1.0);
+
+ profile.addPoint(.9, 1.0);
+ profile.addPoint(.9, 1.1);
+ profile.addPoint(.8, 1.1);
+ profile.addPoint(.8, 1.0);
+
+ profile.addPoint(0.7, 1.0);
+ profile.addPoint(0.6, 2.0);
+ profile.addPoint(0.0, 2.0);
+
+ var lot = new Building.BuildingLot();
+ var subdivs = 25;
+ for(var i = 0; i < subdivs; i++)
+ {
+ var a = i * Math.PI * 2 / subdivs;
+ var r = 1.0 - Math.pow(Math.sin(a * 10) * .5 + .5, 5.0) * .5 + 1.0 ;
+ lot.addPoint(Math.cos(a) * r, Math.sin(a) * r);
+ }
+
+ // var shape = new Building.MassShape(lot, profile);
+ // var mesh = shape.generateMesh();
+ // scene.add(mesh);
+
+ // var rule = new Building.Rule();
+ // rule.componentWise = true;
+ // rule.evaluate(shape, scene);
+
+ Engine.rubik = new Rubik.Rubik();
+ var rubikMesh = Engine.rubik.build();
+ scene.add(rubikMesh);
+
+ // Init Engine stuff
+ Engine.scene = scene;
+ Engine.renderer = renderer;
+ Engine.clock = new THREE.Clock();
+ Engine.camera = camera;
+ Engine.initialized = true;
+
+ var random = new Random(Random.engines.mt19937().seed(2545));
+
+ var speed = .45;
+
+ var city = new City.Generator();
+ var cityBlocks = city.build(scene);
+
+ Engine.rubik.attachShapesToFace(cityBlocks);
+
+ var callback = function() {
+ Engine.rubik.animate(random.integer(0, 2), random.integer(0, 2), speed, callback);
+ // Engine.rubik.animate(1,0, speed, callback);
+ };
+
+ Engine.rubik.animate(0, 0, speed, callback);
+}
+
+// called on frame updates
+function onUpdate(framework)
+{
+ if(Engine.initialized)
+ {
+ var screenSize = new THREE.Vector2( framework.renderer.getSize().width, framework.renderer.getSize().height );
+ var deltaTime = Engine.clock.getDelta();
+
+ Engine.time += deltaTime;
+ Engine.cameraTime += deltaTime;
+ Engine.deltaTime = deltaTime;
+
+ Engine.rubik.update(deltaTime);
+ }
+}
+
+// when the scene is done initializing, it will call onLoad, then on frame updates, call onUpdate
+Framework.init(onLoad, onUpdate);
diff --git a/src/rubik.js b/src/rubik.js
new file mode 100644
index 00000000..81f55e83
--- /dev/null
+++ b/src/rubik.js
@@ -0,0 +1,308 @@
+const THREE = require('three');
+
+function fequals(a, b)
+{
+ return Math.abs(a-b) < .001;
+}
+
+class Rubik
+{
+ constructor()
+ {
+ this.segments = [];
+ this.animating = false;
+ this.currentAxis = 0;
+ this.currentPlane = 0;
+ this.currentLength = 1.0;
+ this.time = 0;
+ this.callback = null;
+ }
+
+ animate(axis, plane, length, callback)
+ {
+ this.animating = true;
+ this.currentAxis = axis;
+ this.currentPlane = plane;
+ this.currentLength = length;
+ this.time = 0;
+ this.callback = callback;
+ }
+
+ update(deltaTime)
+ {
+ if(!this.animating)
+ return;
+
+ this.time += deltaTime;
+
+ var t = THREE.Math.clamp(this.time / this.currentLength, 0.0, 1.0);
+
+ t = THREE.Math.smoothstep(t, 0, 1);
+ var angle = t * 90;
+ var swapped = false;
+
+ if(this.currentAxis == 0)
+ swapped = this.rotateX(angle, this.currentPlane);
+ else if(this.currentAxis == 1)
+ swapped = this.rotateY(angle, this.currentPlane);
+ else if(this.currentAxis == 2)
+ swapped = this.rotateZ(angle, this.currentPlane);
+
+ if(swapped)
+ {
+ this.animating = false;
+ this.time = 0;
+
+ if(this.callback != null)
+ this.callback();
+ }
+ }
+
+ rotateX(degrees, xPlane)
+ {
+ return this.rotateInternal(degrees, xPlane, new THREE.Vector3( 1, 0, 0), function(x, y, plane) {
+ return new THREE.Vector3( plane, x, y );
+ });
+ }
+
+ rotateY(degrees, yPlane)
+ {
+ return this.rotateInternal(degrees, yPlane, new THREE.Vector3( 0, 1, 0), function(x, y, plane) {
+ return new THREE.Vector3( y, plane, x );
+ });
+ }
+
+ rotateZ(degrees, zPlane)
+ {
+ return this.rotateInternal(degrees, zPlane, new THREE.Vector3( 0, 0, 1), function(x, y, plane) {
+ return new THREE.Vector3( x, y, plane );
+ });
+ }
+
+ rotateInternal(degrees, plane, axis, indexFunction)
+ {
+ var rad = THREE.Math.degToRad(THREE.Math.clamp(degrees, -90, 90));
+ var tMatrix = new THREE.Matrix4();
+ var sMatrix = new THREE.Matrix4();
+ var rMatrix = new THREE.Matrix4();
+ // sMatrix.makeScale(.9, .9, .9);
+
+ for(var i = 0; i < 3; i++)
+ {
+ for(var j = 0; j < 3; j++)
+ {
+ var p = indexFunction(i, j, plane);
+ var cube = this.segments[p.x][p.y][p.z];
+
+ var matrix = new THREE.Matrix4();
+ cube.matrix.copy(matrix);
+
+ rMatrix.makeRotationAxis(axis, rad);
+ tMatrix.makeTranslation(p.x - 1, p.y - 1, p.z - 1);
+
+ matrix = rMatrix.clone();
+ matrix.multiply(tMatrix);
+ matrix.multiply(cube.userData.rotationAccum);
+
+ cube.applyMatrix(matrix);
+ }
+ }
+
+ // If the rotation is full, then we need to update the data
+ // structure
+ var tmpArray = [];
+ var shapeArray = [];
+ if(fequals(degrees, -90))
+ {
+ for(var x = 0; x < 3; x++)
+ {
+ tmpArray.push(new Array());
+ shapeArray.push(new Array());
+
+ for(var y = 0; y < 3; y++)
+ {
+ var p = indexFunction(2 - y, x, plane);
+ tmpArray[x][y] = this.segments[p.x][p.y][p.z];
+ }
+ }
+ }
+ else if(fequals(degrees, 90))
+ {
+ for(var x = 0; x < 3; x++)
+ {
+ tmpArray.push(new Array());
+ shapeArray.push(new Array());
+
+ for(var y = 0; y < 3; y++)
+ {
+ var p = indexFunction(y, 2 - x, plane);
+ tmpArray[x][y] = this.segments[p.x][p.y][p.z];
+ }
+ }
+ }
+
+ // Now we effectively swap everything
+ if(tmpArray.length > 0)
+ {
+ for(var x = 0; x < 3; x++)
+ for(var y = 0; y < 3; y++)
+ {
+ var p = indexFunction(x, y, plane);
+
+ var segment = tmpArray[x][y];
+ segment.userData.rotationAccum.premultiply(rMatrix);
+ this.segments[p.x][p.y][p.z] = segment;
+ }
+
+ // We swapped
+ return true;
+ }
+
+ return false;
+ }
+
+ // Assumes userData = { index, offset}
+ attachShapesToFace(shapes)
+ {
+ for(var i = 0; i < shapes.length; i++)
+ {
+ var shape = shapes[i];
+ var index = shape.userData.index;
+ var offset = shape.userData.offset;
+
+ var overallScale = .55 / 3;
+
+ // Front face
+ if(index >= 27 && index < 36)
+ {
+ var x = (index - 27) % 3;
+ var y = Math.floor((index - 27) / 3);
+ var z = 0;
+
+ shape.rotateX(Math.PI * -.5);
+ shape.translateY(.5);
+ shape.scale.set(overallScale, overallScale, overallScale);
+ this.segments[x][y][z].add(shape);
+ this.segments[x][y][z].userData.shape = shape;
+ }
+
+ // Top face
+ if(index >= 0 && index < 9)
+ {
+ var x = index % 3;
+ var y = 2;
+ var z = Math.floor(index / 3);
+
+ // shape.rotateX(Math.PI * -.5);
+ shape.translateY(.5);
+ shape.scale.set(overallScale, overallScale, overallScale);
+ this.segments[x][y][z].add(shape);
+ this.segments[x][y][z].userData.shape = shape;
+ }
+
+ // Down face
+ if(index >= 9 && index < 18)
+ {
+ var x = (index - 9) % 3;
+ var y = 0;
+ var z = Math.floor((index - 9) / 3);
+
+ shape.rotateX(Math.PI * -1);
+ shape.translateY(.5);
+ shape.scale.set(overallScale, overallScale, overallScale);
+ this.segments[x][y][z].add(shape);
+ this.segments[x][y][z].userData.shape = shape;
+ }
+
+ // Left face
+ if(index >= 18 && index < 27)
+ {
+ var x = 2;
+ var y = (index - 18) % 3;
+ var z = Math.floor((index - 18) / 3);
+
+ shape.rotateZ(Math.PI * -.5);
+ shape.translateY(.5);
+ shape.scale.set(overallScale, overallScale, overallScale);
+ this.segments[x][y][z].add(shape);
+ this.segments[x][y][z].userData.shape = shape;
+ }
+
+ // Right face
+ if(index >= 36 && index < 45)
+ {
+ var x = 0;
+ var y = (index - 36) % 3;
+ var z = Math.floor((index - 36) / 3);
+
+ shape.rotateZ(Math.PI * .5);
+ shape.translateY(.5);
+ shape.scale.set(overallScale, overallScale, overallScale);
+ this.segments[x][y][z].add(shape);
+ this.segments[x][y][z].userData.shape = shape;
+ }
+
+ // Last face
+ if(index >= 45 && index < 54)
+ {
+ var x = (index - 45) % 3;
+ var y = Math.floor((index - 45) / 3);
+ var z = 2;
+
+ shape.rotateX(Math.PI * .5);
+ shape.translateY(.5);
+ shape.scale.set(overallScale, overallScale, overallScale);
+ this.segments[x][y][z].add(shape);
+ this.segments[x][y][z].userData.shape = shape;
+ }
+ }
+ }
+
+ build()
+ {
+ var container = new THREE.Object3D();
+ var boxGeo = new THREE.BoxGeometry( 1, 1, 1 );
+ var planeGeo = new THREE.PlaneGeometry(1, 1, 1, 1 );
+
+ this.segments = []
+
+ for(var x = 0; x < 3; x++)
+ {
+ this.segments.push(new Array());
+
+ for(var y = 0; y < 3; y++)
+ {
+ this.segments[x].push(new Array());
+
+ for(var z = 0; z < 3; z++)
+ {
+ var colorDebugging = z;
+ var mat = new THREE.MeshPhongMaterial( {color: 0x888888} );
+ mat.shininess = 5;
+ mat.specular = new THREE.Color(.2,.2,.3);
+ // mat.color = new THREE.Color(1, 0.0, 0.0 );
+
+ // if(colorDebugging == 1)
+ // mat.color = new THREE.Color( 0.0, 1, 0.0 );
+ // else if(colorDebugging == 2)
+ // mat.color = new THREE.Color(0.0, 0.0, 1);
+
+ var cube = new THREE.Mesh( boxGeo, mat );
+ cube.position.copy(new THREE.Vector3( x - 1, y - 1, z - 1));
+ // cube.scale.copy(new THREE.Vector3( .9, .9, .9 ))
+ container.add(cube);
+
+ // Index is x * 3 * 3 + y * 3 + z
+ this.segments[x][y].push(cube);
+ cube.userData = { rotationAccum : new THREE.Matrix4() };
+ }
+ }
+ }
+
+ return container;
+ }
+
+
+}
+
+export {Rubik}
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 00000000..57dce485
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,28 @@
+const path = require('path');
+
+module.exports = {
+ entry: path.join(__dirname, "src/main"),
+ output: {
+ filename: "./bundle.js"
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ exclude: /(node_modules|bower_components)/,
+ loader: 'babel',
+ query: {
+ presets: ['es2015']
+ }
+ },
+ {
+ test: /\.glsl$/,
+ loader: "webpack-glsl"
+ },
+ ]
+ },
+ devtool: 'source-map',
+ devServer: {
+ port: 7000
+ }
+}
\ No newline at end of file