DRAGGING: { txt: 'dragging' }
EDITING: { txt: 'editing' }
}
+TYPE_UNKNOWN = 0
TYPE_WIDGET = 1
TYPE_CONTROL = 2
class Visible
# required args: svg
constructor: (args) ->
+ @type = TYPE_UNKNOWN
@id = next_widget_id
next_widget_id += 1
@svg = args.svg
@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
@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) ->
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
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
@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
{ 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) ->
@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
+ 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 += ' '
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
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
return (dxy) =>
- for i in [1...@nodes.length]
- @nodes[i].x -= dxy.x
- @nodes[i].y -= dxy.y
+ 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
@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
for n in @nodes
ret.push x: @x + n.x, y: @y + n.y
return ret
- make_controls: (args) -> # create controls, return them
- console.log 'make line controls'
- 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: @node_dragger i
shape: shape_node_move
}
- return @controls
+ 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
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, x: 0, nodes: [{x: 0, y: 50}, {x: 17, y: 0}, {x: 33, y: 50}, {x: 50, 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
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()
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) ->
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
return true
#($ document).keydown (e) ->
+ if window.location.hash
+ load window.location.hash
+
$ init