JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
major code cleanup (layers/controls)
[crayon_mockup.git] / main.coffee
index 48c001b..632567f 100644 (file)
@@ -29,6 +29,7 @@ STATES = {
        DRAGGING: { txt: 'dragging' }
        EDITING:  { txt: 'editing' }
 }
+TYPE_UNKNOWN = 0
 TYPE_WIDGET = 1
 TYPE_CONTROL = 2
 
@@ -46,11 +47,59 @@ json_to_svg = (json) ->
                                el.setAttribute k, v
        return el
 
+resizer_nw = (widget) ->
+       return (dxy) ->
+               widget.resize w: widget.width - dxy.x, h: widget.height - dxy.y
+               widget.move x: widget.x + dxy.x, y: widget.y + dxy.y
+resizer_n = (widget) ->
+       return (dxy) ->
+               widget.resize w: widget.width, h: widget.height - dxy.y
+               widget.move x: widget.x, y: widget.y + dxy.y
+resizer_ne = (widget) ->
+       return (dxy) ->
+               widget.resize w: widget.width + dxy.x, h: widget.height - dxy.y
+               widget.move x: widget.x, y: widget.y + dxy.y
+resizer_e = (widget) ->
+       return (dxy) ->
+               widget.resize w: widget.width + dxy.x, h: widget.height
+resizer_se = (widget) ->
+       return (dxy) ->
+               widget.resize w: widget.width + dxy.x, h: widget.height + dxy.y
+resizer_s = (widget) ->
+       return (dxy) ->
+               widget.resize w: widget.width, h: widget.height + dxy.y
+resizer_sw = (widget) ->
+       return (dxy) ->
+               widget.resize w: widget.width - dxy.x, h: widget.height + dxy.y
+               widget.move x: widget.x + dxy.x, y: widget.y
+resizer_w = (widget) ->
+       return (dxy) ->
+               widget.resize w: widget.width - dxy.x, h: widget.height
+               widget.move x: widget.x + dxy.x, y: widget.y
+resizers = [
+       resizer_nw
+       resizer_n
+       resizer_ne
+       resizer_e
+       resizer_se
+       resizer_s
+       resizer_sw
+       resizer_w
+]
+resizer_shapes = [
+       -> return "M#{@x - 5} #{@y - 5}h6l-2 2 4 4 2 -2v6h-6l2-2-4-4-2 2z"
+       -> return "M #{@x},#{@y - 7} l 4,4 -2.5,0 0,5 2.5,0 -4,4 -4,-4 2.5,0 0,-5 -2.5,0 z"
+       -> return "M#{@x + 5} #{@y - 5}v6l-2-2-4 4 2 2h-6v-6l2 2 4-4-2-2z"
+       -> return "M #{@x - 7},#{@y} l 4,-4 0,2.5 5,0 0,-2.5 4,4 -4,4 0,-2.5 -5,0 0,2.5 z"
+]
+shape_node_move = -> "M#{@x} #{@y - 9}l-2.5 4.5h2v2.404a2.156 2.156 0 0 0-1.596 1.596h-2.404v-2l-4.5 2.5 4.5 2.5v-2h2.404a2.156 2.156 0 0 0 1.596 1.596v2.404h-2l2.5 4.5 2.5-4.5h-2v-2.404a2.156 2.156 0 0 0 1.596-1.596h2.404v2l4.5-2.5-4.5-2.5v2h-2.404a2.156 2.156 0 0 0-1.596-1.596v-2.404h2l-2.5-4.5z"
+
 next_widget_id = 0
 # public vars: x, y, width, height, el
 class Visible
        # required args: svg
        constructor: (args) ->
+               @type = TYPE_UNKNOWN
                @id = next_widget_id
                next_widget_id += 1
                @svg = args.svg
@@ -60,91 +109,119 @@ class Visible
                @height = args.height ? 34
                @state = args.state ? STATES.NORMAL
                @hover = false
+               @shown = false
+               @el = null
+               return
        destruct: ->
+               @hide()
+               return
        update_class: ->
                css_class = "#{@css_class} #{@state.txt}"
                if @hover
                        css_class += " hover"
                @el.setAttribute 'class', css_class
-       set_hover: (tf) ->
-               if tf != @hover
-                       @hover = tf
+               return
+       set_hover: (bool) ->
+               if bool != @hover
+                       @hover = bool
                        @update_class()
+               return
+       hide: ->
+               if @el and @shown
+                       @svg.removeChild @el
+                       @shown = false
+               return
+       show: ->
+               if @el? and not @shown
+                       @svg.appendChild @el
+                       @shown = true
+               return
        move: (xy) -> # just move
                @x = xy.x
                @y = xy.y
+               return
        drag: (dxy) -> # react to mouse drag (obey constraints, etc.)
                @move x: @x + dxy.x, y: @y + dxy.y
+               return
        proximity: (xy) -> # return the square of the distance to your visible bits
                return PROX_TOO_FAR
        set_state: (state) ->
                @state = state
+               @update_class()
+               return
 
 class Control extends Visible
        constructor: (args) ->
                super args
                @type = TYPE_CONTROL
                @on_drag = args.drag
-               @on_destruct = args.done ? null
-       destruct: ->
-               super()
-               if @on_destruct?
-                       @on_destruct @
+               return
        drag: (args) -> # call this when control point is being manipulated directly
                @on_drag args
+               return
        proximity: (xy) -> # return the square of the distance to your visible bits
                dx = xy.x - @x
                dy = xy.y - @y
                return dx * dx + dy * dy
-
-class ControlNWSE extends Control
+class ControlPath extends Control
        constructor: (args) ->
                super args
                @css_class = 'control_point'
+               @make_path = args.shape
                @el = json_to_svg path:
                        d: @make_path()
                        class: 'control_point normal'
-               @svg.appendChild @el
-       make_path: ->
-               # / return "M#{@x + 5} #{@y - 5}v6l-2-2-4 4 2 2h-6v-6l2 2 4-4-2-2z"
-               return "M#{@x - 5} #{@y - 5}h6l-2 2 4 4 2 -2v6h-6l2-2-4-4-2 2z"
-       destruct: ->
-               super()
-               if @el?
-                       @svg.removeChild @el
+               return
        move: (args) ->
                super args
                @el.setAttribute 'd', @make_path()
+               return
 
+# A widget is a visable thing that can be edited via controls
 class Widget extends Visible
-       #sub-classes are expected to implement all of these:
        constructor: (args) ->
                super args
-               @controls = []
                @type = TYPE_WIDGET
+               @controls = null
+               @controls_shown = false
        destruct: ->
-               @kill_controls()
+               super()
+               @hide_controls()
+               return
        clone: ->
                return new Widget @
-       make_controls: -> # create controls, return them
-               return []
-       kill_controls: ->
-               console.log 'kill_controls'
-               for c in @controls
-                       c.destruct()
+       make_controls: ->
                @controls = []
                return
-       move: (xy) -> # just move
+       hide_controls: ->
+               return unless @controls? and @controls_shown
+               for c in @controls
+                       c.hide()
+               @controls_shown = false
+               return
+       show_controls: ->
+               return if @controls? and @controls_shown
+               unless @controls?
+                       @make_controls()
+               return unless @controls?
+               for c in @controls
+                       c.show()
+               @controls_shown = true
+               return
+       move: (xy) ->
                dx = xy.x - @x
                dy = xy.y - @y
                super xy
-               for c in @controls
-                       c.move x: c.x + dx, y: c.y + dy
+               if @controls?
+                       for c in @controls
+                               c.move x: c.x + dx, y: c.y + dy
+               return
        set_state: (state) ->
                return if @state is state
                if @state is STATES.EDITING
-                       @kill_controls()
+                       @hide_controls()
                super state
+               return
 
 class RectWidget extends Widget
        constructor: (args) ->
@@ -156,20 +233,24 @@ class RectWidget extends Widget
                        width: @width - 2
                        height: @height - 2
                        class: 'box normal'
-               @svg.appendChild @el
-       destruct: ->
-               super()
-               if @el?
-                       @svg.removeChild @el
+               @show()
+               return
        clone: ->
                return new RectWidget @
-       set_state: (state) ->
-               super state
-               @update_class()
+       as_array: ->
+               return [@x, @y, @width, @height]
+       from_array: (args, a) ->
+               args.x = a[0]
+               args.y = a[1]
+               args.width = a[2]
+               args.height = a[3]
+               return new RectWidget args
        move: (args) ->
                super args
                @el.setAttribute 'x', @x + 1
                @el.setAttribute 'y', @y + 1
+               @reposition_controls()
+               return
        proximity: (xy) -> # return the square of the distance to your visible bits
                x = xy.x
                y = xy.y
@@ -194,8 +275,9 @@ class RectWidget extends Widget
                        new_prox *= new_prox
                        if new_prox < prox
                                prox = new_prox
-               if in_x and in_y and prox > PROX_MAX
-                       prox = PROX_MAX - 1
+               # "hit" anything inside
+               #if in_x and in_y and prox > PROX_MAX
+               #       prox = PROX_MAX - 1
                return prox
        resize: (wh) ->
                dw = wh.w - @width
@@ -204,23 +286,176 @@ class RectWidget extends Widget
                @el.setAttribute 'width', @width - 2
                @height = wh.h
                @el.setAttribute 'height', @height - 2
-               if @controls.length > 1
-                       @controls[1].move x: @controls[1].x + dw, y: @controls[1].y + dh
-       make_controls: (args) -> # create controls, return them
-               console.log 'make_controls'
-               if @controls.length > 0
-                       console.log "warning: re-adding controls"
-                       @kill_controls()
-               w = @
-               @controls = [
-                       new ControlNWSE svg: @svg, x: @x - 7, y: @y - 7, done: args.done, drag: (dxy) ->
-                               w.resize w: w.width - dxy.x, h: w.height - dxy.y
-                               w.move x: w.x + dxy.x, y: w.y + dxy.y
-                       new ControlNWSE svg: @svg, x: @x + @width + 7, y: @y + @height + 7, done: args.done, drag: (dxy) ->
-                               w.resize w: w.width + dxy.x, h: w.height + dxy.y
+               @reposition_controls()
+               return
+       reposition_controls: ->
+               if @controls? and @controls.length > 1
+                       positions = @control_positions()
+                       for i in [0...positions.length]
+                               @controls[i].move x: positions[i].x, y: positions[i].y
+               return
+       control_positions: ->
+               gap = 7
+               mgap = 9
+               w2p = Math.floor(@width / 2) + 0.5
+               h2p = Math.floor(@height / 2) + 0.5
+               return [
+                       { x: @x - gap, y: @y - gap }
+                       { x: @x + w2p, y: @y - mgap }
+                       { x: @x + @width + gap, y: @y - gap }
+                       { x: @x + @width + mgap, y: @y + h2p }
+                       { x: @x + @width + gap, y: @y + @height + gap }
+                       { x: @x + w2p, y: @y + @height + mgap }
+                       { x: @x - gap, y: @y + @height + gap }
+                       { x: @x - mgap, y: @y + h2p }
                ]
-               return @controls
+       make_controls: ->
+               @controls = []
+               positions = @control_positions()
+               for i in [0...positions.length]
+                       @controls.push new ControlPath {
+                               svg: @svg
+                               x: positions[i].x
+                               y: positions[i].y
+                               drag: resizers[i] @
+                               shape: resizer_shapes[i % resizer_shapes.length]
+                       }
+               return
+
+class PolylineWidget extends Widget
+       constructor: (args) ->
+               super args
+               @css_class = 'polyline'
+               @nodes = []
+               for n in args.nodes
+                       @nodes.push x: n.x, y: n.y
+               @el = json_to_svg path:
+                       d: @my_path_d()
+                       class: 'polyline normal'
+               @show()
+       as_array: ->
+               ret = []
+               for n in @nodes
+                       ret.push @x + n.x
+                       ret.push @y + n.y
+               return ret
+       from_array: (args, a) ->
+               # the first node is also used as the @x,@y
+               args.x = a[0]
+               args.y = a[1]
+               args.nodes = []
+               while a.length
+                       xy = {}
+                       xy.x = a.shift() - args.x
+                       xy.y = a.shift() - args.y
+                       args.nodes.push xy
+               return new PolylineWidget args
+       clone: ->
+               return new PolylineWidget @
+       my_path_d: ->
+               ret = ''
+               for n in @nodes
+                       if ret is ''
+                               ret += 'M'
+                       else
+                               ret += 'L'
+                       ret += n.x + @x
+                       ret += ' '
+                       ret += n.y + @y
+               return ret
+       move: (args) ->
+               super args
+               @el.setAttribute 'd', @my_path_d()
+               @reposition_controls()
+               return
+       proximity: (xy) -> # return the square of the distance to your visible bits
+               prox = PROX_TOO_FAR
+               for n, i in @nodes
+                       dx = @x + n.x - xy.x
+                       dy = @y + n.y - xy.y
+                       p = dx * dx + dy * dy
+                       if p < prox
+                               prox = p
+                       if i > 0
+                               l1x = @x + @nodes[i-1].x
+                               l1y = @y + @nodes[i-1].y
+                               l2x = @x + @nodes[i].x
+                               l2y = @y + @nodes[i].y
+                               ldx = l2x - l1x
+                               ldy = l2y - l1y
+                               if ldx is 0 # vertical line
+                                       if (xy.y < l1y) is (xy.y < l2y)
+                                               continue
+                                       dx = l1x - xy.x
+                                       p = dx * dx
+                               else if ldy is 0 # horizontal line
+                                       if (xy.x < l1x) is (xy.x < l2x)
+                                               continue
+                                       dy = l1y - xy.y
+                                       p = dy * dy
+                               else # slanty line
+                                       # https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
+                                       a = ldy
+                                       b = -1 * ldx
+                                       c = l2x * l1y - l2y * l1x
+                                       y_on_line = (a * (a * xy.y - b * xy.x) - b * c) / (a * a + b * b)
+                                       if (y_on_line < l1y) is (y_on_line < l2y)
+                                               continue
+                                       p = (a * xy.x + b * xy.y + c) / Math.sqrt(a * a + b * b)
+                                       p *= p
+
+                               if p < prox
+                                       prox = p
+               return prox
+       resize: (wh) ->
+               # TODO implement
+               #@el.setAttribute 'd', @my_path_d()
+               #@reposition_controls()
+               return
+       node_dragger: (i) ->
+               if i is 0
+                       return (dxy) =>
+                               for j in [1...@nodes.length]
+                                       @nodes[j].x -= dxy.x
+                                       @nodes[j].y -= dxy.y
+                               @move x: @x + dxy.x, y: @y + dxy.y
+               return (dxy) =>
+                       @nodes[i].x += dxy.x
+                       @nodes[i].y += dxy.y
+                       @el.setAttribute 'd', @my_path_d()
+                       @reposition_controls()
+       reposition_controls: ->
+               if @controls? and @controls.length > 1
+                       positions = @control_positions()
+                       for i in [0...positions.length]
+                               @controls[i].move x: positions[i].x, y: positions[i].y
+               return
+       control_positions: ->
+               ret = []
+               for n in @nodes
+                       ret.push x: @x + n.x, y: @y + n.y
+               return ret
+       make_controls: ->
+               @controls = []
+               positions = @control_positions()
+               for i in [0...positions.length]
+                       @controls.push new ControlPath {
+                               svg: @svg
+                               x: positions[i].x
+                               y: positions[i].y
+                               drag: @node_dragger i
+                               shape: shape_node_move
+                       }
+               return
 
+CSS_CLASS_TO_PICKLE_TYPE = {
+       box: '0'
+       polyline: '1'
+}
+PICKLE_TYPE_TO_WIDGET_CLASS = [
+       RectWidget
+       PolylineWidget
+]
 # called automatically on domcontentloaded
 init = ->
        svg_offset = null
@@ -237,7 +472,7 @@ init = ->
                ]
        svg.appendChild json_to_svg style:
                type: 'text/css'
-               contents: '.box.normal{filter: url(#crayon)}'
+               contents: '.box.normal,.polyline.normal{filter: url(#crayon)}'
 
        # create canvas border
        svg.appendChild json_to_svg rect:
@@ -248,59 +483,121 @@ init = ->
                class: 'canvas_border'
 
        supply = {}
-       for args, i in [
-               { width: 40, height: 40 }
-               { width: 12, height: 50 }
-               { width: 70, height: 12 }
-       ]
-               widget = new RectWidget {
-                       width: args.width
-                       height: args.height
-                       x: 30 + i * 90 + (70 - args.width) / 2
-                       y: (supply_height - args.height) / 2
-                       svg: svg
-               }
-               supply[widget.id] = widget
+       supply_count = 0
+       supply_add = (type, args) ->
+               args.x ?= 0
+               args.y ?= 0
+               args.x += 30 + supply_count * 90
+               args.y += (supply_height - 50) / 2
+               args.svg = svg
+               w = new type args
+               supply[w.id] = w
+               supply_count += 1
+       supply_add RectWidget, width: 50, height: 50
+       supply_add PolylineWidget, y: 25, nodes: [{x: 0, y: 0}, {x: 50, y: 0}]
+       supply_add PolylineWidget, x: 25, nodes: [{x: 0, y: 0}, {x: 0, y: 50}]
+       supply_add PolylineWidget, x: 10, nodes: [{x: 0, y: 0}, {x: 15, y: 50}, {x: 30, y: 0}]
+       supply_add PolylineWidget, y: 50, nodes: [{x: 0, y: 0}, {x: 17, y: -50}, {x: 33, y: 0}, {x: 50, y: -50}]
 
        # editor state
-       controls_layer = { all: {}, selected: {} }
-       widget_layer = { all: {}, selected: {}, editing: null }
-       layers = [controls_layer, widget_layer]
-       hovered = null # can be in any layer
+       widgets = {}
+       selected = {}
+       selected_count = 0
+       hovered = null
+       drag_targets = null
        dragging = false # mouse state
-       drag_layer = null
        drag_from = x: 0, y: 0 # mouse was here at last frame of drag
        shift_key_down = false
 
+       lc = "abcdefghijklmnopqrstuvwxyz"
+       uc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+       cc_lca = lc.charCodeAt 0
+       cc_lcz = lc.charCodeAt 25
+       cc_uca = uc.charCodeAt 0
+       cc_ucz = uc.charCodeAt 25
+       cc_0 = '0'.charCodeAt 0
+       cc_9 = '9'.charCodeAt 0
+       # takes an array of positive integers, and encodes it as a string
+       pickle = (a) ->
+               ret = ''
+               for i in a
+                       cs = lc
+                       r = ''
+                       while i > 0 or r is ''
+                               digit = i % cs.length
+                               i -= digit
+                               i /= cs.length
+                               r = cs.charAt(digit) + r
+                               cs = uc
+                       ret += r
+               return ret
+       pickle_widgets = ->
+               ret = '0' # version of this encoding scheme
+               for id, w of widgets
+                       if CSS_CLASS_TO_PICKLE_TYPE[w.css_class]?
+                               ret += CSS_CLASS_TO_PICKLE_TYPE[w.css_class]
+                               ret += pickle w.as_array()
+               return ret
+       save = ->
+               window.location.hash = pickle_widgets()
+       load = (str) ->
+               return if str.charAt(1) isnt '0'
+               wtype = null
+               args = []
+               ii = 0
+               load_1 = (next_type) ->
+                       unless wtype?
+                               if next_type?
+                                       wtype = next_type
+                               return
+                       w = PICKLE_TYPE_TO_WIDGET_CLASS[wtype]::from_array svg: svg, args
+                       widgets[w.id] = w
+                       wtype = next_type
+                       args = []
+                       ii = 0
+               for i in [2...str.length]
+                       c = str.charCodeAt(i)
+                       if cc_0 <= c <= cc_9
+                               load_1 c - cc_0
+                       else if cc_lca <= c <= cc_lcz
+                               ii *= lc.length
+                               ii += c - cc_lca
+                               args.push ii
+                               ii = 0
+                       else if cc_uca <= c <= cc_ucz
+                               ii *= lc.length
+                               ii += c - cc_uca
+               load_1 null
+               return
        stop_editing = ->
-               if widget_layer.editing
-                       widget_layer.editing.kill_controls()
-                       widget_layer.editing = null
-       deselect = (layer, s) ->
-               return unless layer.selected[s.id]?
-               s.set_state STATES.NORMAL
-               delete layer.selected[s.id]
-               if widget_layer.editing is s
-                       widget_layer.editing = null
-               return
-       deselect_all = (layer, except = null) ->
-               for id, s of layer.selected
-                       deselect layer, s
-               return
-       _select = (layer, s) -> # don't call this directly, use select_only() or select_also()
-               s.set_state STATES.SELECTED
-               layer.selected[s.id] = s
-               return
-       select_only = (layer, s) ->
-               deselect_all layer, s
-               return if layer.selected[s.id]?
-               _select layer, s
-               return
-       select_also = (layer, s) ->
-               return if layer.selected[s.id]?
-               if layer is widget_layer
-                       stop_editing()
-               _select layer, s
+               for id, w of widgets
+                       w.hide_controls()
+               return
+       deselect = (w) ->
+               return unless selected[w.id]?
+               w.set_state STATES.NORMAL
+               delete selected[w.id]
+               selected_count -= 1
+               w.hide_controls()
+               return
+       deselect_all = (except = null) ->
+               for id, w of selected
+                       if w isnt except
+                               deselect w
+               return
+       _select = (w) -> # don't call this directly, use select_only() or select_also()
+               w.set_state STATES.SELECTED
+               selected[w.id] = w
+               return
+       select_only = (w) ->
+               deselect_all w
+               return if selected[w.id]?
+               _select w
+               return
+       select_also = (w) ->
+               return if selected[w.id]?
+               stop_editing()
+               _select w
                return
        find_closest = (widgets, xy) ->
                prox = PROX_TOO_FAR
@@ -313,6 +610,27 @@ init = ->
                if prox > PROX_MAX
                        return null
                return closest
+       find_closest_control = (xy) ->
+               prox = PROX_TOO_FAR
+               closest = null
+               for id, s of selected
+                       if s.controls_shown
+                               for c in s.controls
+                                       if c.shown
+                                               new_prox = c.proximity xy
+                                               if new_prox < prox
+                                                       prox = new_prox
+                                                       closest = c
+               if prox > PROX_MAX
+                       return null
+               return closest
+       find_closest_thing = (xy) ->
+               if xy.y < supply_height
+                       return find_closest supply, xy
+               else
+                       hit = find_closest_control xy
+                       hit ?= find_closest widgets, xy
+                       return hit
        svg_event_to_xy = (e) ->
                unless svg_offset?
                        svg_offset = $svg.offset()
@@ -320,72 +638,64 @@ init = ->
                        x: Math.round(e.pageX - svg_offset.left)
                        y: Math.round(e.pageY - svg_offset.top)
                }
-       closest_in_layers = (xy) ->
-               for layer in layers
-                       s = find_closest layer.selected, xy
-                       return layer: layer, s: s if s?
-                       s = find_closest layer.all, xy
-                       return layer: layer, s: s if s?
-               return null
        mousedown = (e) ->
                hit = null
                closest = null
-               layer = null
                mousemove e
                if dragging # two mousedowns in a row?! it happens
                        return mouseup e
                xy = svg_event_to_xy e
                if xy.y < supply_height
-                       s = find_closest supply, xy
-                       if s?
-                               hit = {
-                                       s: s.clone()
-                                       layer: widget_layer
-                               }
-                               widget_layer.all[hit.s.id] = hit.s
+                       hit = find_closest supply, xy
+                       if hit?
+                               hit = hit.clone()
+                               widgets[hit.id] = hit
                else
-                       hit = closest_in_layers xy
+                       hit = find_closest_control xy
+                       unless hit?
+                               hit = find_closest widgets, xy
                if hit?
-                       if hit.layer.selected[hit.s.id]
-                               # already selected
-                               # TODO start detection of a click that doesn't drag (to shrink selection)
-                       else if xy.y < supply_height
-                               # dragging a new thing in
-                               select_only hit.layer, hit.s
-                       else if shift_key_down
-                               select_also hit.layer, hit.s
+                       if hit.type is TYPE_WIDGET
+                               if selected[hit.id]
+                                       # already selected
+                                       # TODO start detection of a click that doesn't drag (to shrink selection)
+                               else if xy.y < supply_height
+                                       # dragging a new thing in
+                                       select_only hit
+                               else if shift_key_down
+                                       select_also hit
+                               else
+                                       select_only hit
+                               for id, w of selected
+                                       w.set_state STATES.DRAGGING
+                               drag_targets = selected
                        else
-                               select_only hit.layer, hit.s
-                       for id, s of hit.layer.selected
-                               s.set_state STATES.DRAGGING
+                               drag_targets = {}
+                               drag_targets[hit.id] = hit
                        dragging = true
-                       drag_layer = hit.layer
                        drag_from = xy
-                       console.log hit
                else
-                       deselect_all widget_layer
+                       deselect_all()
                return
        mouseup = (e) ->
+               save()
                mousemove e
                if dragging
-                       selected_count = 0
-                       for id, s of drag_layer.selected
-                               if s.y < supply_height and drag_layer is widget_layer
-                                       deselect drag_layer, s
-                                       s.destruct()
-                                       delete drag_layer.all[id]
-                               else
-                                       selected_count += 1
-                                       s.set_state STATES.SELECTED
-                       if drag_layer is widget_layer and selected_count is 1
-                               for id, s of drag_layer.selected
-                                       s.set_state STATES.EDITING
-                                       cs = s.make_controls done: (c) ->
-                                               deselect controls_layer, c
-                                               delete controls_layer.all[c.id]
-                                       for c in cs
-                                               controls_layer.all[c.id] = c
-                                       widget_layer.editing = s
+                       widgets_remaining = 0
+                       for id, s of drag_targets
+                               if s.type is TYPE_WIDGET
+                                       if s.y < supply_height
+                                               deselect s
+                                               s.destruct()
+                                               delete widgets[id]
+                                       else
+                                               widgets_remaining += 1
+                                               s.set_state STATES.SELECTED
+                       if widgets_remaining is 1
+                               for id, s of drag_targets
+                                       if s.type is TYPE_WIDGET
+                                               s.set_state STATES.EDITING
+                                               s.show_controls()
                dragging = false
                return
        mousemove = (e) ->
@@ -395,15 +705,17 @@ init = ->
                        rel_x = xy.x - drag_from.x
                        rel_y = xy.y - drag_from.y
                        drag_from = xy
-                       for id, w of drag_layer.selected
+                       for id, w of drag_targets
                                w.drag x: rel_x, y: rel_y
                else
-                       hit = closest_in_layers xy
-                       if hovered and hovered isnt hit?.s
+                       hit = find_closest_thing xy
+                       return if hit is hovered # both null, or both the same
+                       return if hit?.type is TYPE_CONTROL # hovering a control doesn't change display
+                       if hovered?
                                hovered.set_hover false
-                       return unless hit?
-                       hovered = hit.s
-                       hovered.set_hover true
+                       if hit?
+                               hovered = hit
+                               hit.set_hover true
                return
        $svg.mousedown (e) ->
                mousedown e
@@ -419,4 +731,7 @@ init = ->
                return true
        #($ document).keydown (e) ->
 
+       if window.location.hash
+               load window.location.hash
+
 $ init