JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
major code cleanup (layers/controls)
[crayon_mockup.git] / main.coffee
index 3b896dd..632567f 100644 (file)
@@ -29,6 +29,9 @@ STATES = {
        DRAGGING: { txt: 'dragging' }
        EDITING:  { txt: 'editing' }
 }
+TYPE_UNKNOWN = 0
+TYPE_WIDGET = 1
+TYPE_CONTROL = 2
 
 # json (compiled to javascript and minified) is ~8% smaller than the resulting xml
 json_to_svg = (json) ->
@@ -96,6 +99,7 @@ next_widget_id = 0
 class Visible
        # required args: svg
        constructor: (args) ->
+               @type = TYPE_UNKNOWN
                @id = next_widget_id
                next_widget_id += 1
                @svg = args.svg
@@ -105,37 +109,56 @@ 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
@@ -148,42 +171,57 @@ class ControlPath extends Control
                @el = json_to_svg path:
                        d: @make_path()
                        class: 'control_point normal'
-               @svg.appendChild @el
-       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: ->
-               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) ->
@@ -195,11 +233,8 @@ 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 @
        as_array: ->
@@ -210,14 +245,12 @@ class RectWidget extends Widget
                args.width = a[2]
                args.height = a[3]
                return new RectWidget args
-       set_state: (state) ->
-               super state
-               @update_class()
        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
@@ -254,11 +287,13 @@ class RectWidget extends Widget
                @height = wh.h
                @el.setAttribute 'height', @height - 2
                @reposition_controls()
+               return
        reposition_controls: ->
-               if @controls.length > 1
+               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
@@ -274,22 +309,18 @@ class RectWidget extends Widget
                        { x: @x - gap, y: @y + @height + gap }
                        { x: @x - mgap, y: @y + h2p }
                ]
-       make_controls: (args) -> # create controls, return them
-               if @controls.length > 0
-                       if console?.log?
-                               console.log "warning: re-adding controls"
-                       @kill_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
-                               done: args.done
                                drag: resizers[i] @
                                shape: resizer_shapes[i % resizer_shapes.length]
                        }
-               return @controls
+               return
 
 class PolylineWidget extends Widget
        constructor: (args) ->
@@ -301,11 +332,7 @@ class PolylineWidget extends Widget
                @el = json_to_svg path:
                        d: @my_path_d()
                        class: 'polyline normal'
-               @svg.appendChild @el
-       destruct: ->
-               super()
-               if @el?
-                       @svg.removeChild @el
+               @show()
        as_array: ->
                ret = []
                for n in @nodes
@@ -313,6 +340,7 @@ class PolylineWidget extends Widget
                        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 = []
@@ -335,13 +363,11 @@ class PolylineWidget extends Widget
                        ret += ' '
                        ret += n.y + @y
                return ret
-       set_state: (state) ->
-               super state
-               @update_class()
        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
@@ -382,13 +408,9 @@ class PolylineWidget extends Widget
                                        prox = p
                return prox
        resize: (wh) ->
-               # FIXME (apply to more than just 2nd node)
-               @nodes[1].x = wh.w
-               @nodes[1].y = wh.h
-               @width = wh.w
-               @height = wh.h
-               @el.setAttribute 'd', @my_path_d()
-               @reposition_controls()
+               # TODO implement
+               #@el.setAttribute 'd', @my_path_d()
+               #@reposition_controls()
                return
        node_dragger: (i) ->
                if i is 0
@@ -403,7 +425,7 @@ class PolylineWidget extends Widget
                        @el.setAttribute 'd', @my_path_d()
                        @reposition_controls()
        reposition_controls: ->
-               if @controls.length > 1
+               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
@@ -413,22 +435,18 @@ class PolylineWidget extends Widget
                for n in @nodes
                        ret.push x: @x + n.x, y: @y + n.y
                return ret
-       make_controls: (args) -> # create controls, return them
-               if @controls.length > 0
-                       if console?.log?
-                               console.log "warning: re-adding line controls"
-                       @kill_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
-                               done: args.done
                                drag: @node_dragger i
                                shape: shape_node_move
                        }
-               return @controls
+               return
 
 CSS_CLASS_TO_PICKLE_TYPE = {
        box: '0'
@@ -482,12 +500,12 @@ init = ->
        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
 
@@ -515,7 +533,7 @@ init = ->
                return ret
        pickle_widgets = ->
                ret = '0' # version of this encoding scheme
-               for id, w of widget_layer.all
+               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()
@@ -533,7 +551,7 @@ init = ->
                                        wtype = next_type
                                return
                        w = PICKLE_TYPE_TO_WIDGET_CLASS[wtype]::from_array svg: svg, args
-                       widget_layer.all[w.id] = w
+                       widgets[w.id] = w
                        wtype = next_type
                        args = []
                        ii = 0
@@ -550,35 +568,36 @@ init = ->
                                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
@@ -591,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()
@@ -598,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
                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) ->
@@ -673,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