changeset 1624:4a751aaaee52

big timeline rewrites. hopefully it's faster and less leaky Ignore-this: f11cbe9b5f0950af7edfb91682470e60
author Drew Perttula <drewp@bigasterisk.com>
date Fri, 09 Jun 2017 12:29:35 +0000
parents c4e990d1dacf
children 9fe3052f8ced
files light9/web/graph.coffee light9/web/timeline/timeline.coffee
diffstat 2 files changed, 138 insertions(+), 102 deletions(-) [+]
line wrap: on
line diff
--- a/light9/web/graph.coffee	Fri Jun 09 08:48:29 2017 +0000
+++ b/light9/web/graph.coffee	Fri Jun 09 12:29:35 2017 +0000
@@ -222,6 +222,14 @@
     # runs your func once, tracking graph calls. if a future patch
     # matches what you queried, we runHandler your func again (and
     # forget your queries from the first time).
+
+    # helps with memleak? not sure yet. The point was if two matching
+    # labels get puushed on, we should run only one. So maybe
+    # appending a serial number is backwards.
+    @serial = 1 if not @serial
+    @serial += 1
+    #label = label + @serial
+    
     @_autoDeps.runHandler(func, label)
 
   _singleValue: (s, p) ->
@@ -242,6 +250,7 @@
     key = s + '|' + p
     hit = @cachedFloatValues.get(key)
     return hit if hit != undefined
+    #log('float miss', s, p)
 
     ret = parseFloat(N3.Util.getLiteralValue(@_singleValue(s, p)))
     @cachedFloatValues.set(key, ret)
--- a/light9/web/timeline/timeline.coffee	Fri Jun 09 08:48:29 2017 +0000
+++ b/light9/web/timeline/timeline.coffee	Fri Jun 09 12:29:35 2017 +0000
@@ -224,7 +224,6 @@
       for note in @selection.selected()
         deleteNote(@graph, @song, note, @selection)
 
-
   makeZoomAdjs: ->
     yMid = => @$.audio.offsetTop + @$.audio.offsetHeight / 2
     dur = @viewState.zoomSpec.duration
@@ -400,7 +399,7 @@
 
     notesForThisRow = []
     i = 0
-    for n in _.sortBy(@graph.objects(@song, U(':note')))
+    for n in _.sortBy(@graph.objects(@song, U(':note')), 'uri')
       if (i % ROW_COUNT) == @rowIndex
         notesForThisRow.push(n)
       i++
@@ -421,17 +420,6 @@
       e.zoomInX = @zoomInX
 
 
-getCurvePoints = (graph, curve, xOffset) ->
-  worldPts = []
-  uris = graph.objects(curve, graph.Uri(':point'))
-  for pt in uris
-    v = $V([xOffset + graph.floatValue(pt, graph.Uri(':time')),
-            graph.floatValue(pt, graph.Uri(':value'))])
-    v.uri = pt
-    worldPts.push(v)
-  worldPts.sort((a,b) -> a.e(1) > b.e(1))
-  return [uris, worldPts]
-
 Polymer
   is: 'light9-timeline-note'
   behaviors: [ Polymer.IronResizableBehavior ]
@@ -449,11 +437,11 @@
     'update(graph, dia, uri, zoomInX, setAdjuster)'
     ]
   ready: ->
-    @adjusterIds = {}
+    @adjusterIds = {} # id : true
 
   detached: ->
     log('detatch', @uri)
-    @dia.clearElem(@uri, ['/area', '/label'])
+    @dia.clearNote(@uri)
     @isDetached = true
     @clearAdjusters()
 
@@ -477,6 +465,7 @@
     return true
             
   update: (patch) ->
+    # update our note DOM and SVG elements based on the graph
     if not @patchCouldAffectMe(patch)
       # as autodep still fires all handlers on all patches, we just
       # need any single dep to cause another callback. (without this,
@@ -485,40 +474,44 @@
       return
     if @isDetached?
       return
- 
-    @updateDisplay()
 
-  updateDisplay: ->
-      
-    # update our note DOM and SVG elements based on the graph
+    @_updateDisplay()
+
+  _updateDisplay: ->
     U = (x) => @graph.Uri(x)
 
+    # @offsetTop causes some CSS layout to run!
     yForV = (v) => @offsetTop + (1 - v) * @offsetHeight
 
     originTime = @graph.floatValue(@uri, U(':originTime'))
     effect = @graph.uriValue(@uri, U(':effectClass'))
     for curve in @graph.objects(@uri, U(':curve'))
       if @graph.uriValue(curve, U(':attr')) == U(':strength')
-        @updateStrengthCurveEtc(originTime, curve, yForV, effect)
-        
-  updateStrengthCurveEtc: (originTime, curve, yForV, effect) ->
-    U = (x) => @graph.Uri(x)
-    [@pointUris, @worldPts] = getCurvePoints(@graph, curve, originTime) # (song time, value)
 
-    curveWidth = =>
-      tMin = @graph.floatValue(@worldPts[0].uri, U(':time'))
-      tMax = @graph.floatValue(@worldPts[3].uri, U(':time'))
-      tMax - tMin            
+        [@pointUris, @worldPts] = @_getCurvePoints(curve, originTime)
+        curveWidthCalc = () => @_curveWidth(@worldPts)
+    
+        screenPts = ($V([@zoomInX(pt.e(1)), @offsetTop + (1 - pt.e(2)) * @offsetHeight]) for pt in @worldPts)
+    
+        @dia.setNote(@uri, screenPts, effect)
+        @_updateAdjusters(screenPts, curveWidthCalc, yForV)
+        @_updateInlineAttrs(screenPts)
+        
+  _updateAdjusters: (screenPts, curveWidthCalc, yForV) ->   
+    if screenPts[screenPts.length - 1].e(1) - screenPts[0].e(1) < 100
+      @clearAdjusters()
+    else
+      @_makeOffsetAdjuster(yForV, curveWidthCalc)
+      @_makeCurvePointAdjusters(yForV, @worldPts)
 
-    screenPts = ($V([@zoomInX(pt.e(1)), @offsetTop + (1 - pt.e(2)) * @offsetHeight]) for pt in @worldPts)
-    @dia.setNote(@uri, screenPts, effect)
-
+  _updateInlineAttrs: (screenPts) ->
     leftX = Math.max(2, screenPts[Math.min(1, screenPts.length - 1)].e(1) + 5)
     rightX = screenPts[Math.min(2, screenPts.length - 1)].e(1) - 5
     if screenPts.length < 3
       rightX = leftX + 120
     w = 430
     h = 80
+    wasHidden = @inlineRect?.display == 'none'
     @inlineRect = {
       left: leftX,
       top: @offsetTop + @offsetHeight - h - 5,
@@ -526,23 +519,62 @@
       height: h,
       display: if rightX - leftX > w then 'block' else 'none'
       }
-    if @inlineRect.display != 'none'
+    if wasHidden and @inlineRect.display != 'none'
       @async =>
         @querySelector('light9-timeline-note-inline-attrs')?.displayed()
+    
+  _getCurvePoints: (curve, xOffset) ->
+    worldPts = []
+    uris = @graph.objects(curve, @graph.Uri(':point'))
+    for pt in uris
+      v = $V([xOffset + @graph.floatValue(pt, @graph.Uri(':time')),
+              @graph.floatValue(pt, @graph.Uri(':value'))])
+      v.uri = pt
+      worldPts.push(v)
+    worldPts.sort((a,b) -> a.e(1) > b.e(1))
+    return [uris, worldPts]
 
-    if screenPts[screenPts.length - 1].e(1) - screenPts[0].e(1) < 100
-      @clearAdjusters()
-      # also kill their connectors
-      return
+  _curveWidth: (worldPts) ->
+    tMin = @graph.floatValue(worldPts[0].uri, @graph.Uri(':time'))
+    tMax = @graph.floatValue(worldPts[3].uri, @graph.Uri(':time'))
+    tMax - tMin
+    
+  _makeCurvePointAdjusters: (yForV, worldPts) ->
+    for pointNum in [0, 1, 2, 3]
+      @_makePointAdjuster(yForV, worldPts, pointNum)
 
-    @makeCurveAdjusters(curveWidth, yForV, @worldPts)
-    
-  makeCurveAdjusters: (curveWidth, yForV, worldPts) ->
+  _makePointAdjuster: (yForV, worldPts, pointNum) ->
     U = (x) => @graph.Uri(x)
 
-    if true
-      @adjusterIds[@uri+'/offset'] = true
-      @setAdjuster(@uri+'/offset', => new AdjustableFloatObject({
+    adjId = @uri + '/p' + pointNum
+    @adjusterIds[adjId] = true
+    @setAdjuster adjId, =>
+      adj = new AdjustableFloatObject({
+        graph: @graph
+        subj: worldPts[pointNum].uri
+        pred: U(':time')
+        ctx: U(@song)
+        getTargetPosForValue: (value) =>
+          $V([@zoomInX(value), yForV(worldPts[pointNum].e(2))])
+        getValueForPos: (pos) =>
+          origin = @graph.floatValue(@uri, U(':originTime'))
+          (@zoomInX.invert(pos.e(1)) - origin)
+        getSuggestedTargetOffset: () => @_suggestedOffset(worldPts[pointNum]),
+      })
+      adj._getValue = (=>
+        # note: don't use originTime from the closure- we need the
+        # graph dependency
+        adj._currentValue + @graph.floatValue(@uri, U(':originTime'))
+        )
+      adj
+
+  _makeOffsetAdjuster: (yForV, curveWidthCalc) ->
+    U = (x) => @graph.Uri(x)
+
+    adjId = @uri + '/offset'
+    @adjusterIds[adjId] = true
+    @setAdjuster adjId, => 
+      adj = new AdjustableFloatObject({
         graph: @graph
         subj: @uri
         pred: U(':originTime')
@@ -550,37 +582,18 @@
         getDisplayValue: (v, dv) => "o=#{dv}"
         getTargetPosForValue: (value) =>
           # display bug: should be working from pt[0].t, not from origin
-          $V([@zoomInX(value + curveWidth() / 2), yForV(.5)])
+          $V([@zoomInX(value + curveWidthCalc() / 2), yForV(.5)])
         getValueForPos: (pos) =>
-          @zoomInX.invert(pos.e(1)) - curveWidth() / 2
+          @zoomInX.invert(pos.e(1)) - curveWidthCalc() / 2
         getSuggestedTargetOffset: () => $V([-10, 0])
-      }))
-
-    for pointNum in [0, 1, 2, 3]
-      do (pointNum) =>
-        @adjusterIds[@uri+'/p'+pointNum] = true
-        @setAdjuster(@uri+'/p'+pointNum, =>
-            adj = new AdjustableFloatObject({
-              graph: @graph
-              subj: worldPts[pointNum].uri
-              pred: U(':time')
-              ctx: U(@song)
-              getTargetPosForValue: (value) =>
-                $V([@zoomInX(value),
-                    yForV(worldPts[pointNum].e(2))])
-              getValueForPos: (pos) =>
-                origin = @graph.floatValue(@uri, U(':originTime'))
-                (@zoomInX.invert(pos.e(1)) - origin)
-              getSuggestedTargetOffset: () => $V([0, (if worldPts[pointNum].e(2) > .5 then 30 else -30)])
-            })
-            adj._getValue = (=>
-              # note: don't use originTime from the closure- we need the
-              # graph dependency
-              adj._currentValue + @graph.floatValue(@uri, U(':originTime'))
-              )
-            adj
-          )
-
+      })
+      adj
+    
+  _suggestedOffset: (pt) ->
+    if pt.e(2) > .5
+      $V([0, 30])
+    else
+      $V([0, -30])
     
     
 
@@ -633,7 +646,7 @@
     @graph.runHandler(@update.bind(@))
     
   update: ->
-    console.time('attrs update')
+    #console.time('attrs update')
     U = (x) => @graph.Uri(x)
     @effect = @graph.uriValue(@uri, U(':effectClass'))
     @effectLabel = @graph.stringValue(@effect, U('rdfs:label')) or (@effect.replace(/.*\//, ''))
@@ -650,7 +663,7 @@
     if existingColorScaleSetting == null
       @colorScaleFromGraph = '#ffffff'
       @colorScale = '#ffffff'
-    console.timeEnd('attrs update')
+    #console.timeEnd('attrs update')
 
 
   onDel: ->
@@ -781,7 +794,7 @@
         @adjs[adjId] = adj
         adj.id = adjId
 
-    @redraw()
+    @debounce('adj redraw', @redraw.bind(@))
 
     window.debug_adjsCount = Object.keys(@adjs).length
 
@@ -915,29 +928,53 @@
         moreBuild(elem)
     return elem
 
-  clearElem: (uri, suffixes) -> # todo: caller shouldn't have to know suffixes!
+  _clearElem: (uri, suffixes) ->
     for suff in suffixes
       elem = @elemById[uri+suff]
       if elem
         ko.removeNode(elem)
         delete @elemById[uri+suff]
 
-  anyPointsInView: (pts) ->
+  _anyPointsInView: (pts) ->
     for pt in pts
       # wrong:
       if pt.e(1) > -100 && pt.e(1) < 2500
         return true
     return false
     
-  setNote: (uri, curvePts, effect, classes) ->
+  setNote: (uri, curvePts, effect) ->
+    @debounce("setNote #{uri}", () => @_setNoteThrottle(uri, curvePts, effect))
+    
+  _setNoteThrottle: (uri, curvePts, effect) ->
     areaId = uri + '/area'
-    labelId = uri + '/label'
-    if not @anyPointsInView(curvePts)
-      @clearElem(uri, ['/area', '/label'])
+    if not @_anyPointsInView(curvePts)
+      @clearNote(uri)
       return
-    # for now these need to be pretty transparent since they're
-    # drawing on top of the inline-attrs widget :(
+
+    attrs = @_noteAttrs(effect)
+    elem = @getOrCreateElem areaId, 'notes', 'path', attrs, (elem) =>
+      @_addNoteListeners(elem, uri)
+    elem.setAttribute('d', Drawing.svgPathFromPoints(curvePts))
+    @_updateNotePathClasses(uri, elem)
 
+  _addNoteListeners: (elem, uri) ->
+    log("new note listeneers", uri)
+    elem.addEventListener 'mouseenter', =>
+      @selection.hover(uri)
+    elem.addEventListener 'mousedown', (ev) =>
+      sel = @selection.selected()
+      if ev.getModifierState('Control')
+        if uri in sel
+          sel = _.without(sel, uri)
+        else
+          sel.push(uri)
+      else
+        sel = [uri]
+      @selection.selected(sel)
+    elem.addEventListener 'mouseleave', =>
+      @selection.hover(null)
+
+  _noteAttrs: (effect) ->
     if effect in ['http://light9.bigasterisk.com/effect/blacklight',
       'http://light9.bigasterisk.com/effect/strobewarm']
       hue = 0
@@ -949,27 +986,17 @@
       hue = (hash * 8) % 360
       sat = 40 + (hash % 20) # don't conceal colorscale too much
 
-    attrs = {style: "fill:hsla(#{hue}, #{sat}%, 58%, 0.313);"}
-    elem = @getOrCreateElem areaId, 'notes', 'path', attrs, (elem) =>
-      elem.addEventListener 'mouseenter', =>
-        @selection.hover(uri)
-      elem.addEventListener 'mousedown', (ev) =>
-        sel = @selection.selected()
-        if ev.getModifierState('Control')
-          if uri in sel
-            sel = _.without(sel, uri)
-          else
-            sel.push(uri)
-        else
-          sel = [uri]
-        @selection.selected(sel)
-      elem.addEventListener 'mouseleave', =>
-        @selection.hover(null)
-    elem.setAttribute('d', Drawing.svgPathFromPoints(curvePts))
-    @updateNotePathClasses(uri, elem)
+    {style: "fill:hsla(#{hue}, #{sat}%, 58%, 0.313);"}
+
+  clearNote: (uri) ->
+    @_clearElem(uri, ['/area'])
 
-  updateNotePathClasses: (uri, elem) ->
+  _noteInDiagram: (uri) ->
+    return !!@elemById[uri + '/area']
+
+  _updateNotePathClasses: (uri, elem) ->
     ko.computed =>
+      return if not @_noteInDiagram(uri)
       classes = 'light9-timeline-diagram-layer ' + (if @selection.hover() == uri then 'hover' else '') + ' '  + (if uri in @selection.selected() then 'selected' else '')
       elem.setAttribute('class', classes)