summaryrefslogtreecommitdiff
path: root/client/assets/javascripts/rectangles
diff options
context:
space:
mode:
Diffstat (limited to 'client/assets/javascripts/rectangles')
-rw-r--r--client/assets/javascripts/rectangles/_env.js49
-rw-r--r--client/assets/javascripts/rectangles/engine/map/_map.js40
-rw-r--r--client/assets/javascripts/rectangles/engine/map/draw.js162
-rw-r--r--client/assets/javascripts/rectangles/engine/map/ui.js131
-rw-r--r--client/assets/javascripts/rectangles/engine/rooms/_rooms.js41
-rw-r--r--client/assets/javascripts/rectangles/engine/rooms/builder.js294
-rw-r--r--client/assets/javascripts/rectangles/engine/rooms/clipper.js104
-rw-r--r--client/assets/javascripts/rectangles/engine/rooms/mover.js82
-rw-r--r--client/assets/javascripts/rectangles/engine/scenery/_scenery.js45
-rw-r--r--client/assets/javascripts/rectangles/engine/scenery/image/_image.js52
-rw-r--r--client/assets/javascripts/rectangles/engine/scenery/image/move.js103
-rw-r--r--client/assets/javascripts/rectangles/engine/scenery/image/resize.js201
-rw-r--r--client/assets/javascripts/rectangles/models/rect.js176
-rw-r--r--client/assets/javascripts/rectangles/models/room.js213
-rw-r--r--client/assets/javascripts/rectangles/models/tree.js37
-rw-r--r--client/assets/javascripts/rectangles/models/vec2.js99
-rw-r--r--client/assets/javascripts/rectangles/models/vec3.js5
-rw-r--r--client/assets/javascripts/rectangles/models/wall.js120
-rw-r--r--client/assets/javascripts/rectangles/util/colors.js57
-rw-r--r--client/assets/javascripts/rectangles/util/constants.js57
-rw-r--r--client/assets/javascripts/rectangles/util/debug.js8
-rw-r--r--client/assets/javascripts/rectangles/util/keys.js165
-rw-r--r--client/assets/javascripts/rectangles/util/mouse.js165
-rw-r--r--client/assets/javascripts/rectangles/util/sort.js86
-rw-r--r--client/assets/javascripts/rectangles/util/wheel.js62
25 files changed, 2554 insertions, 0 deletions
diff --git a/client/assets/javascripts/rectangles/_env.js b/client/assets/javascripts/rectangles/_env.js
new file mode 100644
index 0000000..972b1fe
--- /dev/null
+++ b/client/assets/javascripts/rectangles/_env.js
@@ -0,0 +1,49 @@
+
+var environment = new function(){}
+environment.init = function(){
+ if (window.scene) {
+ scene.camera.move({
+ "x": 0,
+ "y": 0,
+ "z": 0,
+ "rotationX": 0, // PI/2,
+ "rotationY": PI/2, // PI
+// "rotationX": 0,
+// "rotationY": PI
+ })
+
+ scene.camera.radius = 20
+ }
+
+// map.center.a = scene.camera.x
+// map.center.b = scene.camera.z
+ map.center.a = 0
+ map.center.b = 0
+
+ Rooms.list.push( new Room ({
+ rect: new Rect(-500,-500, 500,500),
+ height: 500,
+ }))
+ Rooms.list.push( new Room ({
+ rect: new Rect(600,0, 1100,500),
+ height: 500,
+ }))
+ Rooms.list.push( new Room ({
+ rect: new Rect(450,150, 650,350),
+ height: 300,
+ }))
+
+ app.movements.gravity(true)
+
+ $("#map").hide()
+
+ Rooms.init()
+ Scenery.init()
+
+ scene.update()
+ environment.update()
+}
+environment.update = function(t){
+ map.update()
+ z = false
+}
diff --git a/client/assets/javascripts/rectangles/engine/map/_map.js b/client/assets/javascripts/rectangles/engine/map/_map.js
new file mode 100644
index 0000000..53084bb
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/map/_map.js
@@ -0,0 +1,40 @@
+/*
+*/
+
+window.ctx = window.w = window.h = null;
+
+var map = new function(){
+ var base = this
+ base.el = document.querySelector("#map")
+ base.dimensions = new vec2(500,500)
+ base.bounds = new vec2(500,500)
+ base.center = new vec2(0,0)
+
+ base.sides = function(){
+ var sides = base.bounds.clone().div(2).div(base.zoom)
+ return new Rect( base.center.a - sides.a, -base.center.b - sides.b,
+ base.center.a + sides.a, -base.center.b + sides.b )
+ }
+
+ base.zoom = 1/8
+ base.zoom_exponent = -3
+ base.set_zoom = function (n) {
+ base.zoom_exponent = n
+ base.zoom = pow(2, base.zoom_exponent)
+ }
+
+ var canvas = document.createElement("canvas")
+ var ctx = window.ctx = canvas.getContext("2d")
+ var w = window.w = canvas.width = 500
+ var h = window.h = canvas.height = 500
+ document.querySelector("#map").appendChild(canvas)
+
+ base.update = function(){
+ base.draw.animate()
+ }
+
+ base.toggle = function(){
+ $(base.el).toggle()
+ }
+
+}
diff --git a/client/assets/javascripts/rectangles/engine/map/draw.js b/client/assets/javascripts/rectangles/engine/map/draw.js
new file mode 100644
index 0000000..b2fc05f
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/map/draw.js
@@ -0,0 +1,162 @@
+
+map.draw = new function(){
+
+ var base = this
+
+ base.animate = function(){
+ ctx.save()
+ map.draw.clear_canvas()
+ map.draw.ruler()
+
+ ctx.translate( map.bounds.a * 1/2, map.bounds.b * 1/2)
+ ctx.scale( map.zoom, map.zoom )
+ ctx.translate( map.center.a, map.center.b)
+ ctx.scale( -1, 1 )
+
+ map.draw.regions(Rooms.regions)
+ map.draw.mouse(map.ui.mouse.cursor)
+ map.draw.coords()
+ scene && map.draw.camera(scene.camera)
+
+ ctx.restore()
+ }
+
+ base.clear_canvas = function(){
+ ctx.fillStyle = "rgba(255,255,255,0.99)"
+ ctx.clearRect(0,0,w,h)
+ ctx.fillRect(0,0,w,h)
+ }
+
+ base.ruler = function (){
+ ctx.strokeStyle = "rgba(80,80,80,0.5)"
+ ctx.lineWidth = 1
+ var len = 5
+ for (var i = 0.5; i < w; i += 10) {
+ line(i, 0, i, len)
+ line(0, i, len, i)
+ }
+ }
+
+ base.regions = function(regions){
+ for (var i = 0; i < regions.length; i++) {
+ if (regions[i].dupe) continue
+ ctx.fillStyle = colors[i % colors.length]
+ fill_region(regions[i])
+ stroke_sides(regions[i])
+ }
+ }
+
+ base.mouse = function(mouse){
+ var radius = 3 / map.zoom
+
+ ctx.fillStyle = "rgba(255,0,0,0.4)";
+ ctx.beginPath();
+ ctx.arc(mouse.x.b, mouse.y.b, radius, 0, 2*Math.PI, false);
+ ctx.fill();
+
+ if (mouse.width() != 0 && mouse.height() != 0) {
+ if (map.ui.dragging) {
+ stroke_rect(mouse)
+ }
+ else {
+ ctx.fillStyle = "rgba(255,255,0,0.5)"
+ fill_region( mouse.clone().translate() )
+ }
+ }
+ }
+
+ base.camera = function(cam){
+ ctx.lineWidth = 0.5
+
+ ctx.save()
+
+ ctx.translate(cam.x, cam.z)
+ ctx.rotate(cam.rotationY)
+
+ var radius = 3 / map.zoom
+
+ ctx.fillStyle = '#888';
+
+ ctx.beginPath();
+ ctx.moveTo(0,0)
+ ctx.lineTo(-radius,-radius/2)
+ ctx.lineTo(0,radius*3)
+ ctx.lineTo(radius,-radius/2)
+ ctx.moveTo(0,0)
+ ctx.fill()
+
+ ctx.restore()
+ }
+
+ base.coords = function(){
+ ctx.fillStyle = "#888";
+ dot_at(0,0)
+ ctx.fillStyle = "#bbb";
+ dot_at(100,0)
+ dot_at(0,100)
+ ctx.fillStyle = "#ddd";
+ dot_at(200,0)
+ dot_at(0,200)
+ ctx.fillStyle = "#eee";
+ dot_at(300,0)
+ dot_at(0,300)
+
+ ctx.strokeStyle = "rgba(0,0,0,0.1)"
+ ctx.lineWidth = 1/map.zoom
+
+ var sides = map.sides()
+ var quant = sides.clone().quantize(200)
+ for (var x = quant.x.a - 200; x <= quant.x.b; x += 200) {
+ line(x, sides.y.a, x, sides.y.b)
+ }
+ for (var y = quant.y.a - 200; y <= quant.y.b; y += 200) {
+ line(sides.x.a, y, sides.x.b, y)
+ }
+ }
+
+ //
+
+ function line (x,y,a,b,translation){
+ if (translation) {
+ x += translation.a
+ a += translation.a
+ y += translation.b
+ b += translation.b
+ }
+ ctx.beginPath()
+ ctx.moveTo(x,y)
+ ctx.lineTo(a,b)
+ ctx.stroke()
+ }
+
+ function fill_region(r){
+ ctx.fillRect(r.x.a + r.translation.a,
+ r.y.a + r.translation.b,
+ r.x.length(),
+ r.y.length())
+ }
+
+ function stroke_sides (r){
+ if (r.sides & FRONT) line(r.x.a, r.y.a, r.x.b, r.y.a)
+ if (r.sides & BACK) line(r.x.a, r.y.b, r.x.b, r.y.b)
+ if (r.sides & LEFT) line(r.x.a, r.y.a, r.x.a, r.y.b)
+ if (r.sides & RIGHT) line(r.x.b, r.y.a, r.x.b, r.y.b)
+ }
+
+ function stroke_rect (r){
+ line(r.x.a, r.y.a, r.x.b, r.y.b, r.translation)
+ }
+
+ function dot_at (x,z){
+ ctx.save()
+ ctx.translate(x,z)
+
+ var radius = 2 / map.zoom
+
+ ctx.beginPath();
+ ctx.arc(0, 0, radius, 0, 2*Math.PI, false);
+ ctx.fill();
+
+ ctx.restore()
+ }
+} \ No newline at end of file
diff --git a/client/assets/javascripts/rectangles/engine/map/ui.js b/client/assets/javascripts/rectangles/engine/map/ui.js
new file mode 100644
index 0000000..6e9a5ab
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/map/ui.js
@@ -0,0 +1,131 @@
+
+map.ui = new function(){
+
+ var base = this
+
+ base.creating = base.dragging = base.resizing = false
+
+ base.mouse = new mouse({
+ el: map.el,
+ down: down,
+ move: move,
+ drag: drag,
+ up: up,
+ rightclick: rightclick,
+ })
+
+ base.wheel = new wheel({
+ el: map.el,
+ update: mousewheel,
+ })
+
+ //
+
+ function down (e, cursor){
+ cursor.x.div(w).add(0.5).mul(map.bounds.a / map.zoom).add(map.center.a)
+ cursor.y.div(h).sub(0.5).mul(map.bounds.b / map.zoom).sub(map.center.b)
+
+ if (e.ctrlKey || e.which === 3) {
+ cursor.quantize(1)
+ map.center.a = cursor.x.a
+ map.center.b = -cursor.y.a
+ console.log(map.center+"")
+ cursor.x.b = cursor.x.a
+ cursor.y.b = cursor.y.a
+ base.mouse.down = false
+ e.preventDefault()
+ e.stopPropagation()
+ return
+ }
+
+ var intersects = Rooms.filter(function(r){
+ return r.focused = r.rect.contains(cursor.x.a, cursor.y.a)
+ })
+
+ if (intersects.length) {
+ base.dragging = intersects[0]
+ base.resizing = base.dragging.rect.nearEdge(cursor.x.a, cursor.y.a, resize_margin / map.zoom)
+ base.dragging.rect.translation.sides = base.resizing
+ }
+ else {
+ base.creating = true
+ }
+
+ if (e.shiftKey && base.dragging) {
+ base.dragging.rect.quantize(10)
+ }
+ }
+
+ function move (e, cursor) {
+ cursor.x.div(w).add(0.5).mul(map.bounds.a / map.zoom).add(map.center.a)
+ cursor.y.div(h).sub(0.5).mul(map.bounds.b / map.zoom).sub(map.center.b)
+ }
+
+ function drag (e, cursor) {
+ cursor.x.b = ((cursor.x.b/w)+0.5) * map.bounds.a / map.zoom + map.center.a
+ cursor.y.b = ((cursor.y.b/h)-0.5) * map.bounds.b / map.zoom - map.center.b
+
+ if (base.resizing) {
+ var x_length = base.dragging.rect.x.length(),
+ y_length = base.dragging.rect.y.length()
+
+ if (base.resizing & LEFT) {
+ base.dragging.rect.translation.a = clamp( cursor.x.magnitude(), x_length - side_max, x_length - side_min )
+ }
+ if (base.resizing & RIGHT) {
+ base.dragging.rect.translation.a = clamp( cursor.x.magnitude(), side_min - x_length, side_max - x_length )
+ }
+ if (base.resizing & FRONT) {
+ base.dragging.rect.translation.b = clamp( cursor.y.magnitude(), y_length - side_max, y_length - side_min )
+ }
+ if (base.resizing & BACK) {
+ base.dragging.rect.translation.b = clamp( cursor.y.magnitude(), side_min - y_length, side_max - y_length )
+ }
+ }
+ else if (base.dragging) {
+ base.dragging.rect.translation.a = cursor.x.magnitude()
+ base.dragging.rect.translation.b = cursor.y.magnitude()
+ }
+ }
+
+ function up (e, cursor, new_cursor) {
+ new_cursor.x.div(w).add(0.5).mul(map.bounds.a / map.zoom).add(map.center.a)
+ new_cursor.y.div(h).sub(0.5).mul(map.bounds.b / map.zoom).sub(map.center.b)
+
+ if (base.creating) {
+ if (cursor.height() > side_min && cursor.width() > side_min) {
+ cursor.x.abs().quantize(1)
+ cursor.y.abs().quantize(1)
+ Rooms.add_with_rect( cursor )
+ }
+ }
+
+ if (base.resizing) {
+ base.dragging.rect.resize()
+ }
+ else if (base.dragging) {
+ base.dragging.rect.translate()
+ }
+
+ base.creating = base.dragging = base.resizing = false
+ }
+
+ function mousewheel (e, val, delta){
+ var cursor = base.mouse.cursor
+
+ var intersects = Rooms.filter(function(r){
+ return r.focused = r.rect.contains(cursor.x.a, cursor.y.a)
+ })
+
+ if (intersects.length) {
+ intersects[0].height = clamp( ~~(intersects[0].height - delta), height_min, height_max )
+ Rooms.clipper.update()
+ }
+ else {
+ map.set_zoom(map.zoom_exponent - delta/20)
+ }
+ }
+
+ function rightclick (e){
+ }
+}
diff --git a/client/assets/javascripts/rectangles/engine/rooms/_rooms.js b/client/assets/javascripts/rectangles/engine/rooms/_rooms.js
new file mode 100644
index 0000000..411a093
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/rooms/_rooms.js
@@ -0,0 +1,41 @@
+var Rooms = new function(){
+
+ var base = this
+
+ base.list = []
+ base.regions = []
+
+ base.init = function(){
+ Rooms.builder.init()
+ Rooms.clipper.init()
+ Rooms.mover.init()
+ }
+
+ base.filter = function(f){
+ return base.list.filter(f)
+ }
+
+ base.add_with_rect = function(rect){
+ var room = new Room({
+ id: base.list.length,
+ rect: rect,
+ height: quantize(randrange(300,800), 50),
+ })
+ base.list.push(room)
+ }
+
+ base.forEach = function(f){
+ return base.list.forEach(f)
+ }
+
+ base.sorted_by_position = function(){
+ return sort_rooms_by_position( base.list )
+ }
+ base.sorted_by_height = function(){
+ return sort_rooms_by_height( base.list )
+ }
+ base.sorted_by_area = function(){
+ return sort_rooms_by_area( base.list )
+ }
+
+}
diff --git a/client/assets/javascripts/rectangles/engine/rooms/builder.js b/client/assets/javascripts/rectangles/engine/rooms/builder.js
new file mode 100644
index 0000000..8586a7b
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/rooms/builder.js
@@ -0,0 +1,294 @@
+
+Rooms.builder = new function(){
+ var base = this
+
+ var els = []
+
+ base.init = function(){
+ base.bind()
+ }
+
+ base.bind = function(){
+ app.on("clip", rebuild)
+ }
+
+ function rebuild(){
+ if (window.scene) {
+ clear()
+ build()
+ bind()
+ }
+ }
+ function build (){
+ Rooms.regions.forEach(function(room){
+ build_walls(room).forEach(function(el){
+ els.push(el)
+ scene.add(el)
+ })
+ })
+
+ Rooms.sorted_by_height().forEach(function(room){
+ build_floors(room).forEach(function(el){
+ els.push(el)
+ scene.add(el)
+ })
+ })
+ }
+
+ function bind (){
+ Rooms.forEach(function(room){
+ room.walls = room.group_mx_walls()
+ room.walls.forEach(function(wall){
+ wall.bind()
+ wall.randomize_colors()
+ })
+ })
+ }
+
+ function clear (){
+ els.forEach(function(el){
+ scene.remove(el)
+ el.destroy && el.destroy()
+ })
+ els = []
+ }
+
+ function build_walls (region){
+ var room = Rooms.list[ region.id ]
+
+ var list = [], el = null
+
+ var width = region.x.length()
+ var depth = region.y.length()
+ var height = room.height
+
+ if (region.sides & FRONT) {
+ el = make_wall('.front')
+ el.width = width
+ el.height = height
+ el.rotationY = PI
+ el.x = region.x.a + width/2
+ el.y = height/2
+ el.z = region.y.a
+ el.rect = region
+ el.side = FRONT
+ room.mx_walls.push(el)
+ list.push(el)
+ }
+ if (region.sides & BACK) {
+ var el = make_wall('.back')
+ el.width = width
+ el.height = height
+ el.rotationY = 0
+ el.x = region.x.b - width/2
+ el.y = height/2
+ el.z = region.y.b
+ el.rect = region
+ el.side = BACK
+ room.mx_walls.push(el)
+ list.push(el)
+ }
+ if (region.sides & LEFT) {
+ el = make_wall('.left')
+ el.rotationY = HALF_PI
+ el.height = height
+ el.width = depth
+ el.x = region.x.a
+ el.y = height/2
+ el.z = region.y.a + depth/2
+ el.rect = region
+ el.side = LEFT
+ room.mx_walls.push(el)
+ list.push(el)
+ }
+ if (region.sides & RIGHT) {
+ el = make_wall('.right')
+ el.rotationY = -HALF_PI
+ el.height = height
+ el.width = depth
+ el.x = region.x.b
+ el.y = height/2
+ el.z = region.y.b - depth/2
+ el.rect = region
+ el.side = RIGHT
+ room.mx_walls.push(el)
+ list.push(el)
+ }
+
+ return list
+ }
+
+ function build_floors(room){
+ var list = [], el = null
+
+ var constructed = room.intersects.filter(function(room){ return room.constructed })
+ sort_rooms_by_height(constructed)
+
+ if (constructed.length > 0) {
+ // render the regions that don't intersect with anything we've already rendered
+ // if the height is different, calculate the overlapping sides and render half-walls
+ room.regions.forEach(function(region){
+ var intersected = false
+ for (var i = 0; i < constructed.length; i++) {
+ if (constructed[i].rect.contains(region)) {
+ intersected = true
+ // r.sides = 0xf
+ // half_sides
+ }
+ else if (constructed[i].rect.intersects(region)) {
+ intersected = true
+ if (room.height < constructed[i].height) {
+ var ceiling_walls = make_ceiling_walls( room, constructed[i], region )
+ list = list.concat(ceiling_walls)
+ }
+ }
+ }
+ if (! intersected) {
+ el = make_floor(room, region)
+ list.push( el )
+ room.mx_floor.push(el)
+
+ el = make_ceiling(room, region)
+ list.push( el )
+ room.mx_ceiling.push(el)
+ }
+ })
+
+ }
+ else {
+ // render floor and ceiling for the entire rectangle
+ el = make_floor(room, room.rect)
+ list.push( el )
+ room.mx_floor.push(el)
+
+ el = make_ceiling(room, room.rect)
+ list.push( el )
+ room.mx_ceiling.push(el)
+ }
+
+ room.constructed = true
+ return list
+ }
+
+ function make_ceiling_walls( lo, hi, region ){
+ var list = []
+
+ var width = region.x.length()
+ var depth = region.y.length()
+ var height = hi.height - lo.height
+
+ if (! (region.half_sides & LEFT) && region.x.a == hi.rect.x.a) {
+ el = make_wall('.left')
+ el.rotationY = HALF_PI
+ el.height = height
+ el.width = depth
+ el.x = region.x.a
+ el.y = lo.height + height/2
+ el.z = region.y.a + depth/2
+ el.rect = region
+ list.push(el)
+ hi.mx_walls.push(el)
+ region.half_sides |= LEFT
+ el.half_side = LEFT
+ }
+
+ if (! (region.half_sides & RIGHT) && region.x.b == hi.rect.x.b) {
+ el = make_wall('.right')
+ el.rotationY = -HALF_PI
+ el.height = height
+ el.width = depth
+ el.x = region.x.b
+ el.y = lo.height + height/2
+ el.z = region.y.b - depth/2
+ el.rect = region
+ list.push(el)
+ hi.mx_walls.push(el)
+ region.half_sides |= RIGHT
+ el.half_side = RIGHT
+ }
+
+ if (! (region.half_sides & FRONT) && region.y.a == hi.rect.y.a) {
+ el = make_wall('.front')
+ el.width = width
+ el.height = height
+ el.rotationY = PI
+ el.x = region.x.a + width/2
+ el.y = lo.height + height/2
+ el.z = region.y.a
+ el.rect = region
+ list.push(el)
+ hi.mx_walls.push(el)
+ region.half_sides |= FRONT
+ el.half_side = FRONT
+ }
+
+ if (! (region.half_sides & BACK) && region.y.b == hi.rect.y.b) {
+ el = make_wall('.back')
+ el.width = width
+ el.height = height
+ el.rotationY = 0
+ el.x = region.x.b - width/2
+ el.y = lo.height + height/2
+ el.z = region.y.b
+ el.rect = region
+ list.push(el)
+ hi.mx_walls.push(el)
+ region.half_sides |= BACK
+ el.half_side = BACK
+ }
+ return list
+ }
+
+ function make_floor(room, region){
+ var width = region.x.length()
+ var depth = region.y.length()
+
+ var el = make_wall('.floor')
+ el.height = depth
+ el.width = width
+ el.x = region.x.a + width/2
+ el.y = 0
+ el.z = region.y.a + depth/2
+ el.rotationX = PI/2
+ el.rect = region
+ el.side = FLOOR
+ return el
+ }
+ function make_ceiling(room, region){
+ var width = region.x.length()
+ var depth = region.y.length()
+ var height = room.height
+
+ var el = make_wall('.ceiling')
+ el.height = depth
+ el.width = width
+ el.x = region.x.a + width/2
+ el.y = height
+ el.z = region.y.a + depth/2
+ el.rotationX = -PI/2
+ el.rect = region
+ el.side = CEILING
+ return el
+ }
+
+ function make_wall(klass){
+ var el = new MX.Object3D(".face" + (klass || ""))
+ el.width = el.height = el.scaleX = el.scaleY = el.scaleZ = 1
+ el.z = el.y = el.x = 0
+ el.side = 0
+ el.type = "Face"
+ el.el.style.opacity = 1.0
+ el.side = 0
+ el.rect = null
+ el.destroy = function(){
+ this.el = this.rect = null
+ }
+
+ // possible if walls are opaque
+ // el.el.classList.add("backface-hidden")
+
+ return el
+ }
+
+}
+
diff --git a/client/assets/javascripts/rectangles/engine/rooms/clipper.js b/client/assets/javascripts/rectangles/engine/rooms/clipper.js
new file mode 100644
index 0000000..8989ba8
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/rooms/clipper.js
@@ -0,0 +1,104 @@
+
+Rooms.clipper = new function(){
+ var base = this
+
+ base.init = function(){
+ base.bind()
+ base.update()
+ }
+
+ base.bind = function(){
+ map.ui.mouse.tube.on("up", function(){ base.update() })
+ }
+
+ base.update = function(){
+ base.solve_rects()
+ app.tube("clip")
+ }
+
+ var rooms, regions
+
+ // Given a set of overlapping rooms, clip any intersections, then cull any duplicate polygons
+ base.solve_rects = function(){
+ if (Rooms.list.length == 0) return
+
+ base.reset_rects()
+ base.clip_rects()
+ base.cull_rects()
+
+ Rooms.regions = sort_rects_by_position(regions)
+ }
+
+ // Reset the clipping/culling states of each of the rooms
+ base.reset_rects = function(){
+ for (var i = 0; i < Rooms.list.length; i++) {
+ Rooms.list[i].reset()
+ }
+ }
+
+ // Compare each room to the rooms it overlaps, and subdivide
+ base.clip_rects = function(){
+ var rooms = Rooms.sorted_by_position()
+ regions = []
+
+ var left, right
+ for (var i = 0; i < rooms.length; i++) {
+ left = rooms[i]
+ for (var j = i+1; j < rooms.length; j++) {
+ right = rooms[j]
+ if (left.rect.intersects(right.rect)) {
+ left.clipTo(right.rect)
+ right.clipTo(left.rect)
+ left.intersects.push(right)
+ right.intersects.push(left)
+ }
+ if (left.rect.x.b < right.rect.x.a) {
+ break
+ }
+ }
+ }
+ for (var i = 0; i < rooms.length; i++) {
+ rooms[i].regions = rooms[i].regions.filter(function(r){ return !!r })
+ regions = regions.concat(rooms[i].regions)
+ }
+ }
+
+ // Find overlapping regions (of the same size) and dedupe
+ base.cull_rects = function(){
+ regions = sort_rects_by_area( regions )
+
+ var ty = new Tree (regions[0].y.a, [regions[0]])
+ var tx = new Tree (regions[0].x.a, ty)
+ var ttx, tty
+
+ for (var i = 1; i < regions.length; i++) {
+ ttx = tx.add (regions[i].x.a, null)
+ if (ttx.data) {
+ tty = ttx.data.add (regions[i].y.a, null)
+ // duplicate polygon?
+ if (tty.data) {
+ tty.data.forEach(function(yy, ii){
+ if (yy.intersects(regions[i])) {
+ if (yy.area() > regions[i].area()) {
+ regions[i].dupe = true
+ }
+ else {
+ yy.dupe = true
+ tty.data[ii] = regions[i]
+ }
+ }
+ })
+ }
+ else {
+ tty.data = [regions[i]]
+ }
+ }
+ else {
+ ttx.data = new Tree (regions[i].y.a, [regions[i]])
+ }
+ }
+ }
+
+ return base
+}
+
diff --git a/client/assets/javascripts/rectangles/engine/rooms/mover.js b/client/assets/javascripts/rectangles/engine/rooms/mover.js
new file mode 100644
index 0000000..e67d9bc
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/rooms/mover.js
@@ -0,0 +1,82 @@
+Rooms.mover = new function(){
+
+ var base = this
+ base.room = null
+ base.noclip = false
+
+ base.init = function(){
+ base.bind()
+ base.update(scene.camera)
+ }
+
+ base.bind = function(){
+ app.on("move", base.update)
+ keys.on("backslash", function(){
+ base.noclip = ! base.noclip
+ base.room = null
+ app.movements.gravity( ! base.noclip )
+ })
+ }
+
+ base.update = function(pos){
+ var radius = scene.camera.radius
+
+ if (base.noclip) {
+ cam.x = pos.x
+ cam.y = pos.y
+ cam.z = pos.z
+ return
+ }
+
+ cam.y = pos.y
+
+ // if we were in a room already..
+ if (base.room) {
+ // check if we're still in the room
+ if (base.room.rect.containsDisc(pos.x, pos.z, radius)) {
+ cam.x = pos.x
+ cam.z = pos.z
+ return
+ }
+
+ // check if we've breached one of the walls.. clamp position if so
+ var collision = base.room.collidesDisc(pos.x, pos.z, radius)
+
+ if (collision) {
+ if (! (collision & LEFT_RIGHT)) {
+ cam.x = base.room.rect.x.clampDisc(pos.x, radius)
+ }
+ else {
+ // cam.x = base.room.rect.x.clampDisc(pos.x, radius)
+ }
+ if (! (collision & FRONT_BACK)) {
+ cam.z = base.room.rect.y.clampDisc(pos.z, radius)
+ }
+ return
+ }
+
+ // in this case, we appear to have left the room..
+ $(".face.active").removeClass("active")
+ base.room = null
+ }
+
+ // collision test failed, so update position
+ cam.x = pos.x
+ cam.z = pos.z
+
+ // determine what room we are in now
+ var intersects = Rooms.filter(function(r){
+ return r.rect.contains(pos.x, pos.z)
+ })
+
+ // did we actually enter a room?
+ if (intersects.length) {
+ base.room = intersects[0]
+ base.room.mx_floor.forEach(function(w){ $(w.el).addClass("active") })
+ base.room.mx_ceiling.forEach(function(w){ $(w.el).addClass("active") })
+ base.room.mx_walls.forEach(function(w){ $(w.el).addClass("active") })
+ }
+
+ }
+
+}
diff --git a/client/assets/javascripts/rectangles/engine/scenery/_scenery.js b/client/assets/javascripts/rectangles/engine/scenery/_scenery.js
new file mode 100644
index 0000000..867bb6f
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/scenery/_scenery.js
@@ -0,0 +1,45 @@
+
+var Scenery = new function(){
+
+ var base = this;
+
+ base.images = []
+
+ base.mouse = new mouse ({ use_offset: false })
+
+ base.init = function(){
+ base.resize.init()
+
+ var urls = [
+ "http://okfocus.s3.amazonaws.com/office/ducks/duck1.jpg",
+ "http://okfocus.s3.amazonaws.com/office/ducks/duck2.jpg",
+ "http://okfocus.s3.amazonaws.com/office/ducks/duck3.jpg",
+ "http://okfocus.s3.amazonaws.com/office/ducks/duck4.jpg",
+ "http://okfocus.s3.amazonaws.com/office/ducks/duck5.jpg",
+ ]
+ var loader = new Loader(function(){
+ base.load(loader.images)
+ })
+ loader.preloadImages(urls)
+ }
+
+ base.load = function(images){
+ images.forEach(function(img){
+ img.width = 300
+ img.height = ~~(300 * img.naturalHeight/img.naturalWidth)
+ })
+
+ Rooms.forEach(function(room){
+ room.walls.forEach(function(wall){
+ var img = choice(images)
+ if (wall.fits(img)) {
+ var scene_img = new Scenery.image (wall, img)
+ base.images.push(scene_img)
+ scene_img.init()
+ }
+ })
+ })
+ }
+
+ return base
+}
diff --git a/client/assets/javascripts/rectangles/engine/scenery/image/_image.js b/client/assets/javascripts/rectangles/engine/scenery/image/_image.js
new file mode 100644
index 0000000..dadb2d2
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/scenery/image/_image.js
@@ -0,0 +1,52 @@
+Scenery.image = function (wall, img) {
+
+ var base = this
+
+ base.wall = wall
+ base.img = img
+ base.dimensions = new vec2(img.width, img.height)
+ base.center = wall.center()
+ base.bounds = wall.bounds_for(img)
+
+ // should be proportional to distance from wall
+ var cursor_amp = 1.5
+
+ base.init = function(){
+ base.build()
+ base.bind()
+ }
+
+ base.build = function(){
+ base.mx_img = new MX.Image({
+ src: img.src,
+ x: base.center.a,
+ y: Rooms.list[wall.room].height/2 - img.height/2 - 20,
+ z: base.center.b,
+ scale: 300/img.naturalWidth,
+ rotationY: wall_rotation[ wall.side ],
+ backface: false,
+ })
+ scene.add( base.mx_img )
+ base.move = new Scenery.image.move (base)
+ }
+
+ base.bind = function(){
+ base.move.bind()
+// base.resize.bind()
+ $(base.mx_img.el).bind({
+ mouseenter: function(e){
+// console.log('entered an image')
+ // show the resize points for this image
+ Scenery.resize.show(base)
+ Scenery.image.hovering = true
+ },
+ mouseleave: function(e){
+// console.log('left an image')
+ Scenery.resize.defer_hide(base)
+ Scenery.image.hovering = false
+ }
+ })
+ }
+
+ return base
+}
diff --git a/client/assets/javascripts/rectangles/engine/scenery/image/move.js b/client/assets/javascripts/rectangles/engine/scenery/image/move.js
new file mode 100644
index 0000000..e79ede9
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/scenery/image/move.js
@@ -0,0 +1,103 @@
+
+
+Scenery.image.move = function(base){
+
+ var x, y, z, bounds
+ var dragging = false
+
+ this.bind = function(){
+ Scenery.mouse.bind_el(base.mx_img.el)
+ Scenery.mouse.on("down", down)
+ Scenery.mouse.on("enter", switch_wall)
+ Scenery.mouse.on("drag", drag)
+ Scenery.mouse.on("up", up)
+ }
+
+ this.unbind = function(){
+ Scenery.mouse.bind_el(base.mx_img.el)
+ Scenery.mouse.off("down", down)
+ Scenery.mouse.off("enter", switch_wall)
+ Scenery.mouse.off("drag", drag)
+ Scenery.mouse.off("up", up)
+ }
+
+ function down (e, cursor){
+ if (e.target != base.mx_img.el) return;
+ dragging = true
+ x = base.mx_img.x
+ y = base.mx_img.y
+ z = base.mx_img.z
+ bounds = base.bounds
+ document.body.classList.add("dragging")
+ }
+
+ function drag (e, cursor){
+ if (! dragging) return
+
+ base.mx_img.y = bounds.y.clamp( y - cursor.y.magnitude()*cursor_amp )
+ switch (base.wall.side) {
+ case FRONT:
+ case BACK:
+ base.mx_img.x = bounds.x.clamp( x + cos(wall_rotation[base.wall.side]) * cursor.x.magnitude()*cursor_amp )
+ break
+ case LEFT:
+ case RIGHT:
+ base.mx_img.z = bounds.x.clamp( z + sin(wall_rotation[base.wall.side]) * cursor.x.magnitude()*cursor_amp )
+ break
+ }
+
+ Scenery.resize.move_dots()
+ }
+
+ function up (e, cursor){
+ dragging = false
+ document.body.classList.remove("dragging")
+ }
+
+ function switch_wall (e, new_wall, cursor){
+ if (! dragging) return
+ if (new_wall.uid == base.wall.uid) return
+ if (! new_wall.fits(base.img)) return
+
+ base.bounds = bounds = new_wall.bounds_for(base.img)
+ base.center = new_wall.center()
+
+ x = base.center.a
+ z = base.center.b
+
+ var wall_group = base.wall.side | new_wall.side
+
+ if (base.wall.side !== new_wall.side && wall_group !== FRONT_BACK && wall_group !== LEFT_RIGHT) {
+ switch (base.wall.side) {
+ case FRONT:
+ z = bounds.x.a
+ break
+ case BACK:
+ z = bounds.x.b
+ break
+ case LEFT:
+ x = bounds.x.a
+ break
+ case RIGHT:
+ x = bounds.x.b
+ break
+ }
+ }
+
+ cursor.x.a = cursor.x.b
+
+ base.mx_img.move({
+ x: x,
+ z: z,
+ rotationY: wall_rotation[ new_wall.side ]
+ })
+
+ base.wall = new_wall
+
+ Scenery.resize.rotate_dots()
+ Scenery.resize.move_dots()
+ }
+
+ return this
+
+} \ No newline at end of file
diff --git a/client/assets/javascripts/rectangles/engine/scenery/image/resize.js b/client/assets/javascripts/rectangles/engine/scenery/image/resize.js
new file mode 100644
index 0000000..a0a98c5
--- /dev/null
+++ b/client/assets/javascripts/rectangles/engine/scenery/image/resize.js
@@ -0,0 +1,201 @@
+Scenery.resize = new function(){
+
+ var base = this
+
+ var obj
+ var x, y, z, bounds
+ var dragging = false
+ var dimensions, position
+
+ var dots = [], dot, selected_dot
+
+ base.init = function(){
+ base.build()
+ base.bind()
+ }
+
+ // create 9 dots at the corners of the div
+ base.build = function(){
+ [ TOP,
+ TOP_RIGHT,
+ RIGHT,
+ BOTTOM_RIGHT,
+ BOTTOM,
+ BOTTOM_LEFT,
+ LEFT,
+ TOP_LEFT ].forEach(base.build_dot)
+ }
+
+ // generate a dot element
+ base.build_dot = function(side) {
+ var dot = new MX.Object3D('.dot')
+ dot.width = dot.height = dot_side
+ dot.side = side
+ $(dot.el).on({
+ mouseenter: function(){ base.hovering = true },
+ mouseleave: function(){ base.hovering = false },
+ })
+ dots.push(dot)
+ }
+
+ base.add_dots = function(){
+ dots.forEach(function(dot){
+ scene.add(dot)
+ })
+ }
+
+ // rotate the dots as appropriate
+ base.rotate_dots = function(){
+ rotationY = wall_rotation[obj.wall.side]
+ dots.forEach(function(dot){
+ dot.rotationY = rotationY
+ })
+ }
+
+ // move all the dots to the object's current position
+ base.move_dots = function(){
+ x = obj.mx_img.x + sin(rotationY) * dot_distance_from_picture
+ y = obj.mx_img.y
+ z = obj.mx_img.z - cos(rotationY) * dot_distance_from_picture
+
+ dots.forEach(function(dot){
+ base.move_dot(dot)
+ })
+ }
+
+ // move a dot .. to the initial position of the image
+ base.move_dot = function(dot){
+ dot.x = x
+ dot.y = y
+ dot.z = z
+
+ if (dot.side & TOP) {
+ dot.y += obj.mx_img.height * obj.mx_img.scale / 2
+ }
+ if (dot.side & BOTTOM) {
+ dot.y -= obj.mx_img.height * obj.mx_img.scale / 2
+ }
+ if (dot.side & LEFT) {
+ dot.x -= cos(rotationY) * (obj.mx_img.width * obj.mx_img.scale) / 2
+ dot.z -= sin(rotationY) * (obj.mx_img.width * obj.mx_img.scale) / 2
+ }
+ if (dot.side & RIGHT) {
+ dot.x += cos(rotationY) * (obj.mx_img.width * obj.mx_img.scale) / 2
+ dot.z += sin(rotationY) * (obj.mx_img.width * obj.mx_img.scale) / 2
+ }
+ }
+
+ // pick a new object to focus on and show the dots
+ base.show = function(new_object) {
+ if (obj === new_object) return
+
+ obj = new_object
+
+ base.add_dots()
+ base.rotate_dots()
+ base.move_dots()
+ }
+
+ // dismiss the dots on blur
+ var dotsHideTimeout;
+ base.defer_hide = function(){
+ clearTimeout(dotsHideTimeout)
+
+ dotsHideTimeout = setTimeout(function(){
+ if (Scenery.image.hovering || Scenery.resize.hovering || Scenery.mouse.down) return
+ Scenery.resize.hide()
+ }, dot_hide_delay)
+ }
+ base.hide = function () {
+ obj = null
+ dots.forEach(function(dot){
+ scene.remove(dot)
+ })
+ }
+
+ base.bind = function(){
+ dots.forEach(function(dot){
+ Scenery.mouse.bind_el(dot.el)
+ $(dot.el).bind({
+ mouseenter: function(e){
+ Scenery.resize.hovering = true
+ },
+ mouseleave: function(e){
+ Scenery.resize.hovering = false
+ base.defer_hide()
+ }
+ })
+ })
+ Scenery.mouse.on("down", down)
+ Scenery.mouse.on("drag", drag)
+ Scenery.mouse.on("up", up)
+ }
+
+ this.unbind = function(){
+ dots.forEach(function(dot){
+ Scenery.mouse.unbind_el(dot.el)
+ })
+ Scenery.mouse.off("down", down)
+ Scenery.mouse.off("drag", drag)
+ Scenery.mouse.off("up", up)
+ }
+
+ function down (e, cursor){
+ var selection = dots.filter(function(dot){return e.target == dot.el})
+ if (! selection.length) return
+
+ selected_dot = selection[0]
+ dragging = true
+
+ dimensions = new vec2(obj.mx_img.width, obj.mx_img.height)
+ position = new vec3(obj.mx_img.x, obj.mx_img.y, obj.mx_img.z)
+
+ console.log("down on", sidesToString(selected_dot.side))
+
+ document.body.classList.add("dragging")
+ }
+
+ function drag (e, cursor){
+ if (! dragging) return
+ // cursor.x.magnitude()*cursor_amp
+
+ var x_sign = selected_dot.side & LEFT ? -1 : selected_dot.side & RIGHT ? 1 : 0
+ var y_sign = selected_dot.side & TOP ? -1 : selected_dot.side & BOTTOM ? 1 : 0
+
+ var translation = new vec2( x_sign * cursor.x.magnitude() * cursor_amp, y_sign * cursor.y.magnitude() * cursor_amp )
+
+ if (selected_dot.side & LEFT_RIGHT) {
+ obj.mx_img.width = dimensions.a + translation.a
+ obj.mx_img.x = position.a + x_sign * cos(rotationY) * translation.a/2 * obj.mx_img.scale
+ obj.mx_img.z = position.c + x_sign * sin(rotationY) * translation.a/2 * obj.mx_img.scale
+ }
+ if (selected_dot.side & TOP_BOTTOM) {
+ obj.mx_img.height = dimensions.b + translation.b
+ obj.mx_img.y = position.b - y_sign * translation.b/2 * obj.mx_img.scale
+ }
+
+// bounds = obj.wall.bounds_for(dimensions)
+
+// base.mx_img.y = bounds.y.clamp( y - cursor.y.magnitude()*cursor_amp )
+// switch (base.wall.side) {
+// case FRONT:
+// case BACK:
+// base.mx_img.x = bounds.x.clamp( x + cos(wall_rotation[base.wall.side]) * cursor.x.magnitude()*cursor_amp )
+// break
+// case LEFT:
+// case RIGHT:
+// base.mx_img.z = bounds.x.clamp( z + sin(wall_rotation[base.wall.side]) * cursor.x.magnitude()*cursor_amp )
+// break
+// }
+
+ base.move_dots()
+
+ }
+
+ function up (e, cursor){
+ dragging = false
+ selected_dot = null
+ document.body.classList.remove("dragging")
+ }
+
+}
diff --git a/client/assets/javascripts/rectangles/models/rect.js b/client/assets/javascripts/rectangles/models/rect.js
new file mode 100644
index 0000000..7a2ac6f
--- /dev/null
+++ b/client/assets/javascripts/rectangles/models/rect.js
@@ -0,0 +1,176 @@
+
+window.Rect = (function(){
+ var Rect = function (x0,y0,x1,y1){
+ if (x0 instanceof vec2) {
+ this.x = x0
+ this.y = y0
+ }
+ else if (x1 === undefined) {
+ this.x = new vec2(x0,x0)
+ this.y = new vec2(y0,y0)
+ }
+ else {
+ this.x = new vec2(x0,x1)
+ this.y = new vec2(y0,y1)
+ }
+ this.translation = new vec2(0,0)
+ this.sides = FRONT | BACK | LEFT | RIGHT
+ }
+ Rect.prototype.clone = function(){
+ return new Rect( this.x.clone(), this.y.clone() )
+ }
+ Rect.prototype.center = function(){
+ return new vec2(this.x.midpoint(), this.y.midpoint())
+ }
+ Rect.prototype.area = function(){
+ return this.x.length() * this.y.length()
+ }
+
+ Rect.prototype.mul = function(n){
+ this.x.mul(n)
+ this.y.mul(n)
+ return this
+ }
+ Rect.prototype.div = function(n){
+ this.x.div(n)
+ this.y.div(n)
+ return this
+ }
+
+ Rect.prototype.translate = function(translation){
+ var translation = translation || this.translation
+ this.x.abs().add(translation.a)
+ this.y.abs().add(translation.b)
+ this.translation.a = this.translation.b = 0
+ return this
+ }
+ Rect.prototype.resize = function(translation, sides){
+ var translation = translation || this.translation
+ sides = sides || translation.sides
+
+ if (sides & LEFT) {
+ this.x.a += translation.a
+ }
+ if (sides & RIGHT) {
+ this.x.b += translation.a
+ }
+ if (sides & FRONT) {
+ this.y.a += translation.b
+ }
+ if (sides & BACK) {
+ this.y.b += translation.b
+ }
+ this.translation.a = this.translation.b = 0
+ }
+ Rect.prototype.contains = function(x,y){
+ return this.x.contains(x) && this.y.contains(y)
+ }
+ Rect.prototype.containsDisc = function(x,y,r){
+ return this.x.containsDisc(x,r) && this.y.containsDisc(y,r)
+ }
+ Rect.prototype.intersects = function(r){
+ return this.x.intersects(r.x) && this.y.intersects(r.y)
+ }
+ Rect.prototype.nearEdge = function (x, y, r) {
+ var edges = 0
+ if (x < this.x.a+r) {
+ edges |= LEFT
+ }
+ else if (x > this.x.b-r) {
+ edges |= RIGHT
+ }
+ if (y < this.y.a+r) {
+ edges |= FRONT
+ }
+ else if (y > this.y.b-r) {
+ edges |= BACK
+ }
+ return edges
+ }
+ Rect.prototype.width = function(){ return this.x.length() }
+ Rect.prototype.height = function(){ return this.y.length() }
+ Rect.prototype.toString = function(){
+ var sides = sidesToString(this.sides)
+ var s = "[" + this.x.toString() + " " + this.y.toString() + "] " + sides
+ return s
+ }
+ Rect.prototype.quantize = function(n){
+ this.x.quantize(n)
+ this.y.quantize(n)
+ return this
+ }
+ Rect.prototype.split = function(r){
+ var rz = this
+ var splits = []
+ var split_contains = 0
+ var x_intervals = [], y_intervals = []
+ var sides = this.sides
+
+ // Split vertically
+ if (this.x.contains(r.x.a) && r.x.contains(this.x.b)) {
+ x_intervals.push([ new vec2( this.x.a, r.x.a ), LEFT ])
+ x_intervals.push([ new vec2( r.x.a, this.x.b ), RIGHT ])
+ split_contains |= RIGHT
+ }
+
+ else if (r.x.contains(this.x.a) && this.x.contains(r.x.b)) {
+ x_intervals.push([ new vec2( this.x.a, r.x.b ), LEFT ])
+ x_intervals.push([ new vec2( r.x.b, this.x.b ), RIGHT ])
+ split_contains |= LEFT
+ }
+
+ else if (this.x.contains(r.x.a) && this.x.contains(r.x.b)) {
+ x_intervals.push([ new vec2( this.x.a, r.x.a ), LEFT ])
+ x_intervals.push([ new vec2( r.x.a, r.x.b ), 0 ])
+ x_intervals.push([ new vec2( r.x.b, this.x.b ), RIGHT ])
+ split_contains |= LEFT | RIGHT
+ }
+
+ else { // if (r.x.contains(this.x.a) && r.x.contains(r.x.b)) {
+ x_intervals.push([ new vec2( this.x.a, this.x.b ), LEFT | RIGHT ])
+ split_contains |= LEFT | RIGHT
+ }
+
+ // Split horizontally
+ if (this.y.contains(r.y.a) && r.y.contains(this.y.b)) {
+ y_intervals.push([ new vec2( this.y.a, r.y.a ), FRONT ])
+ y_intervals.push([ new vec2( r.y.a, this.y.b ), BACK ])
+ split_contains |= BACK
+ }
+
+ else if (r.y.contains(this.y.a) && this.y.contains(r.y.b)) {
+ y_intervals.push([ new vec2( this.y.a, r.y.b ), FRONT ])
+ y_intervals.push([ new vec2( r.y.b, this.y.b ), BACK ])
+ split_contains |= FRONT
+ }
+
+ else if (this.y.contains(r.y.a) && this.y.contains(r.y.b)) {
+ y_intervals.push([ new vec2( this.y.a, r.y.a ), FRONT ])
+ y_intervals.push([ new vec2( r.y.a, r.y.b ), 0 ])
+ y_intervals.push([ new vec2( r.y.b, this.y.b ), BACK ])
+ split_contains |= FRONT | BACK
+ }
+
+ else { // if (r.y.contains(this.y.a) && this.y.contains(r.y.b)) {
+ y_intervals.push([ new vec2( this.y.a, this.y.b ), FRONT | BACK ])
+ split_contains |= FRONT | BACK
+ }
+
+ x_intervals.forEach(function(x){
+ y_intervals.forEach(function(y){
+ var rn = new Rect(x[0], y[0])
+ rn.id = rz.id
+ rn.sides = ((x[1] | y[1]) & sides)
+ if (r.intersects(rn)) {
+ rn.sides = 0
+ }
+ rn.focused = rz.focused
+ splits.push(rn)
+ })
+ })
+ return splits
+ }
+
+ return Rect
+
+})()
diff --git a/client/assets/javascripts/rectangles/models/room.js b/client/assets/javascripts/rectangles/models/room.js
new file mode 100644
index 0000000..731411c
--- /dev/null
+++ b/client/assets/javascripts/rectangles/models/room.js
@@ -0,0 +1,213 @@
+window.Room = (function(){
+
+ var Room = function(opt){
+ this.id = opt.id || Rooms.list.length
+ this.rect = opt.rect
+ this.regions = []
+ this.walls = []
+ this.floor = []
+ this.ceiling = []
+
+ this.height = opt.height || 200
+ this.focused = false
+ }
+
+ Room.prototype.toString = function(){
+ return this.rect.toString()
+ }
+
+ Room.prototype.reset = function(){
+ var copy = this.rect.clone()
+ copy.id = this.id
+ copy.sides = FRONT | BACK | LEFT | RIGHT
+ this.regions = [ copy ]
+
+ this.intersects = []
+ this.constructed = false
+
+ this.walls = []
+
+ this.mx_walls = []
+ this.mx_floor = []
+ this.mx_ceiling = []
+ }
+
+ Room.prototype.bind = function(){
+ var base = this
+ base.mx_walls.forEach(function(wall){
+ $(wall.el).bind({
+ mouseover: function(){
+ },
+ mousemove: function(e){
+ var color = choice(window.palettes.colors)
+ base.mx_walls.forEach(function(wall){
+ $(wall.el).css("background-color", color)
+ })
+ },
+ mousedown: function(){
+ }
+ })
+ })
+ }
+
+ Room.prototype.group_mx_walls = function(){
+ var base = this
+ var side_groups = {}, walls = []
+
+ // group the walls by side
+ base.mx_walls.forEach(function(wall){
+
+ // ignore half-walls for now
+ var side = wall.side || wall.half_side
+
+ if (side_groups[side]) {
+ side_groups[side].push(wall)
+ }
+ else {
+ side_groups[side] = [wall]
+ }
+ })
+
+ // sort the subgroups, and then combine mx objects under wall objects
+ pairs(side_groups).forEach(function(pair){
+ var side = pair[0], els = pair[1]
+
+ if (side & LEFT_RIGHT) {
+ els.sort(compare_x)
+ }
+ else if (side & FRONT_BACK) {
+ els.sort(compare_z)
+ }
+
+ var wall
+
+ els.forEach(function(el){
+ if (el.half_side) {
+ wall = new_wall(el)
+ walls.push(wall)
+ wall = null
+ }
+ else if (! wall) {
+ wall = new_wall(el)
+ walls.push(wall)
+ }
+ else if (side & FRONT_BACK && wall.rect.x.b == el.rect.x.a) {
+ wall.rect.x.b = el.rect.x.b
+ wall.mx.push(el)
+ }
+ else if (side & LEFT_RIGHT && wall.rect.y.b == el.rect.y.a) {
+ wall.rect.y.b = el.rect.y.b
+ wall.mx.push(el)
+ }
+ else {
+ wall = new_wall(el)
+ walls.push(wall)
+ }
+ })
+ })
+
+ function new_wall (el) {
+ return new Wall ({
+ room: base.id,
+ side: el.side | el.half_side,
+ half_side: el.half_side,
+ rect: el.rect.clone(),
+ el: el,
+ })
+ }
+
+ return walls
+ }
+
+ Room.prototype.clipTo = function(r){
+ // for each of this rect's regions split the region if necessary
+ var regions = this.regions
+ var splits
+ for (var i = 0, len = regions.length; i < len; i++) {
+ if (regions[i] && regions[i].intersects(r)) {
+ splits = regions[i].split(r)
+ regions = regions.concat(splits)
+ regions[i] = null
+ }
+ }
+ this.regions = regions
+ }
+
+ Room.prototype.collides = function(x,y){
+ var collision = 0, wall_collision, contains_x, contains_y
+ this.regions.forEach(function(r){
+ if (! r.sides) return
+
+ wall_collision = 0
+
+ if ((r.sides & FRONT) && y < r.y.a) {
+ wall_collision |= FRONT
+ }
+ if ((r.sides & BACK) && r.y.b < y) {
+ wall_collision |= BACK
+ }
+ if ((r.sides & LEFT) && x < r.x.a) {
+ wall_collision |= LEFT
+ }
+ if ((r.sides & RIGHT) && r.x.b < x) {
+ wall_collision |= RIGHT
+ }
+ if (! wall_collision) return
+
+ contains_y = r.y.contains(y)
+ contains_x = r.x.contains(x)
+
+ if (contains_x) {
+ collision |= wall_collision & FRONT_BACK
+ }
+ else if (contains_y) {
+ collision |= wall_collision & LEFT_RIGHT
+ }
+ else if (bitcount(wall_collision) > 1) {
+ collision |= wall_collision
+ }
+ })
+ return collision
+ }
+
+ Room.prototype.collidesDisc = function(x,y,radius){
+ var collision = 0, wall_collision, contains_x, contains_y
+ this.regions.forEach(function(r){
+ if (! r.sides) return
+
+ wall_collision = 0
+
+ if ((r.sides & FRONT) && y-radius < r.y.a) {
+ wall_collision |= FRONT
+ }
+ if ((r.sides & BACK) && r.y.b < y+radius) {
+ wall_collision |= BACK
+ }
+ if ((r.sides & LEFT) && x-radius < r.x.a) {
+ wall_collision |= LEFT
+ }
+ if ((r.sides & RIGHT) && r.x.b < x+radius) {
+ wall_collision |= RIGHT
+ }
+ if (! wall_collision) return
+
+ contains_x = r.x.contains(x, radius)
+ contains_y = r.y.contains(y, radius)
+
+ if (contains_x) {
+ collision |= wall_collision & FRONT_BACK
+ }
+ else if (contains_y) {
+ collision |= wall_collision & LEFT_RIGHT
+ }
+ else if (bitcount(wall_collision) > 1) {
+ collision |= wall_collision
+ }
+ })
+ return collision
+ }
+
+ return Room
+
+})()
+
diff --git a/client/assets/javascripts/rectangles/models/tree.js b/client/assets/javascripts/rectangles/models/tree.js
new file mode 100644
index 0000000..8193988
--- /dev/null
+++ b/client/assets/javascripts/rectangles/models/tree.js
@@ -0,0 +1,37 @@
+var Tree = function(n, data){
+ this.lo = null
+ this.hi = null
+ this.value = n
+ this.data = data
+}
+Tree.prototype.find = function(n){
+ if (n == this.value) return this
+ if (n < this.value) return this.lo ? this.lo.find(n) : this
+ if (n > this.value) return this.hi ? this.hi.find(n) : this
+}
+Tree.prototype.add = function(n, data){
+ var closest = this.find(n)
+ if (n == closest.value) return closest
+ if (n < closest.value) return closest.lo = new Tree(n, data)
+ if (n > closest.value) return closest.hi = new Tree(n, data)
+}
+Tree.prototype.toArray = function(){
+ var a = []
+ if (this.lo) a = a.concat(this.lo.toArray())
+ a.push(this.data)
+ if (this.hi) a = a.concat(this.hi.toArray())
+ return a
+}
+Tree.prototype.toString = function(){
+ var s = "";
+ if (this.lo) s += this.lo.toString()
+ s += this.value + ","
+ if (this.hi) s += this.hi.toString()
+ return s
+}
+Tree.prototype.depth = function(){
+ if (this.lo && this.hi) return 1 + max(this.lo.depth(), this.hi.depth())
+ else if (this.lo) return 1 + this.lo.depth()
+ else if (this.hi) return 1 + this.hi.depth()
+ else return 0
+}
diff --git a/client/assets/javascripts/rectangles/models/vec2.js b/client/assets/javascripts/rectangles/models/vec2.js
new file mode 100644
index 0000000..9b0447c
--- /dev/null
+++ b/client/assets/javascripts/rectangles/models/vec2.js
@@ -0,0 +1,99 @@
+function vec2(a,b){
+ this.a = a
+ this.b = b
+}
+vec2.prototype.magnitude = function(){
+ return this.b-this.a
+}
+vec2.prototype.length = function(){
+ return abs(this.b-this.a)
+}
+vec2.prototype.clone = function(){
+ return new vec2(this.a, this.b)
+}
+vec2.prototype.abs = function(){
+ if (this.b < this.a) {
+ this.invert()
+ }
+ return this
+}
+vec2.prototype.invert = function(){
+ this.a = this.a ^ this.b
+ this.b = this.a ^ this.b
+ this.a = this.a ^ this.b
+ return this
+}
+vec2.prototype.midpoint = function(){
+ return lerp(0.5, this.a, this.b)
+}
+vec2.prototype.eq = function(v){
+ return this.a == v.a && this.b == v.b
+}
+vec2.prototype.add = function(n){
+ this.a += n
+ this.b += n
+ return this
+}
+vec2.prototype.sub = function(n){
+ this.a -= n
+ this.b -= n
+ return this
+}
+vec2.prototype.mul = function(n){
+ this.a *= n
+ this.b *= n
+ return this
+}
+vec2.prototype.div = function(n){
+ this.a /= n
+ this.b /= n
+ return this
+}
+vec2.prototype.normalize = function(){
+ var dim = max(this.a, this.b)
+ this.a = this.a/dim
+ this.b = this.b/dim
+ return this
+}
+vec2.prototype.contains = function(n){
+ return this.a <= n && n <= this.b
+}
+vec2.prototype.containsDisc = function(n,r){
+ return this.a <= n-r && n+r <= this.b
+}
+vec2.prototype.clamp = function(n){
+ return clamp(n, this.a, this.b)
+}
+vec2.prototype.clampDisc = function(n,r){
+ return clamp(n, this.a+r, this.b-r)
+}
+vec2.prototype.intersects = function(v){
+ if (this.a < v.a) {
+ return (v.a < this.b && this.b <= v.b) || (this.a < v.b && v.b <= this.b)
+ }
+ else if (this.a == v.a) {
+ return true
+ }
+ else if (this.a > v.a) {
+ return (this.a < v.b && v.b <= this.b) || (v.a < this.b && this.b <= v.b)
+ }
+}
+vec2.prototype.union = function(v){
+ if (this.intersects(v)) {
+ return new vec2( min(this.a,v.a), max(this.b, v.b) )
+ }
+}
+vec2.prototype.intersection = function(v){
+ if (this.intersects(v)) {
+ return new vec2( max(this.a,v.a), min(this.b, v.b) )
+ }
+}
+vec2.prototype.toString = function(){
+ return "[" + ~~this.a + " " + ~~this.b + "]"
+}
+vec2.prototype.quantize = function(n){
+ n = n || 10
+ this.a = quantize(this.a, n)
+ this.b = quantize(this.b, n)
+}
+
diff --git a/client/assets/javascripts/rectangles/models/vec3.js b/client/assets/javascripts/rectangles/models/vec3.js
new file mode 100644
index 0000000..4e9f3cb
--- /dev/null
+++ b/client/assets/javascripts/rectangles/models/vec3.js
@@ -0,0 +1,5 @@
+function vec3(a,b,c){
+ this.a = a
+ this.b = b
+ this.c = c
+}
diff --git a/client/assets/javascripts/rectangles/models/wall.js b/client/assets/javascripts/rectangles/models/wall.js
new file mode 100644
index 0000000..4270551
--- /dev/null
+++ b/client/assets/javascripts/rectangles/models/wall.js
@@ -0,0 +1,120 @@
+window.Wall = (function(){
+
+ var Wall = function(opt){
+ this.id = opt.id
+ this.uid = Uid()
+ this.room = opt.room
+ this.rect = opt.rect || new Rect (0,0,0,0)
+ this.rect.sides = opt.side
+ this.side = opt.side
+ this.mx = []
+ this.els = []
+ if (opt.el) {
+ this.mx.push(opt.el)
+ }
+ }
+
+ Wall.prototype.toString = function(){
+ return this.rect.toString()
+ }
+
+ Wall.prototype.reset = function(){
+ }
+
+ Wall.prototype.destroy = function(){
+ this.mx.forEach(function(mx){
+ mx.destroy && mx.destroy()
+ })
+ this.room = this.rect = this.mx = this.els = null
+ }
+
+ Wall.prototype.bind = function(){
+ var base = this
+ base.$walls = $( this.mx.map(function(mx){ return mx.el }) )
+ base.$walls.bind({
+ mouseover: function(){
+ },
+ mouseenter: function(e){
+ Scenery.mouse.mouseenter(e, base)
+ },
+ mousemove: function(e){
+ },
+ mousedown: function(){
+ base.randomize_colors()
+ console.log(sidesToString(base.side))
+ }
+ })
+ }
+
+ Wall.prototype.bounds_for = function(img) {
+ var coord = this.side & FRONT_BACK ? this.rect.x : this.rect.y
+ return new Rect( new vec2( coord.a + img.width/2, coord.b - img.width/2 ),
+ new vec2( img.height/2, Rooms.list[this.room].height - img.height/2 ) )
+ }
+ Wall.prototype.fits = function(img){
+ if (this.side & FRONT_BACK && this.rect.x.length() < img.width) {
+ return false
+ }
+ if (this.side & LEFT_RIGHT && this.rect.y.length() < img.width) {
+ return false
+ }
+ return true
+ }
+
+ Wall.prototype.center = function(offset){
+
+ offset = offset || 0
+
+ var major_axis, minor_axis
+ if (this.side & FRONT_BACK) {
+ major_axis = this.rect.x
+ minor_axis = this.rect.y
+ }
+ else {
+ major_axis = this.rect.y
+ minor_axis = this.rect.x
+ }
+
+ switch (this.side) {
+ case FRONT:
+ x = major_axis.midpoint()
+ z = minor_axis.a + painting_distance_from_wall + offset
+ break
+ case BACK:
+ x = major_axis.midpoint()
+ z = minor_axis.b - painting_distance_from_wall + offset
+ break
+ case LEFT:
+ x = minor_axis.a + painting_distance_from_wall + offset
+ z = major_axis.midpoint()
+ break
+ case RIGHT:
+ x = minor_axis.b - painting_distance_from_wall + offset
+ z = major_axis.midpoint()
+ break
+ }
+
+ return new vec2 (x, z)
+ }
+
+ Wall.prototype.color = function(color){
+ this.$walls && this.$walls.css("background-color", color)
+ }
+
+ Wall.prototype.siblings = function(){
+ var base = this
+ var match = base.side | base.half_side
+ var walls = Rooms.list[this.room].walls.filter(function(w){
+ return (w.side | w.half_side) & match
+ })
+ return walls;
+ }
+
+ Wall.prototype.randomize_colors = function(){
+ var color = choice(window.colors)
+ this.siblings().forEach(function(w){ w.color(color) })
+ }
+
+ return Wall
+
+})()
diff --git a/client/assets/javascripts/rectangles/util/colors.js b/client/assets/javascripts/rectangles/util/colors.js
new file mode 100644
index 0000000..e8e60d0
--- /dev/null
+++ b/client/assets/javascripts/rectangles/util/colors.js
@@ -0,0 +1,57 @@
+(function(){
+ var color_palettes = {
+ alpha: [
+ "rgba(0,0,0,0.1)",
+ ],
+ redblue: [
+ "rgba(0,0,0,0.2)",
+ "rgba(255,0,0,0.2)",
+ "rgba(0,0,255,0.2)",
+ "rgba(0,255,0,0.2)",
+ ],
+ gray: [
+ "rgba(0,0,0,0.1)",
+ "rgba(0,0,0,0.2)",
+ "rgba(0,0,0,0.3)",
+ "rgba(0,0,0,0.4)",
+ ],
+ bone: [
+ "hsla(0,0%,90%,0.95)",
+ "hsla(0,0%,80%,0.95)",
+ "hsla(0,0%,85%,0.95)",
+ "hsla(0,0%,75%,0.95)",
+ ],
+ colors: [
+ "rgba(255,0,0,0.5)",
+ "rgba(255,128,0,0.5)",
+ "rgba(128,255,0,0.5)",
+ "rgba(0,255,0,0.5)",
+ "rgba(0,255,128,0.5)",
+ "rgba(0,128,255,0.5)",
+ "rgba(0,0,255,0.5)",
+ "rgba(128,0,255,0.5)",
+ "rgba(255,0,255,0.5)",
+ "rgba(255,0,128,0.5)",
+ ],
+ white: [
+ "rgba(255,255,255,0.9)",
+ ],
+ black: [
+ "rgba(0,0,0,0.9)",
+ ],
+ }
+
+ var select = document.querySelector("#palette")
+ select.addEventListener("change", function(){
+ colors = color_palettes[select.value]
+ Rooms.list.forEach(function(room){
+ room.walls.forEach(function(wall){
+ wall.randomize_colors()
+ })
+ })
+ select.blur()
+ })
+
+ window.colors = color_palettes[select.value]
+ window.palettes = color_palettes
+})()
diff --git a/client/assets/javascripts/rectangles/util/constants.js b/client/assets/javascripts/rectangles/util/constants.js
new file mode 100644
index 0000000..58cb1a5
--- /dev/null
+++ b/client/assets/javascripts/rectangles/util/constants.js
@@ -0,0 +1,57 @@
+var FRONT = 0x1, BACK = 0x2, LEFT = 0x4, RIGHT = 0x8, FLOOR = 0x10, CEILING = 0x20
+ FRONT_BACK = FRONT | BACK, LEFT_RIGHT = LEFT | RIGHT, FLOOR_CEILING = FLOOR | CEILING
+
+var TOP = CEILING, BOTTOM = FLOOR,
+ TOP_LEFT = TOP | LEFT,
+ TOP_RIGHT = TOP | RIGHT,
+ BOTTOM_LEFT = BOTTOM | LEFT,
+ BOTTOM_RIGHT = BOTTOM | RIGHT,
+ TOP_BOTTOM = TOP | BOTTOM
+
+var height_min = 200,
+ height_max = 2000,
+ side_min = 10,
+ side_max = 5000,
+ resize_margin = 8,
+ cursor_amp = 1.5
+
+var painting_distance_from_wall = 8,
+ dot_distance_from_picture = 3
+
+var dot_hide_delay = 50, // ms
+ dot_side = 20
+
+var wall_rotation = {}
+wall_rotation[FRONT] = PI
+wall_rotation[BACK] = 0
+wall_rotation[LEFT] = HALF_PI
+wall_rotation[RIGHT] = -HALF_PI
+
+
+
+function sidesToString(sides){
+ var s = ""
+ if (sides & FRONT) s += "front "
+ if (sides & BACK) s += "back "
+ if (sides & LEFT) s += "left "
+ if (sides & RIGHT) s += "right "
+ if (sides & TOP) s += "top "
+ if (sides & BOTTOM) s += "bottom "
+ return s
+}
+
+function side_direction (a, b) {
+ if (a === b) return 0
+ if ((a | b) === FRONT_BACK) return 0
+ if ((a | b) === LEFT_RIGHT) return 0
+ switch (a) {
+ case FRONT:
+ return b & LEFT ? -1 : 1
+ case BACK:
+ return b & RIGHT ? -1 : 1
+ case LEFT:
+ return b & FRONT ? -1 : 1
+ case RIGHT:
+ return b & BACK ? -1 : 1
+ }
+}
diff --git a/client/assets/javascripts/rectangles/util/debug.js b/client/assets/javascripts/rectangles/util/debug.js
new file mode 100644
index 0000000..924c816
--- /dev/null
+++ b/client/assets/javascripts/rectangles/util/debug.js
@@ -0,0 +1,8 @@
+/*
+ on mousedown, z := true
+
+ if (z) // log stuff
+*/
+
+window.z = true;
+document.body.addEventListener("mousedown", function(){ z = true })
diff --git a/client/assets/javascripts/rectangles/util/keys.js b/client/assets/javascripts/rectangles/util/keys.js
new file mode 100644
index 0000000..5a5c9d2
--- /dev/null
+++ b/client/assets/javascripts/rectangles/util/keys.js
@@ -0,0 +1,165 @@
+var keys = (function(){
+
+ var base = new function(){}
+ base.tube = new Tube ()
+ base.debug = false
+
+ base.on = function(){
+ base.tube.on.apply(base.tube, arguments)
+ }
+
+ base.off = function(){
+ base.tube.off.apply(base.tube, arguments)
+ }
+
+ $(window).keydown(function(e){
+ var key = KEY_NAMES[e.keyCode];
+ switch (key) {
+ case undefined:
+ break;
+ default:
+ if (keys.debug) console.log(key)
+ base.tube(key)
+ break;
+ }
+ })
+ var KEYMAP = {
+ STRG: 17,
+ CTRL: 17,
+ CTRLRIGHT: 18,
+ CTRLR: 18,
+ SHIFT: 16,
+ RETURN: 13,
+ ENTER: 13,
+ BACKSPACE: 8,
+ BCKSP:8,
+ ALT: 18,
+ ALTR: 17,
+ ALTRIGHT: 17,
+ SPACE: 32,
+ WIN: 91,
+ MAC: 91,
+ FN: null,
+ UP: 38,
+ DOWN: 40,
+ LEFT: 37,
+ RIGHT: 39,
+ ESC: 27,
+ DEL: 46,
+ F1: 112,
+ F2: 113,
+ F3: 114,
+ F4: 115,
+ F5: 116,
+ F6: 117,
+ F7: 118,
+ F8: 119,
+ F9: 120,
+ F10: 121,
+ F11: 122,
+ F12: 123
+ },
+ KEYCODES = {
+ 'backspace' : '8',
+ 'tab' : '9',
+ 'enter' : '13',
+ 'shift' : '16',
+ 'ctrl' : '17',
+ 'alt' : '18',
+ 'pause_break' : '19',
+ 'caps_lock' : '20',
+ 'escape' : '27',
+ 'page_up' : '33',
+ 'page down' : '34',
+ 'end' : '35',
+ 'home' : '36',
+ 'left_arrow' : '37',
+ 'up_arrow' : '38',
+ 'right_arrow' : '39',
+ 'down_arrow' : '40',
+ 'insert' : '45',
+ 'delete' : '46',
+ '0' : '48',
+ '1' : '49',
+ '2' : '50',
+ '3' : '51',
+ '4' : '52',
+ '5' : '53',
+ '6' : '54',
+ '7' : '55',
+ '8' : '56',
+ '9' : '57',
+ 'a' : '65',
+ 'b' : '66',
+ 'c' : '67',
+ 'd' : '68',
+ 'e' : '69',
+ 'f' : '70',
+ 'g' : '71',
+ 'h' : '72',
+ 'i' : '73',
+ 'j' : '74',
+ 'k' : '75',
+ 'l' : '76',
+ 'm' : '77',
+ 'n' : '78',
+ 'o' : '79',
+ 'p' : '80',
+ 'q' : '81',
+ 'r' : '82',
+ 's' : '83',
+ 't' : '84',
+ 'u' : '85',
+ 'v' : '86',
+ 'w' : '87',
+ 'x' : '88',
+ 'y' : '89',
+ 'z' : '90',
+ 'left_window key' : '91',
+ 'right_window key' : '92',
+ 'select_key' : '93',
+ 'numpad 0' : '96',
+ 'numpad 1' : '97',
+ 'numpad 2' : '98',
+ 'numpad 3' : '99',
+ 'numpad 4' : '100',
+ 'numpad 5' : '101',
+ 'numpad 6' : '102',
+ 'numpad 7' : '103',
+ 'numpad 8' : '104',
+ 'numpad 9' : '105',
+ 'multiply' : '106',
+ 'add' : '107',
+ 'subtract' : '109',
+ 'decimal point' : '110',
+ 'divide' : '111',
+ 'f1' : '112',
+ 'f2' : '113',
+ 'f3' : '114',
+ 'f4' : '115',
+ 'f5' : '116',
+ 'f6' : '117',
+ 'f7' : '118',
+ 'f8' : '119',
+ 'f9' : '120',
+ 'f10' : '121',
+ 'f11' : '122',
+ 'f12' : '123',
+ 'num_lock' : '144',
+ 'scroll_lock' : '145',
+ 'semi_colon' : '186',
+ 'equal_sign' : '187',
+ 'comma' : '188',
+ 'dash' : '189',
+ 'period' : '190',
+ 'forward_slash' : '191',
+ 'grave_accent' : '192',
+ 'open_bracket' : '219',
+ 'backslash' : '220',
+ 'closebracket' : '221',
+ 'single_quote' : '222'
+ }
+ var KEY_NAMES = invert_hash(KEYCODES)
+
+ return base
+})() \ No newline at end of file
diff --git a/client/assets/javascripts/rectangles/util/mouse.js b/client/assets/javascripts/rectangles/util/mouse.js
new file mode 100644
index 0000000..3aa7cfc
--- /dev/null
+++ b/client/assets/javascripts/rectangles/util/mouse.js
@@ -0,0 +1,165 @@
+/*
+ usage:
+
+ base.mouse = new mouse({
+ el: document.querySelector("#map"),
+ down: function(e, cursor){
+ // do something with val
+ // cursor.x.a
+ // cursor.y.a
+ },
+ move: function(e, cursor, delta){
+ // delta.a (x)
+ // delta.b (y)
+ },
+ up: function(e, cursor, new_cursor){
+ // cursor.x.a
+ // cursor.y.a
+ },
+ })
+
+*/
+
+function mouse (opt) {
+ var base = this
+
+ opt = defaults(opt, {
+ el: null,
+ down: null,
+ move: null,
+ drag: null,
+ enter: null,
+ up: null,
+ rightclick: null,
+ propagate: false,
+ locked: false,
+ use_offset: true,
+ val: 0,
+ })
+
+ base.down = false
+
+ base.creating = false
+ base.dragging = false
+
+ base.cursor = new Rect(0,0,0,0)
+
+ base.tube = new Tube ()
+ opt.down && base.tube.on("down", opt.down)
+ opt.move && base.tube.on("move", opt.move)
+ opt.drag && base.tube.on("drag", opt.drag)
+ opt.enter && base.tube.on("enter", opt.enter)
+ opt.leave && base.tube.on("leave", opt.leave)
+ opt.up && base.tube.on("up", opt.up)
+ opt.rightclick && base.tube.on("rightclick", opt.rightclick)
+
+ var offset = (opt.use_offset && opt.el) ? opt.el.getBoundingClientRect() : null
+
+ base.init = function (){
+ base.bind()
+ }
+
+ base.on = function(){
+ base.tube.on.apply(base.tube, arguments)
+ }
+
+ base.off = function(){
+ base.tube.off.apply(base.tube, arguments)
+ }
+
+ base.bind = function(){
+ if (opt.el) {
+ opt.el.addEventListener("mousedown", base.mousedown)
+ opt.el.addEventListener("contextmenu", base.contextmenu)
+ }
+ window.addEventListener("mousemove", base.mousemove)
+ window.addEventListener("mouseup", base.mouseup)
+ }
+
+ base.bind_el = function(el){
+ el.addEventListener("mousedown", base.mousedown)
+ el.addEventListener("mousemove", base.mousemove)
+ }
+ base.unbind_el = function(el){
+ el.removeEventListener("mousedown", base.mousedown)
+ el.removeEventListener("mousemove", base.mousemove)
+ }
+
+ function positionFromMouse(e) {
+ if (offset) {
+ return new vec2(offset.left - e.pageX, e.pageY - offset.top)
+ }
+ else {
+ return new vec2(e.pageX, e.pageY)
+ }
+ }
+
+ base.mousedown = function(e){
+ e.stopPropagation()
+
+ if (opt.use_offset) {
+ offset = this.getBoundingClientRect()
+ }
+
+ var pos = positionFromMouse(e)
+
+ var x = pos.a, y = pos.b
+ base.cursor = new Rect (x,y, x,y)
+ base.down = true
+
+ base.tube("down", e, base.cursor)
+ }
+ base.mousemove = function(e){
+ e.stopPropagation()
+
+ if (opt.use_offset && ! offset) return
+
+ var pos = positionFromMouse(e)
+
+ if (e.shiftKey) {
+ pos.quantize(10)
+ }
+
+ var x = pos.a, y = pos.b
+
+ if (base.down) {
+ base.cursor.x.b = x
+ base.cursor.y.b = y
+ base.tube("drag", e, base.cursor)
+ }
+ else {
+ base.cursor.x.a = base.cursor.x.b = x
+ base.cursor.y.a = base.cursor.y.b = y
+ base.tube("move", e, base.cursor)
+ }
+ }
+ base.mouseenter = function(e, el){
+ if (! base.down) return
+ if (opt.use_offset && ! offset) return
+ base.tube("enter", e, el, base.cursor)
+ }
+ base.mouseleave = function(e, el){
+ if (! base.down) return
+ if (opt.use_offset && ! offset) return
+ base.tube("leave", e, el, base.cursor)
+ }
+ base.mouseup = function(e){
+ var pos, new_cursor
+
+ if (base.down) {
+ e.stopPropagation()
+ base.down = false
+ pos = positionFromMouse(e)
+ new_cursor = new Rect (pos.a, pos.b)
+ base.tube("up", e, base.cursor, new_cursor)
+ base.cursor = new_cursor
+ }
+ }
+ base.contextmenu = function(e){
+ e.preventDefault()
+ base.tube("rightclick", e, base.cursor)
+ }
+
+ base.init()
+}
+
diff --git a/client/assets/javascripts/rectangles/util/sort.js b/client/assets/javascripts/rectangles/util/sort.js
new file mode 100644
index 0000000..0985b75
--- /dev/null
+++ b/client/assets/javascripts/rectangles/util/sort.js
@@ -0,0 +1,86 @@
+
+
+function compare_rect_position(a,b){
+ if (a[0].x.a < b[0].x.a) {
+ return -1
+ }
+ if (a[0].x.a > b[0].x.a) {
+ return 1
+ }
+ if (a[0].y.a < b[0].y.a) {
+ return -1
+ }
+ if (a[0].y.a > b[0].y.a) {
+ return 1
+ }
+ return 0
+}
+
+function compare_car_reversed (a,b){
+ if (a[0] < b[0]) {
+ return 1
+ }
+ if (a[0] > b[0]) {
+ return -1
+ }
+ return 0
+}
+function compare_car (a,b){
+ if (a[0] < b[0]) {
+ return -1
+ }
+ if (a[0] > b[0]) {
+ return 1
+ }
+ return 0
+}
+
+function room_id_tuple (r){ return [r.id, r] }
+function room_height_tuple (r){ return [r.height, r] }
+function room_area_tuple (r){ return [r.rect.area(), r] }
+function rect_area_tuple (r){ return [r.area(), r] }
+
+function room_rect_tuple (r){ return [r.rect, r] }
+function identity_tuple (r){ return [r, r] }
+function car (r){ return r[0] }
+function cdr (r){ return r[1] }
+
+
+function sort_rooms_by_id(list){
+ return list.map(room_id_tuple)
+ .sort(compare_car)
+ .map(cdr)
+}
+function sort_rooms_by_height(list){
+ return list.map(room_height_tuple)
+ .sort(compare_car_reversed)
+ .map(cdr)
+}
+function sort_rooms_by_position(list){
+ return list.map(room_rect_tuple)
+ .sort(compare_rect_position)
+ .map(cdr)
+}
+function sort_rooms_by_area(list){
+ return list.map(room_area_tuple)
+ .sort(compare_car)
+ .map(cdr)
+}
+
+function sort_rects_by_position(list){
+ return list.map(identity_tuple)
+ .sort(compare_rect_position)
+ .map(cdr)
+}
+function sort_rects_by_area(list){
+ return list.map(rect_area_tuple)
+ .sort(compare_car)
+ .map(cdr)
+}
+
+function compare_z(a,b){
+ return a.rect.y.a < b.rect.y.a ? -1 : a.rect.y.a == b.rect.y.a ? 0 : 1
+}
+function compare_x(a,b){
+ return a.rect.x.a > b.rect.x.a ? -1 : a.rect.x.a == b.rect.x.a ? 0 : 1
+}
diff --git a/client/assets/javascripts/rectangles/util/wheel.js b/client/assets/javascripts/rectangles/util/wheel.js
new file mode 100644
index 0000000..6836772
--- /dev/null
+++ b/client/assets/javascripts/rectangles/util/wheel.js
@@ -0,0 +1,62 @@
+/*
+ usage:
+
+ base.wheel = new wheel({
+ el: document.querySelector("#map"),
+ update: function(e, val, delta){
+ // do something with val
+ },
+ })
+
+*/
+
+function wheel (opt) {
+ opt = defaults(opt, {
+ el: document,
+ fn: function(e, val, delta){},
+ propagate: false,
+ locked: false,
+ reversible: true,
+ ratio: 0.02,
+ val: 0,
+ })
+
+ opt.el.addEventListener('mousewheel', onMouseWheel, false);
+ opt.el.addEventListener('DOMMouseScroll', onMouseWheel, false);
+
+ function onMouseWheel (e) {
+ if (opt.locked) {
+ return
+ }
+ if (! opt.propagate) {
+ e.stopPropagation()
+ e.preventDefault()
+ }
+
+ var delta = 0;
+
+ // WebKit
+ if ( event.wheelDeltaY ) {
+ delta -= event.wheelDeltaY * opt.ratio
+ }
+ // Opera / Explorer 9
+ else if ( event.wheelDelta ) {
+ delta -= event.wheelDelta * opt.ratio
+ }
+ // Firefox
+ else if ( event.detail ) {
+ delta += event.detail * 2
+ }
+ if (! opt.reversible && delta < 0) return;
+
+ opt.val = clamp(opt.val + delta, opt.min, opt.max)
+
+ opt.update(e, opt.val, delta)
+ }
+
+ opt.lock = function(){ opt.locked = true }
+ opt.unlock = function(){ opt.locked = false }
+
+ return opt
+}
+