Changeset - 468a13c4b910
[Not reviewed]
default
0 2 0
Drew Perttula - 7 years ago 2018-05-24 06:23:28
drewp@bigasterisk.com
fix dropping effect into timeline
Ignore-this: 9d9b651de46b3d33cd9109b321ec4c5d
2 files changed with 11 insertions and 13 deletions:
0 comments (0 inline, 0 general)
light9/web/graph.coffee
Show inline comments
 
@@ -172,97 +172,97 @@ class window.SyncedGraph
 
    part = curie.split(':')
 
    return @prefixFuncs(part[0])(part[1])
 

	
 
  Literal: (jsValue) ->
 
    N3.DataFactory.literal(jsValue)
 

	
 
  LiteralRoundedFloat: (f) ->
 
    N3.DataFactory.literal(d3.format(".3f")(f),
 
                          @Uri("http://www.w3.org/2001/XMLSchema#double"))
 

	
 
  Quad: (s, p, o, g) -> N3.DataFactory.quad(s, p, o, g)
 

	
 
  toJs: (literal) ->
 
    # incomplete
 
    parseFloat(literal.value)
 

	
 
  loadTrig: (trig, cb) -> # for debugging
 
    patch = {delQuads: [], addQuads: []}
 
    parser = N3.Parser()
 
    parser.parse trig, (error, quad, prefixes) =>
 
                  if error
 
                    throw new Error(error)
 
                  if (quad)
 
                    patch.addQuads.push(quad)
 
                  else
 
                    @_applyPatch(patch)
 
                    @_addPrefixes(prefixes)
 
                    cb() if cb
 
                    
 
  quads: () -> # for debugging
 
    [q.subject, q.predicate, q.object, q.graph] for q in @graph.getQuads()
 

	
 
  applyAndSendPatch: (patch) ->
 
    console.time('applyAndSendPatch')
 
    if !Array.isArray(patch.addQuads) || !Array.isArray(patch.delQuads)
 
      throw new Error("corrupt patch: #{JSON.stringify(patch)}")
 

	
 
    @_validatePatch(patch)
 

	
 
    @_applyPatch(patch)
 
    @_client.sendPatch(patch) if @_client
 
    console.timeEnd('applyAndSendPatch')
 

	
 
  _validatePatch: (patch) ->
 
    for qs in [patch.addQuads, patch.delQuads]
 
      for q in qs
 
        if not q.equals
 
          throw new Error("doesn't look like a proper Quad")
 
        if not q.subject.id or not q.graph.id?
 
        if not q.subject.id or not q.graph.id? or not q.predicate.id?
 
          throw new Error("corrupt patch: #{JSON.stringify(q)}")
 
    
 
  _applyPatch: (patch) ->
 
    # In most cases you want applyAndSendPatch.
 
    # 
 
    # This is the only method that writes to @graph!
 
    @cachedFloatValues.clear()
 
    for quad in patch.delQuads
 
      #log("remove #{JSON.stringify(quad)}")      
 
      did = @graph.removeQuad(quad)
 
      #log("removed: #{did}")
 
    for quad in patch.addQuads
 
      @graph.addQuad(quad)
 
    #log('applied patch locally', patchSizeSummary(patch))
 
    @_autoDeps.graphChanged(patch)
 

	
 
  getObjectPatch: (s, p, newObject, g) ->
 
    # make a patch which removes existing values for (s,p,*,c) and
 
    # adds (s,p,newObject,c). Values in other graphs are not affected.
 
    existing = @graph.getQuads(s, p, null, g)
 
    return {
 
      delQuads: existing,
 
      addQuads: [@Quad(s, p, newObject, g)]
 
    }
 

	
 
  patchObject: (s, p, newObject, g) ->
 
    @applyAndSendPatch(@getObjectPatch(s, p, newObject, g))
 
  
 
  runHandler: (func, label) ->
 
    # 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) ->
 
    @_autoDeps.askedFor(s, p, null, null)
 
    quads = @graph.getQuads(s, p)
 
    objs = new Set(q.object for q in quads)
 
    
 
    switch objs.size
light9/web/timeline/timeline.coffee
Show inline comments
 
log = console.log
 
RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
 
Drawing = window.Drawing
 
ROW_COUNT = 7
 

	
 
class Project
 
  constructor: (@graph) ->
 

	
 
  makeEffect: (uri) ->
 
    U = (x) => @graph.Uri(x)
 
    effect = U(uri + '/effect')
 
    quad = (s, p, o) => {subject: s, predicate: p, object: o, graph: effect}
 
    
 
    quads = [
 
      quad(effect, U('rdf:type'), U(':Effect')),
 
      quad(effect, U(':copiedFrom'), uri),
 
      quad(effect, U('rdfs:label'), @graph.Literal(uri.replace(/.*capture\//, ''))),
 
      quad(effect, U(':publishAttr'), U(':strength')),
 
      ]
 

	
 
    fromSettings = @graph.objects(uri, U(':setting'))
 

	
 
    toSettings = @graph.nextNumberedResources(effect + '_set', fromSettings.length)
 
      
 
    for fs in fromSettings
 
      ts = toSettings.pop()
 
      # full copies of these since I may have to delete captures
 
      quads.push(quad(effect, U(':setting'), ts))
 
      quads.push(quad(ts, U(':device'), @graph.uriValue(fs, U(':device'))))
 
      quads.push(quad(ts, U(':deviceAttr'), @graph.uriValue(fs, U(':deviceAttr'))))
 
      try
 
        quads.push(quad(ts, U(':value'), @graph.uriValue(fs, U(':value'))))
 
      catch
 
        quads.push(quad(ts, U(':scaledValue'), @graph.uriValue(fs, U(':scaledValue'))))
 

	
 
    @graph.applyAndSendPatch({delQuads: [], addQuads: quads})
 
    return effect
 

	
 
  makeNewNote: (song, effect, dropTime, desiredWidthT) ->
 
    U = (x) => @graph.Uri(x)
 
    quad = (s, p, o) => {subject: s, predicate: p, object: o, graph: song}
 
    quad = (s, p, o) => @graph.Quad(s, p, o, song)
 
      
 
    newNote = @graph.nextNumberedResource("#{@song.value}/n")
 
    newNote = @graph.nextNumberedResource("#{song.value}/n")
 
    newCurve = @graph.nextNumberedResource("#{newNote.value}c")
 
    points = @graph.nextNumberedResources("#{newCurve.value}p", 4)
 

	
 
    curveQuads = [
 
        quad(song, U(':note'), newNote)
 
        quad(newNote, RDF + 'type', U(':Note'))
 
        quad(newNote, U('rdf:type'), U(':Note'))
 
        quad(newNote, U(':originTime'), @graph.LiteralRoundedFloat(dropTime))
 
        quad(newNote, U(':effectClass'), effect)
 
        quad(newNote, U(':curve'), newCurve)
 
        quad(newCurve, RDF + 'type', U(':Curve'))
 
        quad(newCurve, U('rdf:type'), U(':Curve'))
 
        quad(newCurve, U(':attr'), U(':strength'))
 
      ]        
 
    pointQuads = []
 

	
 
   
 
    
 
    for i in [0...4]
 
      pt = points[i]
 
      pointQuads.push(quad(newCurve, U(':point'), pt))
 
      pointQuads.push(quad(pt, U(':time'), @graph.LiteralRoundedFloat(i/3 * desiredWidthT)))
 
      pointQuads.push(quad(pt, U(':value'), @graph.LiteralRoundedFloat(i == 1 or i == 2)))
 

	
 
    patch = {
 
      delQuads: []
 
      addQuads: curveQuads.concat(pointQuads)
 
      }
 
    @graph.applyAndSendPatch(patch)
 
    
 
  getCurvePoints: (curve, xOffset) ->
 
    worldPts = []
 
    uris = @graph.objects(curve, @graph.Uri(':point'))
 
    for pt in uris
 
      tm = @graph.floatValue(pt, @graph.Uri(':time'))
 
      val = @graph.floatValue(pt, @graph.Uri(':value'))
 
      v = $V([xOffset + tm, val])
 
      v.uri = pt
 
      worldPts.push(v)
 
    worldPts.sort((a,b) -> a.e(1) > b.e(1))
 
    return [uris, worldPts]
 

	
 
  curveWidth: (worldPts) ->
 
    tMin = @graph.floatValue(worldPts[0].uri, @graph.Uri(':time'))
 
    tMax = @graph.floatValue(worldPts[3].uri, @graph.Uri(':time'))
 
    tMax - tMin
 
      
 
  deleteNote: (song, note, selection) ->
 
    patch = {delQuads: [@graph.Quad(song, graph.Uri(':note'), note, song)], addQuads: []}
 
    @graph.applyAndSendPatch(patch)
 
    if note in selection.selected()
 
      selection.selected(_.without(selection.selected(), note))
 

	
 
    
 
coffeeElementSetup(class TimelineEditor extends Polymer.mixinBehaviors([Polymer.IronResizableBehavior], Polymer.Element)
 
  @is: 'light9-timeline-editor'
 
  @getter_properties:
 
    viewState: { type: Object }
 
    debug: {type: String}
 
    graph: {type: Object, notify: true}
 
    project: {type: Object}
 
    setAdjuster: {type: Function, notify: true}
 
    playerSong: {type: String, notify: true}
 
    followPlayerSong: {type: Boolean, notify: true, value: true}
 
    song: {type: String, notify: true}
 
    show: {type: String, notify: true}
 
    songTime: {type: Number, notify: true}
 
    songDuration: {type: Number, notify: true}
 
    songPlaying: {type: Boolean, notify: true}
 
    selection: {type: Object, notify: true}
 
  @getter_observers: [
 
    '_onSong(playerSong, followPlayerSong)',
 
    '_onGraph(graph)',
 
    '_onSongDuration(songDuration, viewState)',
 
    '_onSongTime(songTime, viewState)',
 
    '_onSetAdjuster(setAdjuster)',
 
  ]
 
  constructor: ->
 
    super()
 
    @viewState = new ViewState()
 
    window.viewState = @viewState
 
    
 
  ready: ->
 
    super.ready()
 
    @addEventListener 'mousedown', (ev) => @$.adjustersCanvas.onDown(ev)
 
    @addEventListener 'mousemove', (ev) => @$.adjustersCanvas.onMove(ev)
 
    @addEventListener 'mouseup', (ev) => @$.adjustersCanvas.onUp(ev)
 

	
 
    ko.options.deferUpdates = true
 
    
 
    @selection = {hover: ko.observable(null), selected: ko.observable([])}
 

	
 
    window.debug_zoomOrLayoutChangedCount = 0
 
    window.debug_adjUpdateDisplay = 0
 
    
 
    ko.computed(@zoomOrLayoutChanged.bind(@))
 

	
 
    @trackMouse()
 
    @bindKeys()
 
    @bindWheelZoom(@)
 

	
 
    setInterval(@updateDebugSummary.bind(@), 100)
 

	
 
    @addEventListener('iron-resize', @_onIronResize.bind(@))
 
    Polymer.RenderStatus.afterNextRender(this, @_onIronResize.bind(@))
 

	
 
    #zoomed = @$.zoomed 
 
    #setupDrop(@dia.shadowRoot.querySelector('svg'),
 
    #          zoomed.$.rows, @, zoomed.onDrop.bind(zoomed))
 
    Polymer.RenderStatus.afterNextRender this, =>
 
      setupDrop(@$.zoomed.$.rows, @$.zoomed.$.rows, @, @$.zoomed.onDrop.bind(@$.zoomed))
 
            
 
  _onIronResize: ->
 
    @viewState.setWidth(@offsetWidth)
 
    @viewState.coveredByDiagramTop(@$.coveredByDiagram.offsetTop)
 
    @viewState.rowsY(@$.zoomed.$.rows.offsetTop) if @$.zoomed?.$?.rows?
 
    @viewState.audioY(@$.audio.offsetTop)
 
    @viewState.audioH(@$.audio.offsetHeight)
 
    if @$.zoomed?.$?.time?
 
      @viewState.zoomedTimeY(@$.zoomed.$.time.offsetTop)
 
      @viewState.zoomedTimeH(@$.zoomed.$.time.offsetHeight)
 
    
 
  _onSongTime: (t) ->
 
    @viewState.cursor.t(t)
 
    
 
  _onSongDuration: (d) ->
 
    d = 700 if d < 1 # bug is that asco isn't giving duration, but 0 makes the scale corrupt
 
    @viewState.zoomSpec.duration(d)
 
    
 
  _onSong: (s) ->
 
    @song = @playerSong if @followPlayerSong
 
    
 
  _onGraph: (graph) ->
 
    @project = new Project(graph)
 
    @show = 'http://light9.bigasterisk.com/show/dance2017'
 

	
 
  _onSetAdjuster: () ->
 
    @makeZoomAdjs()
 
    
 
  updateDebugSummary: ->
 
    elemCount = (tag) -> document.getElementsByTagName(tag).length
 
    @debug = "#{window.debug_zoomOrLayoutChangedCount} layout change,
 
     #{elemCount('light9-timeline-note')} notes,
 
     #{@selection.selected().length} selected
 
     #{elemCount('light9-timeline-graph-row')} rows,
 
     #{window.debug_adjsCount} adjuster items registered,
 
     #{window.debug_adjUpdateDisplay} adjuster updateDisplay calls,
 
    "
 

	
 
  zoomOrLayoutChanged: ->
 
    vs = @viewState
 
    dependOn = [vs.zoomSpec.t1(), vs.zoomSpec.t2(), vs.width()]
 

	
 
    # shouldn't need this- deps should get it
 
    @$.zoomed.gatherNotes() if @$.zoomed?.gatherNotes?
 
  
 
    # todo: these run a lot of work purely for a time change
 
    if @$.zoomed?.$?.audio?
 
      #@dia.setTimeAxis(vs.width(), @$.zoomed.$.audio.offsetTop, vs.zoomInX)
 
@@ -328,109 +326,109 @@ coffeeElementSetup(class TimeZoomed exte
 
    @$.rows.firstChild.style.position = 'relative'
 
    @$.rows.firstChild.style.top = -@viewState.rowsY() + 'px'
 

	
 
    @renderer.resize(@clientWidth, @clientHeight + @viewState.rowsY())
 
    
 
    @renderer.render(@stage)
 
  
 
  _onGraph: (graph, setAdjuster, song, viewState, project)->
 
    return unless @song # polymer will call again
 
    @graph.runHandler(@gatherNotes.bind(@), 'zoom notes')
 
  onZoom: ->
 
    updateZoomFlattened = ->
 
      log('updateZoomFlattened')
 
      @zoomFlattened = ko.toJS(@viewState.zoomSpec)
 
    ko.computed(updateZoomFlattened.bind(@))
 

	
 
  gatherNotes: ->
 
    U = (x) => @graph.Uri(x)
 
    return unless @song?
 

	
 
    songNotes = @graph.objects(U(@song), U(':note'))
 
    
 
    @stage.removeChildren()
 
    n.destroy() for n in @notes
 
    @notes = []
 
    
 
    noteNum = 0
 
    for uri in _.sortBy(songNotes, 'id')
 
      con = new PIXI.Container()
 
      con.interactive=true
 
      @stage.addChild(con)
 
      
 
      row = noteNum % 6
 
      rowTop = @viewState.rowsY() + 20 + 150 * row
 
      note = new Note(@, con, @project, @graph, @selection, uri, @setAdjuster, U(@song), @viewState, rowTop, rowTop + 140)
 
      @notes.push(note)
 
      noteNum = noteNum + 1
 
 
 
    @renderer.render(@stage)
 
    
 
  onDrop: (effect, pos) ->
 
    U = (x) => @graph.Uri(x)
 

	
 
    return unless effect and effect.match(/^http/)
 

	
 
    # we could probably accept some initial overrides right on the
 
    # effect uri, maybe as query params
 

	
 
    if not @graph.contains(effect, RDF + 'type', U(':Effect'))
 
      if @graph.contains(effect, RDF + 'type', U(':LightSample'))
 
    if not @graph.contains(effect, U('rdf:type'), U(':Effect'))
 
      if @graph.contains(effect, U('rdf:type'), U(':LightSample'))
 
        effect = @project.makeEffect(effect)
 
      else
 
        log("drop #{effect} is not an effect")
 
        return
 

	
 
    dropTime = @viewState.zoomInX.invert(pos.e(1))
 

	
 
    desiredWidthX = @offsetWidth * .3
 
    desiredWidthT = @viewState.zoomInX.invert(desiredWidthX) - @viewState.zoomInX.invert(0)
 
    desiredWidthT = Math.min(desiredWidthT, @zoom.duration() - dropTime)
 
    @project.makeNewNote(U(@song), effect, dropTime, desiredWidthT)
 
    desiredWidthT = Math.min(desiredWidthT, @viewState.zoomSpec.duration() - dropTime)
 
    @project.makeNewNote(U(@song), U(effect), dropTime, desiredWidthT)
 

	
 
  updateInlineAttrs: (note, config) ->
 
    if not config?
 
      index = 0
 
      for c in @inlineAttrConfigs
 
        if c.uri.equals(note)
 
          @splice('inlineAttrConfigs', index)
 
          return
 
        index += 1
 
    else
 
      index = 0
 
      for c in @inlineAttrConfigs
 
        if c.uri.equals(note)
 
          @splice('inlineAttrConfigs', index, 1, config)
 
          return
 
        index += 1
 
      @push('inlineAttrConfigs', config)
 
)
 

	
 

	
 
coffeeElementSetup(class TimeAxis extends Polymer.Element
 
  @is: "light9-timeline-time-axis",
 
  @getter_properties:
 
    viewState: { type: Object, notify: true, observer: "onViewState" }
 
  onViewState: ->
 
    ko.computed =>
 
      dependOn = [@viewState.zoomSpec.t1(), @viewState.zoomSpec.t2()]
 
      pxPerTick = 50
 
      axis = d3.axisTop(@viewState.zoomInX).ticks(@viewState.width() / pxPerTick)
 
      d3.select(@$.axis).call(axis)
 
)
 

	
 

	
 
# Maintains a pixi object, some adjusters, and inlineattrs corresponding to a note
 
# in the graph.
 
class Note
 
  constructor: (@parentElem, @container, @project, @graph, @selection, @uri, @setAdjuster, @song, @viewState, @rowTopY, @rowBotY) ->
 
    @adjusterIds = {} # id : true
 
    @graph.runHandler(@draw.bind(@), 'note draw')
 

	
 
  destroy: ->
 
    log('destroy', @uri.value)
 
    @isDetached = true
 
    @clearAdjusters()
 
    @parentElem.updateInlineAttrs(@uri, null)
 

	
 
  clearAdjusters: ->
 
    for i in Object.keys(@adjusterIds)
0 comments (0 inline, 0 general)