PROX_TOO_FAR = PROX_MAX + 1 # no need to be precice when it's too far
# constants
-STYLE_NORMAL = 0
-STYLE_SELECTED = 1
-STYLE_HOVER = 2
-STYLE_EDITING = 3
-STYLE_DRAGGING = 4
-STYLE_TO_CLASS = [
- "normal"
- "selected"
- "hover"
- "editing"
- "dragging"
-]
+STATES = {
+ NORMAL: { txt: 'normal' }
+ SELECTED: { txt: 'selected' }
+ DRAGGING: { txt: 'dragging' }
+ EDITING: { txt: 'editing' }
+}
TYPE_WIDGET = 1
TYPE_CONTROL = 2
-set_style_class = (args) ->
- args.el.setAttribute 'class', "#{args.class} #{STYLE_TO_CLASS[args.style]}"
-
# json (compiled to javascript and minified) is ~8% smaller than the resulting xml
json_to_svg = (json) ->
for tag, attrs of json
@y = args.y ? 1
@width = args.width ? 50
@height = args.height ? 34
- @style = args.style ? STYLE_NORMAL
+ @state = args.state ? STATES.NORMAL
+ @hover = false
destruct: ->
+ 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
+ @update_class()
move: (xy) -> # just move
@x = xy.x
@y = xy.y
- drag: (xy) -> # react to mouse drag (obey constraints, etc.)
- @move xy
+ drag: (dxy) -> # react to mouse drag (obey constraints, etc.)
+ @move x: @x + dxy.x, y: @y + dxy.y
proximity: (xy) -> # return the square of the distance to your visible bits
return PROX_TOO_FAR
- set_style: (style) ->
- @style = style
+ set_state: (state) ->
+ @state = state
class Control extends Visible
constructor: (args) ->
dy = xy.y - @y
return dx * dx + dy * dy
-class ControlPoint extends Control
+class ControlNWSE extends Control
constructor: (args) ->
super args
- @el = json_to_svg circle:
- cx: @x + 1
- cy: @y + 1
- r: 6
+ @css_class = 'control_point'
+ @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
move: (args) ->
super args
- @el.setAttribute 'cx', @x
- @el.setAttribute 'cy', @y
+ @el.setAttribute 'd', @make_path()
class Widget extends Visible
#sub-classes are expected to implement all of these:
super xy
for c in @controls
c.move x: c.x + dx, y: c.y + dy
+ set_state: (state) ->
+ return if @state is state
+ if @state is STATES.EDITING
+ @kill_controls()
+ super state
class RectWidget extends Widget
constructor: (args) ->
@svg.removeChild @el
clone: ->
return new RectWidget @
- set_style: (style) ->
- super style
- set_style_class el: @el, class: 'box', style: style
+ set_state: (state) ->
+ super state
+ @update_class()
move: (args) ->
super args
@el.setAttribute 'x', @x + 1
prox = PROX_MAX - 1
return prox
resize: (wh) ->
+ dw = wh.w - @width
+ dh = wh.h - @height
@width = wh.w
- @el.setAttribute 'width', @width
+ @el.setAttribute 'width', @width - 2
@height = wh.h
- @el.setAttribute 'height', @height
+ @el.setAttribute 'height', @height - 2
if @controls.length > 1
- @controls[1].move x: @x + @width, y: @y + @height
+ @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
@kill_controls()
w = @
@controls = [
- new ControlPoint svg: @svg, x: @x, y: @y, done: args.done, drag: (xy) ->
- dx = xy.x - @x
- dy = xy.y - @y
- w.resize w: w.width - dx, h: w.height - dy
- w.move x: w.x + dx, y: w.y + dy
- new ControlPoint svg: @svg, x: @x + @width, y: @y + @height, done: args.done, drag: (xy) ->
- dx = xy.x - @x
- dy = xy.y - @y
- w.resize w: w.width + dx, h: w.height + dy
+ 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
]
return @controls
]
svg.appendChild json_to_svg style:
type: 'text/css'
- contents: '.box.normal,.box.hover,.box.selected{filter: url(#crayon)}'
+ contents: '.box.normal{filter: url(#crayon)}'
# create canvas border
svg.appendChild json_to_svg rect:
supply[widget.id] = widget
# editor state
- on_canvas = {}
- selected = {}
- controls = {}
- editing = {} # has controls
- dragging = false
+ controls_layer = { all: {}, selected: {} }
+ widget_layer = { all: {}, selected: {}, editing: null }
+ layers = [controls_layer, widget_layer]
+ hovered = null # can be in any layer
+ 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
- selected_type = ->
- for s of selected
- return s.type
- return null
- deselect = (s) ->
- s.set_style STYLE_NORMAL
- if s.type is TYPE_WIDGET
- s.kill_controls()
- delete selected[s.id]
+ 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
- deselect_all = (args) ->
- except = args?.except ? null
- for id, s of selected
- deselect s
+ _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 = (sel) ->
- deselect_all except: sel
- return if selected[sel.id]?
- selected[sel.id] = sel
- select_also = (sel) ->
- return if selected[sel.id]?
- sel_type = selected_type()
- if sel_type isnt sel.type
- deselect_all()
- selected[sel.id] = sel
+ 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
return
find_closest = (widgets, xy) ->
prox = PROX_TOO_FAR
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
- closest = find_closest supply, xy
- if closest?
- closest = closest.clone()
- on_canvas[closest.id] = closest
+ s = find_closest supply, xy
+ if s?
+ hit = {
+ s: s.clone()
+ layer: widget_layer
+ }
+ widget_layer.all[hit.s.id] = hit.s
else
- closest = find_closest controls, xy
- unless closest?
- closest = find_closest on_canvas, xy
- if closest?
- console.log closest
- if selected[closest.id]
+ hit = closest_in_layers 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 closest
+ select_only hit.layer, hit.s
else if shift_key_down
- select_also closest
+ select_also hit.layer, hit.s
else
- select_only closest
- for id, s of selected
- s.set_style STYLE_DRAGGING
+ select_only hit.layer, hit.s
+ for id, s of hit.layer.selected
+ s.set_state STATES.DRAGGING
dragging = true
+ drag_layer = hit.layer
drag_from = xy
+ console.log hit
else
- deselect_all()
- return false
+ deselect_all widget_layer
+ return
mouseup = (e) ->
mousemove e
if dragging
- for id, w of selected
- if w.y < supply_height
- deselect w
- w.destruct()
- delete on_canvas[id]
+ 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
- w.set_style STYLE_SELECTED
- if w.type is TYPE_WIDGET
- cs = w.make_controls done: (c) ->
- if controls[c.id]?
- delete controls[c.id]
- for c in cs
- controls[c.id] = c
- editing[w.id] = w
- delete selected[w.id]
+ 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
dragging = false
- return false
- prev_hover = null
+ return
mousemove = (e) ->
xy = svg_event_to_xy e
if dragging
rel_x = xy.x - drag_from.x
rel_y = xy.y - drag_from.y
drag_from = xy
- for id, w of selected
- w.drag x: w.x + rel_x, y: w.y + rel_y
+ for id, w of drag_layer.selected
+ w.drag x: rel_x, y: rel_y
else
- hover = find_closest on_canvas, xy
- unless hover?
- hover = find_closest supply, xy
- if hover != prev_hover
- if prev_hover?
- # FIXME
- if selected[prev_hover.id]?
- prev_hover.set_style STYLE_SELECTED
- else
- prev_hover.set_style STYLE_NORMAL
- if hover?
- hover.set_style STYLE_HOVER
- prev_hover = hover
+ hit = closest_in_layers xy
+ if hovered and hovered isnt hit?.s
+ hovered.set_hover false
+ return unless hit?
+ hovered = hit.s
+ hovered.set_hover true
+ return
+ $svg.mousedown (e) ->
+ mousedown e
+ return false
+ $svg.mouseup (e) ->
+ mouseup e
+ return false
+ $svg.mousemove (e) ->
+ mousemove e
return false
- $svg.mousedown mousedown
- $svg.mouseup mouseup
- $svg.mousemove mousemove
$(document).on 'keyup keydown', (e) ->
shift_key_down = e.shiftKey
return true