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..23e6e6a3 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,56 @@
# 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.
+## Generating Buildings
+All buildings start off as a single box geometry of sizes (small, medium, large)
+with some minor variations. The height of the buildings are determined by a population map
+that can be imported from an image file.
+
+The first iteration of the shape grammar determines whether the building will have
+side panels.
+
+A=>C, A=>S
+
+In the next iteration, the side panels will terminate. The other symbols will randomly
+select between uniform vertical division of the block, or the same division with
+differentiation of the top and bottom levels.
+
+C=>U, C=>T/U/B, S=>X
+
+The next iteration adds details to the levels. The top levels have a ticker or a
+scaled block. The bottom levels are scaled larger. The middle levels select between
+alternating level sizes, billboards or signs.
+
+U=>{billboards, signs, alternating scaling}
+
+## Placing Buildings
+The entire city is represented by a grid system like Sim City. Each time
+a building is placed, the grid locations are marked as occupied to prevent overlapping buildings.
+
+Buildings are placed probabilistically based on the population map using
+the pointalism algorithm we talked about in class. Random points are selected from
+the grid and will be kept or discarded using the population map value as a threshold.
+
+I also implemented a HTML5 canvas based lsystem to generate the roads. This is
+still a work in progress as it does not take into account any additional parameters.
+My intention is to translate the canvas values into the city grid. This way, buildings
+will not over lap with roads.
+
+## Fun Features
+The ticker textures on the top of buildings are generated procedurally. Normally,
+we load an image into Three.js to use as a texture, but its actually possible to use
+an HTML5 canvas instead [[2]](http://learningthreejs.com/blog/2013/08/02/how-to-do-a-procedural-city-in-100lines/) ! Just draw on a canvas element and then pass it directly
+into THREE.texture(canvas).
+
+## Demo
+
+Demo: https://iambrian.github.io/Project4-Shape-Grammar/
+
+# Resources
+[1] Borrowed some of the ticker messages from Sim City: http://simcity.wikia.com/wiki/List_of_news_ticker_messages
+
+[2] Procedural Textures using canvas: http://learningthreejs.com/blog/2013/08/02/how-to-do-a-procedural-city-in-100lines/
+
+[3] Subversion city generator for inspiration: https://www.youtube.com/watch?v=FR9xI0GgrBY
+
+[4] Also Mirror's Edge for inspiration: http://imgur.com/LsvEPJW
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/index.html b/index.html
new file mode 100644
index 00000000..78231237
--- /dev/null
+++ b/index.html
@@ -0,0 +1,20 @@
+
+
+
+ HW2: LSystems
+
+
+
+
+
+
+
diff --git a/maps/population.png b/maps/population.png
new file mode 100644
index 00000000..93db95c7
Binary files /dev/null and b/maps/population.png differ
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/OBJLoader.js b/src/OBJLoader.js
new file mode 100644
index 00000000..3277449f
--- /dev/null
+++ b/src/OBJLoader.js
@@ -0,0 +1,718 @@
+
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+ const THREE = require('three'); // older modules are imported like this. You shouldn't have to worry about this much
+
+
+THREE.OBJLoader = function(manager) {
+
+ this.manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager;
+
+ this.materials = null;
+
+ this.regexp = {
+ // v float float float
+ vertex_pattern: /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
+ // vn float float float
+ normal_pattern: /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
+ // vt float float
+ uv_pattern: /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
+ // f vertex vertex vertex
+ face_vertex: /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/,
+ // f vertex/uv vertex/uv vertex/uv
+ face_vertex_uv: /^f\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+))?/,
+ // f vertex/uv/normal vertex/uv/normal vertex/uv/normal
+ face_vertex_uv_normal: /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/,
+ // f vertex//normal vertex//normal vertex//normal
+ face_vertex_normal: /^f\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)(?:\s+(-?\d+)\/\/(-?\d+))?/,
+ // o object_name | g group_name
+ object_pattern: /^[og]\s*(.+)?/,
+ // s boolean
+ smoothing_pattern: /^s\s+(\d+|on|off)/,
+ // mtllib file_reference
+ material_library_pattern: /^mtllib /,
+ // usemtl material_name
+ material_use_pattern: /^usemtl /
+ };
+
+};
+
+THREE.OBJLoader.prototype = {
+
+ constructor: THREE.OBJLoader,
+
+ load: function(url, onLoad, onProgress, onError) {
+
+ var scope = this;
+
+ var loader = new THREE.XHRLoader(scope.manager);
+ loader.setPath(this.path);
+ loader.load(url, function(text) {
+
+ onLoad(scope.parse(text));
+
+ }, onProgress, onError);
+
+ },
+
+ setPath: function(value) {
+
+ this.path = value;
+
+ },
+
+ setMaterials: function(materials) {
+
+ this.materials = materials;
+
+ },
+
+ _createParserState: function() {
+
+ var state = {
+ objects: [],
+ object: {},
+
+ vertices: [],
+ normals: [],
+ uvs: [],
+
+ materialLibraries: [],
+
+ startObject: function(name, fromDeclaration) {
+
+ // If the current object (initial from reset) is not from a g/o declaration in the parsed
+ // file. We need to use it for the first parsed g/o to keep things in sync.
+ if (this.object && this.object.fromDeclaration === false) {
+
+ this.object.name = name;
+ this.object.fromDeclaration = (fromDeclaration !== false);
+ return;
+
+ }
+
+ if (this.object && typeof this.object._finalize === 'function') {
+
+ this.object._finalize();
+
+ }
+
+ var previousMaterial = (this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined);
+
+ this.object = {
+ name: name || '',
+ fromDeclaration: (fromDeclaration !== false),
+
+ geometry: {
+ vertices: [],
+ normals: [],
+ uvs: []
+ },
+ materials: [],
+ smooth: true,
+
+ startMaterial: function(name, libraries) {
+
+ var previous = this._finalize(false);
+
+ // New usemtl declaration overwrites an inherited material, except if faces were declared
+ // after the material, then it must be preserved for proper MultiMaterial continuation.
+ if (previous && (previous.inherited || previous.groupCount <= 0)) {
+
+ this.materials.splice(previous.index, 1);
+
+ }
+
+ var material = {
+ index: this.materials.length,
+ name: name || '',
+ mtllib: (Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : ''),
+ smooth: (previous !== undefined ? previous.smooth : this.smooth),
+ groupStart: (previous !== undefined ? previous.groupEnd : 0),
+ groupEnd: -1,
+ groupCount: -1,
+ inherited: false,
+
+ clone: function(index) {
+ return {
+ index: (typeof index === 'number' ? index : this.index),
+ name: this.name,
+ mtllib: this.mtllib,
+ smooth: this.smooth,
+ groupStart: this.groupEnd,
+ groupEnd: -1,
+ groupCount: -1,
+ inherited: false
+ };
+ }
+ };
+
+ this.materials.push(material);
+
+ return material;
+
+ },
+
+ currentMaterial: function() {
+
+ if (this.materials.length > 0) {
+ return this.materials[this.materials.length - 1];
+ }
+
+ return undefined;
+
+ },
+
+ _finalize: function(end) {
+
+ var lastMultiMaterial = this.currentMaterial();
+ if (lastMultiMaterial && lastMultiMaterial.groupEnd === -1) {
+
+ lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
+ lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
+ lastMultiMaterial.inherited = false;
+
+ }
+
+ // Guarantee at least one empty material, this makes the creation later more straight forward.
+ if (end !== false && this.materials.length === 0) {
+ this.materials.push({
+ name: '',
+ smooth: this.smooth
+ });
+ }
+
+ return lastMultiMaterial;
+
+ }
+ };
+
+ // Inherit previous objects material.
+ // Spec tells us that a declared material must be set to all objects until a new material is declared.
+ // If a usemtl declaration is encountered while this new object is being parsed, it will
+ // overwrite the inherited material. Exception being that there was already face declarations
+ // to the inherited material, then it will be preserved for proper MultiMaterial continuation.
+
+ if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === "function") {
+
+ var declared = previousMaterial.clone(0);
+ declared.inherited = true;
+ this.object.materials.push(declared);
+
+ }
+
+ this.objects.push(this.object);
+
+ },
+
+ finalize: function() {
+
+ if (this.object && typeof this.object._finalize === 'function') {
+
+ this.object._finalize();
+
+ }
+
+ },
+
+ parseVertexIndex: function(value, len) {
+
+ var index = parseInt(value, 10);
+ return (index >= 0 ? index - 1 : index + len / 3) * 3;
+
+ },
+
+ parseNormalIndex: function(value, len) {
+
+ var index = parseInt(value, 10);
+ return (index >= 0 ? index - 1 : index + len / 3) * 3;
+
+ },
+
+ parseUVIndex: function(value, len) {
+
+ var index = parseInt(value, 10);
+ return (index >= 0 ? index - 1 : index + len / 2) * 2;
+
+ },
+
+ addVertex: function(a, b, c) {
+
+ var src = this.vertices;
+ var dst = this.object.geometry.vertices;
+
+ dst.push(src[a + 0]);
+ dst.push(src[a + 1]);
+ dst.push(src[a + 2]);
+ dst.push(src[b + 0]);
+ dst.push(src[b + 1]);
+ dst.push(src[b + 2]);
+ dst.push(src[c + 0]);
+ dst.push(src[c + 1]);
+ dst.push(src[c + 2]);
+
+ },
+
+ addVertexLine: function(a) {
+
+ var src = this.vertices;
+ var dst = this.object.geometry.vertices;
+
+ dst.push(src[a + 0]);
+ dst.push(src[a + 1]);
+ dst.push(src[a + 2]);
+
+ },
+
+ addNormal: function(a, b, c) {
+
+ var src = this.normals;
+ var dst = this.object.geometry.normals;
+
+ dst.push(src[a + 0]);
+ dst.push(src[a + 1]);
+ dst.push(src[a + 2]);
+ dst.push(src[b + 0]);
+ dst.push(src[b + 1]);
+ dst.push(src[b + 2]);
+ dst.push(src[c + 0]);
+ dst.push(src[c + 1]);
+ dst.push(src[c + 2]);
+
+ },
+
+ addUV: function(a, b, c) {
+
+ var src = this.uvs;
+ var dst = this.object.geometry.uvs;
+
+ dst.push(src[a + 0]);
+ dst.push(src[a + 1]);
+ dst.push(src[b + 0]);
+ dst.push(src[b + 1]);
+ dst.push(src[c + 0]);
+ dst.push(src[c + 1]);
+
+ },
+
+ addUVLine: function(a) {
+
+ var src = this.uvs;
+ var dst = this.object.geometry.uvs;
+
+ dst.push(src[a + 0]);
+ dst.push(src[a + 1]);
+
+ },
+
+ addFace: function(a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd) {
+
+ var vLen = this.vertices.length;
+
+ var ia = this.parseVertexIndex(a, vLen);
+ var ib = this.parseVertexIndex(b, vLen);
+ var ic = this.parseVertexIndex(c, vLen);
+ var id;
+
+ if (d === undefined) {
+
+ this.addVertex(ia, ib, ic);
+
+ } else {
+
+ id = this.parseVertexIndex(d, vLen);
+
+ this.addVertex(ia, ib, id);
+ this.addVertex(ib, ic, id);
+
+ }
+
+ if (ua !== undefined) {
+
+ var uvLen = this.uvs.length;
+
+ ia = this.parseUVIndex(ua, uvLen);
+ ib = this.parseUVIndex(ub, uvLen);
+ ic = this.parseUVIndex(uc, uvLen);
+
+ if (d === undefined) {
+
+ this.addUV(ia, ib, ic);
+
+ } else {
+
+ id = this.parseUVIndex(ud, uvLen);
+
+ this.addUV(ia, ib, id);
+ this.addUV(ib, ic, id);
+
+ }
+
+ }
+
+ if (na !== undefined) {
+
+ // Normals are many times the same. If so, skip function call and parseInt.
+ var nLen = this.normals.length;
+ ia = this.parseNormalIndex(na, nLen);
+
+ ib = na === nb ? ia : this.parseNormalIndex(nb, nLen);
+ ic = na === nc ? ia : this.parseNormalIndex(nc, nLen);
+
+ if (d === undefined) {
+
+ this.addNormal(ia, ib, ic);
+
+ } else {
+
+ id = this.parseNormalIndex(nd, nLen);
+
+ this.addNormal(ia, ib, id);
+ this.addNormal(ib, ic, id);
+
+ }
+
+ }
+
+ },
+
+ addLineGeometry: function(vertices, uvs) {
+
+ this.object.geometry.type = 'Line';
+
+ var vLen = this.vertices.length;
+ var uvLen = this.uvs.length;
+
+ for (var vi = 0, l = vertices.length; vi < l; vi++) {
+
+ this.addVertexLine(this.parseVertexIndex(vertices[vi], vLen));
+
+ }
+
+ for (var uvi = 0, l = uvs.length; uvi < l; uvi++) {
+
+ this.addUVLine(this.parseUVIndex(uvs[uvi], uvLen));
+
+ }
+
+ }
+
+ };
+
+ state.startObject('', false);
+
+ return state;
+
+ },
+
+ parse: function(text) {
+
+ console.time('OBJLoader');
+
+ var state = this._createParserState();
+
+ if (text.indexOf('\r\n') !== -1) {
+
+ // This is faster than String.split with regex that splits on both
+ text = text.replace('\r\n', '\n');
+
+ }
+
+ var lines = text.split('\n');
+ var line = '',
+ lineFirstChar = '',
+ lineSecondChar = '';
+ var lineLength = 0;
+ var result = [];
+
+ // Faster to just trim left side of the line. Use if available.
+ var trimLeft = (typeof ''.trimLeft === 'function');
+
+ for (var i = 0, l = lines.length; i < l; i++) {
+
+ line = lines[i];
+
+ line = trimLeft ? line.trimLeft() : line.trim();
+
+ lineLength = line.length;
+
+ if (lineLength === 0) continue;
+
+ lineFirstChar = line.charAt(0);
+
+ // @todo invoke passed in handler if any
+ if (lineFirstChar === '#') continue;
+
+ if (lineFirstChar === 'v') {
+
+ lineSecondChar = line.charAt(1);
+
+ if (lineSecondChar === ' ' && (result = this.regexp.vertex_pattern.exec(line)) !== null) {
+
+ // 0 1 2 3
+ // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"]
+
+ state.vertices.push(
+ parseFloat(result[1]),
+ parseFloat(result[2]),
+ parseFloat(result[3]));
+
+ } else if (lineSecondChar === 'n' && (result = this.regexp.normal_pattern.exec(line)) !== null) {
+
+ // 0 1 2 3
+ // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"]
+
+ state.normals.push(
+ parseFloat(result[1]),
+ parseFloat(result[2]),
+ parseFloat(result[3]));
+
+ } else if (lineSecondChar === 't' && (result = this.regexp.uv_pattern.exec(line)) !== null) {
+
+ // 0 1 2
+ // ["vt 0.1 0.2", "0.1", "0.2"]
+
+ state.uvs.push(
+ parseFloat(result[1]),
+ parseFloat(result[2]));
+
+ } else {
+
+ throw new Error("Unexpected vertex/normal/uv line: '" + line + "'");
+
+ }
+
+ } else if (lineFirstChar === "f") {
+
+ if ((result = this.regexp.face_vertex_uv_normal.exec(line)) !== null) {
+
+ // f vertex/uv/normal vertex/uv/normal vertex/uv/normal
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12
+ // ["f 1/1/1 2/2/2 3/3/3", "1", "1", "1", "2", "2", "2", "3", "3", "3", undefined, undefined, undefined]
+
+ state.addFace(
+ result[1], result[4], result[7], result[10],
+ result[2], result[5], result[8], result[11],
+ result[3], result[6], result[9], result[12]);
+
+ } else if ((result = this.regexp.face_vertex_uv.exec(line)) !== null) {
+
+ // f vertex/uv vertex/uv vertex/uv
+ // 0 1 2 3 4 5 6 7 8
+ // ["f 1/1 2/2 3/3", "1", "1", "2", "2", "3", "3", undefined, undefined]
+
+ state.addFace(
+ result[1], result[3], result[5], result[7],
+ result[2], result[4], result[6], result[8]);
+
+ } else if ((result = this.regexp.face_vertex_normal.exec(line)) !== null) {
+
+ // f vertex//normal vertex//normal vertex//normal
+ // 0 1 2 3 4 5 6 7 8
+ // ["f 1//1 2//2 3//3", "1", "1", "2", "2", "3", "3", undefined, undefined]
+
+ state.addFace(
+ result[1], result[3], result[5], result[7],
+ undefined, undefined, undefined, undefined,
+ result[2], result[4], result[6], result[8]);
+
+ } else if ((result = this.regexp.face_vertex.exec(line)) !== null) {
+
+ // f vertex vertex vertex
+ // 0 1 2 3 4
+ // ["f 1 2 3", "1", "2", "3", undefined]
+
+ state.addFace(
+ result[1], result[2], result[3], result[4]);
+
+ } else {
+
+ throw new Error("Unexpected face line: '" + line + "'");
+
+ }
+
+ } else if (lineFirstChar === "l") {
+
+ var lineParts = line.substring(1).trim().split(" ");
+ var lineVertices = [],
+ lineUVs = [];
+
+ if (line.indexOf("/") === -1) {
+
+ lineVertices = lineParts;
+
+ } else {
+
+ for (var li = 0, llen = lineParts.length; li < llen; li++) {
+
+ var parts = lineParts[li].split("/");
+
+ if (parts[0] !== "") lineVertices.push(parts[0]);
+ if (parts[1] !== "") lineUVs.push(parts[1]);
+
+ }
+
+ }
+ state.addLineGeometry(lineVertices, lineUVs);
+
+ } else if ((result = this.regexp.object_pattern.exec(line)) !== null) {
+
+ // o object_name
+ // or
+ // g group_name
+
+ var name = result[0].substr(1).trim();
+ state.startObject(name);
+
+ } else if (this.regexp.material_use_pattern.test(line)) {
+
+ // material
+
+ state.object.startMaterial(line.substring(7).trim(), state.materialLibraries);
+
+ } else if (this.regexp.material_library_pattern.test(line)) {
+
+ // mtl file
+
+ state.materialLibraries.push(line.substring(7).trim());
+
+ } else if ((result = this.regexp.smoothing_pattern.exec(line)) !== null) {
+
+ // smooth shading
+
+ // @todo Handle files that have varying smooth values for a set of faces inside one geometry,
+ // but does not define a usemtl for each face set.
+ // This should be detected and a dummy material created (later MultiMaterial and geometry groups).
+ // This requires some care to not create extra material on each smooth value for "normal" obj files.
+ // where explicit usemtl defines geometry groups.
+ // Example asset: examples/models/obj/cerberus/Cerberus.obj
+
+ var value = result[1].trim().toLowerCase();
+ state.object.smooth = (value === '1' || value === 'on');
+
+ var material = state.object.currentMaterial();
+ if (material) {
+
+ material.smooth = state.object.smooth;
+
+ }
+
+ } else {
+
+ // Handle null terminated files without exception
+ if (line === '\0') continue;
+
+ throw new Error("Unexpected line: '" + line + "'");
+
+ }
+
+ }
+
+ state.finalize();
+
+ var container = new THREE.Group();
+ container.materialLibraries = [].concat(state.materialLibraries);
+
+ for (var i = 0, l = state.objects.length; i < l; i++) {
+
+ var object = state.objects[i];
+ var geometry = object.geometry;
+ var materials = object.materials;
+ var isLine = (geometry.type === 'Line');
+
+ // Skip o/g line declarations that did not follow with any faces
+ if (geometry.vertices.length === 0) continue;
+
+ var buffergeometry = new THREE.BufferGeometry();
+
+ buffergeometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(geometry.vertices), 3));
+
+ if (geometry.normals.length > 0) {
+
+ buffergeometry.addAttribute('normal', new THREE.BufferAttribute(new Float32Array(geometry.normals), 3));
+
+ } else {
+
+ buffergeometry.computeVertexNormals();
+
+ }
+
+ if (geometry.uvs.length > 0) {
+
+ buffergeometry.addAttribute('uv', new THREE.BufferAttribute(new Float32Array(geometry.uvs), 2));
+
+ }
+
+ // Create materials
+
+ var createdMaterials = [];
+
+ for (var mi = 0, miLen = materials.length; mi < miLen; mi++) {
+
+ var sourceMaterial = materials[mi];
+ var material = undefined;
+
+ if (this.materials !== null) {
+
+ material = this.materials.create(sourceMaterial.name);
+
+ // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material.
+ if (isLine && material && !(material instanceof THREE.LineBasicMaterial)) {
+
+ var materialLine = new THREE.LineBasicMaterial();
+ materialLine.copy(material);
+ material = materialLine;
+
+ }
+
+ }
+
+ if (!material) {
+
+ material = (!isLine ? new THREE.MeshPhongMaterial() : new THREE.LineBasicMaterial());
+ material.name = sourceMaterial.name;
+
+ }
+
+ material.shading = sourceMaterial.smooth ? THREE.SmoothShading : THREE.FlatShading;
+
+ createdMaterials.push(material);
+
+ }
+
+ // Create mesh
+
+ var mesh;
+
+ if (createdMaterials.length > 1) {
+
+ for (var mi = 0, miLen = materials.length; mi < miLen; mi++) {
+
+ var sourceMaterial = materials[mi];
+ buffergeometry.addGroup(sourceMaterial.groupStart, sourceMaterial.groupCount, mi);
+
+ }
+
+ var multiMaterial = new THREE.MultiMaterial(createdMaterials);
+ mesh = (!isLine ? new THREE.Mesh(buffergeometry, multiMaterial) : new THREE.Line(buffergeometry, multiMaterial));
+
+ } else {
+
+ mesh = (!isLine ? new THREE.Mesh(buffergeometry, createdMaterials[0]) : new THREE.Line(buffergeometry, createdMaterials[0]));
+ }
+
+ mesh.name = object.name;
+
+ container.add(mesh);
+
+ }
+
+ console.timeEnd('OBJLoader');
+
+ return container;
+
+ }
+
+};
diff --git a/src/builder.js b/src/builder.js
new file mode 100644
index 00000000..37d2b218
--- /dev/null
+++ b/src/builder.js
@@ -0,0 +1,564 @@
+const THREE = require('three')
+
+var SHAPES = {
+ BOX: 0,
+ ICOSAHEDRON: 1
+}
+
+var SYMBOLS = {
+ A: 0,
+ C: 1,
+ U: 2, // uniform middle levels
+ T: 3, // top levels
+ B: 4, // bottom levels
+ S: 5, // side appendages
+ X: 6 // terminal
+}
+
+var Random = {
+ sign: function() { return Math.random() > 0.5 ? -1 : 1; }
+}
+
+export default class Builder {
+
+ constructor() {
+ this.materials = {};
+ this.objects = {};
+ }
+
+ getRandomAd() {
+ if (Math.random() < 0.7) {
+ var c = Math.floor(Math.random() * 16777215);
+ return new THREE.MeshBasicMaterial({color: c});
+ } else {
+ return this.materials.billboard[Math.floor(Math.random() * this.materials.billboard.length)];
+ }
+ }
+
+ getRandomTicker() {
+ var phrases = [
+ 'NASDAQ',
+ '105 43 21',
+ 'Dow 30',
+ '20,619.77',
+ '-2.03 (-0.09%)',
+ 'S&P 500',
+ 'Pigeon Alert! Extreme Pigeon Danger!',
+ 'Lunar Eclipse Obscured By Clouds',
+ 'Rumor Of Kitty Kibble Shortage Causes Futures To Drop; Consumers Stockpile',
+ ];
+
+ var top_phrase = phrases[Math.floor(Math.random() * phrases.length)];
+ var bot_phrase = phrases[Math.floor(Math.random() * phrases.length)];
+
+ var w = 1024;
+ var h = 256;
+ var canvas = document.createElement('canvas');
+ canvas.width = w;
+ canvas.height = h;
+ var context = canvas.getContext('2d');
+ context.fillStyle = '#000000';
+ context.fillRect(0,0,w,h);
+ context.font = "bold 100px Arial";
+ context.fillStyle = '#ffc935'
+ context.fillText(top_phrase,128,128);
+ context.fillText(bot_phrase,800,200);
+
+ context.fillStyle = '#000000';
+ context.lineWidth = 3;
+ for (var i = 0; i < w; i+=10) {
+ context.beginPath();
+ context.moveTo(i,0);
+ context.lineTo(i,h);
+ context.stroke();
+ }
+
+ for (var i = 0; i < h; i+=10) {
+ context.beginPath();
+ context.moveTo(0,i);
+ context.lineTo(w,i);
+ context.stroke();
+ }
+ var texture = new THREE.Texture(canvas);
+ texture.needsUpdate = true;
+ var material = new THREE.MeshLambertMaterial({
+ map: texture,
+ side: THREE.DoubleSide
+ });
+
+ return material;
+ }
+
+ getRandomSign() {
+ var c = Math.floor(Math.random() * 16777215);
+ return new THREE.MeshBasicMaterial({color: c});
+ }
+
+ loadResources(callback) {
+ var resources = [];
+
+ var tex_loader = new THREE.TextureLoader();
+ var load_texture = (function(set, key, path, resolve, reject) {
+ var texture = tex_loader.load(path);
+ if (set !== undefined) {
+ if (!this.materials[set]) {
+ this.materials[set] = [];
+ }
+ this.materials[set].push(new THREE.MeshLambertMaterial({map: texture, overdraw: 0.5}));
+ } else {
+ this.materials[key] = new THREE.MeshLambertMaterial({map: texture, overdraw: 0.5});
+ }
+ if (texture) {
+ resolve("Successfully loaded texture");
+ } else {
+ reject(Error("Could not load texture"));
+ }
+ }).bind(this);
+
+ resources.push(new Promise(function(resolve, reject) {
+ load_texture('billboard', 'ad1', './textures/ad1.jpg', resolve, reject);
+ }));
+
+ resources.push(new Promise(function(resolve, reject) {
+ load_texture('billboard', 'ad2', './textures/ad2.jpg', resolve, reject);
+ }));
+
+ resources.push(new Promise(function(resolve, reject) {
+ load_texture('billboard', 'ad2', './textures/ad3.png', resolve, reject);
+ }));
+
+ return Promise.all(resources);
+ }
+
+ /*************************** Basic Operations *****************************/
+
+ scale(parent, sl, sw, sh) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+
+ switch (parent.type) {
+ case SHAPES.BOX:
+ var child = (JSON.parse(JSON.stringify(parent)));
+ child.l = parent.l * sl;
+ child.w = parent.w * sw;
+ child.h = parent.h * sh;
+ return [child];
+ }
+
+ return [parent];
+ }
+
+ rotate(parent, rx, ry, rz) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+
+ switch (parent.type) {
+ case SHAPES.BOX:
+ var child = (JSON.parse(JSON.stringify(parent)));
+ child.rx = parent.rx + rx;
+ child.ry = parent.ry + ry;
+ child.rz = parent.rz + rz;
+ return [child];
+ }
+
+ return [];
+ }
+
+ billboard(parent) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+
+ switch (parent.type) {
+ case SHAPES.BOX:
+ var child = (JSON.parse(JSON.stringify(parent)));
+ var sign = Random.sign();
+ if (Math.random() > 0.5) {
+ child.l = parent.l * 0.8;
+ child.w = parent.w * 0.1;
+ child.h = parent.h;
+ child.x = parent.x;
+ child.y = parent.y + 0.5 * parent.h - 0.1 * parent.h - 0.5 * child.h;
+ child.z = parent.z + sign * 0.5 * parent.w;
+ child.material = this.getRandomAd();
+ child.isTerminal = true;
+ } else {
+ child.l = parent.l * 0.1;
+ child.w = parent.w * 0.8;
+ child.h = parent.h;
+ child.x = parent.x + sign * 0.5 * parent.l;
+ child.y = parent.y + 0.5 * parent.h - 0.1 * parent.h - 0.5 * child.h;
+ child.z = parent.z;
+ child.material = this.getRandomAd();
+ child.isTerminal = true;
+ }
+
+ parent.isTerminal = true; // makes sure the sign isnt floating
+
+ return [parent, child];
+ }
+
+ return [parent];
+ }
+
+ sign(parent) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+
+ switch (parent.type) {
+ case SHAPES.BOX:
+ var child = (JSON.parse(JSON.stringify(parent)));
+ var sign1 = Random.sign(); // which part face
+ var sign2 = Random.sign(); // which face
+
+ if (Math.random() > 0.5) {
+ child.l = parent.w * 0.05; // sign thickness
+ child.w = parent.y * 0.1; // sign wideness
+ child.h = parent.h * 0.5; // sign height
+ child.x = parent.x + sign1 * (0.5 * parent.l - child.l);
+ child.y = parent.y;
+ child.z = parent.z + sign2 * (0.5 * parent.w + 0.5 * child.w);
+ child.isTerminal = true;
+ } else {
+ child.l = parent.y * 0.1; // sign wideness
+ child.w = parent.w * 0.05; // sign thickness
+ child.h = parent.h * 0.5; // sign height
+ child.x = parent.x + sign2 * (0.5 * parent.l + 0.5 * child.l);
+ child.y = parent.y;
+ child.z = parent.z + sign1 * (0.5 * parent.w - child.w);
+ child.isTerminal = true;
+ }
+ child.material = this.getRandomSign();
+ parent.isTerminal = true; // makes sure the sign isnt floating
+ return [parent, child];
+ }
+
+ return [parent];
+ }
+
+ ticker(parent) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+
+ switch (parent.type) {
+ case SHAPES.BOX:
+ var child = (JSON.parse(JSON.stringify(parent)));
+ child.l = parent.l * 1.1;
+ child.w = parent.w * 1.1;
+ child.h = parent.h * 0.9;
+ child.material = this.getRandomTicker();
+ child.isTerminal = true;
+
+ parent.isTerminal = true; // makes sure the sign isnt floating
+ return [parent, child];
+ }
+ return [parent];
+ }
+
+
+ hdivide(parent) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+
+ switch (parent.type) {
+ case SHAPES.BOX:
+ var child1 = (JSON.parse(JSON.stringify(parent)));
+ child1.l = parent.l / 2;
+ child1.x = parent.x + parent.l / 4;
+
+ var child2 = (JSON.parse(JSON.stringify(parent)));
+ child2.l = parent.l / 2;
+ child2.x = parent.x - parent.l / 4;
+
+ return [child1, child2];
+ }
+
+ return [parent];
+ }
+
+ hzdivide(parent) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+
+ switch (parent.type) {
+ case SHAPES.BOX:
+ var child1 = (JSON.parse(JSON.stringify(parent)));
+ child1.w = parent.w / 2;
+ child1.z = parent.z + parent.w / 4;
+
+ var child2 = (JSON.parse(JSON.stringify(parent)));
+ child2.w = parent.w / 2;
+ child2.z = parent.z - parent.w / 4;
+
+ return [child1, child2];
+ }
+
+ return [parent];
+ }
+
+
+ vdivide(parent) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+
+ switch (parent.type) {
+ case SHAPES.BOX:
+ var child1 = (JSON.parse(JSON.stringify(parent)));
+ child1.h = parent.h / 2;
+ child1.y = parent.y + (parent.h / 4);
+
+ var child2 = (JSON.parse(JSON.stringify(parent)));
+ child2.h = parent.h / 2;
+ child2.y = parent.y - (parent.h / 4);
+
+ return [child1, child2];
+ }
+
+ return [parent];
+ }
+
+ subdivide(parent) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+
+ var children = [];
+ var v_parts = this.vdivide(parent);
+ var temp = [];
+ v_parts.forEach((function(s) {
+ temp.push(this.hdivide(s));
+ }).bind(this));
+ var h_parts = [].concat.apply([], temp);
+ var temp = [];
+ h_parts.forEach((function(s) {
+ temp.push(this.hzdivide(s));
+ }).bind(this));
+ children.push([].concat.apply([], temp));
+
+ return [].concat.apply([], children);
+ }
+
+ swap(parent, type) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+
+ switch (type) {
+ case SHAPES.ICOSAHEDRON:
+ var child = (JSON.parse(JSON.stringify(parent)));
+ child.type = SHAPES.ICOSAHEDRON;
+ child.detail = 1;
+ child.radius = Math.min(child.l, child.w);
+ return [child];
+ }
+ return [parent];
+ }
+
+ deleteShape(parent) {
+ if (parent.isTerminal) {
+ return [parent];
+ }
+ return [];
+ }
+
+ /************************** Composite Operations **************************/
+
+ uniform(parent, n) {
+ var children = [parent];
+ for (var j = 0; j < n; j++) {
+ var levels = [];
+ for (var i = 0; i < children.length; i++) {
+ var c = this.vdivide(children[i]);
+ levels.push(c[0]);
+ levels.push(c[1]);
+ }
+ children = levels;
+ }
+ return children;
+ }
+
+ huniform(parent, n) {
+ var children = [parent];
+ for (var j = 0; j < n; j++) {
+ var levels = [];
+ for (var i = 0; i < children.length; i++) {
+ var c = this.hdivide(children[i]);
+ levels.push(c[0]);
+ levels.push(c[1]);
+ }
+ children = levels;
+ }
+ return children;
+ }
+
+
+ /************************** Assembly Operations ***************************/
+
+ iterate(shape) {
+ switch (shape.symbol) {
+ case SYMBOLS.A:
+ var r = Math.random();
+ if (Math.random() < 0.7) {
+ shape.symbol = SYMBOLS.C;
+ return [shape];
+ } else {
+ var levels = this.huniform(shape, 2);
+ for (var i = 0; i < levels.length; i++) {
+ levels[i].symbol = SYMBOLS.C;
+ }
+ levels[0].symbol = SYMBOLS.S;
+ levels[levels.length-1].symbol = SYMBOLS.S;
+ return levels;
+ }
+ case SYMBOLS.C:
+ if (Math.random() > 0.3) { // UNIFORM
+ var levels = this.uniform(shape, 3);
+ for (var i = 0; i < levels.length; i++) {
+ levels[i].symbol = SYMBOLS.U;
+ }
+ } else { // TOP-BOTTOM
+ var levels = this.uniform(shape, 3);
+ for (var i = 0; i < levels.length; i++) {
+ levels[i].symbol = SYMBOLS.U;
+ }
+ levels[0].symbol = SYMBOLS.T;
+ levels[levels.length-1].symbol = SYMBOLS.B;
+ }
+ return levels;
+ case SYMBOLS.U:
+ switch (this.uparam.type) {
+ case 0:
+ var level = this.billboard(shape);
+ level[0].symbol = SYMBOLS.X;
+ level[1].symbol = SYMBOLS.X;
+ break;
+ case 1:
+ var level = this.sign(shape);
+ level[0].symbol = SYMBOLS.X;
+ level[1].symbol = SYMBOLS.X;
+ break;
+ case 2:
+ var levels = this.vdivide(shape);
+ var ratio = this.uparam.r;
+ var l0 = this.scale(levels[0], 1, 1, 1 + ratio);
+ l0[0].symbol = SYMBOLS.X;
+ var l1 = this.scale(levels[1], 0.9, 0.9, 1 - ratio);
+ l1[0].symbol = SYMBOLS.X;
+ var level = [l0[0], l1[0]];
+ break;
+ }
+ return level;
+ case SYMBOLS.T:
+ if (Math.random() < 0.5) {
+ var level = this.ticker(shape);
+ level[0].symbol = SYMBOLS.X;
+ level[1].symbol = SYMBOLS.X;
+ } else {
+ var sr = Math.random() / 2 + 0.5;
+ var level = this.scale(shape, sr, sr, 1);
+ level[0].symbol = SYMBOLS.X;
+ }
+ return level;
+ case SYMBOLS.B:
+ var level = this.scale(shape, 1.2, 1.2, 1);
+ level[0].symbol = SYMBOLS.X;
+ return level;
+ case SYMBOLS.S:
+ var level = this.scale(shape, 0.8, 0.8, 0.8);
+ level[0].symbol = SYMBOLS.X;
+ return level;
+ default:
+ return [shape];
+ }
+ }
+
+ evalShapes(scene, shapes, pos) {
+ for (var i = 0; i < shapes.length; i++) {
+ var shape = shapes[i];
+ switch (shape.type) {
+ case SHAPES.BOX:
+ var geometry = new THREE.BoxGeometry(shape.l, shape.h, shape.w);
+ geometry.applyMatrix(new THREE.Matrix4().makeTranslation(shape.x, shape.y, shape.z));
+ geometry.rotateX(shape.rx);
+ geometry.rotateY(shape.ry);
+ geometry.rotateZ(shape.rz);
+ if (shape.material) {
+ var material = shape.material;
+ } else {
+ var material = new THREE.MeshLambertMaterial({color: this.color});
+ }
+ var mesh = new THREE.Mesh(geometry, material);
+ mesh.position.set(pos.x, 0, pos.z);
+ scene.add(mesh);
+ var geo = new THREE.EdgesGeometry(mesh.geometry);
+ var mat = new THREE.LineBasicMaterial( { color: 0xcccccc, linewidth: 1 } );
+ var wireframe = new THREE.LineSegments(geo, mat);
+ mesh.add(wireframe);
+
+ break;
+ case SHAPES.ICOSAHEDRON:
+ var geometry = new THREE.IcosahedronGeometry(shape.radius, shape.detail);
+ geometry.applyMatrix(new THREE.Matrix4().makeTranslation(shape.x, shape.y, shape.z));
+ geometry.rotateX(shape.rx);
+ geometry.rotateY(shape.ry);
+ geometry.rotateZ(shape.rz);
+ var c = Math.floor(Math.random() * 16777215);
+ var material = new THREE.MeshBasicMaterial({color: c, wireframe: false});
+ var mesh = new THREE.Mesh(geometry, material);
+ mesh.position.set(pos.x, 0, pos.z);
+ scene.add(mesh);
+ var geo = new THREE.EdgesGeometry(mesh.geometry);
+ var mat = new THREE.LineBasicMaterial( { color: 0xcccccc, linewidth: 1 } );
+ var wireframe = new THREE.LineSegments(geo, mat);
+ mesh.add(wireframe);
+
+ break;
+ }
+ }
+ }
+
+ generateBuilding(scene, options) {
+ var box_length = options.length * 2;
+ var box_width = options.width * 2;
+ var box_height = options.height;
+ this.color = 0xA8A8A8;
+ this.uparam = {
+ type: Math.floor(Math.random() * 10) % 3,
+ r: Math.random()
+ };
+
+ var shape = {
+ symbol: SYMBOLS.A,
+ type: SHAPES.BOX,
+ l: options.length,
+ w: options.width,
+ h: options.height,
+ x: 0,
+ y: 0.5 * options.height,
+ z: 0,
+ rx: 0,
+ ry: 0,
+ rz: 0
+ };
+
+ var building = [shape];
+ for (var i = 0; i < 3; i++) {
+ var temp = [];
+ building.forEach((function(shape) {
+ temp.push(this.iterate(shape));
+ }).bind(this));
+ building = [].concat.apply([], temp);
+ }
+
+ this.evalShapes(scene, building, {x: options.x, z: options.z});
+ return;
+ }
+
+}
diff --git a/src/cturtle.js b/src/cturtle.js
new file mode 100644
index 00000000..90b389c5
--- /dev/null
+++ b/src/cturtle.js
@@ -0,0 +1,121 @@
+const THREE = require('three')
+
+var CTurtleState = function(pos, dir) {
+ return {
+ pos: new THREE.Vector3(pos.x, pos.y, pos.z),
+ dir: new THREE.Vector3(dir.x, dir.y, dir.z)
+ }
+}
+
+export default class CTurtle {
+
+ constructor(w,h) {
+ var startX = Math.floor(Math.random() * w);
+ var startY = Math.floor(Math.random() * h);
+ this.state = new CTurtleState(new THREE.Vector3(startX, startY, 0), new THREE.Vector3(1,0,0));
+ this.stateStack = [];
+ this.canvas = document.createElement('canvas');
+ this.canvas.width = w;
+ this.canvas.height = h;
+ this.context = this.canvas.getContext('2d');
+ this.context.fillStyle = '#ffffff';
+ this.context.fillRect( 0, 0, w, h );
+
+ // TODO: Start by adding rules for '[' and ']' then more!
+ // Make sure to implement the functions for the new rules inside Turtle
+ if (typeof grammar === "undefined") {
+ this.renderGrammar = {
+ '+' : this.rotateTurtle.bind(this, 37),
+ '-' : this.rotateTurtle.bind(this, -58),
+ '*' : this.rotateTurtle.bind(this, 100),
+ '/' : this.rotateTurtle.bind(this, -90),
+ 'M' : this.moveTurtle.bind(this, 20),
+ '[' : this.saveState.bind(this),
+ ']' : this.restoreState.bind(this),
+ };
+ } else {
+ this.renderGrammar = grammar;
+ }
+ }
+
+ saveState() {
+ this.stateStack.push(new CTurtleState(this.state.pos, this.state.dir));
+ }
+
+ restoreState() {
+ this.state = this.stateStack.pop();
+ }
+
+ // Resets the turtle's position to the origin
+ // and its orientation to the Y axis
+ clear() {
+ this.state = new CTurtleState(new THREE.Vector3(0,0,0), new THREE.Vector3(1,0,0));
+ }
+
+ resetState() {
+ var w = this.canvas.width;
+ var h = this.canvas.height;
+ var startX = Math.floor(Math.random() * w);
+ var startY = Math.floor(Math.random() * h);
+ this.state = new CTurtleState(new THREE.Vector3(startX, startY, 0), new THREE.Vector3(1,0,0));
+ }
+
+ // A function to help you debug your turtle functions
+ // by printing out the turtle's current state.
+ printState() {
+ console.log(this.state.pos)
+ console.log(this.state.dir)
+ }
+
+ // Rotate the turtle's _dir_ vector by each of the
+ // Euler angles indicated by the input.
+ rotateTurtle(degrees) {
+ var e = new THREE.Euler(
+ 0,
+ 0,
+ degrees * 3.14/180);
+ this.state.dir.applyEuler(e);
+ }
+
+ moveTurtle(len) {
+ var pos = this.state.pos;
+ var newpos = this.state.pos.clone();
+ newpos.addScaledVector(this.state.dir, len);
+
+ var px = pos.x;
+ var py = pos.y;
+ var npx = newpos.x;
+ var npy = newpos.y;
+
+ if (npx < 0 || npx > this.canvas.width || npy < 0 || npy > this.canvas.height) {
+ this.resetState();
+ return;
+ }
+
+ this.context.beginPath();
+ this.context.lineWidth = 1;
+ this.context.moveTo(px, py);
+ this.context.lineTo(npx,npy);
+ this.context.stroke();
+ this.state.pos = newpos;
+ }
+
+ // Call the function to which the input symbol is bound.
+ // Look in the Turtle's constructor for examples of how to bind
+ // functions to grammar symbols.
+ renderSymbol(symbolNode) {
+ var func = this.renderGrammar[symbolNode.character];
+ if (func) {
+ func();
+ }
+ };
+
+ // Invoke renderSymbol for every node in a linked list of grammar symbols.
+ renderSymbols(linkedList) {
+ var currentNode;
+ for(currentNode = linkedList.head; currentNode != null; currentNode = currentNode.next) {
+ this.renderSymbol(currentNode);
+ }
+ return this.canvas;
+ }
+}
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/lsystem.js b/src/lsystem.js
new file mode 100644
index 00000000..ef5851f8
--- /dev/null
+++ b/src/lsystem.js
@@ -0,0 +1,182 @@
+// A class that represents a symbol replacement rule to
+// be used when expanding an L-system grammar.
+function Rule(prob, str) {
+ this.probability = prob; // The probability that this Rule will be used when replacing a character in the grammar string
+ this.successorString = str; // The string that will replace the char that maps to this Rule
+}
+
+// TODO: Implement a linked list class and its requisite functions
+// as described in the homework writeup
+function LinkedList() {
+ this.head = undefined;
+ this.tail = undefined;
+}
+
+function LinkedListNode() {
+ this.character = '';
+ this.prev = undefined;
+ this.next = undefined;
+}
+
+// TODO: Turn the string into linked list
+export function StringToLinkedList(input_string) {
+ // ex. assuming input_string = "F+X"
+ // you should return a linked list where the head is
+ // at Node('F') and the tail is at Node('X')
+ var ll = new LinkedList();
+
+ var prev = undefined;
+ for (var i = 0; i < input_string.length; i++) {
+ var node = new LinkedListNode();
+ node.character = input_string.charAt(i);
+ node.prev = prev;
+ if (prev) {
+ prev.next = node;
+ }
+ prev = node;
+
+ if (i == 0) {
+ ll.head = node;
+ }
+
+ if (i == input_string.length - 1) {
+ ll.tail = node;
+ }
+ }
+
+ return ll;
+}
+
+// TODO: Return a string form of the LinkedList
+export function LinkedListToString(linkedList) {
+ // ex. Node1("F")->Node2("X") should be "FX"
+ var temp = [];
+ for (var node = linkedList.head; node != linkedList.tail; node = node.next) {
+ temp.push(node.character);
+ }
+ var result = temp.join("");
+ return result;
+}
+
+// TODO: Given the node to be replaced,
+// insert a sub-linked-list that represents replacementString
+function replaceNode(linkedList, node, replacementString) {
+}
+
+function pickRuleFromDistr(distribution) {
+ var rules = [];
+ var probs = [];
+ for (var i = 0; i < distribution.length; i++) {
+ rules.push(distribution[i].successorString);
+ probs.push(distribution[i].probability);
+ }
+ var filtered = probs.filter(function(val) { return val > 0; });
+ var minProb = Math.min.apply(null, filtered);
+ var probs = probs.map(function(x) { return Math.round(x / minProb); });
+ var cmlProbs = [];
+ probs.reduce(function(a,b,i) { return cmlProbs[i] = a+b; }, 0);
+
+ var r = Math.random() * cmlProbs[cmlProbs.length-1]-0.0000001; // ensure always less than max
+ for (var i = 0; i < cmlProbs.length; i++) {
+ if (r < cmlProbs[i]) {
+ return rules[i];
+ }
+ }
+ console.log("No Rules to pick from!");
+}
+
+export default function Lsystem(axiom, grammar, iterations) {
+ // default LSystem
+ this.axiom = "X";
+ this.grammar = {};
+ // this.grammar['X'] = [
+ // new Rule(1.0, '[-FX][+FX]')
+ // ];
+ this.iterations = 0;
+ this.expansions = {};
+
+ // Set up the axiom string
+ if (typeof axiom !== "undefined") {
+ this.axiom = axiom;
+ }
+
+ // Set up the grammar as a dictionary that
+ // maps a single character (symbol) to a Rule.
+ if (typeof grammar !== "undefined") {
+ this.grammar = Object.assign({}, grammar);
+ }
+
+ // Set up iterations (the number of times you
+ // should expand the axiom in DoIterations)
+ if (typeof iterations !== "undefined") {
+ this.iterations = iterations;
+ }
+
+ // A function to alter the axiom string stored
+ // in the L-system
+ this.UpdateAxiom = function(axiom) {
+ // Setup axiom
+ if (typeof axiom !== "undefined") {
+ this.axiom = axiom;
+ this.expansions = {};
+ }
+ }
+
+ this.UpdateRules = function(rules) {
+ this.grammar = {};
+ this.expansions = {};
+ for (var i = 0; i < rules.length; i++) {
+ var entry = rules[i];
+ var prob = entry.Prob;
+ var data = entry.Rule.replace(/\s/g, "").split("=");
+ if (data.length == 2) {
+ var symbol = data[0];
+ var rule = data[1];
+ var R = new Rule(prob, rule);
+ if (!this.grammar[symbol]) {
+ this.grammar[symbol] = [];
+ }
+ this.grammar[symbol].push(R);
+ } else {
+ console.log("Invalid Rule: " + i);
+ }
+ }
+ }
+
+ this.DoExpansion = function(n) {
+ if (n < 0) {
+ throw 'Invalid number of expansions!';
+ }
+
+ if (n == 0) {
+ this.expansions[0] = this.axiom;
+ } else if (!this.expansions[n]) {
+ var prev = this.DoExpansion(n-1);
+ var expsn = [];
+ for (var i = 0; i < prev.length; i++) {
+ var c = prev.charAt(i);
+ if (this.grammar[c] && this.grammar[c].length > 0) {
+ var r = pickRuleFromDistr(this.grammar[c]);
+ expsn.push(r);
+ } else {
+ expsn.push(c);
+ }
+ }
+ this.expansions[n] = expsn.join("");
+ }
+ return this.expansions[n];
+ }
+
+ // TODO
+ // This function returns a linked list that is the result
+ // of expanding the L-system's axiom n times.
+ // The implementation we have provided you just returns a linked
+ // list of the axiom.
+ this.DoIterations = function(n) {
+ if (!this.expansions[n]) {
+ this.DoExpansion(n);
+ }
+ var lSystemLL = StringToLinkedList(this.expansions[n]);
+ return lSystemLL;
+ }
+}
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 00000000..eef6b62e
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,452 @@
+
+const THREE = require('three'); // older modules are imported like this. You shouldn't have to worry about this much
+import Framework from './framework'
+import Builder from './builder.js'
+import Turtle from './turtle.js'
+import CTurtle from './cturtle.js'
+import Lsystem, {LinkedListToString} from './lsystem.js'
+
+var settings = {
+ seed: 1.0,
+ resetCamera: function() {},
+ newSeed: function(newVal) { settings.seed = Math.random(); },
+ size: 10.0,
+ resolution: 128,
+ split: 0.0,
+ numBuildings: 1000
+}
+
+var lsystem_settings = {
+ Axiom: "A",
+ Rules: [
+ {Rule: "A=[[[RRRM]RRRM]RRRM]", Prob: 1.0},
+ {Rule: "B=RMB", Prob: 1.0},
+ {Rule: "C=MC", Prob: 5.0},
+ {Rule: "C=M", Prob: 1.0},
+ {Rule: "R=-MR", Prob: 1.0},
+ {Rule: "R=+MR", Prob: 1.0},
+ {Rule: "R=*MR", Prob: 1.0},
+ {Rule: "R=/MR", Prob: 1.0},
+ {Rule: "P=*MR", Prob: 1.0},
+ {Rule: "P=/MR", Prob: 1.0},
+ {Rule: "M=[RB]PMC", Prob: 1.0},
+ ],
+ iterations: 4,
+ Render: function() {}
+}
+
+var turtle;
+
+var ZONES = {
+ UNZONED : {value: 1, name: "Unzoned", color: 0xd1cfca},
+ ROAD : {value: 2, name: "Road", color: 0x2c2a2d},
+};
+
+var STATUS = {
+ VACANT : {value: 1, name: "Vacant", color: 0xd1cfca},
+ OCCUPIED : {value: 2, name: "Occupied", color: 0x2c2a2d},
+};
+
+var city = {
+ grid: [],
+ maps: {}
+}
+
+// called after the scene loads
+function onLoad(framework) {
+ var scene = framework.scene;
+ var camera = framework.camera;
+ var renderer = framework.renderer;
+ var gui = framework.gui;
+ var stats = framework.stats;
+
+ // initialize a simple box and material
+ var directionalLight = new THREE.DirectionalLight( 0xffffff, 1 );
+ directionalLight.color.setHSL(0.1, 1, 0.95);
+ directionalLight.position.set(1, 3, 2);
+ directionalLight.position.multiplyScalar(10);
+ scene.add(directionalLight);
+
+ // set camera position
+ camera.position.set(0,10,10);
+ camera.lookAt(new THREE.Vector3(0,0,0));
+
+ gui.add(camera, 'fov', 0, 180).onChange(function(newVal) {
+ camera.updateProjectionMatrix();
+ });
+ gui.add(settings, 'resetCamera').onChange(function() {
+ camera.position.set(0,10,10);
+ camera.lookAt(new THREE.Vector3(0,0,0));
+ });
+ gui.add(settings, 'split', 0, 10);
+
+
+ // var roads = gui.addFolder('Roads');
+ // roads.add(lsystem_settings, 'Axiom')
+ // for (var i = 0; i < lsystem_settings.Rules.length; i++) {
+ // roads.add(lsystem_settings.Rules[i], 'Rule');
+ // roads.add(lsystem_settings.Rules[i], 'Prob');
+ // }
+ // roads.add(lsystem_settings, 'Render').onChange(function() {
+ // regenerateCity(framework);
+ // });
+ // roads.open();
+
+ regenerateCity(framework);
+}
+
+function resizeCanvas(canvas) {
+ var canvas2 = document.createElement('canvas');
+ canvas2.width = 512;
+ canvas2.height = 512;
+ var context = canvas2.getContext( '2d' );
+ context.imageSmoothingEnabled = false;
+ context.webkitImageSmoothingEnabled = false;
+ context.mozImageSmoothingEnabled = false;
+ context.drawImage( canvas, 0, 0, canvas2.width, canvas2.height );
+ return canvas2;
+}
+
+function getMapValue(canvas, ni, nj) {
+ var i = canvas.width * ni;
+ var j = canvas.height * nj;
+ var context = canvas.getContext('2d');
+ var pixel = context.getImageData(i, j, 1, 1);
+ var data = pixel.data;
+ return new THREE.Vector4(data[0], data[1], data[2], data[3]/255);
+}
+
+function generateTexture() {
+ var size = settings.resolution;
+ var canvas = document.createElement('canvas');
+ canvas.width = size;
+ canvas.height = size;
+ var context = canvas.getContext('2d');
+ context.fillStyle = '#ffffff';
+ context.fillRect(0, 0, size, size);
+
+ // generate grass texture
+ for (var i = 0; i < size; i++) {
+ for (var j = 0; j < size; j++) {
+ var r = Math.floor(50 * Math.random());
+ var g = Math.floor(50 * Math.random()) + 150;
+ var b = Math.floor(50 * Math.random());
+ context.fillStyle = 'rgb(' + [r, r, r].join( ',' ) + ')';
+ context.fillRect( i, j, 1, 1 );
+ }
+ }
+
+ // generate roads
+ for (var i = 0; i < size; i++) {
+ for (var j = 0; j < size; j++) {
+ var norm_i = i / size;
+ var norm_j = j / size;
+ var rgba = getMapValue(city.maps['roads'], norm_i, norm_j);
+ if (rgba.x < 250) {
+ context.fillStyle = 'rgba(' + [200, 200, 200, (255 - rgba.x) / 255].join( ',' ) + ')';
+ context.fillRect( i, j, 1, 1 );
+ }
+ }
+ }
+ return resizeCanvas(canvas);
+}
+
+function loadMapFromCanvas(framework, key, canvas) {
+ var scene = framework.scene;
+ var renderer = framework.renderer;
+ var texture = new THREE.Texture(canvas);
+ texture.anisotropy = renderer.getMaxAnisotropy();
+ texture.needsUpdate = true;
+ var material = new THREE.MeshBasicMaterial({
+ map: texture,
+ side: THREE.DoubleSide
+ });
+ var geometry = new THREE.PlaneBufferGeometry( settings.size, settings.size, 1 );
+ var plane = new THREE.Mesh( geometry, material );
+ plane.rotateX(Math.PI / 2.0);
+ plane.name = key;
+ scene.add(plane);
+ city.maps[key] = canvas;
+ return canvas;
+}
+
+function loadMapFromPath(framework, key, path) {
+ var promise = new Promise(function(resolve, reject) {
+ var img = new Image();
+ img.onload = function () {
+ var canvas = document.createElement('canvas');
+ canvas.width = 512;
+ canvas.height = 512;
+ var context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ if (loadMapFromCanvas(framework, key, canvas)) {
+ resolve("Stuff worked!");
+ } else {
+ reject(Error("It broke"));
+ }
+ };
+ img.src = path;
+ });
+ return promise;
+}
+
+function getZoneProperties(zoneValue) {
+ var myKeys = Object.keys(ZONES);
+ var matchingKeys = myKeys.filter(function(key){
+ return ZONES[key].value == zoneValue;
+ });
+ return ZONES[matchingKeys[0]];
+}
+
+function regenerateCity(framework) {
+ var scene = framework.scene;
+ var camera = framework.camera;
+ var renderer = framework.renderer;
+ var gui = framework.gui;
+ var stats = framework.stats;
+
+ // clear scene
+ scene.children.forEach(function(object){
+ scene.remove(object);
+ });
+
+ // reset grid
+ city.grid = [];
+ for (var i = 0; i < settings.resolution; i++) {
+ city.grid.push([]);
+ for (var j = 0; j < settings.resolution; j++) {
+ city.grid[i].push({
+ zone: ZONES.UNZONED.value,
+ status: STATUS.VACANT.value
+ });
+ }
+ }
+
+ // generate roads
+ var lsys = new Lsystem(lsystem_settings.Axiom);
+ lsys.UpdateRules(lsystem_settings.Rules);
+ var result = lsys.DoIterations(lsystem_settings.iterations);
+ // turtle = new Turtle(city.grid);
+ // turtle.renderSymbols(result);
+
+ var cturtle = new CTurtle(256,256);
+ var turtle_canvas = cturtle.renderSymbols(result);
+ loadMapFromCanvas(framework, 'roads', turtle_canvas);
+ // var debug = document.getElementById('debug');
+ // debug.appendChild(turtle_canvas);
+
+ loadMapFromPath(framework, 'population', './maps/population.png')
+ .then(function(response) {
+ try {
+ generateTerrain(framework);
+ generateBuildings(framework);
+ } catch (error) {
+ console.log("error " + error);
+ }
+ }, function(error) {
+ console.error("Failed!", error);
+ });
+
+ var light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
+ scene.add(light);
+ var light = new THREE.PointLight( 0xffffff, 10, 100 );
+ light.position.set( 50, 50, 50 );
+ scene.add(light);
+}
+
+
+function generateTerrain(framework) {
+ var scene = framework.scene;
+ var renderer = framework.renderer;
+
+ var geometry = new THREE.PlaneBufferGeometry( settings.size, settings.size, 1 );
+ var texture = new THREE.Texture(generateTexture());
+ texture.anisotropy = renderer.getMaxAnisotropy();
+ texture.needsUpdate = true;
+ var material = new THREE.MeshBasicMaterial({
+ map: texture,
+ side: THREE.DoubleSide
+ });
+ var plane = new THREE.Mesh( geometry, material );
+
+ plane.rotateX(Math.PI / 2.0);
+ plane.name = "terrain";
+ scene.add(plane);
+}
+
+function generateRoads() {
+ // vertical streets
+ for (var i = 0; i < settings.resolution; i+=16) {
+ for (var j = 0; j < settings.resolution; j++) {
+ city.grid[i][j] = {
+ zone: ZONES.ROAD.value,
+ status: STATUS.OCCUPIED.value
+ };
+ }
+ }
+
+ // horizontal streets
+ for (var i = 0; i < settings.resolution; i+=8) {
+ for (var j = 0; j < settings.resolution; j++) {
+ city.grid[j][i] = {
+ zone: ZONES.ROAD.value,
+ status: STATUS.OCCUPIED.value
+ };
+ }
+ }
+}
+
+
+function generateBuildings(framework) {
+ var scene = framework.scene;
+ var loader = new THREE.TextureLoader();
+ var square_size = settings.size / settings.resolution;
+ var half_square_size = 0.5 * settings.size / settings.resolution;
+ var offset = settings.size / 2.0 - square_size / 2.0;
+ var builder = new Builder();
+
+ builder.loadResources()
+ .then(function(response) {
+ try {
+
+ console.log("hey");
+
+ var building_sizes = {
+ small: [
+ [2,2,2],
+ [3,1,2]
+ ],
+ medium: [
+ [3,3,6],
+ [3,3,10],
+ ],
+ large: [
+ [5,5,9],
+ [5,5,15],
+ [5,5,20]
+ ]
+ }
+
+ var canvas = document.createElement('canvas');
+ canvas.width = settings.resolution;
+ canvas.height = settings.resolution;
+ var context = canvas.getContext('2d');
+ context.fillStyle = '#ffffff';
+ context.fillRect(0,0,settings.resolution, settings.resolution);
+ context.fillStyle = '#000000';
+
+ // var debug = document.getElementById('debug');
+ // debug.appendChild(canvas);
+
+ for (var n = 0; n < settings.numBuildings; n++) {
+ // randomly pick points
+ var norm_i = Math.random();
+ var norm_j = Math.random();
+
+ // keep point based on probability (relative to population)
+ var rgba = getMapValue(city.maps['population'], norm_i, norm_j);
+
+ var keep = Math.random() * 255;
+ if (keep > rgba.x) {
+
+ // pick a building size semi-randomly
+ if (keep < 100) {
+ var building_size = building_sizes.large[Math.floor(Math.random()*6)%3]
+ } else if (keep < 200) {
+ var building_size = building_sizes.medium[Math.floor(Math.random()*4)%2]
+ } else {
+ var building_size = building_sizes.small[Math.floor(Math.random()*3)%2]
+ }
+
+ // check that the building fits in the location
+ var i = Math.floor(norm_i * settings.resolution);
+ var j = Math.floor(settings.resolution - norm_j * settings.resolution);
+ var bbox_x = [-Math.floor((building_size[0]+1)/2), Math.floor((building_size[0]+1)/2)];
+ var bbox_z = [-Math.floor((building_size[1]+1)/2), Math.floor((building_size[1]+1)/2)];
+ var isVacant = true;
+ for (var q = bbox_x[0]; q < bbox_x[1]; q++) {
+ for (var r = bbox_z[0]; r < bbox_z[1]; r++) {
+
+ // bbox must not be out of grid and must not be occupied
+ if ((q+i).clamp(0,settings.resolution-1) != q+i ||
+ (r+j).clamp(0,settings.resolution-1) != r+j ||
+ city.grid[q+i][r+j].status != STATUS.VACANT.value) {
+ isVacant = false;
+ break;
+ }
+ }
+ }
+
+ if (isVacant) {
+ // generate building
+ var scale_factor = settings.size / settings.resolution; // 0.5 bc box size is half size width
+ var density = Math.pow((255 - rgba.x) / 255, 2) * 20;
+ var density = 0.4;
+ var options = {
+ iterations: 5,
+ length: scale_factor * building_size[0],
+ width: scale_factor * building_size[1],
+ height: scale_factor * (building_size[2] + (Math.random() * building_size[2])),
+ x: i * square_size - offset,
+ z: j * square_size - offset
+ };
+ var building = builder.generateBuilding(scene, options);
+
+ // Update the grid
+ for (var q = bbox_x[0]; q < bbox_x[1]; q++) {
+ for (var r = bbox_z[0]; r < bbox_z[1]; r++) {
+ city.grid[q+i][r+j].status = STATUS.OCCUPIED.value;
+ }
+ }
+ }
+ }
+ }
+
+ } catch (error) {
+ console.log("error " + error);
+ }
+
+ }, function(error) {
+ console.error("Failed!", error);
+ });
+}
+
+function within(x, low, high) {
+ return x > low && x < high;
+}
+
+// called on frame updates
+function onUpdate(framework) {
+ var scene = framework.scene;
+ var camera = framework.camera;
+ var renderer = framework.renderer;
+ var gui = framework.gui;
+ var stats = framework.stats;
+
+ var i = 1;
+ for (var key in city.maps) {
+ var plane = scene.getObjectByName(key);
+ plane.position.set(0, -i * settings.split, 0);
+ i++;
+ }
+}
+
+/**
+ * Returns a number whose value is limited to the given range.
+ *
+ * Example: limit the output of this computation to between 0 and 255
+ * (x * 255).clamp(0, 255)
+ *
+ * Source: http://strd6.com/2010/08/useful-javascript-game-extensions-clamp/
+ *
+ * @param {Number} min The lower boundary of the output range
+ * @param {Number} max The upper boundary of the output range
+ * @returns A number in the range [min, max]
+ * @type Number
+ */
+Number.prototype.clamp = function(min, max) {
+ return Math.min(Math.max(this, min), max);
+};
+
+// when the scene is done initializing, it will call onLoad, then on frame updates, call onUpdate
+Framework.init(onLoad, onUpdate);
diff --git a/src/turtle.js b/src/turtle.js
new file mode 100644
index 00000000..0d09b9de
--- /dev/null
+++ b/src/turtle.js
@@ -0,0 +1,195 @@
+const THREE = require('three')
+
+var DIR = {
+ N: 0,
+ NE: 1,
+ E: 2,
+ SE: 3,
+ S: 4,
+ SW: 5,
+ W: 6,
+ NW: 7
+}
+
+
+var ZONES = {
+ UNZONED : {value: 1, name: "Unzoned", color: 0xd1cfca},
+ ROAD : {value: 2, name: "Road", color: 0x2c2a2d},
+ RESIDENTIAL: {value: 3, name: "Residential", color: 0x1fbc14},
+ COMMERCIAL : {value: 4, name: "Commerical", color: 0x7c14bc},
+ INDUSTRIAL : {value: 5, name: "Industrial", color: 0xddac30}
+};
+
+// A class used to encapsulate the state of a turtle at a given moment.
+// The Turtle class contains one TurtleState member variable.
+// You are free to add features to this state class,
+// such as color or whimiscality
+var TurtleState = function(pos, dir) {
+ return {
+ pos: new THREE.Vector2(pos.x, pos.y),
+ dir: dir
+ }
+}
+
+export default class Turtle {
+
+ constructor(grid, grammar) {
+ this.grid = grid;
+ console.log(this.grid);
+ // var startX = Math.floor(Math.random() * grid.length);
+ // var startY = Math.floor(Math.random() * grid[0].length);
+ var startX = 30 % this.grid.length;
+ var startY = 30 % this.grid[0].length;
+ this.state = new TurtleState(new THREE.Vector2(startX, startY), DIR.N);
+ this.stateStack = [];
+
+ // TODO: Start by adding rules for '[' and ']' then more!
+ // Make sure to implement the functions for the new rules inside Turtle
+ if (typeof grammar === "undefined") {
+ this.renderGrammar = {
+ '+' : this.rotateTurtle.bind(this, 45),
+ '-' : this.rotateTurtle.bind(this, -45),
+ '*' : this.rotateTurtle.bind(this, 90),
+ '/' : this.rotateTurtle.bind(this, -90),
+ 'M' : this.moveTurtle.bind(this, 5),
+ '[' : this.saveState.bind(this),
+ ']' : this.restoreState.bind(this),
+ };
+ } else {
+ this.renderGrammar = grammar;
+ }
+ }
+
+ saveState() {
+ console.log("state stored!");
+ console.log(this.state);
+ this.stateStack.push(new TurtleState(this.state.pos, this.state.dir, this.state.ortho));
+ }
+
+ restoreState() {
+ console.log("state restored!");
+ this.state = this.stateStack.pop();
+ console.log(this.state);
+ }
+
+ // Resets the turtle's position to the origin
+ // and its orientation to the Y axis
+ clear() {
+ this.state = new TurtleState(new THREE.Vector2(0,0), DIR.N);
+ }
+
+ // A function to help you debug your turtle functions
+ // by printing out the turtle's current state.
+ printState() {
+ console.log(this.state.pos)
+ console.log(this.state.dir)
+ }
+
+ // Rotate the turtle's _dir_ vector by each of the
+ // Euler angles indicated by the input.
+ rotateTurtle(degrees) {
+ if (degrees = 45) {
+ this.state.dir = (this.state.dir + 1) % 8;
+ } else if (degrees == -45) {
+ this.state.dir = (this.state.dir + 7) % 8;
+ } else if (degrees == 90) {
+ this.state.dir = (this.state.dir + 2) % 8;
+ } else if (degrees == -90) {
+ this.state.dir = (this.state.dir + 6) % 8;
+ } else {
+ console.log("unsupported rotation amount");
+ }
+ }
+
+ inBounds(pos) {
+ return pos.x < this.grid.length &&
+ pos.x >= 0 &&
+ pos.y < this.grid[0].length &&
+ pos.y >= 0;
+ }
+
+ moveTurtleDir(dir) {
+ var direction = new THREE.Vector2(0,0);
+ switch (dir) {
+ case DIR.N:
+ direction = new THREE.Vector2(0,1);
+ break;
+ case DIR.S:
+ direction = new THREE.Vector2(0,-1);
+ break;
+ case DIR.E:
+ direction = new THREE.Vector2(1,0);
+ break;
+ case DIR.W:
+ direction = new THREE.Vector2(-1,0);
+ break;
+ }
+
+ this.grid[this.state.pos.x][this.state.pos.y] = {
+ zone: ZONES.ROAD.value
+ };
+ var newpos = new THREE.Vector2(this.state.pos.x + direction.x, this.state.pos.y + direction.y)
+ if (this.inBounds(newpos)) {
+ this.state.pos = newpos;
+ } else {
+ var startX = Math.floor(Math.random() * this.grid.length);
+ var startY = Math.floor(Math.random() * this.grid[0].length);
+ this.state = new TurtleState(new THREE.Vector2(startX, startY), this.state.dir);
+ }
+ }
+
+ moveTurtle(len) {
+ for (var i = 0; i < len; i++) {
+ switch (this.state.dir) {
+ case DIR.N:
+ this.moveTurtleDir(DIR.N);
+ break;
+ case DIR.S:
+ this.moveTurtleDir(DIR.S);
+ break;
+ case DIR.E:
+ this.moveTurtleDir(DIR.E);
+ break;
+ case DIR.W:
+ this.moveTurtleDir(DIR.W);
+ break;
+ case DIR.NE:
+ this.moveTurtleDir(DIR.N);
+ this.moveTurtleDir(DIR.E);
+ break;
+ case DIR.SE:
+ this.moveTurtleDir(DIR.S);
+ this.moveTurtleDir(DIR.E);
+ break;
+ case DIR.NW:
+ this.moveTurtleDir(DIR.N);
+ this.moveTurtleDir(DIR.W);
+ break;
+ case DIR.SW:
+ this.moveTurtleDir(DIR.S);
+ this.moveTurtleDir(DIR.W);
+ break;
+ }
+ }
+ }
+
+ // Call the function to which the input symbol is bound.
+ // Look in the Turtle's constructor for examples of how to bind
+ // functions to grammar symbols.
+ renderSymbol(symbolNode) {
+ var func = this.renderGrammar[symbolNode.character];
+ if (func) {
+ console.log(symbolNode.character);
+ func();
+ }
+ };
+
+ // Invoke renderSymbol for every node in a linked list of grammar symbols.
+ renderSymbols(linkedList) {
+ var currentNode;
+ for(currentNode = linkedList.head; currentNode != null; currentNode = currentNode.next) {
+ this.renderSymbol(currentNode);
+ }
+ console.log(this.grid);
+ }
+}
diff --git a/textures/.DS_Store b/textures/.DS_Store
new file mode 100644
index 00000000..191cd4a7
Binary files /dev/null and b/textures/.DS_Store differ
diff --git a/textures/ad1.jpg b/textures/ad1.jpg
new file mode 100644
index 00000000..a86a92c4
Binary files /dev/null and b/textures/ad1.jpg differ
diff --git a/textures/ad2.jpg b/textures/ad2.jpg
new file mode 100644
index 00000000..4d37c354
Binary files /dev/null and b/textures/ad2.jpg differ
diff --git a/textures/ad3.png b/textures/ad3.png
new file mode 100644
index 00000000..05282404
Binary files /dev/null and b/textures/ad3.png differ
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