diff --git a/app/lib/world/world_utils.coffee b/app/lib/world/world_utils.coffee
index 1398c409b..67c1d406a 100644
--- a/app/lib/world/world_utils.coffee
+++ b/app/lib/world/world_utils.coffee
@@ -92,60 +92,58 @@ module.exports.consolidateThangs = consolidateThangs = (thangs) ->
   padding = 0
   console.log 'got max width', width, 'height', height, 'left', left, 'bottom', bottom, 'of thangs', thangs.length, 'structural', structural.length if debug
   grid = new Grid structural, width, height, padding, left, bottom
-  console.log grid.toString() if debug
 
-  # Approach: start at bottom left. Go right, then up. At each occupied grid square, find the largest rectangle we can make starting at that corner, add a corresponding Thang to the grid, and unmark all occupied grid squares.
-  # Since it's not like we're going to do any of these:
-  # http://stackoverflow.com/questions/5919298/algorithm-for-finding-the-fewest-rectangles-to-cover-a-set-of-rectangles
-  # http://stackoverflow.com/questions/4701887/find-the-set-of-largest-contiguous-rectangles-to-cover-multiple-areas
   dissection = []
-  for y in grid.columns bottom, height
-    for x in grid.rows left, width
-      continue unless grid.grid[y][x].length
-      rect = largestRectangle grid, y, x, false, debug
-      vertices = rect.vertices()
-      for y2 in [vertices[0].y ... vertices[1].y]  # maybe ..?
-        for x2 in [vertices[0].x ... vertices[2].x]  # maybe ..?
-          grid.grid[y2][x2] = []
-      console.log grid.toString() if debug
-      thang = structural[dissection.length]  # grab one we already know is configured properly
-      console.error 'Hmm, our dissection has more Thangs than the original structural Thangs?', dissection.length unless thang
-      thang.width = rect.width
-      thang.height = rect.height
-      thang.pos.x = rect.x
-      thang.pos.y = rect.y
-      thang.createBodyDef()
-      dissection.push thang
+  addStructuralThang = (rect) ->
+    thang = structural[dissection.length]  # Grab one we already know is configured properly.
+    console.error 'Hmm, our dissection has more Thangs than the original structural Thangs?', dissection.length unless thang
+    thang.pos.x = rect.x
+    thang.pos.y = rect.y
+    thang.width = rect.width
+    thang.height = rect.height
+    thang.createBodyDef()
+    dissection.push thang
+
+  dissectRectangles grid, addStructuralThang, false, debug
+
+  # Now add the new structural thangs back to thangs and return the ones not in the dissection.
   console.log 'Turned', structural.length, 'structural Thangs into', dissection.length, 'dissecting Thangs.'
   thangs.push dissection...
   structural[dissection.length ... structural.length]
 
-module.exports.largestRectangle = largestRectangle = (grid, bottomY, leftX, wantEmpty, debug) ->
-  # If wantEmpty, then we try to cover empty rectangles.
-  # Otherwise, we try to cover occupied rectangles.
-  coveredRows = []
-  shortestCoveredRow = grid.width - leftX
-  for y in grid.columns bottomY, grid.height
-    coveredRow = 0
-    for x in grid.rows leftX, leftX + shortestCoveredRow
-      if Boolean(grid.grid[y][x].length) isnt wantEmpty
-        ++coveredRow
-      else
-        break
-    break unless coveredRow
-    coveredRows.push coveredRow
-    shortestCoveredRow = Math.min(shortestCoveredRow, coveredRow)
-  console.log 'largestRectangle() for', bottomY, leftX, 'got coveredRows', coveredRows if debug
-  [maxArea, maxAreaRows, maxAreaRowLength, shortestRow] = [0, 0, 0, 0]
-  for rowLength, rowIndex in coveredRows
-    shortestRow ||= rowLength
-    area = rowLength * (rowIndex + 1)
-    if area > maxArea
-      maxAreaRows = rowIndex + 1
-      maxAreaRowLength = shortestRow
-      maxArea = area
-    shortestRow = Math.min(rowLength, shortestRow)
-  console.log 'So largest rect has area', maxArea, 'with', maxAreaRows, 'rows of length', maxAreaRowLength if debug
-  rect = new Rectangle leftX + maxAreaRowLength / 2, bottomY + maxAreaRows / 2, maxAreaRowLength, maxAreaRows
-  console.log 'That corresponds to a rectangle', rect.toString() if debug
-  rect
+
+module.exports.dissectRectangles = dissectRectangles = (grid, rectangleCallback, wantEmpty, debug) ->
+  # Mark Maxham's fast sweeper approach: https://github.com/codecombat/codecombat/issues/1090
+  console.log grid.toString() if debug
+  for x in grid.rows grid.left, grid.left + grid.width
+    y = grid.clampColumn grid.bottom
+    while y < grid.clampColumn grid.bottom + grid.height
+      y2 = y  # Note our current y.
+      ++y2 until occ x, y2, grid, wantEmpty  # Sweep through y to expand 1xN rect.
+      if y2 > y  # If we get a hit, sweep X with that swath.
+        x2 = x + 1
+        ++x2 until occCol x2, y, y2, grid, wantEmpty
+        w = x2 - x
+        h = y2 - y
+        rect = addRect grid, x, y, w, h, wantEmpty
+        rectangleCallback rect
+        console.log grid.toString() if debug
+        y = y2
+      ++y
+
+occ = (x, y, grid, wantEmpty) ->
+  return true if y > grid.bottom + grid.height or x > grid.left + grid.width
+  console.error 'trying to check invalid coordinates', x, y, 'from grid', grid.bottom, grid.left, grid.width, grid.height unless grid.grid[y]?[x]
+  Boolean(grid.grid[y][x].length) is wantEmpty
+
+occCol = (x, y1, y2, grid, wantEmpty) ->
+  for j in [y1 ... y2]
+    if occ(x, j, grid, wantEmpty)
+      return true
+  false
+
+addRect = (grid, leftX, bottomY, width, height, wantEmpty) ->
+  for x in [leftX ... leftX + width]
+    for y in [bottomY ... bottomY + height]
+      grid.grid[y][x] = if wantEmpty then [true] else []
+  new Rectangle leftX + width / 2, bottomY + height / 2, width, height
diff --git a/app/schemas/subscriptions/surface.coffee b/app/schemas/subscriptions/surface.coffee
index 2ed519135..73d7116bb 100644
--- a/app/schemas/subscriptions/surface.coffee
+++ b/app/schemas/subscriptions/surface.coffee
@@ -77,12 +77,14 @@ module.exports =  # /app/lib/surface
     point: c.object {required: ['x', 'y']},
       x: {type: 'number'}
       y: {type: 'number'}
+      z: {type: 'number'}
 
   'surface:choose-region': c.object {required: ['points']},
     points: c.array {minItems: 2, maxItems: 2},
       c.object {required: ['x', 'y']},
         x: {type: 'number'}
         y: {type: 'number'}
+        z: {type: 'number'}
 
   'surface:new-thang-added': c.object {required: ['thang', 'sprite']},
     thang: {type: 'object'}
diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index cbe7e6633..8e7b299e9 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -234,7 +234,7 @@ module.exports = class LevelPlaybackView extends CocoView
     button.addClass(classes[2]) if e.volume >= 1.0
 
   onScrub: (e, options) ->
-    e?.preventDefault()
+    e?.preventDefault?()
     options.scrubDuration = 500
     Backbone.Mediator.publish('level:set-time', options)
 
@@ -347,7 +347,7 @@ module.exports = class LevelPlaybackView extends CocoView
   shouldIgnore: -> return @disabled or @realTime
 
   onTogglePlay: (e) ->
-    e?.preventDefault()
+    e?.preventDefault?()
     return if @shouldIgnore()
     button = $('#play-button')
     willPlay = button.hasClass('paused') or button.hasClass('ended')