Skip to content
Snippets Groups Projects
Select Git revision
  • 8764fcddb79a63fb6cb8ad8f61b1c84356a107fe
  • master default protected
  • develop
  • pset7
  • pset6
  • pset3
6 results

index.md

Blame
  • vfloater.js 15.27 KiB
    /*
    view/vfloater.js
    
    Jake Read at the Center for Bits and Atoms
    (c) Massachusetts Institute of Technology 2019
    
    This work may be reproduced, modified, distributed, performed, and
    displayed for any purpose, but must acknowledge the squidworks and cuttlefish projects.
    Copyright is retained and must be preserved. The work is provided as is;
    no warranty is provided, and users accept all liability.
    */
    
    import DomTools from './vdom.js'
    
    function Floater(view, grouptype) {
      // want one,
      // altho, non-ideal ...
      let dt = new DomTools(view)
      // TODO:
      /*
      ... think ... think ...
      -> button to expand, and gather? div-related?
      -> button drags as well ? titlematter does hover, not drag, two funcitons ?
      -> !!!!!!!!!!!!! button for fixed ! fixed !!!!!!!!!!!!!!!
      -> roll sim back, so that fixed / unfixed is meaningful
      -> explosions afterwards
      */
      // a floater is an individual that can be dragged, fixed/unfixed, etc
      // it is exactly the thing we give to d3 to lay out
      this.x = 0
      this.y = 0
      // want these,
      this.w = 101
      this.h = 101
      // if it has fx, fy, it is fixed
      this.r = 101 // to default,
      // these are handy,
      this.bb = {}
      this.bb.x1 = -50
      this.bb.y1 = -50
      this.bb.x2 = 51
      this.bb.y2 = 51
      // by default, no
      this.isFixed = false
      // will have this to track within simulation links
      this.simIndex = null
    
      // every floater has *at least* this ...
      // this is where we put the buttonz
      this.ownElement = $(`<div>`).addClass('float').get(0)
      $(this.ownElement).append(`<i class="em em-pushpin"></i>`)
      $(this.ownElement).on('click', (evt) => {
        if (this.isFixed === true) {
          this.free()
        } else {
          this.fix()
        }
      }).on('mouseover', (evt) => {
        document.body.style.cursor = 'pointer'
      }).on('mouseout', (evt) => [
        document.body.style.cursor = 'auto'
      ])
      $(view.plane).append(this.ownElement)
      // fixed <i class="em em-round_pushpin"></i>
      // open <i class="em em-pushpin"></i>
    
      // this is a list of elements to carry about
      this.bag = [] // gets {element, x, y}
      // fuuu
      let writeTitleMatter = (div, def) => {
        let md = false
        div.onmouseover = () => {
          def.highlight()
          if (!md) document.body.style.cursor = 'grab'
        }
        div.onmouseout = () => {
          def.unhighlight()
          if (!md) document.body.style.cursor = 'auto'
        }
        div.onmousedown = (evt) => {
          md = true
          document.body.style.cursor = 'grabbing'
          // pin to current position,
          this.fix()
          evt.preventDefault()
          evt.stopPropagation()
          let domElemMouseMove = (evt) => {
            // TRANSFORMS here to move div about on drag
            evt.preventDefault()
            evt.stopPropagation()
            // need to find that top-level view's scale transform
            this.fx += evt.movementX / view.tls
            this.fy += evt.movementY / view.tls
            this.newFix()
            this.tick()
          }
    
          function rmOnMouseUp(evt) {
            md = false
            document.body.style.cursor = 'auto'
            document.removeEventListener('mousemove', domElemMouseMove)
            document.removeEventListener('mouseup', rmOnMouseUp)
          }
    
          document.addEventListener('mousemove', domElemMouseMove)
          document.addEventListener('mouseup', rmOnMouseUp)
        }
      }
    
      // -------------------------------------------- TAKE
    
      // take up new things
      this.typeset = []
      this.take = (div, def, stall, resize) => {
        let type = ''
        // cover view-containing ... ??
        // if ($(div).is('.native') && $(div).children().contains('.view'))
        let item = {
          type: '',
          de: div,
          def: def,
          offsetX: 0,
          offsetY: 0
        }
        // always nice to do this first,
        // defs -> the dom
        $(view.plane).append(item.de)
        // only a few cases we will allow,
        if ($(div).is('.inputs')) {
          item.type = 'inputs'
        } else if ($(div).is('.outputs')) {
          item.type = 'outputs'
        } else if ($(div).is('.defcore')) {
          item.type = 'core'
        } else if ($(div).is('.nativewrap')) {
          item.type = 'native'
          // at the gitgo, set this to our current width...
          // this width thing is happening because you're rewriting the
          // so ... not a take, a swap ? !!! FFUUUUU
          // native element's wrapper ...
          this.calculateSizes()
          if (this.w < 200) {
            $(item.de).css('width', `200px`)
          } else {
            $(item.de).css('width', `${this.w}px`)
          }
          // and for non-spec'd heights,
          if (item.de.clientHeight < 10) {
            $(item.de).css('height', '200px')
          }
          // ok then, resizeable also
          dt.makeResizeable(item.de, this.onElementResize, resize)
        } else if ($(div).is('.defbutton')) {
          item.type = 'button'
        } else {
          // best guess, but..
          item.type = 'core'
          console.error('no thx, no recognized type when taking div into floater')
        }
        // put it in the baaaag
        this.typeset.push(item.type)
        this.bag.push(item)
        // (also) write the drag content, if there's a header on board
        let header = $(div).children('.header').get(0)
        if (header) {
          writeTitleMatter(header, item.def)
        }
        // just want to do this once,
        if (stall !== true) this.onChange()
      } // end take
    
      this.fitNativeToFloater = () => {
        this.calculateSizes(true)
        // find it,
        let nt = this.bag.find((cnd) => {
          return cnd.type === 'native'
        })
        if (!nt) {
          console.error('not finding a native element to resize here')
        } else {
          $(nt.de).css('width', `${this.w}px`)
          // want also to reset the resizing handle ...
          // kind of messy call, but
          let w = nt.de.clientWidth
          let h = nt.de.clientHeight
          let handle = $(nt.de).children('.resizehandle').get(0)
          dt.writeTransform(handle, {
            x: w,
            y: h
          })
        }
      }
    
      // to catch,
      this.onElementResize = () => {
        // this is a lot,
        this.onChange()
        // our parent,
        view.kick()
      }
    
      // GROUP TYPES:
      // 'edges' ... (core) with (outputs) and .edges || (inputs)
      // 'unwrapped' ... (core) with (outputs) and .unwrapped || (inputs)
      // 'wrapped' ... (core) with (inputs) and (outputs) and (native .view) and .wrapped
      // 'std' ... (core) with some set of (inputs) and (outputs) and (native)
    
      this.makeEdges = () => {
        this.grouptype === 'edges'
        this.onChange()
      }
    
      // -------------------------------------------- CHANGE
    
      this.grouptype = grouptype
      // don't want to do this all the time
      // to 'open' ... put core with outputs,
      // to 'unwrap' ... put core with inputs, it will go below
      this.onChange = () => {
        // find types,
        let core = this.bag.find((cnd) => {
          return cnd.type === 'core'
        })
        let inputs = this.bag.find((cnd) => {
          return cnd.type === 'inputs'
        })
        let outputs = this.bag.find((cnd) => {
          return cnd.type === 'outputs'
        })
        let native = this.bag.find((cnd) => {
          return cnd.type === 'native'
        })
        // possible to have many,
        let buttons = []
        for (let item of this.bag) {
          if (item.type === 'button') buttons.push(item)
        }
    
        let spaces = 5
    
        // TYPE CASES BY TYPESET
        //console.log('FLT onchange, floater has', core ? '(core)' : '', inputs ? '(inputs)' : '', outputs ? '(outputs)' : '', native ? '(native)' : '')
    
        if (this.grouptype === 'std') {
          // 'std' ... (core) with some set of (inputs) and (outputs) and (native)
          core.offsetX = -core.de.clientWidth
          core.offsetY = 0
          if (inputs) {
            inputs.offsetX = core.offsetX - inputs.de.clientWidth - spaces - 2
            inputs.offsetY = 0
          }
          if (outputs) {
            outputs.offsetX = spaces
            outputs.offsetY = 0
          }
          if (native) {
            // calc sizes, ignoring native
            this.calculateSizes(true)
            native.offsetX = this.bb.x1
            native.offsetY = this.bb.y2 + spaces
            // for resizing,
            if (outputs) {
              let gap = native.de.clientWidth - this.w
              if (gap > 1) {
                outputs.offsetX += gap
              }
            }
          }
        } else if (this.grouptype === 'unwrapped') {
          // 'unwrapped' ... (core) with (outputs) and .unwrapped || (inputs)
          if (core) {
            // unwrapped, left side, core, outputs, etc
            core.offsetX = -core.de.clientWidth
            core.offsetY = 0
            if (outputs) {
              outputs.offsetX = spaces
              outputs.offsetY = 0
            }
            if (native) {
              this.calculateSizes(true)
              native.offsetX = this.bb.x1
              native.offsetY = this.bb.y2 + spaces
              if (outputs) {
                let gap = native.de.clientWidth - this.w
                if (gap > 1) {
                  outputs.offsetX += gap
                }
              }
            }
          } else {
            // unwrapped, right side, it's the lonely inputs
            if (inputs) {
              inputs.offsetX = -inputs.de.clientWidth
              inputs.offsetY = 0
            }
          }
        } else if (this.grouptype === 'wrapped') {
          // should have 'em all let's just wyld out
          // the view attached is king,
          let ntw = native.de.clientWidth
          let nth = native.de.clientHeight
          // left ...
          native.offsetX = -core.de.clientWidth
          native.offsetY = core.de.clientHeight + spaces
          // core
          core.offsetX = -core.de.clientWidth
          core.offsetY = 0
          // inputs, well left
          inputs.offsetX = core.offsetX - inputs.de.clientWidth - spaces
          inputs.offsetY = $(inputs.de).children('.header').get(0).clientHeight + spaces + native.offsetY
          // ouputs, as is tradition
          outputs.offsetX = ntw + spaces + core.offsetX
          outputs.offsetY = inputs.offsetY
        } else if (this.grouptype === 'edges') {
          if(core && outputs){
            this.edgetype = 'left'
            // and on top of self,
            core.offsetX = -core.de.clientWidth
            core.offsetY = -core.de.clientHeight - 25
            outputs.offsetX = core.offsetX
            outputs.offsetY = core.offsetY - outputs.de.clientHeight - spaces
          } else {
            this.edgetype = 'right'
            inputs.offsetX = -inputs.de.clientWidth
            inputs.offsetY = -inputs.de.clientHeight - 25
            // aaand
            view.msgbox.setTopMargin( - inputs.offsetY + 20)
          }
        } else {
          console.error(`this floater grouptype not written or recognized: '${this.grouptype}'`)
        }
    
        // now we can cover buttons, using core offsets
        // cover buttons, if(buttons.length){
        if (buttons.length > 0) {
          if (!core) console.error('watch buttons not-on core?')
          let inc = 0
          for (let btn of buttons) {
            btn.offsetX = core.offsetX + inc
            btn.offsetY = -20 - core.offsetY
            inc += 20
          }
        }
        // ok,
    
        // now we can execute this,
        this.moveTo()
        // oy oy oy
      } // end onChange
    
      let writePosition = (element, x, y) => {
        $(element).css('left', `${x}px`).css('top', `${y}px`)
      }
    
      let readPosition = (element) => {
        let it = $(element)
        return {
          x: parseInt(it.css('left')),
          y: parseInt(it.css('top'))
        }
      }
    
      let readSize = (element) => {
        return {
          w: element.clientWidth,
          h: element.clientHeight
        }
      }
    
      this.moveTo = (x, y, backdoor) => {
        // but still probably want to check our offsets,
        if (this.isFixed && !backdoor) {
          this.x = this.fx
          this.y = this.fy
        }
        // to just draw new position, run with no args
        if (x === undefined) {
          x = this.x
          y = this.y
        } else {
          this.x = x
          this.y = y
        }
        // will return this ...
        let desire = {
          x: 0,
          y: 0
        }
        // check bounds,
        if(!view.isTopLevel){
          // bummer to do so many times !
          let bounds = view.getCurrentBounds()
          this.calculateSizes()
          //
          if(this.grouptype === 'edges'){
            //console.log('bounds', bounds)
            // x1, y1, x2, y2, w, h
            if(this.edgetype === 'left'){
              this.x = - this.bb.x1
              this.y = - this.bb.y1 + 17 + 5
            } else {
              this.x = bounds.x2 - this.bb.x2 - 2
              this.y = bounds.y1 - this.bb.y1 + 17 + 5
            }
          } else {
            //console.log('place bounds', this.bb)
            //console.log('bounds', bounds)
            // x needs to contend with offset,
            // this.bb.x1 probably -ve,
            // this.bb.y1 probably -ve,
            // want to track, but won't push things left or up,
            this.x = Math.max(bounds.x1 - this.bb.x1, this.x) // lower bounds
            this.y = Math.max(bounds.y1 - this.bb.y1, this.y)
    
            // want-right term,
            let rbnd = bounds.x2 - this.bb.x2
            if(this.x > rbnd){
              desire.x = this.x - rbnd
              this.x = rbnd
              // wants to go right
            }
            // want-down term,
            let lbnd = bounds.y2 - this.bb.y2
            if(this.y > lbnd){
              desire.y = this.y - lbnd
              this.y = lbnd
              // wants to go down
            }
          }
        }
        // do work
        writePosition(this.ownElement, x - 16.5, y - 20)
        for (let item of this.bag) {
          writePosition(item.de, this.x + item.offsetX, this.y + item.offsetY)
        }
        // aaaand tell em what we waaaaant
        return desire
      }
    
      this.newFix = () => {
        this.moveTo(this.fx, this.fy, true)
      }
    
      // as is tradition, we'll keep things local,
      // and just make sure we re-calculate during relevant events... ?
      this.calculateSizes = (nonNative) => {
        // from core of xy, values like
        //
        //   * -------------- (y1) -------- *
        //   |                 |            |
        //   |(x1) --------- (x,y) ---- (x2)|
        //   |                |             |
        //   |                |             |
        //   * ------------ (y2) ---------- *
    
        let x1 = 0
        let y1 = 0
        let x2 = 0
        let y2 = 0
        for (let item of this.bag) {
          // items are str8 cut dom elements,
          if (nonNative && item.type === 'native') continue
          let wt = readSize(item.de)
          // we know item offsets, so
          if (item.offsetX < x1) x1 = item.offsetX
          if (item.offsetY < y1) y1 = item.offsetY
          if (item.offsetX + wt.w > x2) x2 = item.offsetX + wt.w
          if (item.offsetY + wt.h > y2) y2 = item.offsetY + wt.h
          // corners
        }
        this.bb.x1 = x1
        this.bb.y1 = y1
        this.bb.x2 = x2
        this.bb.y2 = y2
        // ok,
        this.w = x2 - x1
        this.h = y2 - y1
        // also,
        this.r = Math.max(this.w, this.h) / 2
        // check,
        let aspect = this.w / this.h
        if (aspect > 8 || aspect < 0.125) {
          //console.error(`warning: deviously large aspect for 'circular' collisions: ${aspect}, logging floater`, this)
        }
      }
    
      this.tick = () => {
        // will use to bother the simulation
        view.tick()
      }
    
      this.free = () => {
        this.x = this.fx
        this.y = this.fy
        delete this.fx
        delete this.fy
        this.isFixed = false
        $(this.ownElement).html(`<i class="em em-pushpin"></i>`)
        this.tick()
      }
    
      this.fix = () => {
        $(this.ownElement).html(`<i class="em em-round_pushpin"></i>`)
        this.isFixed = true
        this.fx = this.x
        this.fy = this.y
        this.moveTo(this.fx, this.fy, true)
      }
    
      this.fixTo = (x, y) => {
        this.x = x
        this.y = y
        this.fix()
      }
    
      // RIP
      this.cleanup = () => {
        // js deletes things that are unreachable, so
        // there are a few objects that are on the canvas we want to get rid of
        $(this.ownElement).remove()
        // also, any handle added to a native element
        // not being able to search by js subset is kind of a bummer here, but
        let native = this.bag.find((cnd) => {
          return cnd.type === 'native'
        })
        if (native) {
          $(native.de).children('.resizehandle').remove()
        }
      }
      //
    }
    
    export default Floater