diff options
Diffstat (limited to 'public/assets/javascripts/rectangles')
57 files changed, 3839 insertions, 460 deletions
diff --git a/public/assets/javascripts/rectangles/_env.js b/public/assets/javascripts/rectangles/_env.js index 4b14a21..b3c7d66 100644 --- a/public/assets/javascripts/rectangles/_env.js +++ b/public/assets/javascripts/rectangles/_env.js @@ -1,6 +1,22 @@ var environment = new function(){} environment.init = function(){ + scene = new MX.Scene().addTo('#scene') + scene.width = window.innerWidth + scene.height = window.innerHeight + scene.perspective = window.innerHeight + + cam = scene.camera + cam.y = viewHeight + + if (is_mobile) { + app.movements = new MX.MobileMovements(cam, viewHeight) + } + else { + app.movements = new MX.Movements(cam, viewHeight) + } + app.movements.init() + map = new Map () if (window.scene) { @@ -10,21 +26,30 @@ environment.init = function(){ "z": 0, "rotationX": 0, // PI/2, "rotationY": PI/2, // PI -// "rotationX": 0, -// "rotationY": PI + // "rotationX": 0, + // "rotationY": PI }) scene.camera.radius = 20 } - - app.movements.gravity(true) - + + window.onresize = function () { + scene.width = window.innerWidth + scene.height = window.innerHeight + scene.perspective = window.innerHeight + scene.update() + } + Rooms.init() + Walls.init() Scenery.init() + Sculpture.init() scene.update() environment.update() + app.movements.gravity(true) + var minimap_el = document.querySelector("#minimap .el") if (minimap_el) { window.minimap = new Map ({ @@ -50,8 +75,13 @@ environment.init = function(){ } }) } -environment.update = function(t){ +environment.minimal = function(){ + environment.update = function(t){} +} +environment.update = function(t, dt){ + app.movements.update(dt || 0) + scene.update() map.update() - window.minimap && window.minimap.update && minimap.update() + window.minimap && minimap.update && minimap.update() z = false } diff --git a/public/assets/javascripts/rectangles/engine/map/_map.js b/public/assets/javascripts/rectangles/engine/map/_map.js index 99ede82..ba3ec92 100644 --- a/public/assets/javascripts/rectangles/engine/map/_map.js +++ b/public/assets/javascripts/rectangles/engine/map/_map.js @@ -15,6 +15,8 @@ var Map = function(opt){ var base = this base.el = opt.el + base.$el = $(base.el) + base.opt = opt if (! base.el) return @@ -32,7 +34,6 @@ var Map = function(opt){ scene.camera.x + sides.a, scene.camera.z + sides.b ) } - base.set_zoom = function (n) { n = clamp(n, opt.zoom_min, opt.zoom_max) base.zoom_exponent = n @@ -40,15 +41,40 @@ var Map = function(opt){ } base.set_zoom(opt.zoom) + base.resize = function(w, h){ + if (w && h) { + canvas.width = base.dimensions.a = w + canvas.height = base.dimensions.b = h + } + else { + // resize here - esp if 2d-hires + canvas.width = base.dimensions.a = base.el.parentNode.offsetWidth + canvas.height = base.dimensions.b = base.el.parentNode.offsetHeight + } + } + + base.toggle = function(state){ + return $(base.el).toggle(state).is(':visible') + } + var canvas = base.canvas = document.createElement("canvas") canvas.width = base.dimensions.a canvas.height = base.dimensions.b - + base.el.appendChild(canvas) - + switch (opt.type) { + case "ortho": + base.draw = new Map.Draw (base, { ortho: true }) + base.draw.grid_size = MAP_GRID_SIZE + base.ui = new Map.UI.Ortho (base) + base.sides = base.sides_for_center + $(window).resize(base.resize) + break + case "editor": base.draw = new Map.Draw (base) + base.draw.grid_size = MAP_GRID_SIZE base.ui = new Map.UI.Editor (base) base.sides = base.sides_for_center $(window).resize(base.resize) @@ -56,20 +82,11 @@ var Map = function(opt){ case "minimap": base.draw = new Map.Draw (base, { center: scene.camera, minimap: true }) + base.draw.grid_size = MAP_GRID_SIZE * 10 base.ui = new Map.UI.Minimap (base) base.sides = base.sides_for_camera break } - - base.resize = function(){ - canvas.width = base.dimensions.a = window.innerWidth - canvas.height = base.dimensions.b = window.innerHeight - } - - base.toggle = function(){ - $(base.el).toggle() - } - } Map.prototype.update = function(){ diff --git a/public/assets/javascripts/rectangles/engine/map/draw.js b/public/assets/javascripts/rectangles/engine/map/draw.js index 3e185d2..4f4759f 100644 --- a/public/assets/javascripts/rectangles/engine/map/draw.js +++ b/public/assets/javascripts/rectangles/engine/map/draw.js @@ -5,79 +5,129 @@ Map.Draw = function(map, opt){ var draw = this - var ctx = map.canvas.getContext("2d") + var ctx = draw.ctx = map.canvas.getContext("2d") draw.animate = function(){ ctx.save() draw.clear() - // draw.ruler() + draw.fill("rgba(255,255,255,0.98)") - if (opt.minimap) { + if (opt.ortho) { + } + else if (opt.minimap) { ctx.translate( map.dimensions.a * 1/2, map.dimensions.b * 1/2) ctx.scale( map.zoom, map.zoom ) + if (map.opt.pivot) { ctx.rotate(scene.camera.rotationY + PI) } ctx.translate( opt.center.x, - opt.center.z ) ctx.scale( -1, 1 ) draw.coords() - draw.regions(Rooms.regions, [ "#fff" ]) + if (Rooms.shapesMode) { + shapes.draw(draw.ctx, "#fff", null) + shapes.draw(draw.ctx, null, "#000") + } + else { + draw.regions(Rooms.regions, [ "#fff" ], "#000") + } draw.camera(scene.camera) } - else { ctx.translate( map.dimensions.a * 1/2, map.dimensions.b * 1/2) ctx.scale( map.zoom, map.zoom ) ctx.translate( map.center.a, map.center.b ) ctx.scale( -1, 1 ) - draw.regions(Rooms.regions, [ "#f8f8f8" ]) + draw.regions(Rooms.regions, [ "#f8f8f8" ], "#000") draw.mouse(map.ui.mouse.cursor) + draw.mouse_dimensions(map.ui.mouse.cursor) draw.coords() scene && draw.camera(scene.camera) } ctx.restore() } + draw.translate = function(){ + ctx.translate( map.dimensions.a * 1/2, map.dimensions.b * 1/2) + ctx.scale( map.zoom, map.zoom ) + ctx.translate( map.center.a, map.center.b ) + ctx.scale( -1, 1 ) + } + // changes the ctx temporarily draw.render = function(){ - ctx.save() + var thumbnail_width = 1000 + var thumbnail_height = 750 + + var extent = Rooms.extent() + var center = extent.center() + var extent_width = extent.width() + var extent_height = extent.height() + var zoom + if (extent_width > extent_height) { + zoom = thumbnail_width / extent.width() * 0.9 + } + else { + zoom = thumbnail_height / extent.height() * 0.9 + } + + var canvas = document.createElement("canvas") + canvas.width = thumbnail_width + canvas.height = thumbnail_height + ctx = canvas.getContext('2d') + draw.clear() - ctx.translate( map.dimensions.a * 1/2, map.dimensions.b * 1/2) - ctx.scale( map.zoom, map.zoom ) - ctx.translate( map.center.a, map.center.b ) + ctx.translate( thumbnail_width * 1/2, thumbnail_height * 1/2) + ctx.scale( zoom, zoom ) + ctx.translate( center.a, -center.b ) ctx.scale( -1, 1 ) - draw.regions(Rooms.regions, ["#fff"]) - draw.mouse(map.ui.mouse.cursor) - scene && draw.camera(scene.camera) - + draw.regions(Rooms.regions, ["#fff"], null) + ctx.restore() + + // invert opacity + var pixelData = ctx.getImageData(0, 0, canvas.width, canvas.height) + var pixels = pixelData.data + for (var i = 0, k, _len = pixels.length; i < _len; i++) { + k = i*4 + if (pixels[k+3] == 0) { + pixels[k] = pixels[k+1] = pixels[k+2] = pixels[k+3] = 255 + } + else { + pixels[k] = pixels[k+1] = pixels[k+2] = 255 + pixels[k+3] = 0 + } + } + ctx.putImageData(pixelData, 0, 0) + + // reset the ctx + ctx = draw.ctx + + return canvas } draw.clear = function(){ - ctx.fillStyle = "rgba(255,255,255,0.98)" ctx.clearRect(0, 0, map.dimensions.a, map.dimensions.b) - ctx.fillRect(0, 0, map.dimensions.a, map.dimensions.b) } - draw.ruler = function (){ - ctx.strokeStyle = "rgba(80,80,80,0.5)" - ctx.lineWidth = 1 - var len = 5 - for (var i = 0.5; i < map.dimensions.a; i += 10) { - line(i, 0, i, len) - line(0, i, len, i) - } + draw.fill = function(fillStyle){ + ctx.fillStyle = fillStyle + ctx.fillRect(0, 0, map.dimensions.a, map.dimensions.b) } - draw.regions = function(regions, colors){ + draw.regions = function(regions, colors, stroke){ + if (stroke) { + ctx.strokeStyle = "#000" + ctx.lineWidth = (1 / map.zoom) + } for (var i = 0; i < regions.length; i++) { if (regions[i].dupe) continue ctx.fillStyle = colors[i % colors.length] - ctx.strokeStyle = "#000" - ctx.lineWidth = (1 / map.zoom) fill_region(regions[i]) - stroke_sides(regions[i]) + if (stroke) { + stroke_sides(regions[i]) + } } } @@ -88,7 +138,8 @@ Map.Draw = function(map, opt){ ctx.beginPath(); ctx.arc(mouse.x.b, mouse.y.b, radius, 0, 2*Math.PI, false); ctx.fill(); - + } + draw.mouse_dimensions = function(mouse){ if (mouse.width() != 0 && mouse.height() != 0) { if (map.ui.dragging) { stroke_rect(mouse) @@ -124,36 +175,39 @@ Map.Draw = function(map, opt){ } draw.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) + var quant = sides.clone().quantize(draw.grid_size) + for (var x = quant.x.a - draw.grid_size; x <= quant.x.b; x += draw.grid_size) { + if (Math.round(x) % 360 == 0) { + ctx.strokeStyle = "rgba(0,0,0,0.3)" + ctx.lineWidth = 1/map.zoom + } + else { + ctx.strokeStyle = "rgba(0,0,0,0.05)" + ctx.lineWidth = 1/map.zoom + } + line(x, sides.y.a, x, sides.y.b) } + + for (var y = quant.y.a - draw.grid_size; y <= quant.y.b; y += draw.grid_size) { + if (Math.round(y) % 360 == 0) { + ctx.strokeStyle = "rgba(0,0,0,0.3)" + ctx.lineWidth = 1/map.zoom + } + else { + ctx.strokeStyle = "rgba(0,0,0,0.05)" + ctx.lineWidth = 1/map.zoom + } + line(sides.x.a, y, sides.x.b, y) + } } // - function line (x,y,a,b,translation){ + var line = draw.line = function (x,y,a,b,translation){ if (translation) { x += translation.a a += translation.a @@ -184,11 +238,11 @@ Map.Draw = function(map, opt){ line(r.x.a, r.y.a, r.x.b, r.y.b, r.translation) } - function dot_at (x,z){ + draw.dot_at = function dot_at (x, z, radius){ ctx.save() ctx.translate(x,z) - var radius = 2 / map.zoom + radius = (radius || 2) / map.zoom ctx.beginPath(); ctx.arc(0, 0, radius, 0, 2*Math.PI, false); @@ -196,4 +250,29 @@ Map.Draw = function(map, opt){ ctx.restore() } + + draw.x_at = function x_at (x, z, length){ + ctx.save() + if (x && 'x' in x) { + length = z + ctx.translate(x.x,x.z) + } + else { + ctx.translate(x,z) + } + + var len = (length/2 || 4) / map.zoom + + ctx.lineCap = "square" + ctx.lineWidth = 2/map.zoom + ctx.beginPath() + ctx.moveTo( -len, -len); + ctx.lineTo( len, len); + ctx.moveTo( -len, len); + ctx.lineTo( len, -len); + ctx.stroke(); + + ctx.restore() + } + }
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/_base.js b/public/assets/javascripts/rectangles/engine/map/tools/_base.js new file mode 100644 index 0000000..17b247d --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/_base.js @@ -0,0 +1,11 @@ +var MapTool = Fiber.extend(function(base){ + var exports = { + recenterCursor: true, + down: function(e, cursor){}, + move: function(e, cursor){}, + drag: function(e, cursor){}, + up: function(e, cursor, new_cursor){}, + cancel: function(){}, + } + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/arrow.js b/public/assets/javascripts/rectangles/engine/map/tools/arrow.js new file mode 100644 index 0000000..0b0557e --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/arrow.js @@ -0,0 +1,83 @@ +// Tool used to move corners of polylines + +var ArrowTool = MapTool.extend(function(base){ + var exports = {} + + var selected_point = null, selected_segment = null, original_point = null, selected_shape = null + var src_points, dest_points + + exports.down = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + var p = shapes.findClosestPoint(last_point) + if (p && p.shape.type() !== "ortho") { + selected_shape = p.shape + selected_point = p.point + original_point = selected_point.clone() + return + } + var segment = shapes.findClosestSegment(last_point) + if (segment) { + document.body.style.cursor = "pointer" + + selected_segment = segment + console.log(segment.head, segment.tail) + selected_shape = segment.shape + src_points = segment.shape.cloneSegment( segment ) + dest_points = segment.shape.getSegment( segment ) + + last_point.a = segment.x + last_point.b = segment.y + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + } + else { + map.ui.set_drag_tool("position") + } + } + + exports.move = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + var p = shapes.findClosestPoint(last_point) + if (p) { + document.body.style.cursor = "pointer" + last_point.assign(p.point) + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + return + } + var segment = shapes.findClosestSegment(last_point) + if (segment) { + document.body.style.cursor = "pointer" + last_point.a = segment.x + last_point.b = segment.y + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + } + else { + document.body.style.cursor = "crosshair" + } + } + + exports.drag = function(e, cursor){ + if (selected_point) { + selected_point.a = original_point.a + cursor.x.magnitude() + selected_point.b = original_point.b + cursor.y.magnitude() + selected_shape.rebuild() + } + else if (selected_segment) { + selected_shape.translateSegment( + src_points, dest_points, + cursor.x.magnitude(), cursor.y.magnitude() + ) + selected_shape.rebuild() + } + } + + exports.up = function(e, cursor){ + selected_point = selected_shape = selected_segment = original_point = null + } + + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/eraser.js b/public/assets/javascripts/rectangles/engine/map/tools/eraser.js new file mode 100644 index 0000000..8fc3687 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/eraser.js @@ -0,0 +1,29 @@ +// Tool used to delete lines + +var EraserTool = MapTool.extend(function(base){ + var exports = {} + exports.down = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + var segment = shapes.findClosestSegment(last_point) + if (segment) { + shapes.removeSegment(segment) + } + } + exports.move = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + var segment = shapes.findClosestSegment(last_point) + if (segment) { + document.body.style.cursor = "pointer" + last_point.a = segment.x + last_point.b = segment.y + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + } + else { + document.body.style.cursor = "crosshair" + } + } + return exports +}) diff --git a/public/assets/javascripts/rectangles/engine/map/tools/line.js b/public/assets/javascripts/rectangles/engine/map/tools/line.js new file mode 100644 index 0000000..8175d66 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/line.js @@ -0,0 +1,63 @@ +// Tool is used to define a very simple line between two points. +// It is used by the BlueprintScaler to specify a sample distance to scale. + +var LineTool = MapTool.extend(function(base){ + var exports = {} + + var selected_point = null + + var line = exports.line = [] + + var can_drag, dragging + + exports.down = function(e, cursor){ + + // rightclick? + if (e.ctrlKey || e.which === 3) { + cursor.quantize(1/map.zoom) + app.router.blueprintView.map.center.a = cursor.x.a + app.router.blueprintView.map.center.b = -cursor.y.a + cursor.x.b = cursor.x.a + cursor.y.b = cursor.y.a + return + } + + this.cursor = cursor + switch (line.length) { + case 0: + line[0] = cursor.x_component() + can_drag = true + break + case 1: + line[1] = cursor.x_component() + can_drag = false + break + case 2: + line[0] = cursor.x_component() + line.pop() + can_drag = true + break + } + } + + exports.move = function(e, cursor){ + this.cursor = cursor + } + + exports.drag = function(e, cursor){ + if (dragging) { + line[1].a = cursor.x.b + line[1].b = cursor.y.b + } + else if (can_drag && cursor.magnitude() > 10/map.zoom) { + line[1] = cursor.y_component() + dragging = true + } + } + + exports.up = function(e, cursor){ + can_drag = dragging = false + } + + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/ortho.js b/public/assets/javascripts/rectangles/engine/map/tools/ortho.js new file mode 100644 index 0000000..374c822 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/ortho.js @@ -0,0 +1,118 @@ +// Tool to make a polyline where all walls are orthogonal + +var OrthoPolylineTool = MapTool.extend(function (base) { + + var prev_point, horizontal = false, first_edge_is_horizontal = false + + var exports = {} + exports.down = function(e, cursor){ + // rightclick? + if (e.ctrlKey || e.which === 3) { + e.preventDefault() + e.stopPropagation() + if (map.ui.placing) { + // close polyline or cancel + map.ui.placing = false + if (shapes.workline.points.length > 2) { + shapes.workline.build() + shapes.add(shapes.workline) + } + else { + shapes.workline.reset() + } + return + } + else { + map.ui.tools.position.rightclick(e, cursor) + } + return + } + + // compare to initial point + var p = last_point.clone() + if (map.ui.placing) { + if (shapes.workline.lastPoint().eq(p)) { + return + } + else if (shapes.workline.canCloseWith(p)) { + shapes.workline.close() + shapes.workline.build() + shapes.add(shapes.workline) + map.ui.placing = false + } + else { + shapes.workline.add(p) + prev_point = p + horizontal = ! horizontal + } + } + else { + map.ui.placing = true + shapes.workline = new OrthoPolyline () + shapes.workline.add(p) + first_point = prev_point = p + horizontal = false + } + } + exports.move = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + if (map.ui.placing) { + if (shapes.workline.points.length == 1) { + var x = abs(prev_point.a - last_point.a) + var y = abs(prev_point.b - last_point.b) + if (x > y) { + last_point.b = prev_point.b + first_edge_is_horizontal = horizontal = true + } + else { + last_point.a = prev_point.a + first_edge_is_horizontal = horizontal = false + } + } + else { + if (horizontal) { + last_point.b = prev_point.b + } + else { + last_point.a = prev_point.a + } + if (horizontal == first_edge_is_horizontal) { + // check if this point is within N pixels of the normal + // and lock it into place if so + if (horizontal && abs( first_point.a - last_point.a ) < 10/map.zoom) { + last_point.a = first_point.a + } + else if (! horizontal && abs( first_point.b - last_point.b ) < 10/map.zoom) { + last_point.b = first_point.b + } + } + } + + if (shapes.workline.canCloseWith(last_point)) { + document.body.style.cursor = "pointer" + last_point.assign(first_point) + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + } + return + } + var end_point = shapes.findClosestEndPoint(last_point) + if (end_point) { + document.body.style.cursor = "pointer" + last_point.assign(end_point.point) + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + return + } + else { + document.body.style.cursor = "crosshair" + } + } + exports.cancel = function(){ + if (map.ui.placing) { shapes.workline.reset() } + first_point = null + map.ui.placing = false + } + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/polyline.js b/public/assets/javascripts/rectangles/engine/map/tools/polyline.js new file mode 100644 index 0000000..445ae26 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/polyline.js @@ -0,0 +1,73 @@ +// Tool used to draw polylines with arbitrary angles + +var PolylineTool = MapTool.extend(function (base) { + var exports = {} + exports.down = function(e, cursor){ + + // rightclick? + if (e.ctrlKey || e.which === 3) { + e.preventDefault() + e.stopPropagation() + if (map.ui.placing) { + // close polyline or cancel + map.ui.placing = false + if (shapes.workline.points.length > 2) { + shapes.workline.build() + shapes.add(shapes.workline) + } + else { + shapes.workline.reset() + } + return + } + map.ui.tools.position.rightclick(e, cursor) + return + } + + // compare to initial point + var p = last_point.clone() + if (map.ui.placing) { + if (shapes.workline.canCloseWith(p)) { + shapes.workline.close() + shapes.workline.build() + shapes.add(shapes.workline) + map.ui.placing = false + } + else { + shapes.workline.add(p) + } + } + else { + map.ui.placing = true + shapes.workline = new Polyline () + shapes.workline.add(p) + } + } + exports.move = function(e, cursor){ + last_point.a = cursor.x.a + last_point.b = cursor.y.a + if (map.ui.placing && shapes.workline.canCloseWith(last_point)) { + document.body.style.cursor = "pointer" + last_point.assign(shapes.workline.points[0]) + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + return + } + var end_point = shapes.findClosestEndPoint(last_point) + if (end_point) { + document.body.style.cursor = "pointer" + last_point.assign(end_point.point) + cursor.x.a = cursor.x.b = last_point.a + cursor.y.a = cursor.y.b = last_point.b + return + } + else { + document.body.style.cursor = "crosshair" + } + } + exports.cancel = function(){ + if (map.ui.placing) { shapes.workline.reset() } + map.ui.placing = false + } + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/position.js b/public/assets/javascripts/rectangles/engine/map/tools/position.js new file mode 100644 index 0000000..f8365bc --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/position.js @@ -0,0 +1,19 @@ +// Tool used to set position on the map and let you change the view by dragging. + +var PositionTool = MapTool.extend(function(base){ + var exports = { + recenterCursor: false, + drag: function(e, cursor){ + map.center.a = -cursor.x.magnitude() + map.center.b = cursor.y.magnitude() + }, + rightclick: function(e, cursor){ + cursor.quantize(1/map.zoom) + map.center.a = cursor.x.a + map.center.b = -cursor.y.a + cursor.x.b = cursor.x.a + cursor.y.b = cursor.y.a + } + } + return exports +})
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/map/tools/start.js b/public/assets/javascripts/rectangles/engine/map/tools/start.js new file mode 100644 index 0000000..203a85f --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/tools/start.js @@ -0,0 +1,29 @@ +// Tool is used to set the start position on the map. + +var StartPositionTool = MapTool.extend(function(base){ + var exports = {} + + var selected_point = null + + var line = exports.line = [] + + var can_drag, dragging + + exports.down = function(e, cursor){ + // rightclick? + if (e.ctrlKey || e.which === 3) { + cursor.quantize(1/map.zoom) + app.router.blueprintView.map.center.a = cursor.x.a + app.router.blueprintView.map.center.b = -cursor.y.a + cursor.x.b = cursor.x.a + cursor.y.b = cursor.y.a + return + } + + cam.x = app.controller.startPosition.x = cursor.x.a + cam.z = app.controller.startPosition.z = cursor.y.a + } + + return exports + +}) diff --git a/public/assets/javascripts/rectangles/engine/map/ui_editor.js b/public/assets/javascripts/rectangles/engine/map/ui/editor.js index f9334e6..699597a 100644 --- a/public/assets/javascripts/rectangles/engine/map/ui_editor.js +++ b/public/assets/javascripts/rectangles/engine/map/ui/editor.js @@ -26,6 +26,13 @@ Map.UI.Editor = function(map){ resize: true, destroy: false, }) + + base.blur = function(){ + Rooms.forEach(function(r){ + return r.focused = false + }) + app.tube("builder-pick-nothing") + } // @@ -35,6 +42,7 @@ Map.UI.Editor = function(map){ cursor.y.div(map.dimensions.b).sub(0.5).mul(map.dimensions.b / map.zoom).sub(map.center.b) if (e.ctrlKey || e.which === 3) { + if (Rooms.regions.length == 0) return cursor.quantize(1/map.zoom) map.center.a = cursor.x.a map.center.b = -cursor.y.a @@ -50,7 +58,7 @@ Map.UI.Editor = function(map){ return r.focused = r.rect.contains(cursor.x.a, cursor.y.a) }) - if (intersects.length && base.permissions.destroy) { + if (intersects.length && (base.permissions.destroy || e.altKey)) { base.mouse.down = false room = intersects[0] @@ -63,6 +71,9 @@ Map.UI.Editor = function(map){ Rooms.remove(room) app.tube("builder-destroy-room", room) + + // TODO: watch individual scenery object here + Minotaur.watch( (app.router.builderView || app.router.editorView).settings ) return } else if (intersects.length) { @@ -161,18 +172,22 @@ Map.UI.Editor = function(map){ cursor.y.abs().quantize(1) var room = Rooms.add_with_rect( cursor ) + Rooms.rebuild() + UndoStack.push({ type: "create-room", undo: { id: room.id }, redo: room.copy() }) + Rooms.rebuild() app.tube("builder-pick-room", room) + + // TODO: watch individual scenery object here + Minotaur.watch( app.controller.settings ) } } - if (base.resizing || base.dragging) { - var oldState = base.dragging.copy() if (base.resizing) { @@ -187,24 +202,61 @@ Map.UI.Editor = function(map){ undo: oldState, redo: base.dragging.copy() }) + Rooms.rebuild() + + // TODO: watch individual scenery object here + Minotaur.watch( (app.router.builderView || app.router.editorView).settings ) } + + var intersects = Rooms.filter(function(r){ + return r.focused = r.rect.contains(cursor.x.a, cursor.y.a) + }) + if (! base.dragging && ! intersects.length) { + app.tube("builder-pick-nothing") + } base.creating = base.dragging = base.resizing = false } var wheelState, wheelTimeout - function mousewheel (e, val, delta){ + function mousewheel (e, deltaY, deltaX){ var cursor = base.mouse.cursor var intersects = Rooms.filter(function(r){ - return r.focused = r.rect.contains(cursor.x.a, cursor.y.a) + return r.focused // = r.rect.contains(cursor.x.a, cursor.y.a) }) - if (intersects.length) { + if (intersects.length && window.heightIsGlobal) { + var rooms = Rooms.values() + wheelState = wheelState || rooms[0].height + var height = clamp( ~~(rooms[0].height + deltaY * 2), height_min, height_max ) + rooms.forEach(function(room){ + room.height = height + }) + + app.tube("builder-pick-room", intersects[0]) + + clearTimeout(wheelTimeout) + wheelTimeout = setTimeout(function(){ + UndoStack.push({ + type: "update-rooms-height", + undo: wheelState, + redo: height + }) + Rooms.rebuild() + + // TODO: watch individual scenery object here + Minotaur.watch( (app.router.builderView || app.router.editorView).settings ) + + wheelState = null + }, 250) + } + else if (intersects.length) { wheelState = wheelState || intersects[0].copy() - intersects[0].height = clamp( ~~(intersects[0].height - delta), height_min, height_max ) + intersects[0].height = clamp( ~~(intersects[0].height + deltaY * 2), height_min, height_max ) + app.tube("builder-pick-room", intersects[0]) clearTimeout(wheelTimeout) wheelTimeout = setTimeout(function(){ @@ -213,15 +265,20 @@ Map.UI.Editor = function(map){ undo: wheelState, redo: intersects[0].copy() }) - Rooms.clipper.update() + Rooms.rebuild() + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + wheelState = null - }, 500) + }, 250) } else { - map.set_zoom(map.zoom_exponent - delta/20) + map.set_zoom(map.zoom_exponent + deltaY/20) } } function rightclick (e){ } + } diff --git a/public/assets/javascripts/rectangles/engine/map/ui_minimap.js b/public/assets/javascripts/rectangles/engine/map/ui/minimap.js index fabbdb9..0fdd336 100644 --- a/public/assets/javascripts/rectangles/engine/map/ui_minimap.js +++ b/public/assets/javascripts/rectangles/engine/map/ui/minimap.js @@ -72,8 +72,8 @@ Map.UI.Minimap = function(map){ base.dragging = false } - function mousewheel (e, val, delta){ - map.set_zoom(map.zoom_exponent - delta/20) + function mousewheel (e, deltaY, deltaX){ + map.set_zoom(map.zoom_exponent - deltaY/20) } function rightclick (e){ diff --git a/public/assets/javascripts/rectangles/engine/map/ui/ortho.js b/public/assets/javascripts/rectangles/engine/map/ui/ortho.js new file mode 100644 index 0000000..5be7446 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/map/ui/ortho.js @@ -0,0 +1,107 @@ +Map.UI = Map.UI || {} +Map.UI.Ortho = function(map){ + + var base = this + var last_event = null + + base.creating = base.dragging = base.resizing = false + + base.mouse = new mouse({ + el: map.el, + down: function(e, cursor){ + last_event = e + cursor.x.div(map.dimensions.a).add(0.5).mul(map.dimensions.a / map.zoom) + cursor.y.div(map.dimensions.b).sub(0.5).mul(map.dimensions.b / map.zoom) + if (tool.recenterCursor) { + cursor.x.add(map.center.a) + cursor.y.sub(map.center.b) + base.tools[currentTool].down(e, cursor) + } + else { + base.tools[currentTool].down(e, cursor) + cursor.x.add(map.center.a) + cursor.y.sub(map.center.b) + } + }, + move: function(e, cursor){ + last_event = e + cursor.x.div(map.dimensions.a).add(0.5).mul(map.dimensions.a / map.zoom) + cursor.y.div(map.dimensions.b).sub(0.5).mul(map.dimensions.b / map.zoom) + if (tool.recenterCursor) { + cursor.x.add(map.center.a) + cursor.y.sub(map.center.b) + base.tools[currentTool].move(e, cursor) + } + else { + base.tools[currentTool].move(e, cursor) + cursor.x.add(map.center.a) + cursor.y.sub(map.center.b) + } + }, + drag: function(e, cursor){ + last_event = e + cursor.x.b = ((cursor.x.b/map.dimensions.a)+0.5) * map.dimensions.a / map.zoom + cursor.y.b = ((cursor.y.b/map.dimensions.b)-0.5) * map.dimensions.b / map.zoom + if (tool.recenterCursor) { + cursor.x.b += map.center.a + cursor.y.b -= map.center.b + base.tools[currentTool].drag(e, cursor) + } + else { + base.tools[currentTool].drag(e, cursor) + cursor.x.b += map.center.a + cursor.y.b -= map.center.b + } + }, + up: function(e, cursor, new_cursor){ + last_event = e + new_cursor.x.div(map.dimensions.a).add(0.5).mul(map.dimensions.a / map.zoom) + new_cursor.y.div(map.dimensions.b).sub(0.5).mul(map.dimensions.b / map.zoom) + if (tool.recenterCursor) { + new_cursor.x.add(map.center.a) + new_cursor.y.sub(map.center.b) + base.tools[currentTool].up(e, cursor, new_cursor) + } + else { + base.tools[currentTool].up(e, cursor, new_cursor) + new_cursor.x.add(map.center.a) + new_cursor.y.sub(map.center.b) + } + if (nextTool) { + console.log('found nextTool') + base.set_tool(nextTool) + nextTool = null + } + } + }) + + var currentTool = "polyline", nextTool, tool + base.add_tool = function(name, tool){ + base.tools[name] = tool + } + base.set_tool = function(s){ + console.log("set tool to", s) + if (base.tools[currentTool]) { + base.tools[currentTool].cancel() + } + currentTool = s + tool = base.tools[currentTool] + } + base.set_drag_tool = function(s){ + console.log('set drag tool to', s) + nextTool = currentTool + currentTool = s + tool = base.tools[currentTool] + base.tools[currentTool].down(last_event, base.mouse.cursor) + } + base.tools = {} + + base.wheel = new wheel({ + el: map.el, + update: mousewheel, + }) + + function mousewheel (e, deltaY, deltaX){ + map.set_zoom(map.zoom_exponent - deltaY/20) + } +} diff --git a/public/assets/javascripts/rectangles/engine/rooms/_rooms.js b/public/assets/javascripts/rectangles/engine/rooms/_rooms.js index 6f96275..9aff33f 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/_rooms.js +++ b/public/assets/javascripts/rectangles/engine/rooms/_rooms.js @@ -13,6 +13,7 @@ vec2 = require('../../models/vec2') Rect = require('../../models/rect') Room = require('../../models/room') + Walls = require('./_walls') UidGenerator = require('../../util/uid') sort = require('../../util/sort') _ = require('lodash') @@ -36,6 +37,7 @@ base.list = {} base.regions = [] + base.shapesMode = false base.uid = new UidGenerator(base.list) @@ -64,13 +66,13 @@ base.remove = function(room){ delete base.list[room.id] - Rooms.clipper.update() + Rooms.rebuild() } base.removeAll = function(){ base.list = {} base.regions = [] - Rooms.clipper.update() + Rooms.rebuild() } base.count = function(){ @@ -80,6 +82,10 @@ base.forEach = function(f){ return base.values().forEach(f) } + + base.some = function(f){ + return base.values().some(f) + } base.map = function(f){ return base.values().map(f) @@ -88,6 +94,17 @@ base.values = function(){ return _.values(base.list) } + + base.rebuild = function(walls_data){ + if (base.shapesMode) return + walls_data = walls_data || Walls.serialize() + Rooms.clipper.update() + Rooms.builder.rebuild() + Rooms.grouper.build() + Walls.paint() + Walls.deserialize(walls_data) + app.tube("rooms-built") + } base.serialize = function(){ var rooms = base.map(function(room){ @@ -96,7 +113,7 @@ return rooms } - base.deserialize = function(rooms_data){ + base.deserialize = function(rooms_data, walls_data){ rooms_data.forEach(function(data){ if (! data || ! data.rect) return var rect = new Rect(data.rect.x[0], data.rect.y[0], data.rect.x[1], data.rect.y[1]) @@ -107,7 +124,46 @@ }) base.add(room) }) - Rooms.clipper.update() + Rooms.rebuild(walls_data) + } + + base.deserializeFromShapes = function(data, walls_data) { + walls_data = walls_data || Walls.serialize() + base.shapesMode = true + window.viewHeight = data.viewHeight || app.defaults.viewHeight + window.wallHeight = data.wallHeight || app.defaults.wallHeight + $(".units").val( data.units ) + + Rooms.builder.clear() + + shapes.deserialize( data.shapes ) + // shapes.build() + var regions = RegionList.build() + + regions.forEach(function(region){ + var room = new Room({ + rect: region, + regions: [region], + height: wallHeight, + }) + + room.sides = region.sides + region.id = Rooms.uid("room_") + Rooms.list[ region.id ] = room + var mx_walls = Rooms.builder.build_walls(region) + room.mx_floor = Rooms.builder.make_floor(room, region) + room.mx_ceiling = Rooms.builder.make_ceiling(room, region) + + mx_walls.forEach(function(mx){ scene.add(mx) }) + scene.add(room.mx_floor) + scene.add(room.mx_ceiling) + }) + + Rooms.grouper.build() + + Walls.paint() + Walls.deserialize(walls_data) + app.tube("rooms-built") } base.report = function(){ @@ -118,6 +174,14 @@ return data } + base.extent = function(){ + var extent = new Rect ( new vec2(Infinity, -Infinity), new vec2(Infinity, -Infinity) ) + base.forEach(function(room){ + extent.expand(room.rect) + }) + return extent + } + base.sorted_by_position = function(){ return sort.rooms_by_position( base.values() ) } diff --git a/public/assets/javascripts/rectangles/engine/rooms/_walls.js b/public/assets/javascripts/rectangles/engine/rooms/_walls.js index 82ccb87..38dac84 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/_walls.js +++ b/public/assets/javascripts/rectangles/engine/rooms/_walls.js @@ -35,8 +35,23 @@ var base = this base.list = [] + base.floors = [] base.lookup = {} base.colors = {} + + base.init = function(){ + base.colors = base.copyColors( app.defaults.colors ) + } + + base.copyColors = function(colors){ + var copy = { + wall: colors.wall.slice(), + outline: colors.outline.slice(), + floor: colors.floor.slice(), + ceiling: colors.ceiling.slice(), + } + return copy + } base.first = function(){ for (var i in base.list) { @@ -46,6 +61,10 @@ } } + base.find = function(id){ + return base.lookup[id] + } + base.assign = function(list){ base.list = list base.lookup = {} @@ -53,11 +72,21 @@ base.lookup[wall.id] = wall }) } + + base.assignFloors = function(floors){ + base.floors = floors + floors.forEach(function(floor){ + base.lookup[floor.id] = floor + }) + } base.bind = function(){ base.list.forEach(function(wall){ wall.bind() }) + base.floors.forEach(function(floor){ + floor.bind() + }) } base.count = function(){ @@ -65,16 +94,20 @@ } base.forEach = function(f){ - return base.list.forEach(f) + return base.values().forEach(f) } base.map = function(f){ - return base.list.map(f) + return base.values().map(f) } - + + base.values = function(){ + return _.values(base.lookup) + } + base.serialize = function(){ var data = [] - base.list.forEach(function(wall){ + base.forEach(function(wall){ data.push(wall.serialize()) }) return data @@ -84,6 +117,9 @@ walls_data.forEach(function(wall_data){ if (! wall_data) { return } var wall = base.lookup[ wall_data.id ] + if (! wall) { + return + } wall.deserialize( wall_data ) }) }, @@ -93,21 +129,91 @@ var outlineColor = rgb_string(Walls.colors.outline) var floorColor = rgb_string(Walls.colors.floor) var ceilingColor = rgb_string(Walls.colors.ceiling) - Walls.forEach(function(wall){ + Walls.list.forEach(function(wall){ wall.outline(wallColor, outlineColor) }) - Rooms.forEach(function(room){ - room.setFloorColor(floorColor) - room.setCeilingColor(ceilingColor) + Walls.floors.forEach(function(floor){ + if (floor.ceiling) floor.color(ceilingColor) + else floor.color(floorColor) }) } + base.luminance = function(rgb){ + rgb = rgb || Walls.colors.ceiling + var rgb_max = Math.max.apply(0, rgb) + var rgb_min = Math.min.apply(255, rgb) + return (rgb_max + rgb_min ) / 2 + } + + base.setBodyColor = function(){ + $("#header").toggleClass("black", Walls.luminance() < 100) + $("body").css("background-color", rgb_string( Walls.colors.wall )) + } + base.clearBodyColor = function(){ + $("#header").removeClass("black") + $("body").css("background-color", "transparent") + } + + base.setWallpaper = { + wall: function(background){ + var img = new Image () + img.onload = function(){ + Walls.list.forEach(function(wall){ + wall.wallpaper(background, img) + }) + }.bind(this) + img.src = background.src.replace(/url\(\"?\'?/,"").replace(/\"?\'?\)/,"") + img.complete && img.onload() + }, + floor: function(background){ + Walls.floors.forEach(function(floor){ + if (floor.ceiling) return + floor.wallpaper(background) + }) + }, + ceiling: function(background){ + Walls.floors.forEach(function(floor){ + if (! floor.ceiling) return + floor.wallpaper(background) + }) + }, + } + + base.clearWallpaper = { + wall: function(){ + Walls.list.forEach(function(wall){ + wall.wallpaper("none") + }) + }, + outline: function(){ + }, + floor: function(){ + Walls.floors.forEach(function(floor){ + if (floor.ceiling) return + floor.wallpaper("none") + }) + }, + ceiling: function(){ + Walls.floors.forEach(function(floor){ + if (! floor.ceiling) return + floor.wallpaper("none") + }) + }, + } + base.setColor = { wall: function(rgb){ var rgbaColor = rgba_string(rgb, app.defaults.wallOpacity) + var rgbColor = rgb_string(rgb) + + if (Rooms.mover.room) { + $("#header").toggleClass("black", base.luminance() < 100) + $("body").css("background-color", rgbColor) + } + Walls.colors.wall = rgb - Walls.forEach(function(wall){ + Walls.list.forEach(function(wall){ wall.outline(rgbaColor, null) }) }, @@ -115,7 +221,7 @@ outline: function(rgb){ var rgbColor = rgb_string(rgb) Walls.colors.outline = rgb - Walls.forEach(function(wall){ + Walls.list.forEach(function(wall){ wall.outline(null, rgbColor) }) }, @@ -123,19 +229,20 @@ floor: function(rgb){ var rgbColor = rgb_string(rgb) Walls.colors.floor = rgb - Rooms.forEach(function(room){ - room.setFloorColor(rgbColor) + Walls.floors.forEach(function(floor){ + if (floor.ceiling) return + floor.color(rgbColor) }) }, ceiling: function(rgb){ var rgbColor = rgb_string(rgb) Walls.colors.ceiling = rgb - Rooms.forEach(function(room){ - room.setCeilingColor(rgbColor) + Walls.floors.forEach(function(floor){ + if (! floor.ceiling) return + floor.color(rgbColor) }) }, - } } diff --git a/public/assets/javascripts/rectangles/engine/rooms/builder.js b/public/assets/javascripts/rectangles/engine/rooms/builder.js index 6a89158..f78fb91 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/builder.js +++ b/public/assets/javascripts/rectangles/engine/rooms/builder.js @@ -14,6 +14,7 @@ PI = Math.PI HALF_PI = PI/2 TOP = CEILING, BOTTOM = FLOOR + $ = { browser: { mozilla: false } } function sidesToString(sides){ var s = "" if (sides & FRONT) s += "front " @@ -32,19 +33,16 @@ var els = [] base.init = function(){ - base.bind() + // base.bind() } base.bind = function(){ - app.on("clip", base.rebuild.bind(base)) } base.rebuild = function(){ if (window.scene) { base.clear() base.build() - Rooms.grouper.build() - app.tube("rooms-built") } } @@ -264,6 +262,9 @@ el.rotationX = PI/2 el.rect = region el.side = FLOOR + if ($.browser.mozilla) { + el.el.style.display = "none" + } return el } this.make_ceiling = function (room, region) { @@ -280,10 +281,13 @@ el.rotationX = -PI/2 el.rect = region el.side = CEILING + if ($.browser.mozilla) { + el.el.style.display = "none" + } return el } this.make_wall = function (klass) { - // klass += ".backface-hidden" + klass += ".backface-hidden" 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 @@ -296,7 +300,7 @@ this.el = this.rect = this.face = null } - // possible if walls are opaque + // preferable if walls are opaque // el.el.classList.add("backface-hidden") return el diff --git a/public/assets/javascripts/rectangles/engine/rooms/clipper.js b/public/assets/javascripts/rectangles/engine/rooms/clipper.js index 33e3a84..df83cc8 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/clipper.js +++ b/public/assets/javascripts/rectangles/engine/rooms/clipper.js @@ -38,7 +38,6 @@ base.update = function(){ base.solve_rects() - app.tube("clip") } var regions @@ -49,7 +48,7 @@ Rooms.regions = regions = [] return } - + base.reset_rects() base.clip_rects() var culled = base.cull_rects_iterative() diff --git a/public/assets/javascripts/rectangles/engine/rooms/grouper.js b/public/assets/javascripts/rectangles/engine/rooms/grouper.js index 663d29d..890a476 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/grouper.js +++ b/public/assets/javascripts/rectangles/engine/rooms/grouper.js @@ -48,13 +48,16 @@ base.build = function (){ var walls = [] + var floors = [] var collections = base.collect() base.cull(collections) base.group(walls, collections, FRONT) base.group(walls, collections, BACK) base.group(walls, collections, LEFT) base.group(walls, collections, RIGHT) + base.groupFloors(floors, collections) Walls.assign( walls ) + Walls.assignFloors( floors ) Walls.bind() } base.collect = function(){ @@ -63,12 +66,16 @@ collections[BACK] = [] collections[LEFT] = [] collections[RIGHT] = [] + collections[FLOOR] = [] + collections[CEILING] = [] Rooms.forEach(function(room){ room.mx_walls.forEach(function(mx){ var side = mx.side || mx.half_side collections[side].push(mx) }) + collections[FLOOR] = collections[FLOOR].concat( room.mx_floor ) + collections[CEILING] = collections[CEILING].concat( room.mx_ceiling ) }) base.cull(collections) @@ -161,6 +168,20 @@ return walls } + base.groupFloors = function(floors, collections){ + var floor = new Floor ({ + id: 'floor', + side: FLOOR, + mx: collections[FLOOR] + }) + var ceiling = new Floor ({ + id: 'ceiling', + side: CEILING, + mx: collections[CEILING] + }) + floors.push(floor) + floors.push(ceiling) + } } diff --git a/public/assets/javascripts/rectangles/engine/rooms/mover.js b/public/assets/javascripts/rectangles/engine/rooms/mover.js index 7195fcc..ac27aba 100644 --- a/public/assets/javascripts/rectangles/engine/rooms/mover.js +++ b/public/assets/javascripts/rectangles/engine/rooms/mover.js @@ -4,6 +4,15 @@ Rooms.mover = new function(){ base.room = null base.noclip = false + var cursor = base.cursor = new Rect (0,0,0,0) + var cursor_copy = new Rect (0,0,0,0) + var wall_vec = new Rect (0,0,0,0) + var intersect = new vec2(0,0) + + var origins = new Rect() + origins.x = cursor.x + origins.y = wall_vec.x + base.init = function(){ base.bind() base.update(scene.camera) @@ -11,7 +20,8 @@ Rooms.mover = new function(){ base.bind = function(){ app.on("move", base.update) - keys.on("backslash", function(){ + keys.on("backslash", function(e){ + if ( ! (e.ctrlKey || e.metaKey) ) return base.noclip = ! base.noclip base.room = null app.movements.gravity( ! base.noclip ) @@ -19,15 +29,9 @@ Rooms.mover = new function(){ } base.update = function(pos){ + var intersect, collision, distance 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.. @@ -39,17 +43,41 @@ Rooms.mover = new function(){ 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) { - cam.x = (collision & LEFT_RIGHT) ? base.room.rect.x.clampDisc(pos.x, radius) : pos.x - cam.z = (collision & FRONT_BACK) ? base.room.rect.y.clampDisc(pos.z, radius) : pos.z - return + distance = sqrt(pow(cam.x-pos.x, 2), pow(cam.z-pos.z, 2)) + + // check if we've breached one of the walls.. clamp position if so. + // there are two algorithms for this now, ridiculously.. + // - the first one is smoother, best for keyboard use + // but if the distance travelled is greater than the min. distance from the wall, + // then there is a possibility of ejection from the room. so is bad for phone/mousewheel. + + // - the second one determines fortuitously if we have breached any of the walls + // however it can get jumpy if you run into a wall.. thus it is best for devices like phone or mousewheel + // the benefit is you will never leave the room. + if (base.noclip) { + // in no-clip mode we walk through walls. } + else if (distance < radius) { + collision = base.room.collidesDisc(cam, pos, radius) + + if (collision) { + cam.x = (collision & LEFT_RIGHT) ? base.room.rect.x.clampDisc(pos.x, radius) : pos.x + cam.z = (collision & FRONT_BACK) ? base.room.rect.y.clampDisc(pos.z, radius) : pos.z + return + } + } + else { + intersect = base.intersect(pos) + if (intersect) { + cam.x = intersect.a + cam.z = intersect.b + return + } + } // in this case, we appear to have left the room.. - $(".face.active").removeClass("active") + // $(".face.active").removeClass("active") + Walls.clearBodyColor() base.room = null } @@ -65,11 +93,162 @@ Rooms.mover = new function(){ // 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") }) + if (! base.noclip) { + Walls.setBodyColor() + } + app.tube("change-room", { room: base.room }) } - } + base.intersect = function(pos){ + var closest_intersect, closest_wall, new_t, wall_t, t, min_t = 1 + + cursor_copy.x.a = cursor.x.a = cam.x + cursor_copy.x.b = cursor.x.b = cam.z + cursor_copy.y.a = cursor.y.a = pos.x + cursor_copy.y.b = cursor.y.b = pos.z + + cursor_copy.extend_ends(scene.camera.radius) + + origins.x = cursor_copy.x + origins.y = wall_vec.x + + Walls.list.forEach(function(wall, i){ + var actually_intersects = false, intersecting_face + + wall.get_points(wall_vec) + + t = perp(origins, wall_vec) / ( perp(cursor_copy, wall_vec) || 0.0000001 ) + + if ( min_t < t || t < 0 || 1 < t ) return + + intersect.a = cursor_copy.x.a + ( cursor_copy.y.a - cursor_copy.x.a ) * t + intersect.b = cursor_copy.x.b + ( cursor_copy.y.b - cursor_copy.x.b ) * t + + if ( ! is_collinear( intersect, wall_vec ) ) return + + if (wall.side & LEFT_RIGHT) { + intersecting_face = wall.surface.face_for_x(intersect.b) + } + else { + intersecting_face = wall.surface.face_for_x(intersect.a) + } + + actually_intersects = !! (intersecting_face && intersecting_face.y.a == 0) + if (actually_intersects) { + closest_intersect = intersect.clone() + closest_wall = wall_vec.clone() + min_t = t + } + }) + + if (closest_intersect) { + + var aw, len, dd + aw = angle(closest_wall) + wall_vec.assign(closest_wall) + wall_vec.x.a -= scene.camera.radius * sin(aw) + wall_vec.x.b += scene.camera.radius * cos(aw) + wall_vec.y.a -= scene.camera.radius * sin(aw) + wall_vec.y.b += scene.camera.radius * cos(aw) + + origins.x = cursor.x + origins.y = wall_vec.x + + new_t = perp(origins, wall_vec) / ( perp(cursor, wall_vec) || 0.0000001 ) + wall_t = perp(origins, cursor) / ( perp(wall_vec, cursor) || 0.0000001 ) + + intersect.a = cursor.x.a + ( cursor.y.a - cursor.x.a ) * new_t + intersect.b = cursor.x.b + ( cursor.y.b - cursor.x.b ) * new_t + + // here compare len to the length of the wall in the direction we are travelling + dd = dot2(diff(closest_wall), diff(cursor)) + len = sqrt(dot(wall_vec, wall_vec)) + if (dd > 0) { + len *= 1-abs(wall_t) + } + else { + len *= abs(wall_t) + aw += PI + } + + len = clamp(len, 0, (1-min_t) * sqrt(dot(cursor, cursor))) + + intersect.a += len * cos(aw) + intersect.b += len * sin(aw) + + wall_vec.normalize() + intersect.a = clamp(intersect.a, wall_vec.x.a, wall_vec.y.a) + intersect.b = clamp(intersect.b, wall_vec.x.b, wall_vec.y.b) + + return intersect + } + else { + return cursor.y + } + } + function diff (v) { + return new vec2(v.y.a - v.x.a, v.y.b - v.x.b) + } + function angle (va) { + return atan2(va.y.b - va.x.b, va.y.a - va.x.a) + } + function angle2 (pa, pb) { + return atan2(pb.b - pa.b, pb.a - pa.a) + } + function normal (va) { + return atan2(va.x.a - va.y.a, va.y.b - va.x.b) + } + function dot (va, vb) { + return (va.y.a - va.x.a) * (vb.y.a - vb.x.a) + (va.y.b - va.x.b) * (vb.y.b - vb.x.b) + } + function dot2 (pa, pb) { + return pa.a * pb.a + pa.b * pb.b + } + function perp (va, vb) { + return (va.y.a - va.x.a) * (vb.y.b - vb.x.b) - (va.y.b - va.x.b) * (vb.y.a - vb.x.a) + } + function is_collinear (p, vec) { + var on_x, on_y + var pa = round(p.a), pb = round(p.b) + + if (vec.x.a < vec.y.a) { + on_x = vec.x.a <= pa && pa <= vec.y.a + } + else { + on_x = vec.x.a >= pa && pa >= vec.y.a + } + + if (vec.x.b < vec.y.b) { + on_y = vec.x.b <= pb && pb <= vec.y.b + } + else { + on_y = vec.x.b >= pb && pb >= vec.y.b + } + + return !! (on_x && on_y) + } + cursor_copy.extend_ends = function(n){ + var a = angle(this) + this.x.a -= n*cos(a) + this.x.b -= n*sin(a) + this.y.a += n*cos(a) + this.y.b += n*sin(a) + return this + } + wall_vec.normalize = function(){ + var carry + if (this.x.a > this.y.a) { +// console.log("SWAP X") + carry = this.x.a + this.x.a = this.y.a + this.y.a = carry + } + if (this.x.b > this.y.b) { +// console.log("SWAP Y") + carry = this.x.b + this.x.b = this.y.b + this.y.b = carry + } + } } diff --git a/public/assets/javascripts/rectangles/engine/rooms/projector.js b/public/assets/javascripts/rectangles/engine/rooms/projector.js deleted file mode 100644 index 2eac314..0000000 --- a/public/assets/javascripts/rectangles/engine/rooms/projector.js +++ /dev/null @@ -1,30 +0,0 @@ - -rooms.projector = new function(){ - - projector = new THREE.Projector(); - vector = new THREE.Vector3( mouse.x, mouse.y, 0.5 ); - projector.unprojectVector( vector, camera ); - - raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() ); - intersects = raycaster.intersectObjects( scene.children, true ); - -} - - - -THREE.Projector = function () { - - _viewProjectionMatrix = new THREE.Matrix4(), - - this.unprojectVector = function ( vector, camera ) { - camera.projectionMatrixInverse.getInverse( camera.projectionMatrix ); - - _viewProjectionMatrix.multiplyMatrices( - camera.matrixWorld, - camera.projectionMatrixInverse - ); - - return vector.applyProjection( _viewProjectionMatrix ); - }; - -} diff --git a/public/assets/javascripts/rectangles/engine/scenery/_scenery.js b/public/assets/javascripts/rectangles/engine/scenery/_scenery.js index d34e299..6203c20 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/_scenery.js +++ b/public/assets/javascripts/rectangles/engine/scenery/_scenery.js @@ -5,11 +5,13 @@ var Scenery = new function(){ base.list = {} base.nextMedia = null + base.nextWallpaper = null - base.mouse = new mouse ({ use_offset: false }) + base.mouse = new mouse ({ use_offset: false, mousedownUsesCapture: true }) base.init = function(){ base.resize.init() + base.sound.init() } base.add = function(opt){ @@ -22,20 +24,35 @@ var Scenery = new function(){ case 'video': case 'youtube': case 'vimeo': + if (is_mobile) return scene_media = new Scenery.types.video (opt) break + + case 'soundcloud': + if (is_mobile) return + scene_media = new Scenery.types.audio (opt) + break + + case 'text': + scene_media = new Scenery.types.text (opt) + scene_media.focused = !! opt.newMedia + break } base.list[scene_media.id] = scene_media return scene_media } base.addNextToWall = function(opt){ + opt.newMedia = true opt.media = base.nextMedia opt.index = opt.index || 0 var scene_media = base.add(opt) // test if scenery was placed here - if (! scene_media.bounds) { + if (! scene_media) { + return null + } + else if (! scene_media.bounds) { base.remove( scene_media.id ) return null } @@ -48,7 +65,7 @@ var Scenery = new function(){ base.find = function(id){ return base.list[id] || null } - + base.remove = function(id){ var scene_media = base.list[id] delete base.list[id] @@ -83,6 +100,7 @@ var Scenery = new function(){ } base.deserialize = function(scenery_data){ + var added = [] scenery_data.forEach(function(data){ var wall = Walls.lookup[data.wall_id] || Walls.first() var scene_media = base.add({ @@ -91,9 +109,40 @@ var Scenery = new function(){ media: data.media, id: data.id }) + added.push(scene_media) }) + return added } + base.rewindAll = function(){ + base.forEach(function(scenery){ + if (scenery.type == "video") scenery.seek(0) + }) + } + base.playAll = function(){ + base.forEach(function(scenery){ + if (scenery.type == "video") { + scenery.unmute() + scenery.play() + } + }) + } + base.pauseAll = function(){ + base.forEach(function(scenery){ + if (scenery.type == "video") scenery.pause() + }) + } + base.muteAll = function(){ + base.forEach(function(scenery){ + if (scenery.type == "video") scenery.mx.mute() + }) + } + base.unmuteAll = function(){ + base.forEach(function(scenery){ + if (scenery.type == "video") scenery.mx.unmute() + }) + } + return base } diff --git a/public/assets/javascripts/rectangles/engine/scenery/move.js b/public/assets/javascripts/rectangles/engine/scenery/move.js index 93bccb0..94c6281 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/move.js +++ b/public/assets/javascripts/rectangles/engine/scenery/move.js @@ -2,7 +2,7 @@ Scenery.move = function(base){ var x, y, z, position, dimension, bounds - var dragging = false + var dragging = false, moved = false var oldState this.bind = function(){ @@ -14,6 +14,7 @@ Scenery.move = function(base){ } this.unbind = function(){ + base.focused = false Scenery.mouse.unbind_el(base.mx.el) Scenery.mouse.off("down", down) Scenery.mouse.off("enter", switch_wall) @@ -22,29 +23,26 @@ Scenery.move = function(base){ } function down (e, cursor){ - if (e.target != base.mx.el) return; + if (e.target != base.mx.el && (e.target != base.mx.overlay)) return; if (editor.permissions.destroy) { - UndoStack.push({ - type: 'destroy-scenery', - undo: base.serialize(), - redo: { id: base.id }, - }) - - // TODO: watch individual scenery object here - Minotaur.watch( app.router.editorView.settings ) - - Scenery.remove(base.id) + base.remove() return } - + // load the modal app.controller.pick(base) + if (! base.focused) { + e.clickAccepted = false + base.focused = true + return + } if (! (editor.permissions.move || editor.permissions.resize) ) { e.clickAccepted = false return } dragging = true + moved = false x = base.mx.x y = base.mx.y z = base.mx.z @@ -59,6 +57,8 @@ Scenery.move = function(base){ function drag (e, cursor){ if (! dragging) return + + moved = true var flipX = base.wall.side & (FRONT | RIGHT) @@ -75,13 +75,14 @@ Scenery.move = function(base){ switch (base.wall.side) { case FRONT: case BACK: - base.mx.x = position.a + delta.a * cos(wall_rotation[base.wall.side]) + dimension.a / 2 + base.mx.x = position.a + delta.a * cos(base.wall.rotationY) + dimension.a / 2 break case LEFT: case RIGHT: - base.mx.z = position.a + delta.a * sin(wall_rotation[base.wall.side]) + dimension.a / 2 + base.mx.z = position.a + delta.a * sin(base.wall.rotationY) + dimension.a / 2 break } + if (editor.permissions.resize) { Scenery.resize.move_dots() } @@ -89,21 +90,22 @@ Scenery.move = function(base){ function up (e, cursor){ if (! dragging || ! oldState) return - - dragging = false - document.body.classList.remove("dragging") - - UndoStack.push({ - type: 'update-scenery', - undo: oldState, - redo: base.serialize(), - }) + + if (moved) { + UndoStack.push({ + type: 'update-scenery', + undo: oldState, + redo: base.serialize(), + }) - // TODO: watch individual scenery object here - Minotaur.watch( app.router.editorView.settings ) + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + } + dragging = moved = false oldState = null - } + document.body.classList.remove("dragging") + } function switch_wall (e, target, cursor){ if (! dragging) return @@ -142,7 +144,7 @@ Scenery.move = function(base){ if (editor.permissions.resize) { Scenery.resize.rotate_dots() -// Scenery.resize.move_dots() + Scenery.resize.move_dots() } cursor.x.a = cursor.x.b diff --git a/public/assets/javascripts/rectangles/engine/scenery/randomize.js b/public/assets/javascripts/rectangles/engine/scenery/randomize.js new file mode 100644 index 0000000..6581f38 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/scenery/randomize.js @@ -0,0 +1,115 @@ +/* + // get the list of media we want to place + var media_objs = $(".mediaContainer").toArray().map(function(el){ + return $(el).data("media") + }) + Scenery.randomize.add( media_objs ) +*/ + +Scenery.randomize = {}; + +// Given a list of media objects, generate their ideal dimensions +Scenery.randomize.get_dimensions = function (media_objs){ + var media_list = media_objs.map(function(media){ + var width, height + if (media.width > media.height) { + width = Math.min(DEFAULT_PICTURE_WIDTH, media.width) + height = media.height/media.width * width + } + else { + height = Math.min(DEFAULT_PICTURE_WIDTH, media.height) + width = media.width/media.height * height + } + return { + dimensions: new vec2( width, height ), + media: media, + } + }) + return media_list +} + +// Build the lookup of empty walls. +// Takes a list of walls to use, or undefined to use all empty walls. +// Returns a lookup of walls to use, keyed by wall ID. +Scenery.randomize.get_empty_walls = function(wall_list){ + // get a list of all walls + var walls = {}, removed = []; + + (wall_list || Walls.list).forEach(function(wall){ + walls[wall.id] = wall + }) + + // remove the walls that already have stuff on them + if (! wall_list) { + Scenery.forEach(function(scenery){ + if (scenery.was_randomly_placed) { + // remove it and reuse this wall? + removed.push( scenery.serialize() ) + Scenery.remove( scenery.id ) + } + else { + delete walls[scenery.wall.id] + } + }) + } + + return { walls: walls, removed: removed } +} + +// Randomly place a set of media objects on empty walls. +// Only one object per wall will be added. +// Optionally takes a list of walls to use. +Scenery.randomize.add = function (media_objs, wall_list) { + var media_list = Scenery.randomize.get_dimensions(media_objs) + var empty_data = Scenery.randomize.get_empty_walls(wall_list) + var walls = empty_data.walls + var removed = empty_data.removed + var added = [] + + var wall_ids = _.keys(walls) + if (! wall_ids.length) { return } + + // randomize walls + shuffle(wall_ids) + shuffle(media_list) + + // assign each of the media to the walls, until we run out of either + media_list.some(function(media){ + // bail if we're out of walls + if (wall_ids.length == 0) { return true } + + var i, fits = -1 + + for (i = 0; i < wall_ids.length; i++) { + if (walls[wall_ids[i]].surface.fits(media.dimensions)) { + fits = i + break + } + } + + if (fits != -1) { + var wall = walls[wall_ids[fits]] + wall_ids.splice(fits, 1) + + var scenery = Scenery.add({ + media: media.media, + wall: wall, + index: 0, + }) + scenery.was_randomly_placed = true + added.push(scenery.serialize()) + } + else { + // artwork won't fit anywhere?? + } + + return false + }) + + UndoStack.push({ + type: "randomize-scenery", + undo: { added: added, removed: removed }, + redo: { added: added, removed: removed }, + }) + +}
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/scenery/resize.js b/public/assets/javascripts/rectangles/engine/scenery/resize.js index e26c0a7..252af74 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/resize.js +++ b/public/assets/javascripts/rectangles/engine/scenery/resize.js @@ -6,8 +6,9 @@ Scenery.resize = new function(){ var obj var x, y, z, bounds var dragging = false - var naturalDimension, dimension, position, scale + var naturalDimension, naturalDimensionCopy, dimension, position, scale var oldState + var rotationY var dots = [], dot, selected_dot @@ -31,7 +32,8 @@ Scenery.resize = new function(){ // generate a dot element base.build_dot = function(side) { var dot = new MX.Object3D('.dot') - dot.width = dot.height = dot_side + dot.width = dot.height = dot_side * 2 + dot.scale = 0.5 dot.side = side $(dot.el).on({ mouseenter: function(){ base.hovering = true }, @@ -48,7 +50,8 @@ Scenery.resize = new function(){ // rotate the dots as appropriate base.rotate_dots = function(){ - rotationY = wall_rotation[obj.wall.side] + // console.trace() + rotationY = obj.wall.rotationY dots.forEach(function(dot){ dot.rotationY = rotationY }) @@ -56,45 +59,43 @@ Scenery.resize = new function(){ // move all the dots to the object's current position base.move_dots = function(){ - x = obj.mx.x + sin(rotationY) * dot_distance_from_picture - y = obj.mx.y - z = obj.mx.z - cos(rotationY) * dot_distance_from_picture + var x = obj.mx.x + sin(rotationY) * dot_distance_from_picture + var y = obj.mx.y + var z = obj.mx.z - cos(rotationY) * dot_distance_from_picture dots.forEach(function(dot){ - base.move_dot(dot) + base.move_dot(dot, { x: x, y: y, z: z }) }) } // move a dot .. to the initial position of the image - base.move_dot = function(dot){ - dot.x = x - dot.y = y - dot.z = z - + base.move_dot = function(dot, pos){ if (dot.side & TOP) { - dot.y += obj.mx.height * obj.mx.scale / 2 + pos.y += obj.dimensions.b / 2 } if (dot.side & BOTTOM) { - dot.y -= obj.mx.height * obj.mx.scale / 2 + pos.y -= obj.dimensions.b / 2 } if (dot.side & LEFT) { - dot.x -= cos(rotationY) * (obj.mx.width * obj.mx.scale) / 2 - dot.z -= sin(rotationY) * (obj.mx.width * obj.mx.scale) / 2 + pos.x -= cos(rotationY) * (obj.dimensions.a) / 2 + pos.z -= sin(rotationY) * (obj.dimensions.a) / 2 } if (dot.side & RIGHT) { - dot.x += cos(rotationY) * (obj.mx.width * obj.mx.scale) / 2 - dot.z += sin(rotationY) * (obj.mx.width * obj.mx.scale) / 2 + pos.x += cos(rotationY) * (obj.dimensions.a) / 2 + pos.z += sin(rotationY) * (obj.dimensions.a) / 2 } + dot.move(pos) } // pick a new object to focus on and show the dots base.show = function(new_object) { // if (obj === new_object) return + if (! new_object) return obj = new_object - base.add_dots() base.rotate_dots() base.move_dots() + Sculpture.resize.hide() } // dismiss the dots on blur @@ -119,22 +120,13 @@ Scenery.resize = new function(){ 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(){ + base.unbind = function(){ dots.forEach(function(dot){ Scenery.mouse.unbind_el(dot.el) }) @@ -146,15 +138,19 @@ Scenery.resize = new function(){ 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 naturalDimension = obj.naturalDimensions dimension = obj.dimensions position = new vec3(obj.mx.x, obj.mx.y, obj.mx.z) - scale = obj.mx.scale oldState = obj.serialize() + + if (obj.type == "text") { + naturalDimensionCopy = naturalDimension.clone() + positionCopy = position.clone() + } document.body.classList.add("dragging") } @@ -167,7 +163,6 @@ Scenery.resize = new function(){ var width = cursor.x.magnitude() var height = cursor.y.magnitude() var mag = cursor.magnitude() - var old_width = dimension.a * scale if (abs(width) >Â abs(height)) { mag = x_sign * mag * sign(width) @@ -176,18 +171,23 @@ Scenery.resize = new function(){ mag = y_sign * mag * sign(height) } - obj.set_scale( ( dimension.a + mag ) / naturalDimension.a ) - // dimension.a // scale * (old_width + mag) / old_width + if (obj.type == "text") { + obj.mx.width = obj.media.width = naturalDimension.a = naturalDimensionCopy.a + (mag * 2) + obj.mx.height = obj.media.height = naturalDimension.b = naturalDimensionCopy.b + (mag * 2) + dimension.a = naturalDimension.a * obj.scale + dimension.b = naturalDimension.b * obj.scale + } + else { + obj.set_scale( ( dimension.a + mag ) / naturalDimension.a ) + } -// console.log(scale, obj.mx.scale, dimension.a + mag, naturalDimension.a) - - if (selected_dot.side & LEFT_RIGHT) { - obj.mx.x = position.a + cos(rotationY) * mag/2 * (x_sign) - obj.mx.z = position.c + sin(rotationY) * mag/2 * (x_sign) - } - if (selected_dot.side & TOP_BOTTOM) { - obj.mx.y = position.b - mag/2 * y_sign - } + if (selected_dot.side & LEFT_RIGHT) { + obj.mx.x = position.a + cos(rotationY) * mag/2 * (x_sign) + obj.mx.z = position.c + sin(rotationY) * mag/2 * (x_sign) + } + if (selected_dot.side & TOP_BOTTOM) { + obj.mx.y = position.b - mag/2 * y_sign + } base.move_dots() @@ -195,12 +195,28 @@ Scenery.resize = new function(){ } function up (e, cursor){ + if (! dragging) return dragging = false - selected_dot = null if (! editor.permissions.resize) { return } - obj.scale = obj.mx.ops.scale = obj.mx.scale - obj.dimensions.assign(obj.naturalDimensions).mul(obj.scale) + if (obj.type == "text") { + var newHeight = $(obj.mx.inner).height() + if (selected_dot.side & BOTTOM) { + obj.mx.y = position.b + (naturalDimensionCopy.b - newHeight) / 2 * obj.scale + } + else { + obj.mx.y = dots[0].y - newHeight/2*obj.scale + } + obj.mx.height = obj.media.height = naturalDimension.b = newHeight + dimension.a = naturalDimension.a * obj.scale + dimension.b = naturalDimension.b * obj.scale + base.move_dots() + } + else { + obj.scale = obj.mx.ops.scale = obj.mx.scale + obj.dimensions.assign(obj.naturalDimensions).mul(obj.scale) + } + UndoStack.push({ type: 'update-scenery', undo: oldState, @@ -211,6 +227,7 @@ Scenery.resize = new function(){ Minotaur.watch( app.router.editorView.settings ) document.body.classList.remove("dragging") + selected_dot = null } } diff --git a/public/assets/javascripts/rectangles/engine/scenery/sound.js b/public/assets/javascripts/rectangles/engine/scenery/sound.js new file mode 100644 index 0000000..5a783d6 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/scenery/sound.js @@ -0,0 +1,16 @@ + +Scenery.sound = {} +Scenery.sound.max_distance = 1500 +Scenery.sound.init = function(){ + app.tube.on("move", Scenery.sound.move) +} +Scenery.sound.move = function(){ + Scenery.forEach(function(scenery){ + if ((scenery.type == "video" || scenery.type == "audio")) { // && ! scenery.muted()) { + var distance = dist(cam.x, cam.z, + scenery.mx.x, scenery.mx.z) + var volume = 1 - (clamp( distance, 0, Scenery.sound.max_distance ) / Scenery.sound.max_distance) + scenery.setVolume(volume) + } + }) +} diff --git a/public/assets/javascripts/rectangles/engine/scenery/types/_object.js b/public/assets/javascripts/rectangles/engine/scenery/types/_object.js index 2dbae48..e3b9b4d 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/types/_object.js +++ b/public/assets/javascripts/rectangles/engine/scenery/types/_object.js @@ -28,32 +28,119 @@ Scenery.types.base = Fiber.extend(function(base){ this.dimensions = this.naturalDimensions.clone().mul(this.scale) }, + place: function(opt){ + if (opt.data) { + if (opt.wall) { + var position = opt.wall.mxToPosition(opt.data.position) + opt.index = opt.wall.surface.index_for_x( position.a, 0 ) + } + this.set_wall(opt) + this.deserialize(opt.data) + } + else { + this.set_wall(opt) + if (this.wall && ! this.bounds) { + this.find_minimum_scale(opt) + if (! this.bounds) return + } + + this.recenter() + if (opt.position) { + this.translateTo(opt.position) + } + var mx_position = this.wall.positionToMx( this.position, this.dimensions ) + this.mx.move(mx_position) + } + }, + + find_minimum_scale: function(opt){ + var bounds = this.wall.surface.bounds_at_index_with_dimensions(opt.index || 0, new vec2(50, 50)) + var scale = 1 + if (! bounds || bounds.width() < 50 || bounds.height < 50) return + if (this.naturalDimensions.a > bounds.width()) { + scale = bounds.width() / (this.naturalDimensions.a + 10) + } + if (this.naturalDimensions.b > bounds.height()) { + scale = Math.min(scale, bounds.height() / (this.naturalDimensions.b + 10)) + } + this.set_scale(scale) + this.set_wall(opt) + }, + recenter: function () { if (! this.bounds) return var center = this.bounds.center() center.a -= this.dimensions.a / 2 center.b -= this.dimensions.b / 2 - var mx_position = this.wall.positionToMx( center, this.dimensions ) - this.mx.move(mx_position) this.position.assign(center) }, + translateTo: function (position){ + var flipX = this.wall.side & (FRONT | RIGHT) + var delta = position.clone().subVec(this.position) + delta.a -= this.dimensions.a/2 + delta.b -= this.dimensions.b/2 + + if (flipX) { delta.a *= -1 } + + var new_bounds = this.wall.surface.translate( this.bounds, this.dimensions, this.position, delta ) + + this.position.b += delta.b + + switch (this.wall.side) { + case FRONT: + case BACK: + this.position.a += delta.a * cos(this.wall.rotationY) + break + case LEFT: + case RIGHT: + this.position.a += delta.a * sin(this.wall.rotationY) + break + } + }, + bind: function(){ this.move.bind() - $(this.mx.el).bind({ - mouseenter: this.enter, - mouseleave: this.leave, - }) +// $(this.mx.el).bind({ +// mouseenter: this.enter, +// mouseleave: this.leave, +// }) }, unbind: function(){ this.move.unbind() - $(this.mx.el).unbind({ - mouseenter: this.enter, - mouseleave: this.leave, - }) +// $(this.mx.el).unbind({ +// mouseenter: this.enter, +// mouseleave: this.leave, +// }) }, + remove: function(){ + if (this.removed) return + this.removed = true + + UndoStack.push({ + type: 'destroy-scenery', + undo: this.serialize(), + redo: { id: this.id }, + }) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + + Scenery.remove(this.id) + + Scenery.resize.hide() + if (app.controller.mediaEditor) { + app.controller.mediaEditor.tainted = false + app.controller.mediaEditor.hide() + } + if (app.controller.textEditor) { + app.controller.textEditor.tainted = false + app.controller.textEditor.hide() + } + }, + destroy: function(){ this.unbind() scene.remove(this.mx) @@ -70,17 +157,17 @@ Scenery.types.base = Fiber.extend(function(base){ }, enter: function(e){ - if (editor.permissions.resize) { - Scenery.resize.show(this) - Scenery.hovering = true - } +// if (editor.permissions.resize) { +// Scenery.resize.show(this) +// Scenery.hovering = true +// } }, leave: function(e){ - if (editor.permissions.resize) { - Scenery.resize.defer_hide(this) - Scenery.hovering = false - } +// if (editor.permissions.resize) { +// Scenery.resize.defer_hide(this) +// Scenery.hovering = false +// } }, serialize: function(){ diff --git a/public/assets/javascripts/rectangles/engine/scenery/types/audio.js b/public/assets/javascripts/rectangles/engine/scenery/types/audio.js new file mode 100644 index 0000000..fdd221d --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/scenery/types/audio.js @@ -0,0 +1,77 @@ + +Scenery.types.audio = Scenery.types.base.extend(function(base){ + + var exports = { + + type: 'audio', + + init: function(opt){ + + opt.scale = 1.0 + + base.init.call(this, opt) + + this.build() + this.bind() + this.place(opt) + }, + + build: function(){ + this.mx = new MX.Soundcloud({ + scale: this.scale, + media: this.media, + y: this.scale * this.media.height/2, + backface: false, + }) + scene.add( this.mx ) + this.mx.load() + }, + + serialize: function(){ + var data = base.serialize.call(this) + return data + }, + + deserialize: function(data){ + this.mx.move(data.position) + this.mx.ops.width = data.dimensions.a + this.mx.ops.height = data.dimensions.b + this.dimensions.deserialize(data.dimensions) + }, + + setVolume: function(n){ + this.mx.setVolume(n) + }, + + play: function(){ + this.mx.play() + }, + + pause: function(){ + this.mx.pause() + }, + + toggle: function(){ + this.mx.toggle() + }, + + paused: function(){ + return this.mx.paused + }, + + muted: function(){ + return this.mx.muted + }, + + seek: function(n){ + this.mx.seek(n) + }, + + setLoop: function(shouldLoop){ + this.mx.setLoop(shouldLoop) + }, + + } + + return exports +}) diff --git a/public/assets/javascripts/rectangles/engine/scenery/types/image.js b/public/assets/javascripts/rectangles/engine/scenery/types/image.js index d2fa3ab..0e5e77c 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/types/image.js +++ b/public/assets/javascripts/rectangles/engine/scenery/types/image.js @@ -3,27 +3,17 @@ Scenery.types.image = Scenery.types.base.extend(function(base){ var exports = { + type: 'image', + init: function(opt){ - opt.scale = opt.scale || (opt.data && opt.data.scale) || 300 / max(300, opt.media.width) + opt.scale = opt.scale || (opt.data && opt.data.scale) || DEFAULT_PICTURE_WIDTH / max(DEFAULT_PICTURE_WIDTH, opt.media.width) base.init.call(this, opt) this.build() this.bind() - - if (opt.data) { - if (opt.wall) { - var position = opt.wall.mxToPosition(opt.data.position) - opt.index = opt.wall.surface.index_for_x( position.a, 0 ) - } - this.set_wall(opt) - this.deserialize(opt.data) - } - else { - this.set_wall(opt) - this.bounds && this.recenter() - } + this.place(opt) }, build: function(){ @@ -46,6 +36,7 @@ Scenery.types.image = Scenery.types.base.extend(function(base){ this.mx.move(data.position) this.mx.ops.width = data.dimensions.a this.mx.ops.height = data.dimensions.b + this.dimensions.deserialize(data.dimensions) }, } diff --git a/public/assets/javascripts/rectangles/engine/scenery/types/text.js b/public/assets/javascripts/rectangles/engine/scenery/types/text.js new file mode 100644 index 0000000..6e11da2 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/scenery/types/text.js @@ -0,0 +1,58 @@ + +Scenery.types.text = Scenery.types.base.extend(function(base){ + + var exports = { + + type: 'text', + + init: function(opt){ + + opt.scale = 0.5 + + base.init.call(this, opt) + + this.build() + this.bind() + this.place(opt) + }, + + build: function(){ + this.mx = new MX.Text({ + scale: this.scale, + media: this.media, + y: this.scale * this.media.height/2, + backface: false, + }) + scene.add( this.mx ) + }, + + setText: function(text){ + this.media.description = text + this.mx.setText( text ) + }, + + setFont: function(style){ + for (var i in style) { + if (style.hasOwnProperty(i)) { + this.media.font[i] = style[i] + } + } + this.mx.setFont(this.media.font) + }, + + serialize: function(){ + var data = base.serialize.call(this) + return data + }, + + deserialize: function(data){ + this.mx.move(data.position) + this.mx.ops.width = data.dimensions.a + this.mx.ops.height = data.dimensions.b + this.dimensions.deserialize(data.dimensions) + }, + + } + + return exports +}) diff --git a/public/assets/javascripts/rectangles/engine/scenery/types/video.js b/public/assets/javascripts/rectangles/engine/scenery/types/video.js index ef25d8d..163e19e 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/types/video.js +++ b/public/assets/javascripts/rectangles/engine/scenery/types/video.js @@ -2,27 +2,18 @@ Scenery.types.video = Scenery.types.base.extend(function(base){ var exports = { + + type: 'video', init: function(opt){ - opt.scale = opt.scale || 300 / max(300, opt.media.width) - + + opt.scale = opt.scale || (opt.data && opt.data.scale) || DEFAULT_PICTURE_WIDTH / max(DEFAULT_PICTURE_WIDTH, opt.media.width) + base.init.call(this, opt) this.build() this.bind() - - if (opt.data) { - if (opt.wall) { - var position = opt.wall.mxToPosition(opt.data.position) - opt.index = opt.wall.surface.index_for_x( position.a, 0 ) - } - this.set_wall(opt) - this.deserialize(opt.data) - } - else { - this.set_wall(opt) - this.bounds && this.recenter() - } + this.place(opt) }, build: function(){ @@ -47,7 +38,8 @@ Scenery.types.video = Scenery.types.base.extend(function(base){ backface: false, }) scene.add(this.mx) - this.mx.load() + + this.mx.load() }, play: function(){ @@ -99,6 +91,14 @@ Scenery.types.video = Scenery.types.base.extend(function(base){ } }, + unmute: function(){ + this.mx.unmute() + }, + + setVolume: function(n){ + this.mx.setVolume(n) + }, + serialize: function(){ var data = base.serialize.call(this) return data @@ -106,6 +106,9 @@ Scenery.types.video = Scenery.types.base.extend(function(base){ deserialize: function(data){ this.mx.move(data.position) + this.mx.ops.width = data.dimensions.a + this.mx.ops.height = data.dimensions.b + this.dimensions.deserialize(data.dimensions) }, } diff --git a/public/assets/javascripts/rectangles/engine/scenery/undo.js b/public/assets/javascripts/rectangles/engine/scenery/undo.js index e5624a0..b976ea2 100644 --- a/public/assets/javascripts/rectangles/engine/scenery/undo.js +++ b/public/assets/javascripts/rectangles/engine/scenery/undo.js @@ -4,12 +4,14 @@ type: "create-scenery", undo: function(state){ Scenery.remove(state.id) + Scenery.resize.hide() // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) }, redo: function(state){ - Scenery.deserialize([ state ]) + var scenery = Scenery.deserialize([ state ]) + Scenery.resize.show( scenery ) // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) @@ -19,8 +21,10 @@ type: "update-scenery", undo: function(state){ var scenery = Scenery.find(state.id) + var wall = Walls.find( state.wall_id ) + scenery.deserialize(state) - scenery.set_wall(Walls.find( state.wall_id )) + scenery.set_wall({ wall: wall }) if (editor.permissions.resize) { Scenery.resize.show(scenery) @@ -31,13 +35,15 @@ }, redo: function(state){ var scenery = Scenery.find(state.id) + var wall = Walls.find( state.wall_id ) + scenery.deserialize(state) - scenery.set_wall(Walls.find( state.wall_id )) + scenery.set_wall({ wall: wall }) if (editor.permissions.resize) { Scenery.resize.show(scenery) - Scenery.resize.rotate_dots() Scenery.resize.move_dots() + Scenery.resize.rotate_dots() } // TODO: watch individual scenery object here @@ -47,12 +53,14 @@ { type: "destroy-scenery", undo: function(state){ - Scenery.deserialize([ state ]) + var scenery = Scenery.deserialize([ state ]) + Scenery.resize.show( scenery ) // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) }, redo: function(state){ + Scenery.resize.hide() Scenery.remove(state.id) // TODO: watch individual scenery object here @@ -66,11 +74,11 @@ type: "create-room", undo: function(room){ Rooms.remove(room) - Rooms.clipper.update() + Rooms.rebuild() }, redo: function(room){ Rooms.add(new Room(room)) - Rooms.clipper.update() + Rooms.rebuild() app.tube("builder-pick-room", room) }, }, @@ -80,14 +88,14 @@ var room = Rooms.list[state.id] room.rect.assign( state.rect ) room.height = state.height - Rooms.clipper.update() + Rooms.rebuild() app.tube("builder-pick-room", room) }, redo: function(state){ var room = Rooms.list[state.id] room.rect.assign( state.rect ) room.height = state.height - Rooms.clipper.update() + Rooms.rebuild() app.tube("builder-pick-room", room) }, }, @@ -95,12 +103,22 @@ type: "destroy-room", undo: function(room){ Rooms.add(new Room(room)) - Rooms.clipper.update() + Rooms.rebuild() app.tube("builder-pick-room", room) }, redo: function(room){ Rooms.remove(room) - Rooms.clipper.update() + Rooms.rebuild() + }, + }, + + { + type: "update-rooms-height", + undo: function(state){ + var rooms = Rooms.values() + rooms.forEach(function(room){ + room.height = state + }) }, }, @@ -111,7 +129,32 @@ undo: function(state){ var wall = Walls.lookup[state.id] wall.deserialize(state) - + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "update-all-wallpaper", + undo: function(state){ + Walls.deserialize(state) + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "choose-preset", + undo: function(state){ + app.controller.colorControl.load(state.colors) + Walls.deserialize(state.walls) + Minotaur.watch( app.router.editorView.settings ) + }, + redo: function(state){ + app.controller.presets.loadByName(state) + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "choose-another-preset", + undo: function(state){ + app.controller.presets.loadByName(state) Minotaur.watch( app.router.editorView.settings ) }, }, @@ -119,11 +162,103 @@ type: "update-colors", undo: function(state){ Walls.setColor[ state.mode ]( state.rgb ) - app.router.editorView.lightControl.setSwatchColor( state.mode, state.rgb ) + app.router.editorView.colorControl.setSwatchColor( state.mode, state.rgb ) + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "randomize-scenery", + undo: function(state){ + state.added.forEach(function(_scenery){ + Scenery.remove(_scenery.id) + }) + var scenery_list = Scenery.deserialize(state.removed) + scenery_list.forEach(function(scenery){ + scenery.was_randomly_placed = true + }) + Scenery.resize.hide() + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + redo: function(state){ + state.removed.forEach(function(_scenery){ + Scenery.remove(_scenery.id) + }) + var scenery_list = Scenery.deserialize(state.added) + scenery_list.forEach(function(scenery){ + scenery.was_randomly_placed = true + }) + + // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) }, }, + // + { + type: "create-sculpture", + undo: function(state){ + Sculpture.remove(state.id) + Sculpture.resize.hide() + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + redo: function(state){ + var scenery = Sculpture.deserialize([ state ]) + Sculpture.resize.show( sculpture ) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "update-sculpture", + undo: function(state){ + var sculpture = Sculpture.find(state.id) + + sculpture.deserialize(state) + + if (editor.permissions.resize) { + Sculpture.resize.show(sculpture) + } + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + redo: function(state){ + var sculpture = Sculpture.find(state.id) + + sculpture.deserialize(state) + + if (editor.permissions.resize) { + Sculpture.resize.show(sculpture) + Sculpture.resize.move_dots() + Sculpture.resize.rotate_dots() + } + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + }, + { + type: "destroy-sculpture", + undo: function(state){ + var sculpture = Sculpture.deserialize([ state ]) + Sculpture.resize.show( sculpture ) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + redo: function(state){ + Sculpture.resize.hide() + Sculpture.remove(state.id) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + }, + }, + ]) })() diff --git a/public/assets/javascripts/rectangles/engine/sculpture/_sculpture.js b/public/assets/javascripts/rectangles/engine/sculpture/_sculpture.js new file mode 100644 index 0000000..888b925 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/sculpture/_sculpture.js @@ -0,0 +1,105 @@ + +var Sculpture = new function(){ + + var base = this; + + base.list = {} + + base.mouse = new mouse ({ use_offset: false, mousedownUsesCapture: true }) + + base.init = function(){ + app.on("move", base.updateBillboards) + base.resize.init() + } + + base.updateBillboards = function(){ + base.forEach(function(sculpture){ + if (sculpture.billboard) { + sculpture.mx.rotationY = cam.rotationY + } + }) + if (Sculpture.resize.obj && Sculpture.resize.obj.billboard) { + Sculpture.resize.move_dots() + } + } + + base.add = function(opt){ + var sculpture + switch (opt.media.type) { + case 'image': + sculpture = new Sculpture.types.image (opt) + break + } + base.list[sculpture.id] = sculpture + return sculpture + } + + base.addNext = function(opt){ + opt.newMedia = true + opt.media = Scenery.nextMedia + var sculpture = base.add(opt) + + // test if sculpture was placed here + if (! sculpture) { + return null + } + else { + Scenery.nextMedia = null + return sculpture + } + } + + base.find = function(id){ + return base.list[id] || null + } + + base.remove = function(id){ + var scene_media = base.list[id] + delete base.list[id] + scene_media && scene_media.destroy() + } + + base.removeAll = function(){ + base.forEach(function(scene_media){ + base.remove(scene_media.id) + }) + } + + base.uid = new UidGenerator(base.list) + + base.forEach = function(f){ + return base.values().forEach(f) + } + + base.map = function(f){ + return base.values().map(f) + } + + base.values = function(){ + return _.values(base.list) + } + + base.serialize = function(){ + var sculptures = base.map(function(sculpture){ + return sculpture.serialize() + }) + return sculptures + } + + base.deserialize = function(sculpture_data){ + var added = [] + sculpture_data.forEach(function(data){ + var scene_media = base.add({ + id: data.id, + data: data, + media: data.media, + }) + added.push(scene_media) + }) + return added + } + + return base +} + +Sculpture.types = {} diff --git a/public/assets/javascripts/rectangles/engine/sculpture/move.js b/public/assets/javascripts/rectangles/engine/sculpture/move.js new file mode 100644 index 0000000..0cbeccd --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/sculpture/move.js @@ -0,0 +1,111 @@ + +Sculpture.move = function(base){ + + var x, y, z, position, dimension, bounds + var rotationY + var dragging = false, altPressed = false, shiftPressed = false, moved = false + var oldState + var height, width + + this.bind = function(){ + Sculpture.mouse.bind_el(base.mx.el) + Sculpture.mouse.on("down", down) + Sculpture.mouse.on("drag", drag) + Sculpture.mouse.on("up", up) + } + + this.unbind = function(){ + base.focused = false + Sculpture.mouse.unbind_el(base.mx.el) + Sculpture.mouse.off("down", down) + Sculpture.mouse.off("drag", drag) + Sculpture.mouse.off("up", up) + } + + function down (e, cursor){ + if (e.target != base.mx.el && (e.target != base.mx.overlay)) return; + if (editor.permissions.destroy) { + base.remove() + return + } + + // load the modal + app.controller.pick(base) + + if (! base.focused) { + e.clickAccepted = false + base.focused = true + return + } + if (! (editor.permissions.move || editor.permissions.resize) ) { + e.clickAccepted = false + return + } + altPressed = (e.altKey && ! base.billboard) + shiftPressed = e.shiftKey + dragging = true + moved = false + x = base.mx.x + y = base.mx.y + z = base.mx.z + rotationY = base.mx.rotationY + height = base.mx.height * base.mx.scale + + bounds = base.bounds + dimension = base.dimensions + + oldState = base.serialize() + document.body.classList.add("dragging") + } + + function drag (e, cursor){ + if (! dragging) return + + moved = true + + var delta = cursor.delta() + delta.mul( cursor_amp ) // TODO: this should be proportional to your distance from the wall + delta.b *= -1 + + // here we need to move the element based on the XY coords, as + // base.mx.y = position.b + delta.b + dimension.b / 2 + if (shiftPressed) { + base.mx.y = clamp( y + delta.b, height/2 + sculpture_distance_from_floor, Infinity ) + } + else if (altPressed) { + base.mx.rotationY = mod( rotationY + delta.a / (window.innerWidth / 2) , TWO_PI ) + Sculpture.resize.move_dots() + } + else { + base.mx.x = x + delta.a * cos(cam.rotationY) - delta.b * sin(cam.rotationY) + base.mx.z = z + delta.a * sin(cam.rotationY) + delta.b * cos(cam.rotationY) + } + + if (editor.permissions.resize) { + Sculpture.resize.move_dots() + base.updateOutline() + } + } + + function up (e, cursor){ + if (! dragging || ! oldState) return + + if (moved) { + UndoStack.push({ + type: 'update-sculpture', + undo: oldState, + redo: base.serialize(), + }) + + // TODO: watch individual sculpture object here + Minotaur.watch( app.router.editorView.settings ) + } + + dragging = moved = false + oldState = null + document.body.classList.remove("dragging") + } + + return this + +}
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/sculpture/resize.js b/public/assets/javascripts/rectangles/engine/sculpture/resize.js new file mode 100644 index 0000000..5f21d66 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/sculpture/resize.js @@ -0,0 +1,208 @@ +Sculpture.resize = new function(){ + + var base = this + + var obj + var x, y, z, bounds + var dragging = false + var naturalDimension, naturalDimensionCopy, dimension, position, scale + var oldState + var rotationY + + 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 * 2 + dot.scale = 0.5 + 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) + }) + } + + // move all the dots to the object's current position + base.move_dots = function(){ + rotationY = obj.mx.rotationY + + var x = obj.mx.x + sin(rotationY) * dot_distance_from_picture + var y = obj.mx.y + var z = obj.mx.z - cos(rotationY) * dot_distance_from_picture + + dots.forEach(function(dot){ + base.move_dot(dot, { x: x, y: y, z: z, rotationY: rotationY }) + }) + } + + // move a dot .. to the initial position of the image + base.move_dot = function(dot, pos){ + if (dot.side & TOP) { + pos.y += obj.dimensions.b / 2 + } + if (dot.side & BOTTOM) { + pos.y -= obj.dimensions.b / 2 + } + if (dot.side & LEFT) { + pos.x -= cos(rotationY) * (obj.dimensions.a) / 2 + pos.z -= sin(rotationY) * (obj.dimensions.a) / 2 + } + if (dot.side & RIGHT) { + pos.x += cos(rotationY) * (obj.dimensions.a) / 2 + pos.z += sin(rotationY) * (obj.dimensions.a) / 2 + } + dot.move(pos) + } + + // pick a new object to focus on and show the dots + base.show = function(new_object) { + // if (obj === new_object) return + if (! new_object) return + base.obj = obj = new_object + base.add_dots() + base.move_dots() + } + + // dismiss the dots on blur + var dotsHideTimeout; + base.defer_hide = function(){ + clearTimeout(dotsHideTimeout) + + dotsHideTimeout = setTimeout(function(){ + if (Scenery.hovering || Scenery.resize.hovering || Scenery.mouse.down) return + Scenery.resize.hide() + }, dot_hide_delay) + } + + base.hide = function () { + if (! obj) return + base.obj = obj = null + dots.forEach(function(dot){ + scene.remove(dot) + }) + } + + base.bind = function(){ + dots.forEach(function(dot){ + Sculpture.mouse.bind_el(dot.el) + }) + Sculpture.mouse.on("down", down) + Sculpture.mouse.on("drag", drag) + Sculpture.mouse.on("up", up) + } + + base.unbind = function(){ + dots.forEach(function(dot){ + Sculpture.mouse.unbind_el(dot.el) + }) + Sculpture.mouse.off("down", down) + Sculpture.mouse.off("drag", drag) + Sculpture.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 + + naturalDimension = obj.naturalDimensions + dimension = obj.dimensions + position = new vec3(obj.mx.x, obj.mx.y, obj.mx.z) + oldState = obj.serialize() + + if (obj.type == "text") { + naturalDimensionCopy = naturalDimension.clone() + positionCopy = position.clone() + } + + document.body.classList.add("dragging") + } + + function drag (e, cursor){ + if (! dragging) return + + 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 width = cursor.x.magnitude() + var height = cursor.y.magnitude() + var mag = cursor.magnitude() + + if (abs(width) >Â abs(height)) { + mag = x_sign * mag * sign(width) + } + else { + mag = y_sign * mag * sign(height) + } + + if (obj.type == "text") { + obj.mx.width = obj.media.width = naturalDimension.a = naturalDimensionCopy.a + (mag * 2) + obj.mx.height = obj.media.height = naturalDimension.b = naturalDimensionCopy.b + (mag * 2) + dimension.a = naturalDimension.a * obj.scale + dimension.b = naturalDimension.b * obj.scale + } + else { + obj.set_scale( ( dimension.a + mag ) / naturalDimension.a ) + } + + if (selected_dot.side & LEFT_RIGHT) { + obj.mx.x = position.a + cos(rotationY) * mag/2 * (x_sign) + obj.mx.z = position.c + sin(rotationY) * mag/2 * (x_sign) + } + if (selected_dot.side & TOP_BOTTOM) { + obj.mx.y = position.b - mag/2 * y_sign + } + + base.move_dots() + obj.updateOutline() + + app.controller.sculptureEditor.setDimensions() + } + + function up (e, cursor){ + if (! dragging) return + dragging = false + if (! editor.permissions.resize) { return } + + obj.scale = obj.mx.ops.scale = obj.mx.scale + obj.dimensions.assign(obj.naturalDimensions).mul(obj.scale) + + UndoStack.push({ + type: 'update-sculpture', + undo: oldState, + redo: obj.serialize(), + }) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + + document.body.classList.remove("dragging") + selected_dot = null + } +}
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/sculpture/types/_object.js b/public/assets/javascripts/rectangles/engine/sculpture/types/_object.js new file mode 100644 index 0000000..2f593e8 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/sculpture/types/_object.js @@ -0,0 +1,176 @@ +Sculpture.types.base = Fiber.extend(function(base){ + + var exports = { + + init: function(opt){ + this.id = opt.id || Sculpture.uid("sculpture") + // this.move = new Sculpture.move (this) + this.media = opt.media + this.naturalDimensions = new vec3(this.media.width, this.media.height, this.media.width) + this.dimensions = new vec3(this.media.width, this.media.height, this.media.width) + + this.move = new Sculpture.move (this) + this.billboard = true + this.backface = true + + this.set_scale( opt.scale || this.media.scale || 1.0 ) + this.position = new vec2(0,0) + + this.isSculpture = true + }, + + set_scale: function(scale){ + this.scale = scale || 1.0 + if (this.mx) { + this.mx.scale = this.mx.ops.scale = this.scale + } + this.dimensions = this.naturalDimensions.clone().mul(this.scale) + }, + set_depth: function(depth){ + console.log(this.dimensions.c, this.naturalDimensions.c, depth) + this.dimensions.c = depth + this.naturalDimensions.c = depth / this.scale + }, + + place: function(opt){ + if (opt.data) { + this.deserialize(opt.data) + } + else { + this.mx.move(opt.position) + } + }, + + bind: function(){ + this.move.bind() + }, + + unbind: function(){ + this.move.unbind() + }, + + setOutline: function(val){ + this.outline = val + if (val && ! this.outlineEl) { + this.buildOutline() + } + this.outlineEl.el.style.display = val ? "block" : "none" + }, + setOutlineColor: function(val){ + this.outlineColor = val + if (this.outlineEl) { + this.outlineColor = this.outlineEl.el.style.borderColor = val + } + }, + setBillboard: function(val){ + this.billboard = val + if (this.billboard) { + this.mx.el.classList.add("backface-hidden") + this.mx.el.classList.remove("backface-visible") + this.mx.rotationY = cam.rotationY + } + else { + this.mx.el.classList.add("backface-visible") + this.mx.el.classList.remove("backface-hidden") + this.mx.rotationY = quantize(cam.rotationY, PI/2) + } + Sculpture.resize.move_dots() + }, + + buildOutline: function(){ + this.outlineEl = new MX.Object3D(".mx-outline") + this.outlineEl.width = this.naturalDimensions.a + this.outlineEl.height = this.naturalDimensions.c + this.outlineEl.scale = this.mx.scale + this.outlineEl.rotationX = -PI/2 + this.outlineEl.x = this.mx.x + this.outlineEl.y = sculpture_distance_from_floor + this.outlineEl.z = this.mx.z + this.outlineEl.el.style.borderColor = this.outlineColor || "#000000" + scene.add(this.outlineEl) + }, + updateOutline: function(){ + if (! this.outline) { return } + if (! this.outlineEl) { + this.buildOutline() + } + this.outlineEl.x = this.mx.x + this.outlineEl.y = sculpture_distance_from_floor + this.outlineEl.z = this.mx.z + this.outlineEl.width = this.naturalDimensions.a + this.outlineEl.height = this.naturalDimensions.c + + this.outlineEl.scale = this.mx.scale + }, + + remove: function(){ + if (this.removed) return + this.removed = true + + UndoStack.push({ + type: 'destroy-sculpture', + undo: this.serialize(), + redo: { id: this.id }, + }) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + + Sculpture.remove(this.id) + + Sculpture.resize.hide() + if (app.controller.sculptureEditor) { + app.controller.sculptureEditor.tainted = false + app.controller.sculptureEditor.hide() + } + }, + + destroy: function(){ + this.unbind() + scene.remove(this.mx) + this.mx.media = null + this.mx.ops = null + this.mx = null + this.move = null + this.media = null + this.dimensions = null + this.naturalDimensions = null + this.wall = null + this.bounds = null + this.center = null + }, + + serialize: function(){ + var data = { + id: this.id, + dimensions: this.dimensions.serialize(), + position: app.position(this.mx), + scale: this.scale, + media: this.media, + billboard: this.billboard, + outline: this.outline, + outlineColor: this.outlineColor || "#000000", + backface: this.backface, + } + return data + }, + + deserialize: function(data){ + this.mx.move(data.position) + this.mx.ops.width = data.dimensions.a + this.mx.ops.height = data.dimensions.b + this.billboard = data.billboard + this.outline = data.outline + this.outlineColor = data.outlineColor || "#000000" + this.backface = data.backface + if (! this.backface) this.mx.el.classList.remove("backface-visible") + this.dimensions.deserialize(data.dimensions) + if (this.outline) { + this.buildOutline() + } + }, + } + + return exports + +}) diff --git a/public/assets/javascripts/rectangles/engine/sculpture/types/image.js b/public/assets/javascripts/rectangles/engine/sculpture/types/image.js new file mode 100644 index 0000000..1a53f5b --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/sculpture/types/image.js @@ -0,0 +1,38 @@ + +Sculpture.types.image = Sculpture.types.base.extend(function(base){ + + var exports = { + + type: 'image', + + init: function(opt){ + + opt.scale = opt.scale || (opt.data && opt.data.scale) || DEFAULT_PICTURE_WIDTH / max(DEFAULT_PICTURE_WIDTH, opt.media.width) + + base.init.call(this, opt) + + this.build(opt) + this.bind() + this.place(opt) + }, + + build: function(opt){ + this.mx = new MX.Image({ + src: this.media.url, + scale: this.scale, + media: this.media, + backface: true, + }) + + if (opt.position) { + opt.position.y = opt.position.y || this.scale * this.media.height/2 + sculpture_distance_from_floor + opt.position.rotationY = opt.position.rotationY || scene.camera.rotationY + } + + scene.add( this.mx ) + }, + + } + + return exports +}) diff --git a/public/assets/javascripts/rectangles/engine/shapes/ortho.js b/public/assets/javascripts/rectangles/engine/shapes/ortho.js new file mode 100644 index 0000000..163f646 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/shapes/ortho.js @@ -0,0 +1,66 @@ +// An OrthoPolyline is a Polyline where all angles are 90 degrees. + +if (! ('window' in this) ) { + var Polyline = require("./polyline.js") +} + +var OrthoPolyline = Polyline.extend(function(base){ + var exports = {} + exports.type = function(){ + return "ortho" + } + exports.instantiate = function(){ + return new OrthoPolyline + } + exports.canCloseWith = function(p){ + return (this.points.length > 2 && this.points[0].distanceTo( p ) < 10/map.zoom) + } + exports.draw = function(ctx, fillStyle, strokeStyle){ + var points = this.points + if (! points.length) return + if (points.length == 1) { + ctx.fillStyle = "#f80" + map.draw.dot_at(this.points[0].a, points[0].b, 5) + } + if (points.length > 1) { + ctx.fillStyle = fillStyle + ctx.strokeStyle = strokeStyle + ctx.lineWidth = 2 / map.zoom + ctx.beginPath() + ctx.moveTo(points[0].a, points[0].b) + points.forEach(function(point, i){ + i && ctx.lineTo(point.a, point.b) + }) + strokeStyle && ctx.stroke() + if (! map.ui.placing || this.closed) { + fillStyle && ctx.fill() + } + } + } + exports.translateSegment = function(src, dest, dx, dy) { + if (src[0].a == src[1].a) { + dest[0].a = src[0].a + dx + dest[1].a = src[1].a + dx + if (src.length == 3) { + dest[2].a = src[2].a + dx + } + } + else { + dest[0].b = src[0].b + dy + dest[1].b = src[1].b + dy + if (src.length == 3) { + dest[2].b = src[2].b + dy + } + } + } + exports.close = function(){ + this.points[this.points.length] = this.points[0] + this.closed = true + } + return exports +}) + + +if (! ('window' in this) ) { + module.exports = OrthoPolyline +} diff --git a/public/assets/javascripts/rectangles/engine/shapes/polyline.js b/public/assets/javascripts/rectangles/engine/shapes/polyline.js new file mode 100644 index 0000000..b2cd92f --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/shapes/polyline.js @@ -0,0 +1,212 @@ +// A Polyline is a set of points inputted by the user using the V2 editor to trace a blueprint. +// Additionally, it manages a set of MX objects which correspond to the walls in 3D. +// In this way, it attempts to bridge the 2D (canvas, imperative) and 3D (css, declarative) views. + +if (! ('window' in this) ) { + var Fiber = require("../../../vendor/bower_components/fiber/src/fiber.js") + var vec2 = require("../../models/vec2") +} + +var Polyline = Fiber.extend(function(base){ + var exports = {} + exports.init = function(){ + this.points = [] + this.mx_points = [] + this.closed = false + } + exports.type = function(){ + return "polyline" + } + exports.instantiate = function(){ + return new Polyline + } + exports.add = function(p){ + this.points.push( p ) + this.mx_points.push( new MX.Point(p) ) + } + exports.firstPoint = function(){ + return this.points[0] + } + exports.lastPoint = function(){ + return this.points[this.points.length-1] + } + exports.canCloseWith = function(p){ + return (this.points.length > 2 && this.points[0].distanceTo( p ) < 10/map.zoom) + } + exports.getHeadAtIndex = function(index){ + if (index == 0) { return null } + if (index == this.points.length-1) { return this.clone() } + var head = this.instantiate() + head.points = this.points.slice(0, index+1) + return head + } + exports.getTailAtIndex = function(index){ + if (index == this.points.length-1) { return null } + if (index == 0) { return this.clone() } + var tail = this.instantiate() + tail.points = this.points.slice(index, this.points.length) + return tail + } + exports.clone = function(){ + var clone = this.instantiate() + clone.points = this.points.concat() + } + exports.getSegment = function(segment){ + var seg = [ + this.points[segment.head], + this.points[segment.tail], + ] + if (segment.head == 0) { + seg.push( this.lastPoint() ) + } + else if (segment.tail == this.points.length-1) { + seg.push( this.firstPoint() ) + } + return seg + } + exports.cloneSegment = function(segment){ + return this.getSegment(segment).map(function(point){ return point.clone() }) + } + exports.translateSegment = function(src, dest, dx, dy){ + dest[0].a = src[0].a + dx + dest[0].b = src[0].b + dy + dest[1].a = src[1].a + dx + dest[1].b = src[1].b + dy + if (src.length == 3) { + dest[2].a = src[2].a + dx + dest[2].b = src[2].b + dy + } + } + exports.hasPointNear = function(p){ + var point + for (var i = 0; i <Â this.points.length; i++){ + point = this.points[i] + if (point.distanceTo( p ) < 10/map.zoom) { + return point + } + } + return null + } + exports.hasEndPointNear = function(p){ + if (this.closed || ! this.points.length) return null + if (this.firstPoint().distanceTo( p ) < 10/map.zoom) { + return this.firstPoint() + } + if (this.lastPoint().distanceTo( p ) < 10/map.zoom) { + return this.lastPoint() + } + return null + } + exports.hasSegmentNear = function(p, min_dist){ + var p1, p2, d1, d2, sum, rat + var dx, dy, new_x, new_y, x, y, closest_distance = min_dist || Infinity + var closest_i = -1 + var points = this.points + var p1, p2 = points[0] + for (var i = 1; i < points.length; i++) { + p1 = p2 + p2 = points[i] + d1 = p2.a - p1.a + d2 = p2.b - p1.b + sum = d1*d1 + d2*d2 + rat = ((p.a - p1.a) * d1 + (p.b - p1.b) * d2) / sum + rat = rat < 0 ? 0 : rat < 1 ? rat : 1 + new_x = p1.a + rat * d1 + new_y = p1.b + rat * d2 + dx = new_x - p.a + dy = new_y - p.b + sum2 = sqrt(dx*dx+dy*dy) + if (sum2 < closest_distance) { + x = new_x + y = new_y + closest_distance = sum2 + closest_i = i + } + } + if (closest_i == -1) return null + return { + x: x, + y: y, + distance: closest_distance, + head: closest_i-1, + tail: closest_i, + } + } + exports.draw = function(ctx, fillStyle, strokeStyle){ + var points = this.points + if (! points.length) return + if (points.length == 1) { + ctx.fillStyle = "#f80" + map.draw.dot_at(this.points[0].a, points[0].b, 5) + } + if (points.length > 1) { + ctx.fillStyle = fillStyle + ctx.strokeStyle = strokeStyle + ctx.lineWidth = 2 / map.zoom + ctx.beginPath() + ctx.moveTo(points[0].a, points[0].b) + points.forEach(function(point, i){ + i && ctx.lineTo(point.a, point.b) + }) + strokeStyle && ctx.stroke() + if (! map.ui.placing || this.closed) { + fillStyle && ctx.fill() + } + } + } + exports.draw_line = function (ctx, p){ + var last = this.points[this.points.length-1] + ctx.strokeStyle = "#f80" + ctx.lineWidth = 2 / map.zoom + ctx.beginPath() + ctx.moveTo(last.a, last.b) + ctx.lineTo(p.a, p.b) + ctx.stroke() + } + exports.close = function(){ + this.points[this.points.length] = this.points[0] + this.closed = true + } + exports.build = function(){ + this.mx_points && this.mx_points.forEach(function(mx){ scene.remove(mx) }) + this.mx = new MX.Polyline(this) + } + exports.rebuild = function(){ + this.mx.rebuild() + } + exports.getSegments = function(){ + if (this.points.length == 1) { + return [] + } + var segments = [] + for (var i = 1; i < this.points.length; i++) { + segments.push( [ this.points[i-1], this.points[i] ] ) + } + return segments + } + exports.serialize = function(){ + return { + type: this.type(), + closed: this.closed, + points: this.points.map(function(point){ return [point.a, point.b] }), + } + } + exports.deserialize = function(data){ + this.closed = data.closed || false + this.points = (data.points || data).map(function(point){ return new vec2(point[0], point[1]) }) + } + exports.reset = function(){ + this.mx_points.forEach(function(mx){ scene.remove(mx) }) + this.mx_points.length = 0 + this.points.length = 0 + } + exports.destroy = function(){ + this.reset() + this.mx && this.mx.destroy() + } + return exports +}) + +if (! ('window' in this) ) { + module.exports = Polyline +} diff --git a/public/assets/javascripts/rectangles/engine/shapes/regionlist.js b/public/assets/javascripts/rectangles/engine/shapes/regionlist.js new file mode 100644 index 0000000..8c9e732 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/shapes/regionlist.js @@ -0,0 +1,240 @@ +// This algorithm takes the polylines from ShapeList as input and produces +// a set of Rooms which can be used by the existing V1 mover and editor. + +// The algorithm assumes that +// 1) all angles are orthogonal +// 2) all polylines are closed + +if (! ('window' in this) ) { + var Fiber = require("../../../vendor/bower_components/fiber/src/fiber.js") + var vec2 = require("../../models/vec2") + var Rect = require("../../models/rect") + var sort = require("../../util/sort") +} + +var RegionList = (function(){ + + var RegionList = {} + var regions = RegionList.regions + + // Build a list of regions from the existing shapes. + // Operates on all the shapes at once. + RegionList.build = function(){ + var segments = RegionList.getSortedSegments() + return RegionList.buildRoomsFromSegments(segments) + } + + // Build a list of regions from the individual shapes. + // Same, but operates on the shapes individually + RegionList.buildByShape = function(){ + var shapes = RegionList.getSortedSegmentsByShape() + var region_lists = shapes.map(function(shape){ + return RegionList.buildRoomsFromSegments(shape.segments) + }) + var regions = [] + return regions.concat.apply(regions, region_lists); + } + + RegionList.buildRoomsFromSegments = function(segments){ + + var rooms = [] + var seen_rooms = {} + var open_segments = [] + + var segment, open_segment, x_segment, y_segments, overlapped, seen_segments + + // first pass: generate rooms from the vertical segments only + for (var i = 0; i < segments.length; i++) { + segment = segments[i] + if (! segment.isVertical()) { + continue + } + + // check all the "open segments" we know about, i.e. rooms where we've only found + // the right wall. + overlapped = false + for (var j = 0; j < open_segments.length; j++) { + open_segment = open_segments[j] + + // if these two segments overlap each other, then there is a room between them. + if (segment.y.overlaps(open_segment.y)) { + overlapped = true + open_segments.splice(j--, 1) + + // the X part of the room will be the span between these two vertical segments' + // X components. the Y part of the room is the "split" or subdivision between + // the two horizontal vectors. + + // the split function is non-commutative, + // so we need to call A split B and B split A, + // then dedupe the segments we got back.. + x_segment = new vec2( open_segment.x.a, segment.x.b ) + y_segments = open_segment.y.split(segment.y, 0, 0) + + seen_segments = {} + + // check each of the splits.. if the two segments overlap, then we definitely + // have a room here. + y_segments.forEach(function(seg){ + seen_segments[ seg[0]+"" ] = true + var room = new Rect( x_segment, seg[0] ) + + if (seen_rooms[ room+"" ]) return + seen_rooms[ room+"" ] = true + + room.sides = 0 + + rooms.push(room) + var new_seg = new Rect( segment.x, seg[0] ) + open_segments.unshift(new_seg) + j++ + }) + + // splitting the other way.. + y_segments = segment.y.split(open_segment.y, 0, 0) + y_segments.forEach(function(seg){ + if (seen_segments[ seg[0]+"" ]) return; + var new_seg = new Rect( segment.x, seg[0] ) + open_segments.unshift(new_seg) + j++ + }) + } + } + + // if we have overlap, then re-sort the open segments Y-wise + // and (again) dedupe.. + if (overlapped) { + open_segments = open_segments.sort(function(a,b){ + if (a.y.a < b.y.a) { return -1 } + if (a.y.a == b.y.a) { return 0 } + if (a.y.a > b.y.a) { return 1 } + }) + + if (open_segments.length < 2) { continue } + + for (var k = 1; k < open_segments.length; k++) { + if (open_segments[k-1].y.containsVec(open_segments[k].y)) { + open_segments.splice(k--, 1) + } + else if (open_segments[k-1].y.overlaps(open_segments[k].y)) { + open_segments[k].y = open_segments[k].y.clone() + open_segments[k].y.a = open_segments[k-1].y.b + } + } + } + // if we don't have any overlap, then this is a new open segment. + else { + open_segments.push(segment) + } + } + + var splits, splitter + + // second pass: now that we have a bunch of rooms, assign sides to all of them. + // sides are used in the "mover" script to do bounds checking. + for (var i = 0; i < segments.length; i++) { + segment = segments[i] + var horizontal = segment.isHorizontal(), vertical = segment.isVertical() + for (var r = 0; r < rooms.length; r++){ + room = rooms[r] + + // vertical segments determine the left and right edges of the room, fairly simply. + if (vertical) { + if (segment.y.containsVec(room.y)) { + if (segment.x.a == room.x.a) { + room.sides |= LEFT + } + if (segment.x.a == room.x.b) { + room.sides |= RIGHT + } + } + } + + // horizontal segments determine the front and back edges of the room. + if (horizontal) { + if (segment.x.containsVec(room.x)) { + if (segment.y.a == room.y.a) { + room.sides |= FRONT + } + if (segment.y.a == room.y.b) { + room.sides |= BACK + } + } + + // however, since we did not split on horizontal segments, our rooms may + // only have partial overlap with these segments, in which case we need to + // split the rooms apart. + else if ((segment.y.a == room.y.a || segment.y.a == room.y.b) && room.x.overlaps(segment.x)) { + + // split the room across the segment. preserve whether or not we know the + // room borders a segment on the left or right. + splits = room.x.split(segment.x, room.sides & LEFT, room.sides & RIGHT) + rooms.splice(r--, 1) + for (var k = 0; k < splits.length; k++) { + splitter = splits[k] + var new_room = new Rect( splitter[0], room.y ) + new_room.sides = splitter[1] | (Â room.sides & FRONT_BACK ) + if (segment.x.overlaps( splitter[0] )) { + if (segment.y.a == new_room.y.a) { + new_room.sides |= FRONT + } + if (segment.y.a == new_room.y.b) { + new_room.sides |= BACK + } + } + rooms.unshift(new_room) + r++ + } + } + + } + } + } + + return rooms + } + + // Gets a list of polylines from the ShapeList and sorts the segments. + RegionList.getSortedSegments = function(){ + // get a list of all segments from these polylines + var segments = shapes.getAllSegments() + + segments = segments.map(RegionList.segmentsToRects) + + return sort.rects_by_position(segments) + } + + RegionList.getSortedSegmentsByShape = function(){ + return shapes.getAllShapeSegments().map(function(shape){ + var segments = shape.map(RegionList.segmentsToRects) + return { + shape: shape, + segments: sort.rects_by_position(segments) + } + }) + } + + // re-orient a segment so it's either facing up or right and make it into a rect + RegionList.segmentsToRects = function(segment){ + // vertical + if (segment[0].a == segment[1].a) { + if (segment[0].b > segment[1].b) { + segment.push(segment.shift()) + } + } + // horizontal + else if (segment[0].b == segment[1].b) { + if (segment[0].a > segment[1].a) { + segment.push(segment.shift()) + } + } + return new Rect( segment[0].a, segment[0].b, segment[1].a, segment[1].b ) + } + + if (! ('window' in this) ) { + module.exports = RegionList + } + + return RegionList + +})()
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/engine/shapes/shapelist.js b/public/assets/javascripts/rectangles/engine/shapes/shapelist.js new file mode 100644 index 0000000..21beb76 --- /dev/null +++ b/public/assets/javascripts/rectangles/engine/shapes/shapelist.js @@ -0,0 +1,124 @@ +// The ShapeList manages the list of polylines which form a V2 layout. + +if (! ('window' in this) ) { + var Fiber = require("../../../vendor/bower_components/fiber/src/fiber.js") + var Polyline = require("./polyline.js") + var OrthoPolyline = require("./ortho.js") +} + +var ShapeList = Fiber.extend(function(base){ + var exports = {} + exports.init = function(){ + this.shapes = [] + this.workline = null + } + exports.add = function(shape){ + this.shapes.push(shape) + } + exports.remove = function(shape){ + var index = this.shapes.indexOf(shape) + if (index !== -1) { + this.shapes.splice(index, 1) + } + } + exports.destroy = function(){ + this.shapes.forEach(function(shape){ + shape.destroy() + }) + this.shapes = [] + } + exports.count = function(){ + return this.shapes.length + } + exports.removeSegment = function (segment){ + var shape = segment.shape + var head = shape.getHeadAtIndex(segment.head) + var tail = shape.getTailAtIndex(segment.tail) + this.remove(shape) + shape.destroy() + if (head) { + this.add(head) + head.build() + } + if (tail) { + this.add(tail) + tail.build() + } + } + exports.findClosestPoint = function (p){ + var point + for (var i = 0; i < this.shapes.length; i++) { + point = this.shapes[i].hasPointNear(p) + if (point) return { point: point, shape: this.shapes[i] } + } + return null + } + exports.findClosestEndPoint = function (p){ + var point + for (var i = 0; i < this.shapes.length; i++) { + point = this.shapes[i].hasEndPointNear(p) + if (point) return { point: point, shape: this.shapes[i] } + } + return null + } + exports.findClosestSegment = function (p){ + var segment = null, closest_segment = null + for (var i = 0; i < this.shapes.length; i++) { + segment = this.shapes[i].hasSegmentNear(p, 10) + if (segment && (! closest_segment || segment.distance < closest_segment.distance)) { + closest_segment = segment + closest_segment.shape = this.shapes[i] + } + } + return closest_segment + } + exports.forEach = function(fn){ + this.shapes.forEach(fn) + } + exports.getAllShapeSegments = function(){ + return this.shapes.map(function(shape){ return shape.getSegments() }) + } + exports.getAllSegments = function(){ + var segments = [] + this.shapes.forEach(function(shape){ + segments = segments.concat( shape.getSegments() ) + }) + return segments + } + exports.draw = function(ctx, fillStyle, strokeStyle) { + this.shapes.forEach(function(shape){ + shape.draw(ctx, fillStyle, strokeStyle) + }) + } + exports.serialize = function(){ + return this.shapes.map(function(shape){ + return shape.serialize() + }) + } + exports.deserialize = function(data){ + data && data.forEach(function(shape_data){ + var line + switch (shape_data.type) { + case 'ortho': + line = new OrthoPolyline() + break + default: + line = new Polyline() + break + } + line.deserialize(shape_data) + shapes.add(line) + }.bind(this)) + } + exports.build = function(){ + this.shapes.forEach(function(shape){ + shape.build() + }) + } + return exports +}) + +if (! ('window' in this) ) { + shapes = new ShapeList + module.exports = shapes +} diff --git a/public/assets/javascripts/rectangles/models/floor.js b/public/assets/javascripts/rectangles/models/floor.js new file mode 100644 index 0000000..63eebcc --- /dev/null +++ b/public/assets/javascripts/rectangles/models/floor.js @@ -0,0 +1,218 @@ +(function(){ + + var vec2, Rect, sort + if ('window' in this) { + vec2 = window.vec2 + Rect = window.Rect + sort = window.sort + } + else { + vec2 = require('./vec2') + Rect = require('./rect') + UidGenerator = require('../util/uid') + } + + var Floor = function(opt){ + this.id = opt.id + this.side = opt.side + this.ceiling = opt.side & CEILING + this.mx = opt.mx + } + + Floor.prototype.serialize = function(){ + return { + id: this.id, + background: this.background, + } + } + + Floor.prototype.deserialize = function(data){ + this.wallpaper( data.background ) + } + + Floor.prototype.bind = function(){ + var base = this + base.$els = $( this.mx.map(function(mx){ return mx.el }) ) + + this.mx.forEach(function(mx, index){ + $(mx.el).bind({ + contextmenu: function(e){ + if (! (e.ctrlKey || e.metaKey || e.shiftKey) ) { + e.preventDefault() + } + if (Scenery.nextMedia) { + e.preventDefault() + Scenery.nextMedia = null + app.tube('cancel-scenery') + } + else if (Scenery.nextWallpaper) { + e.preventDefault() + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } + }, + + mousedown: function(e){ + + // right-click + if (e.which == 3) { + if (Scenery.nextMedia) { + e.preventDefault() + Scenery.nextMedia = null + app.tube('cancel-scenery') + } + else if (Scenery.nextWallpaper) { + e.preventDefault() + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } + return + } + + + var offset = offsetFromPoint(e, mx.el) + if (! offset) { return } + + var x = mx.x + mx.width * (offset.left-0.5) + var z = mx.z + mx.height * (0.5-offset.top) + + if (Scenery.nextMedia) { + e.preventDefault() + + var sculpture = Sculpture.addNext({ + position: { x: x, y: 0, z: z }, + }) + + // scenery was not placed + if (! sculpture) { + e.stopPropagation() + return + } + + app.controller.toolbar.resetPermissions() + Sculpture.resize.show(sculpture) + Sculpture.hovering = true + + // app.controller.pick(sculpture) + + UndoStack.push({ + type: 'create-sculpture', + undo: { id: sculpture.id }, + redo: sculpture.serialize(), + }) + + // TODO: watch individual sculpture object here + Minotaur.watch( app.router.editorView.settings ) + } + else if (Scenery.nextWallpaper) { + var oldState = base.serialize() + base.wallpaper(Scenery.nextWallpaper) + // Scenery.nextWallpaper = null + + UndoStack.push({ + type: 'update-wallpaper', + undo: oldState, + redo: base.serialize(), + }) + + // TODO: watch individual scenery object here + Minotaur.watch( app.router.editorView.settings ) + + app.controller.pickWall(base, null) + } + else { + app.controller.pickWall(base, null) + } + } + }) + }) + + // flip the mx order + var shouldFlip = this.side & (CEILING) + if (! shouldFlip) { + this.mx.reverse() + } + } + + Floor.prototype.color = function(color){ + this.$els.css("background-color", color) + } + + Floor.prototype.wallpaper = function(background){ + if (! background) { + background = { src: "none" } + } + else if (typeof background == "string") { + background = { src: background } + } + else if (! background.src) { + background = { src: "none" } + } + background.x = background.x || 0 + background.y = background.y || 0 + background.scale = background.scale || 1 + + this.background = background + this.background.src = this.background.src.replace(/url\(\"?\'?/,"").replace(/\"?\'?\)/,"") + + if (this.background.src == "none") { + this.wallpaperLoad(this.background.src) + return + } + + var img = new Image () + img.onload = function(){ + this.backgroundImage = img + this.wallpaperLoad(this.background.src) + this.wallpaperPosition(background) + }.bind(this) + img.src = this.background.src + img.complete && img.onload() + } + + Floor.prototype.wallpaperLoad = function(url){ + if (url !== "none") { + url = "url(" + url + ")" + } + this.mx.forEach(function(mx){ + mx.el.style.backgroundImage = url + }) + } + + Floor.prototype.wallpaperPosition = function(background){ + if (this.background.src == "none") { return } + + this.background.x = background.x || this.background.x || 0 + this.background.y = background.y || this.background.y || 0 + this.background.scale = background.scale || this.background.scale || 1 + + var mx, dx, dy + var w = Math.round( this.backgroundImage.naturalWidth * this.background.scale ) + var h = Math.round( this.backgroundImage.naturalHeight * this.background.scale ) + + this.mx.forEach(function(mx, i){ + + var region = mx.rect + + if (this.ceiling) { + dx = Math.round( this.background.x - region.x.a ) + dy = Math.round( this.background.y - region.y.a ) + } + else { + dx = Math.round( this.background.x - region.x.a ) + dy = Math.round( this.background.y + region.y.b ) + } + + mx.el.style.backgroundPosition = dx + 'px ' + dy + 'px' + mx.el.style.backgroundSize = w + 'px ' + h + 'px' + }.bind(this)) + bbb = this + } + + if ('window' in this) { + window.Floor = Floor + } + else { + module.exports = Floor + } +})() diff --git a/public/assets/javascripts/rectangles/models/mat4.js b/public/assets/javascripts/rectangles/models/mat4.js deleted file mode 100644 index b061199..0000000 --- a/public/assets/javascripts/rectangles/models/mat4.js +++ /dev/null @@ -1,78 +0,0 @@ -function mat4(e){ - this.elements = [ 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 ] - return this -} -mat4.prototype.set = function (a) { - var els = this.elements - a.forEach(function(n,i){ els[i] = n }) - return this -} -mat4.prototype.clone = function(){ - return (new mat4).set(this.els) -} -mat4.prototype.identity = function () { - this.set([ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ]); - return this; -} -mat4.prototype.getInverse = function (m, throwOnInvertible) { - - // based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm - var te = this.elements; - var me = m.elements; - - var n11 = me[0], n12 = me[4], n13 = me[8], n14 = me[12]; - var n21 = me[1], n22 = me[5], n23 = me[9], n24 = me[13]; - var n31 = me[2], n32 = me[6], n33 = me[10], n34 = me[14]; - var n41 = me[3], n42 = me[7], n43 = me[11], n44 = me[15]; - - te[0] = n23*n34*n42 - n24*n33*n42 + n24*n32*n43 - n22*n34*n43 - n23*n32*n44 + n22*n33*n44; - te[4] = n14*n33*n42 - n13*n34*n42 - n14*n32*n43 + n12*n34*n43 + n13*n32*n44 - n12*n33*n44; - te[8] = n13*n24*n42 - n14*n23*n42 + n14*n22*n43 - n12*n24*n43 - n13*n22*n44 + n12*n23*n44; - te[12] = n14*n23*n32 - n13*n24*n32 - n14*n22*n33 + n12*n24*n33 + n13*n22*n34 - n12*n23*n34; - te[1] = n24*n33*n41 - n23*n34*n41 - n24*n31*n43 + n21*n34*n43 + n23*n31*n44 - n21*n33*n44; - te[5] = n13*n34*n41 - n14*n33*n41 + n14*n31*n43 - n11*n34*n43 - n13*n31*n44 + n11*n33*n44; - te[9] = n14*n23*n41 - n13*n24*n41 - n14*n21*n43 + n11*n24*n43 + n13*n21*n44 - n11*n23*n44; - te[13] = n13*n24*n31 - n14*n23*n31 + n14*n21*n33 - n11*n24*n33 - n13*n21*n34 + n11*n23*n34; - te[2] = n22*n34*n41 - n24*n32*n41 + n24*n31*n42 - n21*n34*n42 - n22*n31*n44 + n21*n32*n44; - te[6] = n14*n32*n41 - n12*n34*n41 - n14*n31*n42 + n11*n34*n42 + n12*n31*n44 - n11*n32*n44; - te[10] = n12*n24*n41 - n14*n22*n41 + n14*n21*n42 - n11*n24*n42 - n12*n21*n44 + n11*n22*n44; - te[14] = n14*n22*n31 - n12*n24*n31 - n14*n21*n32 + n11*n24*n32 + n12*n21*n34 - n11*n22*n34; - te[3] = n23*n32*n41 - n22*n33*n41 - n23*n31*n42 + n21*n33*n42 + n22*n31*n43 - n21*n32*n43; - te[7] = n12*n33*n41 - n13*n32*n41 + n13*n31*n42 - n11*n33*n42 - n12*n31*n43 + n11*n32*n43; - te[11] = n13*n22*n41 - n12*n23*n41 - n13*n21*n42 + n11*n23*n42 + n12*n21*n43 - n11*n22*n43; - te[15] = n12*n23*n31 - n13*n22*n31 + n13*n21*n32 - n11*n23*n32 - n12*n21*n33 + n11*n22*n33; - - var det = n11 * te[ 0 ] + n21 * te[ 4 ] + n31 * te[ 8 ] + n41 * te[ 12 ]; - - if ( det == 0 ) { - var msg = "Matrix4.getInverse(): can't invert matrix, determinant is 0"; - - if ( throwOnInvertible || false ) { - throw new Error( msg ) - } - else { - console.warn( msg ) - } - this.identity(); - return this - } - this.multiplyScalar( 1 / det ); - return this -} -mat4.prototype.multiplyScalar = function (n) { - var els = this.elements - els[0] *= n; els[4] *= n; els[8] *= n; els[12] *= n - els[1] *= n; els[5] *= n; els[9] *= n; els[13] *= n - els[2] *= n; els[6] *= n; els[10] *= n; els[14] *= n - els[3] *= n; els[7] *= n; els[11] *= n; els[15] *= n - return this -} - diff --git a/public/assets/javascripts/rectangles/models/rect.js b/public/assets/javascripts/rectangles/models/rect.js index a08176a..4f73bec 100644 --- a/public/assets/javascripts/rectangles/models/rect.js +++ b/public/assets/javascripts/rectangles/models/rect.js @@ -1,4 +1,3 @@ - (function(){ var vec2 if ('window' in this) { @@ -8,17 +7,17 @@ vec2 = require('./vec2') FRONT = 0x1, BACK = 0x2, LEFT = 0x4, RIGHT = 0x8, FLOOR = 0x10, CEILING = 0x20 TOP = CEILING, BOTTOM = FLOOR - 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 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 + } var Rect = function (x0,y0,x1,y1){ if (x0 instanceof vec2) { @@ -39,6 +38,12 @@ Rect.prototype.clone = function(){ return new Rect( this.x.clone(), this.y.clone() ) } + Rect.prototype.x_component = function(){ + return new vec2( this.x.a, this.y.a ) + } + Rect.prototype.y_component = function(){ + return new vec2( this.x.b, this.y.b ) + } Rect.prototype.assign = function(r) { this.x.assign(r.x) this.y.assign(r.y) @@ -56,6 +61,12 @@ Rect.prototype.maxDimension = function(){ return abs(this.width) > abs(this.height) ? this.width : this.height } + Rect.prototype.isVertical = function(){ + return this.x.isPoint() + } + Rect.prototype.isHorizontal = function(){ + return this.y.isPoint() + } Rect.prototype.mul = function(n){ this.x.mul(n) @@ -141,11 +152,40 @@ Rect.prototype.width = function(){ return this.x.length() } Rect.prototype.height = function(){ return this.y.length() } Rect.prototype.delta = function(){ return new vec2( this.x.magnitude(), this.y.magnitude() ) } + Rect.prototype.expand = function(rect){ + this.x.a = Math.min( this.x.a, rect.x.a ) + this.x.b = Math.max( this.x.b, rect.x.b ) + this.y.a = Math.min( this.y.a, rect.y.a ) + this.y.b = Math.max( this.y.b, rect.y.b ) + return this + } + Rect.prototype.square = function(){ + var width = this.x.length() + var height = this.y.length() + var diff + if (width < height) { + diff = (height - width) / 2 + this.x.a -= diff + this.x.b += diff + } + else { + diff = (width - height) / 2 + this.y.a -= diff + this.y.b += diff + } + return this + } Rect.prototype.toString = function(){ var sides = sidesToString(this.sides) var s = "[" + this.x.toString() + " " + this.y.toString() + "] " + sides return s } + Rect.prototype.exactString = function(){ + var sides = sidesToString(this.sides) + var s = "[" + this.x.exactString() + " " + this.y.exactString() + "] " + sides + return s + } + Rect.prototype.serialize = function(){ return { x: this.x.serialize(), y: this.y.serialize() } } diff --git a/public/assets/javascripts/rectangles/models/room.js b/public/assets/javascripts/rectangles/models/room.js index 0f09325..1abe2ba 100644 --- a/public/assets/javascripts/rectangles/models/room.js +++ b/public/assets/javascripts/rectangles/models/room.js @@ -31,10 +31,14 @@ var Room = function(opt){ this.id = opt.id || Rooms.uid("room_") this.rect = opt.rect - this.regions = [] + this.regions = opt.regions || [] this.height = opt.height || 200 this.focused = false + + this.mx_walls = [] + this.mx_floor = [] + this.mx_ceiling = [] } Room.prototype.copy = function(){ @@ -122,8 +126,10 @@ return collision } - Room.prototype.collidesDisc = function(x,y,radius){ + Room.prototype.collidesDisc = function(src, dest, radius){ + var x = dest.x, y = dest.z var collision = 0, wall_collision, contains_x, contains_y + this.regions.forEach(function(r){ if (! r.sides) return @@ -152,25 +158,10 @@ if (contains_y) { collision |= wall_collision & LEFT_RIGHT } -// if (bitcount(wall_collision) > 1) { -// collision |= wall_collision -// } }) return collision } - - Room.prototype.setFloorColor = function(rgbColor) { - this.mx_floor.map(function(mx){ - mx.el.style.backgroundColor = rgbColor - }) - } - - Room.prototype.setCeilingColor = function(rgbColor) { - this.mx_ceiling.map(function(mx){ - mx.el.style.backgroundColor = rgbColor - }) - } - + if ('window' in this) { window.Room = Room } diff --git a/public/assets/javascripts/rectangles/models/surface.js b/public/assets/javascripts/rectangles/models/surface.js index 53977c8..f031665 100644 --- a/public/assets/javascripts/rectangles/models/surface.js +++ b/public/assets/javascripts/rectangles/models/surface.js @@ -12,6 +12,7 @@ var Surface = function (face){ this.bounds = new Rect (new vec2(0, 0), new vec2(0, 0)) + this.vec = new Rect (new vec2(0, 0), new vec2(0, 0)) this.faces = [] if (face) { this.add(face) @@ -36,7 +37,7 @@ Surface.prototype.fits = function(v){ var faces = this.faces var scratch - if (this.bounds.x.b < v.a || this.bounds.y.b < v.b) { + if (this.bounds.width() < v.a || this.bounds.height() < v.b) { return null } for (var i = 0; i < faces.length; i++) { @@ -46,7 +47,7 @@ } scratch = new Rect (0,0,0,0) for (var i = 0; i < faces.length; i++) { - if (faces[i].y.length() < v.b) { + if (faces[i].height() < v.b) { continue } scratch.x.a = faces[i].x.a @@ -108,7 +109,7 @@ this.clamp_delta( this.bounds, dimension, position, delta ) var new_delta = delta.clone() - if (this.clamp_delta(old_bounds, dimension, position, new_delta).eq(delta)) { + if (old_bounds && this.clamp_delta(old_bounds, dimension, position, new_delta).eq(delta)) { return old_bounds } @@ -154,6 +155,21 @@ } return -1 } + Surface.prototype.face_for_x = function(x, min_i){ + min_i = min_i || 0 + for (var i = min_i; i < this.faces.length; i++) { + if (this.faces[i].x.contains(x)) { + return this.faces[i] + } + } + if (x < this.faces[0].x.a) { + return this.faces[0] + } + else { + return this.faces[this.faces.length-1] + } + return null + } Surface.prototype.bounds_at_index_with_dimensions = function(index, dimensions){ var faces = this.faces diff --git a/public/assets/javascripts/rectangles/models/vec2.js b/public/assets/javascripts/rectangles/models/vec2.js index 0040435..8942d92 100644 --- a/public/assets/javascripts/rectangles/models/vec2.js +++ b/public/assets/javascripts/rectangles/models/vec2.js @@ -37,9 +37,15 @@ vec2.prototype.midpoint = function(){ return lerp(0.5, this.a, this.b) } + vec2.prototype.lerp = function(n){ + return lerp(n, this.a, this.b) + } vec2.prototype.eq = function(v){ return this.a == v.a && this.b == v.b } + vec2.prototype.isPoint = function(){ + return this.a == this.b + } vec2.prototype.add = function(n){ this.a += n this.b += n @@ -60,9 +66,28 @@ this.b /= n return this } + vec2.prototype.addVec = function(v){ + this.a += v.a + this.b += v.b + return this + } + vec2.prototype.subVec = function(v){ + this.a -= v.a + this.b -= v.b + return this + } vec2.prototype.zero = function(){ this.a = this.b = 0 } + vec2.prototype.round = function(){ + this.a = Math.round(this.a) + this.b = Math.round(this.b) + } + vec2.prototype.distanceTo = function(v){ + var va = (this.a - v.a) + var vb = (this.b - v.b) + return Math.sqrt( va*va + vb*vb ) + } vec2.prototype.setPosition = function(n){ var len = this.length() this.a = n @@ -86,6 +111,12 @@ vec2.prototype.containsDisc = function(n,r){ return this.a <= n-r && n+r <= this.b } + vec2.prototype.containsVec = function(v){ + return this.a <= v.a && v.b <= this.b + } + vec2.prototype.containsCenterVec = function(v){ + return this.a < v.a && v.b < this.b + } vec2.prototype.clamp = function(n){ return clamp(n, this.a, this.b) } @@ -183,10 +214,17 @@ } vec2.prototype.toString = function(){ - return "[" + ~~this.a + " " + ~~this.b + "]" + return "[" + Math.round(this.a) + " " + Math.round(this.b) + "]" + } + vec2.prototype.exactString = function(){ + return "[" + this.a + " " + this.b + "]" } vec2.prototype.serialize = function(){ - return [ ~~this.a, ~~this.b ] + return [ Math.round(this.a), Math.round(this.b) ] + } + vec2.prototype.deserialize = function(data){ + this.a = data[0] + this.b = data[1] } vec2.prototype.quantize = function(n){ n = n || 10 diff --git a/public/assets/javascripts/rectangles/models/vec3.js b/public/assets/javascripts/rectangles/models/vec3.js index 4e00b0c..b3825a9 100644 --- a/public/assets/javascripts/rectangles/models/vec3.js +++ b/public/assets/javascripts/rectangles/models/vec3.js @@ -15,6 +15,9 @@ vec3.prototype.sub = function(v){ this.c -= v.c return this } +vec3.prototype.clone = function(){ + return new vec3(this.a, this.b, this.c) +} // input: mat4 projection matrix vec3.prototype.apply_projection = function (m) { @@ -29,3 +32,28 @@ vec3.prototype.apply_projection = function (m) { return this; } + +vec3.prototype.serialize = function(){ + return [ round(this.a), round(this.b), round(this.c) ] +} +vec3.prototype.deserialize = function(data){ + this.a = data[0] + this.b = data[1] + this.c = data[2] || data[0] + return this +} +vec3.prototype.clone = function(){ + return new vec3(this.a, this.b, this.c) +} +vec3.prototype.assign = function(v){ + this.a = v.a + this.b = v.b + this.c = v.c + return this +} +vec3.prototype.mul = function(n) { + this.a *= n + this.b *= n + this.c *= n + return this +}
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/models/wall.js b/public/assets/javascripts/rectangles/models/wall.js index 1a3ef7c..cf3cea8 100644 --- a/public/assets/javascripts/rectangles/models/wall.js +++ b/public/assets/javascripts/rectangles/models/wall.js @@ -10,21 +10,55 @@ vec2 = require('./vec2') Rect = require('./rect') UidGenerator = require('../util/uid') + wall_rotation = {} + wall_rotation[FRONT] = PI + wall_rotation[BACK] = 0 + wall_rotation[LEFT] = HALF_PI + wall_rotation[RIGHT] = -HALF_PI } var Wall = function(opt){ - this.id = [ opt.side, opt.edge, opt.vec.a ].join("_") + this.id = [ opt.side|0, opt.edge|0, opt.vec.a|0 ].join("_") this.vec = opt.vec this.edge = opt.edge this.side = opt.side this.surface = opt.surface + this.rotationY = ('rotationY' in opt) ? opt.rotationY : wall_rotation[opt.side] this.mx = opt.mx - this.background = "" + this.background = { src: "none" } } Wall.prototype.toString = function(){ return this.vec.toString() } + Wall.prototype.get_points = function(wall_vec){ + wall_vec = wall_vec || new Rect () + if (this.side & LEFT) { + wall_vec.x.a = this.edge + wall_vec.x.b = this.vec.b + wall_vec.y.a = this.edge + wall_vec.y.b = this.vec.a + } + else if (this.side & RIGHT) { + wall_vec.x.a = this.edge + wall_vec.x.b = this.vec.a + wall_vec.y.a = this.edge + wall_vec.y.b = this.vec.b + } + else if (this.side & FRONT) { + wall_vec.x.a = this.vec.a + wall_vec.x.b = this.edge + wall_vec.y.a = this.vec.b + wall_vec.y.b = this.edge + } + else if (this.side & BACK) { + wall_vec.x.a = this.vec.b + wall_vec.x.b = this.edge + wall_vec.y.a = this.vec.a + wall_vec.y.b = this.edge + } + return wall_vec + } Wall.prototype.reset = function(){ } @@ -50,13 +84,66 @@ index: index, }) }, +/* mousemove: function(e){ + var offset = offsetFromPoint(e, mx.el) + if (offset) { + var pos = base.mxOffsetToPosition( offset, index ) + + var mx_pos = base.positionToMx(pos, new vec2(5,5)) + var mx_dot = new MX.Object3D + mx_dot.move(mx_pos) + mx_dot.width = 5 + mx_dot.height = 5 + mx_dot.rotationY = base.rotationY + mx_dot.el.style.backgroundColor = "red" + scene.add(mx_dot) + } }, +*/ + contextmenu: function(e){ + if (! (e.ctrlKey || e.metaKey || e.shiftKey) ) { + e.preventDefault() + } + if (Scenery.nextMedia) { + e.preventDefault() + Scenery.nextMedia = null + app.tube('cancel-scenery') + } + else if (Scenery.nextWallpaper) { + e.preventDefault() + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } + }, + mousedown: function(e){ + + // right-click + if (e.which == 3) { + if (Scenery.nextMedia) { + e.preventDefault() + Scenery.nextMedia = null + app.tube('cancel-scenery') + } + else if (Scenery.nextWallpaper) { + e.preventDefault() + Scenery.nextWallpaper = null + app.tube('cancel-wallpaper') + } + return + } + + var offset = offsetFromPoint(e, mx.el) + if (! offset) { return } + + var pos = base.mxOffsetToPosition( offset, index ) + if (Scenery.nextMedia) { var scenery = Scenery.addNextToWall({ wall: base, - index: index + index: index, + position: pos, }) // scenery was not placed @@ -64,20 +151,26 @@ e.stopPropagation() return } - + + app.controller.toolbar.resetPermissions() + Scenery.resize.show(scenery) + Scenery.hovering = true + + app.controller.pick(scenery) + UndoStack.push({ type: 'create-scenery', undo: { id: scenery.id }, redo: scenery.serialize(), }) - + // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) } else if (Scenery.nextWallpaper) { var oldState = base.serialize() base.wallpaper(Scenery.nextWallpaper) - Scenery.nextWallpaper = null + // Scenery.nextWallpaper = null UndoStack.push({ type: 'update-wallpaper', @@ -87,11 +180,13 @@ // TODO: watch individual scenery object here Minotaur.watch( app.router.editorView.settings ) + + app.controller.pickWall(base, pos) } else { - app.controller.hideExtras() + app.controller.pickWall(base, pos) } - } + }, }) }) @@ -100,12 +195,10 @@ if (! shouldFlip) { this.mx.reverse() } - - // this.outline(wallColor, outlineColor) } Wall.prototype.serialize = function(){ - return { + return { id: this.id, background: this.background, } @@ -149,7 +242,7 @@ x: x, y: position.b + dimension.b / 2, z: z, - rotationY: wall_rotation[ this.side ], + rotationY: this.rotationY, } } Wall.prototype.mxToPosition = function(mx, dimension) { @@ -172,30 +265,104 @@ } return position } - + Wall.prototype.mxOffsetToPosition = function( offset, index ) { + var face = this.surface.faces[index] + var shouldFlip = this.side & (RIGHT | FRONT) + var position = new vec2(0,0) + position.a = face.x.lerp(shouldFlip ? 1-offset.left : offset.left) + position.b = face.y.lerp(1-offset.top) + position.round() + return position + } + Wall.prototype.color = function(color){ this.$walls.css("background-color", color) } - Wall.prototype.wallpaper = function(background){ - var useX = this.side & FRONT_BACK - var shouldFlip = this.side & (LEFT | BACK) - - this.background = background || "none" + Wall.prototype.wallpaper = function(background, img){ + if (! background) { + background = { src: "none" } + } + else if (typeof background == "string") { + background = { src: background } + } + else if (! background.src) { + background = { src: "none" } + } + background.x = background.x || 0 + background.y = background.y || 0 + background.scale = background.scale || 1 + + this.background = background + this.background.src = this.background.src.replace(/url\(\"?\'?/,"").replace(/\"?\'?\)/,"") + + if (this.background.src == "none") { + this.wallpaperLoad(this.background.src) + return + } + img = img || new Image () + img.onload = function(){ + this.backgroundImage = img + this.wallpaperLoad(this.background.src) + this.wallpaperPosition(background) + }.bind(this) + img.src = this.background.src + img.complete && img.onload() + } + + Wall.prototype.wallpaperLoad = function(url){ + if (url !== "none") { + url = "url(" + url + ")" + } this.mx.forEach(function(mx){ - var partitionOffset = useX ? mx.x : mx.z - if (shouldFlip) partitionOffset *= -1 - partitionOffset += mx.width/2 - var floorOffset = mx.y + mx.height/2 - - mx.el.style.backgroundImage = background - mx.el.style.backgroundPosition = (~~partitionOffset) + "px " + (~~floorOffset) + "px" + if (mx.el) mx.el.style.backgroundImage = url }) } + + Wall.prototype.wallpaperPosition = function(background){ + if (this.background.src == "none") { return } + + this.background.x = background.x || this.background.x || 0 + this.background.y = background.y || this.background.y || 0 + this.background.scale = background.scale || this.background.scale || 1 + + var mx, dx, dy + var w = Math.round( this.backgroundImage.naturalWidth * this.background.scale ) + var h = Math.round( this.backgroundImage.naturalHeight * this.background.scale ) + + this.surface.faces.forEach(function(face, i){ + // this.mx[i].el.innerHTML = sidesToString(this.side) + + switch (this.side) { + case LEFT: + mx = this.mx[this.mx.length-1-i] + dx = Math.round( this.background.x + face.x.a ) + dy = Math.round( this.background.y + face.y.b ) + break + case RIGHT: + mx = this.mx[this.mx.length-1-i] + dx = Math.round( this.background.x + face.x.b ) + dy = Math.round( this.background.y + face.y.b ) + break + case FRONT: + mx = this.mx[this.mx.length-1-i] + dx = Math.round( this.background.x + face.x.b ) + dy = Math.round( this.background.y + face.y.b ) + break + case BACK: + mx = this.mx[i] + dx = Math.round( this.background.x - face.x.a ) + dy = Math.round( this.background.y + face.y.b ) + break + } + + mx.el.style.backgroundPosition = dx + 'px ' + dy + 'px' + mx.el.style.backgroundSize = w + 'px ' + h + 'px' + }.bind(this)) + } Wall.prototype.outline = function(wallColor, outlineColor){ - var useX = this.side & FRONT_BACK var mx = this.mx var len = this.mx.length diff --git a/public/assets/javascripts/rectangles/util/constants.js b/public/assets/javascripts/rectangles/util/constants.js index 4c6b3cc..522689b 100644 --- a/public/assets/javascripts/rectangles/util/constants.js +++ b/public/assets/javascripts/rectangles/util/constants.js @@ -1,5 +1,6 @@ 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 FRONT_BACK = FRONT | BACK, LEFT_RIGHT = LEFT | RIGHT, FLOOR_CEILING = FLOOR | CEILING +var WALL_SIDES = FRONT | BACK | LEFT | RIGHT var TOP = CEILING, BOTTOM = FLOOR, TOP_LEFT = TOP | LEFT, @@ -19,10 +20,13 @@ var height_min = 200, side_min = 10, side_max = 5000, resize_margin = 8, - cursor_amp = 1.5 + cursor_amp = 1.5, + DEFAULT_PICTURE_WIDTH = 350, + MAP_GRID_SIZE = 36 // 10 feet var painting_distance_from_wall = 10, - dot_distance_from_picture = 3 + dot_distance_from_picture = 3, + sculpture_distance_from_floor = 3 var dot_hide_delay = 50, // ms dot_side = 20 diff --git a/public/assets/javascripts/rectangles/util/coords.js b/public/assets/javascripts/rectangles/util/coords.js new file mode 100644 index 0000000..ff56199 --- /dev/null +++ b/public/assets/javascripts/rectangles/util/coords.js @@ -0,0 +1,33 @@ +function offsetFromPoint(event, element) { + function a(width) { + var l = 0, r = 200; + while (r - l > 0.0001) { + var mid = (r + l) / 2; + var a = document.createElement('div'); + a.style.cssText = 'position: absolute;left:0;top:0;background: red;z-index: 1000;'; + a.style[width ? 'width' : 'height'] = mid.toFixed(3) + '%'; + a.style[width ? 'height' : 'width'] = '100%'; + element.appendChild(a); + var x = document.elementFromPoint(event.clientX, event.clientY); + element.removeChild(a); + if (x === a) { + r = mid; + } else { + if (r === 200) { + return null; + } + l = mid; + } + } + return mid; + } + var l = a(1), + t = a(0); + return l && t ? { + left: l / 100, + top: t / 100, + toString: function () { + return 'left: ' + l + '%, top: ' + t + '%'; + } + } : null; +}
\ No newline at end of file diff --git a/public/assets/javascripts/rectangles/util/measurement.js b/public/assets/javascripts/rectangles/util/measurement.js index d6a0b35..6346eac 100644 --- a/public/assets/javascripts/rectangles/util/measurement.js +++ b/public/assets/javascripts/rectangles/util/measurement.js @@ -28,6 +28,10 @@ function measurementToString( n ) { case 'ft': ft = floor(n / 36) inch = abs(round((n % 36) / 3)) + if (inch == 12) { + inch = 0 + ft += 1 + } s = ft + "'" if (inch > 0) { s += " " + inch + '"' diff --git a/public/assets/javascripts/rectangles/util/minotaur.js b/public/assets/javascripts/rectangles/util/minotaur.js index 0fcc766..8b1abfe 100644 --- a/public/assets/javascripts/rectangles/util/minotaur.js +++ b/public/assets/javascripts/rectangles/util/minotaur.js @@ -4,7 +4,7 @@ var base = this base.$el = $("#minotaur") base.timeout = null - base.delay = 1000 + base.delay = 2500 base.objects = {} base.init = function () { @@ -38,7 +38,7 @@ for (var id in base.objects[type]) { var obj = base.objects[type][id] if (obj) { - obj.save(null, function(){ base.hide() }, function(){}) + obj.save(null, function(){ base.hide() }, function(){ base.hide() }) } delete base.objects[type][id] saving = true @@ -53,9 +53,7 @@ } base.hide = function () { - setTimeout(function(){ - base.$el.removeClass() - }, 500) + base.$el.removeClass('saving') } base.init(); diff --git a/public/assets/javascripts/rectangles/util/mouse.js b/public/assets/javascripts/rectangles/util/mouse.js index cb36038..6d9862c 100644 --- a/public/assets/javascripts/rectangles/util/mouse.js +++ b/public/assets/javascripts/rectangles/util/mouse.js @@ -54,10 +54,20 @@ function mouse (opt) { 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 + var offset; base.init = function (){ base.bind() + base.set_offset() + } + + base.set_offset = function(){ + if (opt.use_offset && opt.el) { + offset = opt.el.getBoundingClientRect() + } + else { + offset = null + } } base.on = function(){ @@ -73,9 +83,20 @@ function mouse (opt) { opt.el.addEventListener("mousedown", base.mousedown) opt.el.addEventListener("contextmenu", base.contextmenu) } + if (opt.use_offset) { + window.addEventListener("resize", base.set_offset) + } window.addEventListener("mousemove", base.mousemove) window.addEventListener("mouseup", base.mouseup) } + base.unbind = function(){ + if (opt.el) { + opt.el.removeEventListener("mousedown", base.mousedown) + opt.el.removeEventListener("contextmenu", base.contextmenu) + } + window.removeEventListener("mousemove", base.mousemove) + window.removeEventListener("mouseup", base.mouseup) + } base.bind_el = function(el){ el.addEventListener("mousedown", base.mousedown) @@ -126,7 +147,7 @@ function mouse (opt) { } var x = pos.a, y = pos.b - + if (base.down) { base.cursor.x.b = x base.cursor.y.b = y diff --git a/public/assets/javascripts/rectangles/util/permissions.js b/public/assets/javascripts/rectangles/util/permissions.js index 1b5a1b5..9e3ef4d 100644 --- a/public/assets/javascripts/rectangles/util/permissions.js +++ b/public/assets/javascripts/rectangles/util/permissions.js @@ -31,7 +31,7 @@ Permissions.prototype.add = function (key) { Permissions.prototype.remove = function (key) { var base = this - base[key] = true + base[key] = false } Permissions.prototype.clear = function () { diff --git a/public/assets/javascripts/rectangles/util/undostack.js b/public/assets/javascripts/rectangles/util/undostack.js index 959e3d1..040a4eb 100644 --- a/public/assets/javascripts/rectangles/util/undostack.js +++ b/public/assets/javascripts/rectangles/util/undostack.js @@ -10,6 +10,7 @@ this.pointer++ this.stack[this.pointer] = action this.purge() + this.debug && console.log("push", action.type) } UndoStack.prototype.purge = function(){ if (this.stack.length-1 == this.pointer) return diff --git a/public/assets/javascripts/rectangles/util/wheel.js b/public/assets/javascripts/rectangles/util/wheel.js index 6836772..4155a70 100644 --- a/public/assets/javascripts/rectangles/util/wheel.js +++ b/public/assets/javascripts/rectangles/util/wheel.js @@ -3,8 +3,8 @@ base.wheel = new wheel({ el: document.querySelector("#map"), - update: function(e, val, delta){ - // do something with val + update: function(e, delta){ + // do something with delta }, }) @@ -13,7 +13,7 @@ function wheel (opt) { opt = defaults(opt, { el: document, - fn: function(e, val, delta){}, + update: function(e, delta){}, propagate: false, locked: false, reversible: true, @@ -21,37 +21,46 @@ function wheel (opt) { val: 0, }) - opt.el.addEventListener('mousewheel', onMouseWheel, false); + opt.el.addEventListener('wheel', onMouseWheel, false); + // 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; + var deltaX = 0, deltaY = 0; // WebKit - if ( event.wheelDeltaY ) { - delta -= event.wheelDeltaY * opt.ratio + if ( event.deltaY ) { + deltaY -= event.deltaY * opt.ratio + deltaX -= event.deltaX * opt.ratio + } + else if ( event.wheelDeltaY ) { + deltaY -= event.wheelDeltaY * opt.ratio + deltaX -= event.wheelDeltaX * opt.ratio } // Opera / Explorer 9 else if ( event.wheelDelta ) { - delta -= event.wheelDelta * opt.ratio + deltaY -= event.wheelDelta * opt.ratio } // Firefox else if ( event.detail ) { - delta += event.detail * 2 + deltaY += event.detail * 2 } - if (! opt.reversible && delta < 0) return; + if (! opt.reversible && (deltaY < 0 && deltaX < 0)) return; - opt.val = clamp(opt.val + delta, opt.min, opt.max) + // opt.val = clamp(opt.val + delta, opt.min, opt.max) - opt.update(e, opt.val, delta) + // deltaX is also passed, but these values tend to be unusable + // try http://vvalls.com/assets/test/wheel.html with a trackpad + opt.update(e, deltaY, deltaX) } opt.lock = function(){ opt.locked = true } |
