Added MySQL to exported moduless

Added MySQL to exported moduless

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/renanhangai/servitude.git/'


Check `@getRawField 'PRODID'` for return value

Check `@getRawField 'PRODID'` for return value before getting `value` field. Was getting a breaking error on an ical feed without a PRODID field (created in Calendar in OS X)

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/clochix/cozy-ical.git/'


Add a todo

Add a todo

--- content_scripts/vimium_frontend.coffee
+++ content_scripts/vimium_frontend.coffee
@@ -360,6 +360,7 @@
   # handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to
   # avoid / being interpreted as ?
   if (((event.metaKey || event.ctrlKey || event.altKey) && event.keyCode > 31) || (
+      # TODO(philc): some events don't have a keyidentifier. How is that possible?
       event.keyIdentifier && event.keyIdentifier.slice(0, 2) != "U+"))
     keyChar = KeyboardUtils.getKeyChar(event)
     # Again, ignore just modifiers. Maybe this should replace the keyCode>31 condition.


adds trailing slashes

adds trailing slashes

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/AppGyver/supersonic.git/'


show first assignments

show first assignments

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/showell/CoffeeCoffee.git/'


added test for color scales

added test for color scales

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/asbien/chroma.js.git/'


gzip size updated

gzip size updated

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/patleb/admineer.git/'


fix MarkDownView observer

fix MarkDownView observer

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/hi104/ember-note.git/'


added benchmark file

added benchmark file

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/genericdave/speedr.js.git/'


Add new control signals

Add new control signals

error: pathspec 'coffee/model/control-signals3.coffee' did not match any file(s) known to git

--- coffee/model/control-signals3.coffee
+++ coffee/model/control-signals3.coffee
@@ -0,0 +1,24 @@
+'use strict'
+
+{random} = Math
+require '../helpers'
+settings = require '../settings'
+ControlSignals = require './control-signals'
+
+class ControlSignals3 extends ControlSignals
+
+  states: [
+        ['', '', '', 'LFR'],
+        ['', 'LFR', '', ''],
+        ['', '', 'LFR', ''],
+        ['LFR', '', '', '']
+  ]
+
+  @property 'state',
+    get: ->
+      stringState = @states[@stateNum % @states.length]
+      if @intersection.roads.length <= 2
+        stringState = ['LFR', 'LFR', 'LFR', 'LFR']
+      (@_decode x for x in stringState)
+
+module.exports = ControlSignals3


Remove unwanted variable

Remove unwanted variable

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -809,9 +809,7 @@
   #
   # returns noop
   onDragStart: (e) ->
-    $target = $(e.target)
-
-    initialPath = $target.data("path")
+    initialPath = $(e.target).data("path")

     e.originalEvent.dataTransfer.effectAllowed = "move"
     e.originalEvent.dataTransfer.setData("initialPath", initialPath)


Interpolate pathToCreate instead of path

This regressed as part of the migration to using helpers from
path instead of fs-utils.

Close #608

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -278,7 +278,7 @@
         try
           if fsUtils.exists(pathToCreate)
             pathType = if fsUtils.isFileSync(pathToCreate) then "file" else "directory"
-            dialog.showError("Error: A #{pathType} already exists at path '#{path}'. Try a different path.")
+            dialog.showError("Error: A #{pathType} already exists at path '#{pathToCreate}'. Try a different path.")
           else if endsWithDirectorySeparator
             fsUtils.makeTree(pathToCreate)
             dialog.cancel()


Prevented error being thrown from timeEnd().

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/urbanetic/utility.git/'


very first version

very first version

--- tank.coffee
+++ tank.coffee
@@ -0,0 +1,146 @@
+class Map
+  constructor: (@name) ->
+  draw: ->
+    grid: ->
+
+class Grid
+  cells: [][]
+  xcell_size: 13
+  ycell_size: 13
+
+class Cell
+  xpixers: 16 * 5
+  ypixers: 9 * 5
+
+class Pixer
+  terrain: ->
+  x_offset:
+
+class Terrain
+  constructor: (@type) ->
+  type: -> @type
+
+class Layer
+  objects: []
+  priority: 0
+  add_object: (o) -> @objects.push(o)
+  each_object: (fun) ->
+    fun.call(this, o) for o in @objects
+
+class FrontendLayer
+  constructor: ->
+    this.priority = 3
+
+class MainLayer
+  constructor: ->
+    this.priority = 2
+
+class BackgroundLayer
+  constructor: ->
+    this.priority = 1
+
+
+class Game
+  start: ->
+  over: ->
+  pause: ->
+
+class Observable
+  observers: []
+  add_observer: (o) -> @observers.push(o)
+  notify_changed: (from, args) ->
+    o.on_changed(from, args) for o in @observers
+
+class Observer
+  on_changed: (from, args) -> alert args
+
+class Tank extends Observable
+  constructor: (@env) ->
+  life: 0
+  decrease_life: (x) ->
+    @life -= x
+    this.notify_changed(this, 'life')
+  increase_life: (x) ->
+    @life += x
+    this.notify_changed(this, 'life')
+  die: ->
+    @life = 0
+    this.notify_changed(this, 'life')
+  is_dead: ->
+    @life <= 0
+  power: 0
+  decrease_power: (x) ->
+    @power -= x
+    notify_changed(this, 'power')
+  increase_power: (x) ->
+    @power += x
+    notify_changed(this, 'power')
+  speed: 0
+  ship: false
+  guard: false
+  gift: 0
+  increase_gift: (x) ->
+  with_ship: ->
+    @ship == true
+  with_guard: ->
+    @guard == true
+  set_ship: (@ship) -> notify_changed(this, 'ship')
+  set_guard: (@guard) -> notify_changed(this, 'guard')
+  direction: 'N'
+  turn: (@direction) -> notify_changed(this, 'direction')
+  pos_x: 0
+  pos_y: 0
+  move: (offset_x, offset_y) ->
+    # consider @env.map
+    @pos_x += offset_x
+    @pos_x = 0 if @pos_x < 0
+    @pos_x = @env.max_x if @pos_x > @env.max_x
+    @pos_y += offset_y
+    @pos_y = 0 if @pos_y < 0
+    @pos_y = @env.max_y if @pos_y > @env.max_y
+    notify_changed(this, 'position')
+  x_target: (origin, offset) ->
+    tx = @pos_x + offset
+    tx = 0 if tx < 0
+    tx = @env.max_x if tx > @env.max_x
+  y_target: (origin, offset) ->
+    ty = @pos_y + offset
+    ty = 0 if ty < 0
+    ty = @env.max_y if ty > @envmax_y.
+  fire: ->
+    @env.fire
+
+class UserTank extends Tank
+  constructor: (@game) ->
+    @life = 1
+    @power = 1
+    @speed = 2
+    super @game
+
+class EnemyTank extends Tank
+  cruise: ->
+    move_next
+    fire
+    # auto move
+
+
+class EnemySlowTank extends Tank
+  constructor: (@game) ->
+    @life = 1
+    @power = 1
+    @speed = 1
+
+
+
+
+
+
+class Missile
+
+class Gift
+
+
+paper = Raphael("holder", 1024, 520)
+
+circle = paper.circle(50, 40, 10)
+circle.attr("fill", "#f00")


Return ERROR if no callback is provided.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/fulcrumapp/fulcrum-expressions.git/'


teams: removed group dashboard link from account popup

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/andrewjcasal/koding.git/'


Load collaboration once joined to channel


biscotto is in build folder not tasks folder

--- build/tasks/docs-task.coffee
+++ build/tasks/docs-task.coffee
@@ -3,7 +3,7 @@

 module.exports = (grunt) ->
   cmd = path.join('node_modules', '.bin', 'coffee')
-  commonArgs = [path.join('tasks', 'node_modules', '.bin', 'biscotto'), '--']
+  commonArgs = [path.join('build', 'node_modules', '.bin', 'biscotto'), '--']
   opts =
     stdio: 'inherit'


Added API endpoint for getting latest bug change date

Fetch the start time for fetching bugs from QA Dashboard. QA Reports
exporter uses the same thing.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/leonidas/qa-dashboard.git/'


update

update

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/blivesta/dotfiles.git/'


reset atom

reset atom

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/kennethaa/dotfiles.git/'


test custom icon feature

test custom icon feature

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/madl3ss/madApp.git/'


Scrollspy component

Scrollspy component

A component that accepts a dom selector that matches portions of the document

It listens for scroll events and notifies it's children
when elements that match the selector are visible in the current viewport

The elements are sorted by what percentage of the screen they occupy
If they are all equivilent, they're returned in document order

error: pathspec 'src/components/scroll-spy.cjsx' did not match any file(s) known to git

--- src/components/scroll-spy.cjsx
+++ src/components/scroll-spy.cjsx
@@ -0,0 +1,62 @@
+React = require 'react/addons'
+_ = require 'underscore'
+
+{GetPositionMixin} = require 'openstax-react-components'
+
+# A component that accepts a dom selector that matches portions of the document
+#
+# It listens for scroll events and notifies it's children
+# when elements that match the selector are visible in the current viewport
+#
+# The elements are sorted by what percentage of the screen they occupy
+# If they are all equivilent, they're returned in document order
+#
+# For instance if the current page is made up of vertically stacked '.panels'
+# ScrollSpy will calculate which .panels are currently visible and notify it's component
+# when the selection changes
+ScrollSpy = React.createClass
+
+  getInitialState: -> {}
+
+  getDefaultProps: ->
+    windowImpl: window
+
+  propTypes:
+    dataSelector: React.PropTypes.string.isRequired
+
+  componentWillMount: ->
+    @props.windowImpl.addEventListener('scroll', @onScroll)
+    @calculateScroll()
+
+  componentWillUnmount: ->
+    @props.windowImpl.removeEventListener('scroll', @onScroll)
+
+  onScroll: _.debounce( ->
+    @calculateScroll()
+  , 100)
+
+  calculateScroll: ->
+    onScreen = []
+    height = @props.windowImpl.innerHeight
+    position = {top: @props.windowImpl.innerHeight}
+    elements = @props.windowImpl.document.querySelectorAll("[#{@props.dataSelector}]")
+
+    for el, index in elements
+      bounds = el.getBoundingClientRect()
+      visibleHeight =
+        Math.min(window.innerHeight, bounds.bottom) - Math.max(0, bounds.top)
+      if visibleHeight > 0
+        key = el.getAttribute(@props.dataSelector)
+        # Calculate the percentage of the screen the element occupies
+        # 0.01 * index is added to it so that equivalent sized elements will be sorted by
+        # the position they occur in
+        onScreen.push([key, (visibleHeight / window.innerHeight) - (0.01 * onScreen.length)])
+
+    @setState(
+      onScreen: _.pluck( _.sortBy(onScreen, '1').reverse(), '0' )
+    )
+
+  render: ->
+    React.cloneElement @props.children, onScreenElements: @state.onScreen
+
+module.exports = ScrollSpy


More keymap refinements.

More keymap refinements.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/demhydraz/til.git/'


add li test

add li test

error: pathspec 'test/li.coffee' did not match any file(s) known to git

--- test/li.coffee
+++ test/li.coffee
@@ -0,0 +1,9 @@
+li = require 'li'
+
+describe 'li', ->
+  it 'can call main with an empty list', ->
+    li.main([])
+
+  describe 'help', ->
+    it 'offers help', ->
+      li.main(['li', 'help'])


Removes console.logs

Removes console.logs

--- components/article/view.coffee
+++ components/article/view.coffee
@@ -302,10 +302,7 @@
         $('.article-edit-container a').attr 'href', editUrl
         @trackPageview()
     $(".article-container[data-id=#{@article.get('id')}]").waypoint (direction) =>
-      console.log 'bottom is in view'
       if direction is 'up'
-        console.log 'bottom is in view and the direction is up'
-        console.log @article.href()
         window.history.replaceState({}, @article.get('id'), @article.href())
         $('.article-edit-container a').attr 'href', editUrl
     , { offset: 'bottom-in-view' }


small added to sticky header so the widths are correct


Rails:simple

Rails:simple

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/sharkspeed/dororis.git/'


refactoring

refactoring

--- client/landing/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
+++ client/landing/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
@@ -1,22 +1,20 @@
 class DomainCreationForm extends KDTabViewWithForms
-
-  domainNameValidation =
-    rules      :
-      required : yes
-      regExp   : /^([\da-z\.-]+)\.([a-z\.]{2,6})$/i
-    messages   :
-      required : "Enter your domain name"
-      regExp   : "This doesn't look like a valid domain name."
-
-  constructor:->
-
-    {nickname, firstName, lastName} = KD.whoami().profile
-
-    domainOptions     = [
+#  domainNameValidation =
+#    rules      :
+#      required : yes
+#      regExp   : /^([\da-z\.-]+)\.([a-z\.]{2,6})$/i
+#    messages   :
+#      required : "Enter your domain name"
+#      regExp   : "This doesn't look like a valid domain name."
+  domainOptions = [
       { title : "Create a subdomain", value : "subdomain" }
       { title : "I want to register a domain", value : "new" }
       { title : "I already have a domain", value : "existing" }
     ]
+
+  constructor:->
+
+    {nickname, firstName, lastName} = KD.whoami().profile

     super
       navigable                       : no
@@ -26,7 +24,7 @@
         "Domain Address"              :
           callback                    : @bound "registerDomain"
           buttons                     :
-            registerButton             :
+            registerButton            :
               title                   : "Register Domain"
               style                   : "cupid-green hidden"
               type                    : "submit"
@@ -111,7 +109,6 @@
                       required        : "Please select a parent domain."
             suggestionBox             :
               type                    : "hidden"
-
     form = @forms["Domain Address"]
     {createButton} = form.buttons

@@ -144,11 +141,11 @@
     @clearSuggestions()

     {DomainOption, domainName, regYears, domains} = form.inputs
-    splittedDomain    = domainName.getValue().split "."
-    domain            = splittedDomain.first
-    tld               = splittedDomain.slice(1).join('')
     domainInput       = domainName
-    domainName        = domainInput.getValue()
+    domainName        = form.inputs.domainName.getValue()
+    splittedDomain    = domainName.match(/([\w\-]+)\.(.*)/)
+    domain            = splittedDomain[1]
+    tld               = splittedDomain[2]

     domainOptionValue = DomainOption.getValue()

@@ -191,7 +188,7 @@
               registerButton.hideLoader()

         showPriceModal = (successCallback) ->
-          KD.remote.api.JDomain.getTldPrice domainName.match(/[\w\-]+\.(.*)/)[1], (tldPrice)->
+          KD.remote.api.JDomain.getTldPrice tld, (tldPrice)->
             registerButton.hideLoader()
             modal           = new KDModalView
               title         : "Confirm Domain Price for #{domainName}"
@@ -224,8 +221,6 @@
           else
             showPriceModal registerTheDomain

-
-
     else if domainOptionValue is 'existing'
       @createDomain {domainName, regYears:0, domainType:'existing'}, (err, domain)=>
         createButton.hideLoader()


bump version to 1.0.11

bump version to 1.0.11

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/kidaa/soundcloud-javascript.git/'


repeat updateTimestamps with setInterval

fatal: remote error: upload-pack: not our ref 3beb193de29293d5ac8a2966a50a62592beaff56
fatal: the remote end hung up unexpectedly


not allow to see the content if user is not admin

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/microweb10/forsale.git/'


Fixes failing spec

Fixes failing spec

--- app/assets/javascripts/gl_dropdown.js.coffee
+++ app/assets/javascripts/gl_dropdown.js.coffee
@@ -237,13 +237,12 @@
       html = @options.renderRow(data)
     else
       selected = if @options.isSelected then @options.isSelected(data) else false
-      url = if @options.url then @options.url(data) else "#"

       # Set URL
       if @options.url?
         url = @options.url(data)
       else
-        url = if data.url? then data.url else ''
+        url = if data.url? then data.url else '#'

       # Set Text
       if @options.text?


commits routes update

commits routes update

[#97707582]

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/netguru/review.git/'


auto load discussion threads

auto load discussion threads

error: pathspec 'common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee' did not match any file(s) known to git

--- common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee
+++ common/static/coffee/src/discussion/views/discussion_thread_list_view.coffee
@@ -0,0 +1,531 @@
+if Backbone?
+  class @DiscussionThreadListView extends Backbone.View
+    events:
+      "click .forum-nav-browse": "toggleBrowseMenu"
+      "keypress .forum-nav-browse-filter-input": (event) => DiscussionUtil.ignoreEnterKey(event)
+      "keyup .forum-nav-browse-filter-input": "filterTopics"
+      "click .forum-nav-browse-menu-wrapper": "ignoreClick"
+      "click .forum-nav-browse-title": "selectTopicHandler"
+      "keydown .forum-nav-search-input": "performSearch"
+      "click .fa-search": "performSearch"
+      "change .forum-nav-sort-control": "sortThreads"
+      "click .forum-nav-thread-link": "threadSelected"
+      "click .forum-nav-load-more-link": "loadMorePages"
+      "change .forum-nav-filter-main-control": "chooseFilter"
+      "change .forum-nav-filter-cohort-control": "chooseCohort"
+      "load body": "retrieveAllThreads"
+
+    initialize: (options) ->
+      @courseSettings = options.courseSettings
+      @displayedCollection = new Discussion(@collection.models, pages: @collection.pages)
+      @collection.on "change", @reloadDisplayedCollection
+      @discussionIds=""
+      @collection.on "reset", (discussion) =>
+        board = $(".current-board").html()
+        @displayedCollection.current_page = discussion.current_page
+        @displayedCollection.pages = discussion.pages
+        @displayedCollection.reset discussion.models
+        # TODO: filter correctly
+        # target = _.filter($("a.topic:contains('#{board}')"), (el) -> el.innerText == "General" || el.innerHTML == "General")
+        # if target.length > 0
+        #   @filterTopic($.Event("filter", {'target': target[0]}))
+      @collection.on "add", @addAndSelectThread
+      @sidebar_padding = 10
+      @boardName
+      @template = _.template($("#thread-list-template").html())
+      @current_search = ""
+      @mode = 'all'
+
+      @searchAlertCollection = new Backbone.Collection([], {model: Backbone.Model})
+
+      @searchAlertCollection.on "add", (searchAlert) =>
+        content = _.template(
+          $("#search-alert-template").html(),
+          {'message': searchAlert.attributes.message, 'cid': searchAlert.cid}
+          )
+        @$(".search-alerts").append(content)
+        @$("#search-alert-" + searchAlert.cid + " a.dismiss").bind "click", searchAlert, (event) =>
+          @removeSearchAlert(event.data.cid)
+
+      @searchAlertCollection.on "remove", (searchAlert) =>
+        @$("#search-alert-" + searchAlert.cid).remove()
+
+      @searchAlertCollection.on "reset", =>
+        @$(".search-alerts").empty()
+
+    addSearchAlert: (message) =>
+      m = new Backbone.Model({"message": message})
+      @searchAlertCollection.add(m)
+      m
+
+    removeSearchAlert: (searchAlert) =>
+      @searchAlertCollection.remove(searchAlert)
+
+    clearSearchAlerts: =>
+      @searchAlertCollection.reset()
+
+    reloadDisplayedCollection: (thread) =>
+      @clearSearchAlerts()
+      thread_id = thread.get('id')
+      content = @renderThread(thread)
+      current_el = @$(".forum-nav-thread[data-id=#{thread_id}]")
+      active = current_el.has(".forum-nav-thread-link.is-active").length != 0
+      current_el.replaceWith(content)
+      @showMetadataAccordingToSort()
+      if active
+        @setActiveThread(thread_id)
+
+
+    #TODO fix this entire chain of events
+    addAndSelectThread: (thread) =>
+      commentable_id = thread.get("commentable_id")
+      menuItem = @$(".forum-nav-browse-menu-item[data-discussion-id]").filter(-> $(this).data("discussion-id") == commentable_id)
+      @setCurrentTopicDisplay(@getPathText(menuItem))
+      @retrieveDiscussion commentable_id, =>
+        @trigger "thread:created", thread.get('id')
+
+    updateSidebar: =>
+
+      scrollTop = $(window).scrollTop();
+      windowHeight = $(window).height();
+
+      discussionBody = $(".discussion-column")
+      discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top
+      discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight()
+
+      sidebar = $(".forum-nav")
+      if scrollTop > discussionsBodyTop - @sidebar_padding
+        sidebar.css('top', scrollTop - discussionsBodyTop + @sidebar_padding);
+      else
+        sidebar.css('top', '0');
+
+      sidebarHeight = windowHeight - Math.max(discussionsBodyTop - scrollTop, @sidebar_padding)
+
+      topOffset = scrollTop + windowHeight
+      discussionBottomOffset = discussionsBodyBottom + @sidebar_padding
+      amount = Math.max(topOffset - discussionBottomOffset, 0)
+
+      sidebarHeight = sidebarHeight - @sidebar_padding - amount
+      sidebarHeight = Math.min(sidebarHeight + 1, discussionBody.outerHeight())
+      sidebar.css 'height', sidebarHeight
+
+      headerHeight = @$(".forum-nav-header").outerHeight()
+      refineBarHeight = @$(".forum-nav-refine-bar").outerHeight()
+      browseFilterHeight = @$(".forum-nav-browse-filter").outerHeight()
+      @$('.forum-nav-thread-list').css('height', (sidebarHeight - headerHeight - refineBarHeight - 2) + 'px')
+      @$('.forum-nav-browse-menu').css('height', (sidebarHeight - headerHeight - browseFilterHeight - 2) + 'px')
+
+
+    # Because we want the behavior that when the body is clicked the menu is
+    # closed, we need to stop propagation of a click in any part of the menu
+    # that is not a link.
+    ignoreClick: (event) ->
+        event.stopPropagation()
+
+    render: ->
+      @timer = 0
+      @$el.html(
+        @template({
+          isCohorted: @courseSettings.get("is_cohorted"),
+          isPrivilegedUser: DiscussionUtil.isPrivilegedUser()
+        })
+      )
+      @$(".forum-nav-sort-control").val(@collection.sort_preference)
+
+      $(window).bind "load scroll resize", @updateSidebar
+
+      @displayedCollection.on "reset", @renderThreads
+      @displayedCollection.on "thread:remove", @renderThreads
+      @displayedCollection.on "change:commentable_id", (model, commentable_id) =>
+        @retrieveDiscussions @discussionIds.split(",") if @mode is "commentables"
+      @renderThreads()
+      @
+
+    renderThreads: =>
+      @$(".forum-nav-thread-list").html("")
+      rendered = $("

")
+      for thread in @displayedCollection.models
+        content = @renderThread(thread)
+        rendered.append content
+
+      @$(".forum-nav-thread-list").html(rendered.html())
+      @showMetadataAccordingToSort()
+
+      @renderMorePages()
+      @updateSidebar()
+      @trigger "threads:rendered"
+
+    showMetadataAccordingToSort: () =>
+      # Ensure that threads display metadata appropriate for the current sort
+      voteCounts = @$(".forum-nav-thread-votes-count")
+      commentCounts = @$(".forum-nav-thread-comments-count")
+      voteCounts.hide()
+      commentCounts.hide()
+      switch @$(".forum-nav-sort-control").val()
+        when "date", "comments"
+          commentCounts.show()
+        when "votes"
+          voteCounts.show()
+
+    renderMorePages: ->
+      if @displayedCollection.hasMorePages()
+        @$(".forum-nav-thread-list").append("
" + gettext("Load more") + "
")
+
+    getLoadingContent: (srText) ->
+      return '
' + srText + '
'
+
+    loadMorePages: (event) =>
+      if event
+        event.preventDefault()
+      loadMoreElem = @$(".forum-nav-load-more")
+      loadMoreElem.html(@getLoadingContent(gettext("Loading more threads")))
+      loadingElem = loadMoreElem.find(".forum-nav-loading")
+      DiscussionUtil.makeFocusTrap(loadingElem)
+      loadingElem.focus()
+      options = {filter: @filter}
+      switch @mode
+        when 'search'
+          options.search_text = @current_search
+          if @group_id
+            options.group_id = @group_id
+        when 'followed'
+          options.user_id = window.user.id
+        when 'commentables'
+          options.commentable_ids = @discussionIds
+          if @group_id
+            options.group_id = @group_id
+        when 'all'
+          if @group_id
+            options.group_id = @group_id
+
+      lastThread = @collection.last()?.get('id')
+      if lastThread
+        # Pagination; focus the first thread after what was previously the last thread
+        @once("threads:rendered", ->
+          $(".forum-nav-thread[data-id='#{lastThread}'] + .forum-nav-thread .forum-nav-thread-link").focus()
+        )
+      else
+        # Totally refreshing the list (e.g. from clicking a sort button); focus the first thread
+        @once("threads:rendered", ->
+          $(".forum-nav-thread-link").first()?.focus()
+        )
+
+      error = =>
+        @renderThreads()
+        DiscussionUtil.discussionAlert(gettext("Sorry"), gettext("We had some trouble loading more threads. Please try again."))
+
+      @collection.retrieveAnotherPage(@mode, options, {sort_key: @$(".forum-nav-sort-control").val()}, error)
+
+    renderThread: (thread) =>
+      content = $(_.template($("#thread-list-item-template").html())(thread.toJSON()))
+      unreadCount = thread.get('unread_comments_count') + (if thread.get("read") then 0 else 1)
+      if unreadCount > 0
+        content.find('.forum-nav-thread-comments-count').attr(
+          "data-tooltip",
+          interpolate(
+            ngettext('%(unread_count)s new comment', '%(unread_count)s new comments', unreadCount),
+            {unread_count: unreadCount},
+            true
+          )
+        )
+      content
+
+    threadSelected: (e) =>
+      # Use .attr('data-id') rather than .data('id') because .data does type
+      # coercion. Usually, this is fine, but when Mongo gives an object id with
+      # no letters, it casts it to a Number.
+
+      thread_id = $(e.target).closest(".forum-nav-thread").attr("data-id")
+      @setActiveThread(thread_id)
+      @trigger("thread:selected", thread_id)  # This triggers a callback in the DiscussionRouter which calls the line above...
+      false
+
+    threadRemoved: (thread_id) =>
+      @trigger("thread:removed", thread_id)
+
+    setActiveThread: (thread_id) ->
+      @$(".forum-nav-thread-link").find(".sr").remove()
+      @$(".forum-nav-thread[data-id!='#{thread_id}'] .forum-nav-thread-link").removeClass("is-active")
+      @$(".forum-nav-thread[data-id='#{thread_id}'] .forum-nav-thread-link").addClass("is-active").find(".forum-nav-thread-wrapper-1").prepend('' + gettext("Current conversation") + '')
+
+    goHome: ->
+      @template = _.template($("#discussion-home-template").html())
+      $(".forum-content").html(@template)
+      $(".forum-nav-thread-list a").removeClass("is-active").find(".sr").remove()
+      $("input.email-setting").bind "click", @updateEmailNotifications
+      url = DiscussionUtil.urlFor("notifications_status",window.user.get("id"))
+      DiscussionUtil.safeAjax
+          url: url
+          type: "GET"
+          success: (response, textStatus) =>
+            if response.status
+              $('input.email-setting').attr('checked','checked')
+            else
+              $('input.email-setting').removeAttr('checked')
+      thread_id = null
+      @trigger("thread:removed")
+      #select all threads
+
+    isBrowseMenuVisible: =>
+      @$(".forum-nav-browse-menu-wrapper").is(":visible")
+
+    showBrowseMenu: =>
+      if not @isBrowseMenuVisible()
+        @$(".forum-nav-browse").addClass("is-active")
+        @$(".forum-nav-browse-menu-wrapper").show()
+        @$(".forum-nav-thread-list-wrapper").hide()
+        $(".forum-nav-browse-filter-input").focus()
+        $("body").bind "click", @hideBrowseMenu
+        @updateSidebar()
+
+    hideBrowseMenu: =>
+      if @isBrowseMenuVisible()
+        @$(".forum-nav-browse").removeClass("is-active")
+        @$(".forum-nav-browse-menu-wrapper").hide()
+        @$(".forum-nav-thread-list-wrapper").show()
+        $("body").unbind "click", @hideBrowseMenu
+        @updateSidebar()
+
+    toggleBrowseMenu: (event) =>
+      event.preventDefault()
+      event.stopPropagation()
+
+      if @isBrowseMenuVisible()
+        @hideBrowseMenu()
+      else
+        @showBrowseMenu()
+
+    # Given a menu item, get the text for it and its ancestors
+    # (starting from the root, separated by " / ")
+    getPathText: (item) ->
+      path = item.parents(".forum-nav-browse-menu-item").andSelf()
+      pathTitles = path.children(".forum-nav-browse-title").map((i, elem) -> $(elem).text()).get()
+      pathText = pathTitles.join(" / ")
+
+    filterTopics: (event) =>
+      query = $(event.target).val()
+      items = @$(".forum-nav-browse-menu-item")
+      if query.length == 0
+        items.show()
+      else
+        # If all filter terms occur in the path to an item then that item and
+        # all its descendants are displayed
+        items.hide()
+        items.each (i, item) =>
+          item = $(item)
+          if not item.is(":visible")
+            pathText = @getPathText(item).toLowerCase()
+            if query.split(" ").every((term) -> pathText.search(term.toLowerCase()) != -1)
+              path = item.parents(".forum-nav-browse-menu-item").andSelf()
+              path.add(item.find(".forum-nav-browse-menu-item")).show()
+
+    setCurrentTopicDisplay: (text) ->
+      @$(".forum-nav-browse-current").text(@fitName(text))
+
+    getNameWidth: (name) ->
+      test = $("
")
+      test.css
+        "font-size": @$(".forum-nav-browse-current").css('font-size')
+        opacity: 0
+        position: 'absolute'
+        left: -1000
+        top: -1000
+      $("body").append(test)
+      test.html(name)
+      width = test.width()
+      test.remove()
+      return width
+
+    fitName: (name) ->
+      @maxNameWidth = @$(".forum-nav-browse").width() -
+        parseInt(@$(".forum-nav-browse").css("padding-left")) -
+        parseInt(@$(".forum-nav-browse").css("padding-right")) -
+        @$(".forum-nav-browse .icon").outerWidth(true) -
+        @$(".forum-nav-browse-drop-arrow").outerWidth(true)
+      width = @getNameWidth(name)
+      if width < @maxNameWidth
+        return name
+      path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/"))
+      prefix = ""
+      while path.length > 1
+        prefix = gettext("…") + "/"
+        path.shift()
+        partialName = prefix + path.join("/")
+        if  @getNameWidth(partialName) < @maxNameWidth
+          return partialName
+      rawName = path[0]
+      name = prefix + rawName
+      while @getNameWidth(name) > @maxNameWidth
+        rawName = rawName[0...rawName.length-1]
+        name =  prefix + rawName + gettext("…")
+      return name
+
+    selectTopicHandler: (event) ->
+      event.preventDefault()
+      @selectTopic $(event.target)
+
+    selectTopic: ($target) ->
+      @hideBrowseMenu()
+      @clearSearch()
+
+      item = $target.closest('.forum-nav-browse-menu-item')
+      @setCurrentTopicDisplay(@getPathText(item))
+      if item.hasClass("forum-nav-browse-menu-all")
+        @discussionIds = ""
+        @$('.forum-nav-filter-cohort').show()
+        @retrieveAllThreads()
+      else if item.hasClass("forum-nav-browse-menu-following")
+        @retrieveFollowed()
+        @$('.forum-nav-filter-cohort').hide()
+      else
+        allItems = item.find(".forum-nav-browse-menu-item").andSelf()
+        discussionIds = allItems.filter("[data-discussion-id]").map(
+          (i, elem) -> $(elem).data("discussion-id")
+        ).get()
+        @retrieveDiscussions(discussionIds)
+        @$(".forum-nav-filter-cohort").toggle(item.data('cohorted') == true)
+
+    chooseFilter: (event) =>
+      @filter = $(".forum-nav-filter-main-control :selected").val()
+      @retrieveFirstPage()
+
+    chooseCohort: (event) =>
+      @group_id = @$('.forum-nav-filter-cohort-control :selected').val()
+      @retrieveFirstPage()
+
+    retrieveDiscussion: (discussion_id, callback=null) ->
+      url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
+      DiscussionUtil.safeAjax
+        url: url
+        type: "GET"
+        success: (response, textStatus) =>
+          @collection.current_page = response.page
+          @collection.pages = response.num_pages
+          @collection.reset(response.discussion_data)
+          Content.loadContentInfos(response.annotated_content_info)
+          @displayedCollection.reset(@collection.models)# Don't think this is necessary because it's called on collection.reset
+          if callback?
+            callback()
+
+
+    retrieveDiscussions: (discussion_ids) ->
+      @discussionIds = discussion_ids.join(',')
+      @mode = 'commentables'
+      @retrieveFirstPage()
+
+    retrieveAllThreads: () ->
+      @mode = 'all'
+      @retrieveFirstPage()
+
+    retrieveFirstPage: (event)->
+      @collection.current_page = 0
+      @collection.reset()
+      @loadMorePages(event)
+
+    sortThreads: (event) ->
+      @displayedCollection.setSortComparator(@$(".forum-nav-sort-control").val())
+
+      @retrieveFirstPage(event)
+
+    performSearch: (event) ->
+      #event.which 13 represent the Enter button
+      if event.which == 13 or event.type == 'click'
+        event.preventDefault()
+        @hideBrowseMenu()
+        @setCurrentTopicDisplay(gettext("Search Results"))
+        text = @$(".forum-nav-search-input").val()
+        @searchFor(text)
+
+    searchFor: (text) ->
+      @clearSearchAlerts()
+      @clearFilters()
+      @mode = 'search'
+      @current_search = text
+      url = DiscussionUtil.urlFor("search")
+      #TODO: This might be better done by setting discussion.current_page=0 and calling discussion.loadMorePages
+      # Mainly because this currently does not reset any pagination variables which could cause problems.
+      # This doesn't use pagination either.
+      DiscussionUtil.safeAjax
+        $elem: @$(".forum-nav-search-input")
+        data: { text: text }
+        url: url
+        type: "GET"
+        dataType: 'json'
+        $loading: $
+        loadingCallback: =>
+          @$(".forum-nav-thread-list").html("
" + @getLoadingContent(gettext("Loading thread list")) + "
")
+        loadedCallback: =>
+          @$(".forum-nav-thread-list .forum-nav-load-more").remove()
+        success: (response, textStatus) =>
+          if textStatus == 'success'
+            # TODO: Augment existing collection?
+            @collection.reset(response.discussion_data)
+            Content.loadContentInfos(response.annotated_content_info)
+            @collection.current_page = response.page
+            @collection.pages = response.num_pages
+            if !_.isNull response.corrected_text
+              message = interpolate(
+                _.escape(gettext('No results found for %(original_query)s. Showing results for %(suggested_query)s.')),
+                {"original_query": "" + _.escape(text) + "", "suggested_query": "" + response.corrected_text + ""},
+                true
+              )
+              @addSearchAlert(message)
+            else if response.discussion_data.length == 0
+              @addSearchAlert(gettext('No threads matched your query.'))
+            # TODO: Perhaps reload user info so that votes can be updated.
+            # In the future we might not load all of a user's votes at once
+            # so this would probably be necessary anyway
+            @displayedCollection.reset(@collection.models) # Don't think this is necessary
+            @searchForUser(text) if text
+
+
+    searchForUser: (text) ->
+      DiscussionUtil.safeAjax
+        data: { username: text }
+        url: DiscussionUtil.urlFor("users")
+        type: "GET"
+        dataType: 'json'
+        error: =>
+          return
+        success: (response) =>
+          if response.users.length > 0
+            message = interpolate(
+              _.escape(gettext('Show posts by %(username)s.')),
+              {"username":
+                _.template('<a class="link-jump" href="<%= url %>"><%- username %>', {
+                  url: DiscussionUtil.urlFor("user_profile", response.users[0].id),
+                  username: response.users[0].username
+                })
+              },
+              true
+            )
+            @addSearchAlert(message)
+
+    clearSearch: ->
+      @$(".forum-nav-search-input").val("")
+      @current_search = ""
+      @clearSearchAlerts()
+
+    clearFilters: ->
+     @$(".forum-nav-filter-main-control").val("all")
+     @$(".forum-nav-filter-cohort-control").val("all")
+
+    retrieveFollowed: () =>
+      @mode = 'followed'
+      @retrieveFirstPage()
+
+    updateEmailNotifications: () =>
+      if $('input.email-setting').attr('checked')
+        DiscussionUtil.safeAjax
+          url: DiscussionUtil.urlFor("enable_notifications")
+          type: "POST"
+          error: () =>
+            $('input.email-setting').removeAttr('checked')
+      else
+        DiscussionUtil.safeAjax
+          url: DiscussionUtil.urlFor("disable_notifications")
+          type: "POST"
+          error: () =>
+            $('input.email-setting').attr('checked','checked')
+
+


Add new keycodes, and enable shortcuts for magic and cropping.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/lmjohns3/thorax-media.git/'


parsing multiple variables: name[]

parsing multiple variables: name[]

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/stevepeak/handyjs.git/'


new (Fs) index.coffee

new (Fs) index.coffee

error: pathspec 'src/addons/Fs/index.coffee' did not match any file(s) known to git

--- src/addons/Fs/index.coffee
+++ src/addons/Fs/index.coffee
@@ -0,0 +1,28 @@
+fs   = require 'fs'
+path = require 'path'
+
+module.exports = {
+    fs:
+        path: path
+
+        readFile: (path, cb) ->
+            fs.readFile(path, 'utf8', (err, data)  ->
+                cb(err, data)
+            )
+
+         readDir: (base, dirsonly, cb) ->
+            ret = []
+            fs.readdir(base, (err, files) ->
+                if err then return cb(err, null)
+                for file, index in files
+                    cur_path = path.join(base, file);
+                    stat = fs.statSync(cur_path)
+                    if(dirsonly && stat.isDirectory())
+                        ret.push(cur_path)
+
+                cb(null, ret)
+            )
+
+}
+
+


Update static/vendors/morrisjs/lib/morris.area.coffee

Signed-off-by: Bernard Ojengwa <fccb4cc00c1ba7e51bf5c2b5f77130b9fe8d917e@gmail.com>

error: pathspec 'static/vendors/morrisjs/lib/morris.area.coffee' did not match any file(s) known to git

--- static/vendors/morrisjs/lib/morris.area.coffee
+++ static/vendors/morrisjs/lib/morris.area.coffee
@@ -0,0 +1,66 @@
+class Morris.Area extends Morris.Line
+  # Initialise
+  #
+  areaDefaults =
+    fillOpacity: 'auto'
+    behaveLikeLine: false
+
+  constructor: (options) ->
+    return new Morris.Area(options) unless (@ instanceof Morris.Area)
+    areaOptions = $.extend {}, areaDefaults, options
+
+    @cumulative = not areaOptions.behaveLikeLine
+
+    if areaOptions.fillOpacity is 'auto'
+      areaOptions.fillOpacity = if areaOptions.behaveLikeLine then .8 else 1
+
+    super(areaOptions)
+
+  # calculate series data point coordinates
+  #
+  # @private
+  calcPoints: ->
+    for row in @data
+      row._x = @transX(row.x)
+      total = 0
+      row._y = for y in row.y
+        if @options.behaveLikeLine
+          @transY(y)
+        else
+          total += (y || 0)
+          @transY(total)
+      row._ymax = Math.max row._y...
+
+  # draw the data series
+  #
+  # @private
+  drawSeries: ->
+    @seriesPoints = []
+    if @options.behaveLikeLine
+      range = [0..@options.ykeys.length-1]
+    else
+      range = [@options.ykeys.length-1..0]
+
+    for i in range
+      @_drawFillFor i
+      @_drawLineFor i
+      @_drawPointFor i
+
+  _drawFillFor: (index) ->
+    path = @paths[index]
+    if path isnt null
+      path = path + "L#{@transX(@xmax)},#{@bottom}L#{@transX(@xmin)},#{@bottom}Z"
+      @drawFilledPath path, @fillForSeries(index)
+
+  fillForSeries: (i) ->
+    color = Raphael.rgb2hsl @colorFor(@data[i], i, 'line')
+    Raphael.hsl(
+      color.h,
+      if @options.behaveLikeLine then color.s * 0.9 else color.s * 0.75,
+      Math.min(0.98, if @options.behaveLikeLine then color.l * 1.2 else color.l * 1.25))
+
+  drawFilledPath: (path, fill) ->
+    @raphael.path(path)
+      .attr('fill', fill)
+      .attr('fill-opacity', @options.fillOpacity)
+      .attr('stroke', 'none')


Remove debug console.log statement

Remove debug console.log statement

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/tombell/hubot-github-identity.git/'


make sure value is calculated at all times

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/artsyca/emberui.git/'


Separate some logic

Separate some logic

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/Morgy93/Rocket.Chat.Cordova.git/'


Disable cache, also return better errors

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/andrewjcasal/koding.git/'


initial version of math3d.js

initial version of math3d.js

--- src/math3d.coffee
+++ src/math3d.coffee
@@ -0,0 +1,955 @@
+###############################################################################
+# Copyright (c) 2013, 2014, William Stein
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+###############################################################################
+
+if not document?
+    document = window.document
+
+math3d = (window.math3d ?= {})
+
+trunc = (s, max_length) ->
+    if not s?
+        return s
+    if not max_length?
+        max_length = 1024
+    if s.length > max_length
+        s.slice(0, max_length-3) + "..."
+    else
+        s
+
+# Returns a new object with properties determined by those of obj1 and
+# obj2.  The properties in obj1 *must* all also appear in obj2.  If an
+# obj2 property has value "defaults.required", then it must appear in
+# obj1.  For each property P of obj2 not specified in obj1, the
+# corresponding value obj1[P] is set (all in a new copy of obj1) to
+# be obj2[P].
+defaults = (obj1, obj2, allow_extra) ->
+    if not obj1?
+        obj1 = {}
+    error = ->
+        try
+            s = "(obj1=#{trunc(JSON.stringify(obj1),1024)}, obj2=#{trunc(JSON.stringify(obj2),1024)})"
+            #console.log s
+            return s
+        catch error
+            return ""
+    if typeof(obj1) isnt 'object'
+        # We put explicit traces before the errors in this function,
+        # since otherwise they can be very hard to debug.
+        console.trace()
+        throw "defaults -- TypeError: function takes inputs as an object #{error()}"
+    r = {}
+    for prop, val of obj2
+        if obj1.hasOwnProperty(prop) and obj1[prop]?
+            if obj2[prop] is defaults.required and not obj1[prop]?
+                console.trace()
+                throw "defaults -- TypeError: property '#{prop}' must be specified: #{error()}"
+            r[prop] = obj1[prop]
+        else if obj2[prop]?  # only record not undefined properties
+            if obj2[prop] is defaults.required
+                console.trace()
+                throw "defaults -- TypeError: property '#{prop}' must be specified: #{error()}"
+            else
+                r[prop] = obj2[prop]
+    if not allow_extra
+        for prop, val of obj1
+            if not obj2.hasOwnProperty prop
+                console.trace()
+                throw "defaults -- TypeError: got an unexpected argument '#{prop}' #{error()}"
+    return r
+
+# WARNING -- don't accidentally use this as a default:
+required = defaults.required = "__!!!!!!this is a required property!!!!!!__"
+
+component_to_hex = (c) ->
+    hex = c.toString 16
+    if hex.length is 1
+        "0" + hex
+    else
+        hex
+
+rgb_to_hex = (r, g, b) -> "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b)
+
+_loading_threejs_callbacks = []
+
+math3d.threejs_src = "http://cdnjs.cloudflare.com/ajax/libs/three.js/r68/three.min.js"
+math3d.orbitcontrols_src = "OrbitControls.js"
+
+VERSION = '68'
+#$.ajaxSetup cache: true # when using getScript, cache result.
+
+loadScript = (script_src, cb) ->
+    script = document.createElement 'script'
+    script.type = 'text/javascript'
+    script.async = true
+    script.src = script_src
+
+    heads = document.getElementsByTagName 'head'
+    if heads.length > 0
+        heads[0].appendChild script
+    else
+        document.documentElement.appendChild script
+
+    cb()
+
+load_threejs = (cb) ->
+    if THREE?
+        return cb()
+
+    _loading_threejs_callbacks.push cb
+    #console.log("load_threejs")
+    if _loading_threejs_callbacks.length > 1
+        #console.log("load_threejs: already loading...")
+        return
+
+    orbitcontrols_checker = ->
+        if THREE.OrbitControls?
+            for cb in _loading_threejs_callbacks
+                cb()
+        else
+            #console.log("load_threejs: waiting for OrbitControls...")
+            setTimeout orbitcontrols_checker, 100
+
+    threejs_checker = ->
+        if THREE?
+            loadScript math3d.orbitcontrols_src, orbitcontrols_checker
+        else
+            #console.log("load_threejs: waiting for threejs...")
+            setTimeout threejs_checker, 100
+
+    loadScript math3d.threejs_src, threejs_checker
+
+math3d.load_threejs = load_threejs
+
+_scene_using_renderer  = undefined
+_renderer = undefined
+dynamic_renderer_type = undefined
+
+get_renderer = (scene) ->
+    # if there is a scene currently using this renderer, tell it to switch to
+    # the static renderer.
+    if _scene_using_renderer? and _scene_using_renderer isnt scene
+        _scene_using_renderer.set_static_renderer()
+
+    # now scene takes over using this renderer
+    _scene_using_renderer = scene
+    if not _renderer?
+        # get the best-possible THREE.js renderer (once and for all)
+        # based on Detector.js's webgl detection
+        try
+            if window.WebGLRenderingContext
+                canvas = document.createElement 'canvas'
+                if canvas.getContext('webgl') or canvas.getContext('experimental-webgl')
+                    dynamic_renderer_type = 'webgl'
+        if dynamic_renderer_type is 'webgl'
+            _renderer = new THREE.WebGLRenderer
+                antialias             : true
+                alpha                 : true
+                preserveDrawingBuffer : true
+        else
+            dynamic_renderer_type = 'canvas'
+            _renderer = new THREE.CanvasRenderer
+                antialias : true
+                alpha     : true
+        _renderer.domElement.className = 'math-3d-dynamic-renderer'
+
+    _renderer
+
+remove_element = (element) ->
+    if element is null
+        return
+    if (parent = element.parentElement) is null
+        return
+    parent.removeChild element
+
+class Math3dThreeJS
+    constructor: (parent_element, opts) ->
+        @opts = defaults opts,
+            width           : undefined
+            height          : undefined
+            renderer        : undefined  # ignored now
+            background      : "#fafafa"
+            foreground      : undefined
+            spin            : false      # if true, image spins by itself when mouse is over it.
+            camera_distance : 10
+            aspect_ratio    : undefined  # undefined does nothing or a triple [x,y,z] of length three, which scales the x,y,z coordinates of everything by the given values.
+            stop_when_gone  : undefined  # if given, animation, etc., stops when this html element (not jquery!) is no longer in the DOM
+            frame           : undefined  # if given call set_frame with opts.frame as input when init_done called
+            cb              : undefined  # opts.cb(undefined, this object)
+
+        load_threejs (err) =>
+            if err
+                msg = "Error loading THREE.js -- #{err}"
+                if not opts.cb?
+                    console.log msg
+                else
+                    opts.cb? msg
+            else
+                @attach_to_dom parent_element
+                @opts.cb? undefined, @
+
+    attach_to_dom: (parent_element) ->
+        if @opts.element?
+            remove_element @opts.element
+        else
+            @opts.element = document.createElement 'span'
+            @opts.element.className = 'math-3d-viewer'
+
+        parent_element.appendChild @opts.element
+
+    # client code should call this when start adding objects to the scene
+    init: ->
+        if @_init
+            return
+        @_init = true
+
+        @init_aspect_ratio_functions()
+
+        @scene = new THREE.Scene()
+
+        # IMPORTANT: There is a major bug in three.js -- if you make the width below more than .5 of the window
+        # width, then after 8 3d renders, things get foobared in WebGL mode.  This happens even with the simplest
+        # demo using the basic cube example from their site with R68.  It even sometimes happens with this workaround, but
+        # at least retrying a few times can fix it.
+        if not @opts.width?
+            @opts.width  = document.documentElement["clientWidth"]*.5
+
+        @opts.height = if @opts.height? then @opts.height else @opts.width*2/3
+
+        @set_dynamic_renderer()
+        @init_orbit_controls()
+        @init_on_mouseover()
+
+        # add a bunch of lights
+        @init_light()
+
+        # set background color
+        @opts.element.style.background = @opts.background
+
+        if not @opts.foreground?
+            c = @opts.element.style.background
+            if not c? or -1 is c.indexOf ')'
+                @opts.foreground = "#000"  # e.g., on firefox - this is best we can do for now
+            else
+                i = c.indexOf ')'
+                z = []
+                for a in c.slice(4, i).split ','
+                    b = parseInt a
+                    if b < 128
+                        z.push 255
+                    else
+                        z.push 0
+                @opts.foreground = rgb_to_hex z[0], z[1], z[2]
+
+    # client code should call this when done adding objects to the scene
+    init_done: ->
+        if @opts.frame?
+            @set_frame @opts.frame
+
+        if @renderer_type isnt 'dynamic'
+            # if we don't have the renderer, swap it in, make a static image, then give it back to whoever had it.
+            owner = _scene_using_renderer
+            @set_dynamic_renderer()
+            @set_static_renderer()
+            owner?.set_dynamic_renderer()
+
+        # possibly show the canvas warning.
+        if dynamic_renderer_type is 'canvas'
+            @opts.element.title = 'WARNING: using slow non-WebGL canvas renderer'
+
+    set_dynamic_renderer: ->
+        # console.log "dynamic renderer"
+        if @renderer_type is 'dynamic'
+            # already have it
+            return
+
+        @renderer = get_renderer @
+        @renderer_type = 'dynamic'
+
+        # remove the current renderer (if it exists)
+        remove_element @opts.element.lastChild
+
+        # place renderer in correct place in the DOM
+        @opts.element.appendChild @renderer.domElement
+
+        @renderer.setClearColor @opts.background, 1
+        @renderer.setSize @opts.width, @opts.height
+        if @controls?
+            @controls.enabled = true
+            if @last_canvas_pos?
+                @controls.object.position.copy @last_canvas_pos
+            if @last_canvas_target?
+                @controls.target.copy @last_canvas_target
+        if @opts.spin
+            @animate render: false
+        @render_scene true
+
+    set_static_renderer: ->
+        # console.log "static renderer"
+        if @renderer_type is 'static'
+            # already have it
+            return
+
+        if not @static_image?
+            @static_image = document.createElement 'img'
+            @static_image.className = 'math-3d-static-renderer'
+            @static_image.style.width = @opts.width
+            @static_image.style.height = @opts.height
+        @static_image.src = @data_url()
+
+        @renderer_type = 'static'
+        if @controls?
+            @controls.enabled = false
+            @last_canvas_pos = @controls.object.position
+            @last_canvas_target = @controls.target
+
+        # remove the current renderer (if it exists)
+        remove_element @opts.element.lastChild
+
+        # place renderer in correct place in the DOM
+        @opts.element.appendChild @static_image
+
+    # On mouseover, we switch the renderer out to use webgl, if available, and also enable spin animation.
+    init_on_mouseover: ->
+
+        @opts.element.onmouseenter = =>
+            @set_dynamic_renderer()
+
+        @opts.element.onmouseleave = =>
+            @set_static_renderer()
+
+        @opts.element.onclick = =>
+            @set_dynamic_renderer()
+
+    # initialize functions to create new vectors, which take into account the scene's 3d frame aspect ratio.
+    init_aspect_ratio_functions: ->
+        if @opts.aspect_ratio?
+            x = @opts.aspect_ratio[0]
+            y = @opts.aspect_ratio[1]
+            z = @opts.aspect_ratio[2]
+            @vector3 = (a, b, c) -> new THREE.Vector3 x*a, y*b, z*c
+            @vector  = (v) -> new THREE.Vector3 x*v[0], y*v[1], z*v[2]
+            @aspect_ratio_scale = (v) -> [x*v[0], y*v[1], z*v[2]]
+        else
+            @vector3 = (a, b, c) -> new THREE.Vector3 a, b, c
+            @vector  = (v) -> new THREE.Vector3 v[0], v[1], v[2]
+            @aspect_ratio_scale = (v) -> v
+
+    data_url: (opts) ->
+        opts = defaults opts,
+            type    : 'png'      # 'png' or 'jpeg' or 'webp' (the best)
+            quality : undefined   # 1 is best quality; 0 is worst; only applies for jpeg or webp
+        # console.log("taking #{JSON.stringify(opts)} snapshot (length=#{s.length})")
+        @renderer.domElement.toDataURL "image/#{opts.type}", opts.quality
+
+    init_orbit_controls: ->
+        if not @camera?
+            @add_camera distance: @opts.camera_distance
+
+        # console.log 'set_orbit_controls'
+        # set up camera controls
+        @controls = new THREE.OrbitControls @camera, @renderer.domElement
+        @controls.damping = 2
+        @controls.noKeys = true
+        @controls.zoomSpeed = 0.4
+        if @_center?
+            @controls.target = @_center
+        if @opts.spin
+            if typeof(@opts.spin) is "boolean"
+                @controls.autoRotateSpeed = 2.0
+            else
+                @controls.autoRotateSpeed = @opts.spin
+            @controls.autoRotate = true
+
+        @controls.addEventListener 'change', =>
+            if @renderer_type is 'dynamic'
+                @rescale_objects()
+                @renderer.render @scene, @camera
+
+    add_camera: (opts) ->
+        opts = defaults opts,
+            distance : 10
+
+        if @camera?
+            return
+
+        view_angle = 45
+        aspect     = @opts.width/@opts.height
+        near       = 0.1
+        far        = Math.max 20000, opts.distance*2
+
+        @camera    = new THREE.PerspectiveCamera view_angle, aspect, near, far
+        @scene.add @camera
+        @camera.position.set opts.distance, opts.distance, opts.distance
+        @camera.lookAt @scene.position
+        @camera.up = new THREE.Vector3 0, 0, 1
+
+    init_light: (color= 0xffffff) ->
+
+        # console.log 'init_light'
+
+        ambient = new THREE.AmbientLight(0x404040)
+        @scene.add ambient
+
+        color = 0xffffff
+        d     = 10000000
+        intensity = 0.5
+
+        for p in [[d,d,d], [d,d,-d], [d,-d,d], [d,-d,-d],[-d,d,d], [-d,d,-d], [-d,-d,d], [-d,-d,-d]]
+            directionalLight = new THREE.DirectionalLight color, intensity
+            directionalLight.position.set(p[0], p[1], p[2]).normalize()
+            @scene.add directionalLight
+
+        @light = new THREE.PointLight color
+        @light.position.set 0, d, 0
+
+    add_text: (opts) ->
+        o = defaults opts,
+            pos              : [0,0,0]
+            text             : required
+            fontsize         : 12
+            fontface         : 'Arial'
+            color            : "#000000"   # anything that is valid to canvas context, e.g., "rgba(249,95,95,0.7)" is also valid.
+            constant_size    : true  # if true, then text is automatically resized when the camera moves;
+            # WARNING: if constant_size, don't remove text from scene (or if you do, note that it is slightly inefficient still.)
+
+        #console.log("add_text: #{JSON.stringify(o)}")
+        # make an HTML5 2d canvas on which to draw text
+        width   = 300  # this determines max text width; beyond this, text is cut off.
+        height  = 150
+        canvas = document.createElement 'canvas'
+        canvas.width = width
+        canvas.height = height
+        context = canvas.getContext "2d"  # get the drawing context
+
+        # set the fontsize and fix for our text.
+        context.font = "Normal " + o.fontsize + "px " + o.fontface
+        context.textAlign = 'center'
+
+        # set the color of our text
+        context.fillStyle = o.color
+
+        # actually draw the text -- right in the middle of the canvas.
+        context.fillText o.text, width/2, height/2
+
+        # Make THREE.js texture from our canvas.
+        texture = new THREE.Texture canvas
+        texture.needsUpdate = true
+
+        # Make a material out of our texture.
+        spriteMaterial = new THREE.SpriteMaterial map: texture
+
+        # Make the sprite itself.  (A sprite is a 3d plane that always faces the camera.)
+        sprite = new THREE.Sprite spriteMaterial
+
+        # Move the sprite to its position
+        p = @aspect_ratio_scale o.pos
+        sprite.position.set p[0], p[1], p[2]
+
+        # If the text is supposed to stay constant size, add it to the list of constant size text,
+        # which gets resized on scene update.
+        if o.constant_size
+            if not @_text?
+                @_text = [sprite]
+            else
+                @_text.push sprite
+
+        # Finally add the sprite to our scene
+        @scene.add sprite
+
+        return sprite
+
+    add_line : (opts) ->
+        o = defaults opts,
+            points     : required
+            thickness  : 1
+            color      : "#000000"
+            arrow_head : false  # TODO
+
+        geometry = new THREE.Geometry()
+        for a in o.points
+            geometry.vertices.push @vector a
+        line = new THREE.Line geometry, new THREE.LineBasicMaterial(color:o.color, linewidth:o.thickness)
+        @scene.add line
+
+    add_point: (opts) ->
+        o = defaults opts,
+            loc  : [0,0,0]
+            size : 5
+            color: "#000000"
+
+        if not @_points?
+            @_points = []
+
+        # IMPORTANT: Below we use sprites instead of the more natural/faster PointCloudMaterial.
+        # Why?  Because usually people don't plot a huge number of points, and PointCloudMaterial is SQUARE.
+        # By using sprites, our points are round, which is something people really care about.
+
+        switch dynamic_renderer_type
+
+            when 'webgl'
+                width         = 50
+                height        = 50
+                canvas        = document.createElement 'canvas'
+                canvas.width  = width
+                canvas.height = height
+
+                context       = canvas.getContext '2d'  # get the drawing context
+                centerX       = width/2
+                centerY       = height/2
+                radius        = 25
+
+                context.beginPath()
+                context.arc centerX, centerY, radius, 0, 2*Math.PI, false
+                context.fillStyle = o.color
+                context.fill()
+
+                texture = new THREE.Texture canvas
+                texture.needsUpdate = true
+                spriteMaterial = new THREE.SpriteMaterial map: texture
+                particle = new THREE.Sprite spriteMaterial
+
+                p = @aspect_ratio_scale o.loc
+                particle.position.set p[0], p[1], p[2]
+                @_points.push [particle, o.size/200]
+
+            when 'canvas'
+                # inspired by http://mrdoob.github.io/three.js/examples/canvas_particles_random.html
+                PI2 = Math.PI * 2
+                program = (context) ->
+                    context.beginPath()
+                    context.arc 0, 0, 0.5, 0, PI2, true
+                    context.fill()
+                material = new THREE.SpriteCanvasMaterial
+                    color   : new THREE.Color o.color
+                    program : program
+                particle = new THREE.Sprite material
+                p = @aspect_ratio_scale o.loc
+                particle.position.set p[0], p[1], p[2]
+                @_points.push [particle, 4*o.size/@opts.width]
+            else
+                throw "bug -- unkown dynamic_renderer_type = #{dynamic_renderer_type}"
+
+        @scene.add particle
+
+    add_obj: (myobj) ->
+        vertices = myobj.vertex_geometry
+        for objects in [0...myobj.face_geometry.length]
+            #console.log("object=", JSON.stringify(myobj))
+            face3 = myobj.face_geometry[objects].face3
+            face4 = myobj.face_geometry[objects].face4
+            face5 = myobj.face_geometry[objects].face5
+
+            geometry = new THREE.Geometry()
+
+
+            for k in [0...vertices.length] by 3
+                geometry.vertices.push @vector vertices.slice k, k+3
+
+            # console.log("vertices=",JSON.stringify(geometry.vertices))
+
+            push_face3 = (a, b, c) ->
+                geometry.faces.push new THREE.Face3 a-1, b-1, c-1
+
+            # include all faces defined by 3 vertices (triangles)
+            for k in [0...face3.length] by 3
+                push_face3 face3[k], face3[k+1], face3[k+2]
+
+            # include all faces defined by 4 vertices (squares), which for THREE.js we must define using two triangles
+            push_face4 = (a, b, c, d) ->
+                push_face3 a, b, c
+                push_face3 a, c, d
+
+            for k in [0...face4.length] by 4
+                push_face4 face4[k], face4[k+1], face4[k+2], face4[k+3]
+
+            # include all faces defined by 5 vertices (???), which for THREE.js we must define using ten triangles (?)
+            for k in [0...face5.length] by 5
+                push_face4 face5[k],   face5[k+1], face5[k+2], face5[k+4]
+                push_face4 face5[k],   face5[k+1], face5[k+2], face5[k+3]
+                push_face4 face5[k],   face5[k+1], face5[k+2], face5[k+4]
+                push_face4 face5[k],   face5[k+2], face5[k+3], face5[k+4]
+                push_face4 face5[k+1], face5[k+2], face5[k+3], face5[k+4]
+           # console.log("faces=",JSON.stringify(geometry.faces))
+
+            geometry.mergeVertices()
+            #geometry.computeCentroids()
+            geometry.computeFaceNormals()
+            #geometry.computeVertexNormals()
+            geometry.computeBoundingSphere()
+
+            #finding material key(mk)
+            name = myobj.face_geometry[objects].material_name
+            mk = 0
+            for item in [0..myobj.material.length-1]
+                if name is myobj.material[item].name
+                    mk = item
+                    break
+
+            if @opts.wireframe or myobj.wireframe
+                if myobj.color
+                    color = myobj.color
+                else
+                    c = myobj.material[mk].color
+                    color = "rgb(#{c[0]*255},#{c[1]*255},#{c[2]*255})"
+                if typeof myobj.wireframe is 'number'
+                    line_width = myobj.wireframe
+                else if typeof @opts.wireframe is 'number'
+                    line_width = @opts.wireframe
+                else
+                    line_width = 1
+
+                material = new THREE.MeshBasicMaterial
+                    wireframe          : true
+                    color              : color
+                    wireframeLinewidth : line_width
+                    side               : THREE.DoubleSide
+            else if not myobj.material[mk]?
+                console.log "BUG -- couldn't get material for ", myobj
+                material = new THREE.MeshBasicMaterial
+                    wireframe : false
+                    color     : "#000000"
+            else
+
+                m = myobj.material[mk]
+
+                material =  new THREE.MeshPhongMaterial
+                    shininess   : "1"
+                    ambient     : 0x0ffff
+                    wireframe   : false
+                    transparent : m.opacity < 1
+
+                material.color.setRGB    m.color[0],    m.color[1],    m.color[2]
+                material.ambient.setRGB  m.ambient[0],  m.ambient[1],  m.ambient[2]
+                material.specular.setRGB m.specular[0], m.specular[1], m.specular[2]
+                material.opacity = m.opacity
+
+            mesh = new THREE.Mesh geometry, material
+            mesh.position.set 0, 0, 0
+            @scene.add mesh
+
+    # always call this after adding things to the scene to make sure track
+    # controls are sorted out, etc.   Set draw:false, if you don't want to
+    # actually *see* a frame.
+    set_frame: (opts) ->
+        o = defaults opts,
+            xmin      : required
+            xmax      : required
+            ymin      : required
+            ymax      : required
+            zmin      : required
+            zmax      : required
+            color     : @opts.foreground
+            thickness : .4
+            labels    : true  # whether to draw three numerical labels along each of the x, y, and z axes.
+            fontsize  : 14
+            draw      : true
+
+        @_frame_params = o
+        eps = 0.1
+        x0 = o.xmin
+        x1 = o.xmax
+        y0 = o.ymin
+        y1 = o.ymax
+        z0 = o.zmin
+        z1 = o.zmax
+        # console.log("set_frame: #{JSON.stringify(o)}")
+        if Math.abs(x1 - x0) < eps
+            x1 += 1
+            x0 -= 1
+        if Math.abs(y1 - y0) < eps
+            y1 += 1
+            y0 -= 1
+        if Math.abs(z1 - z0) < eps
+            z1 += 1
+            z0 -= 1
+
+        mx = (x0+x1)/2
+        my = (y0+y1)/2
+        mz = (z0+z1)/2
+        @_center = @vector3 mx, my, mz
+
+        if @camera?
+            d = 1.5*Math.max @aspect_ratio_scale([x1-x0, y1-y0, z1-z0])...
+            @camera.position.set mx+d, my+d, mz+d/2
+            # console.log("camera at #{JSON.stringify([mx+d,my+d,mz+d])} pointing at #{JSON.stringify(@_center)}")
+
+        if o.draw
+            if @frame?
+                # remove existing frame
+                for x in @frame
+                    @scene.remove x
+                delete @frame
+            @frame = []
+            # trace the edges of a cube
+            v = [[[x0,y0,z0], [x1,y0,z0], [x1,y1,z0], [x0,y1,z0], [x0,y0,z0],
+                  [x0,y0,z1], [x1,y0,z1], [x1,y1,z1], [x0,y1,z1], [x0,y0,z1]],
+                 [[x1,y0,z0], [x1,y0,z1]],
+                 [[x0,y1,z0], [x0,y1,z1]],
+                 [[x1,y1,z0], [x1,y1,z1]]]
+            for points in v
+                line = @add_line
+                    points    : points
+                    color     : o.color
+                    thickness : o.thickness
+                @frame.push line
+
+        if o.draw and o.labels
+
+            if @_frame_labels?
+                for x in @_frame_labels
+                    @scene.remove x
+
+            @_frame_labels = []
+
+            l = (a, b) ->
+                if not b?
+                    z = a
+                else
+                    z = (a+b)/2
+                z = z.toFixed 2
+                return (z*1).toString()
+
+            txt = (x, y, z, text) =>
+                @_frame_labels.push @add_text pos:[x,y,z], text:text, fontsize:o.fontsize, color:o.color, constant_size:false
+
+            offset = 0.075
+            if o.draw
+                e = (y1 - y0)*offset
+                txt x1, y0-e, z0, l z0
+                txt x1, y0-e, mz, "z=#{l z0, z1}"
+                txt x1, y0-e, z1, l z1
+
+                e = (x1 - x0)*offset
+                txt x1+e, y0, z0, l y0
+                txt x1+e, my, z0, "y=#{l y0, y1}"
+                txt x1+e, y1, z0, l y1
+
+                e = (y1 - y0)*offset
+                txt x1, y1+e, z0, l x1
+                txt mx, y1+e, z0, "x=#{l x0, x1}"
+                txt x0, y1+e, z0, l x0
+
+        v = @vector3 mx, my, mz
+        @camera.lookAt v
+        if @controls?
+            @controls.target = @_center
+        @render_scene()
+
+    add_3dgraphics_obj: (opts) ->
+        opts = defaults opts,
+            obj       : required
+            wireframe : undefined
+            set_frame : undefined
+
+        for o in opts.obj
+            switch o.type
+                when 'text'
+                    @add_text
+                        pos           : o.pos
+                        text          : o.text
+                        color         : o.color
+                        fontsize      : o.fontsize
+                        fontface      : o.fontface
+                        constant_size : o.constant_size
+                when 'index_face_set'
+                    if opts.wireframe?
+                        o.wireframe = opts.wireframe
+                    @add_obj o
+                    if o.mesh and not o.wireframe  # draw a wireframe mesh on top of the surface we just drew.
+                        o.color='#000000'
+                        o.wireframe = o.mesh
+                        @add_obj o
+                when 'line'
+                    delete o.type
+                    @add_line o
+                when 'point'
+                    delete o.type
+                    @add_point o
+                else
+                    console.log "ERROR: no renderer for model number = #{o.id}"
+                    return
+
+        if opts.set_frame?
+            @set_frame opts.set_frame
+
+        @render_scene true
+
+
+    animate: (opts = {}) ->
+        opts = defaults opts,
+            fps       : undefined
+            stop      : false
+            mouseover : undefined  # ignored now
+            render    : true
+        #console.log("@animate #{@_animate_started}")
+        if @_animate_started and not opts.stop
+            return
+        @_animate_started = true
+        @_animate opts
+
+    _animate: (opts) ->
+        #console.log("anim?", @opts.element.length, @opts.element.is(":visible"))
+
+        if @renderer_type is 'static'
+            # will try again when we switch to dynamic renderer
+            @_animate_started = false
+            return
+
+        #if not $(@opts.element).is ":visible"
+        if @opts.element.offsetWidth <= 0 and @opts.element.offsetWidth <= 0
+            if @opts.stop_when_gone? and not contains document, @opts.stop_when_gone
+                # console.log("stop_when_gone removed from document -- quit animation completely")
+                @_animate_started = false
+            else if not contains document, @opts.element
+                # console.log("element removed from document; wait 5 seconds")
+                setTimeout (=> @_animate opts), 5000
+            else
+                # console.log("check again after a second")
+                setTimeout (=> @_animate opts), 1000
+            return
+
+        if opts.stop
+            @_stop_animating = true
+            # so next time around will start
+            return
+        if @_stop_animating
+            @_stop_animating = false
+            @_animate_started = false
+            return
+        @render_scene opts.render
+        delete opts.render
+        f = =>
+            requestAnimationFrame (=> @_animate opts)
+        if opts.fps? and opts.fps
+            setTimeout f, 1000/opts.fps
+        else
+            f()
+
+
+    render_scene: (force = false) ->
+        if @renderer_type is 'static'
+            console.log 'render static -- todo'
+            return
+
+        if not @camera?
+            return # nothing to do yet
+
+        @controls?.update()
+
+        pos = @camera.position
+        if not @_last_pos?
+            new_pos = true
+            @_last_pos = pos.clone()
+        else if @_last_pos.distanceToSquared(pos) > .05
+            new_pos = true
+            @_last_pos.copy pos
+        else
+            new_pos = false
+
+        if not new_pos and not force
+            return
+
+        # rescale all text in scene
+        @rescale_objects()
+
+        @renderer.render @scene, @camera
+
+    _rescale_factor: ->
+        if not @_center?
+            return undefined
+        else
+            return @camera.position.distanceTo(@_center) / 3
+
+    rescale_objects: (force = false) ->
+        s = @_rescale_factor()
+        if not s? or (Math.abs(@_last_scale - s) < 0.000001 and not force)
+            return
+        @_last_scale = s
+        if @_text?
+            for sprite in @_text
+                sprite.scale.set s, s, s
+        if @_frame_labels?
+            for sprite in @_frame_labels
+                sprite.scale.set s, s, s
+        if @_points?
+            for z in @_points
+                c = z[1]
+                z[0].scale.set s*c, s*c, s*c
+
+
+math3d.render_3d_scene = (opts) ->
+    opts = defaults opts,
+        scene   : required   # {opts:?, obj:?} or url from which to download (via ajax) a JSON string that parses to {opts:?,obj:?}
+        element : required    # DOM element
+        cb      : undefined   # cb(err, scene object)
+    # Render a 3-d scene
+    #console.log("render_3d_scene: url='#{opts.url}'")
+
+    scene_obj = undefined
+    async.series([
+        (cb) ->
+            switch typeof(opts.scene)
+                when 'string'
+                    $.ajax(
+                        url     : opts.scene
+                        timeout : 30000
+                        success : (data) ->
+                            try
+                                opts.scene = JSON.parse data
+                                cb()
+                            catch err
+                                cb err
+                    ).fail ->
+                        cb "error downloading #{opts.scene}"
+                when 'object'
+                    cb()
+                else
+                    cb "bad scene value: #{opts.scene}"
+        (cb) ->
+            # do this initialization *after* we create the 3d renderer
+            init = (err, scene) ->
+                if err
+                    cb err
+                else
+                    scene_obj = scene
+                    scene.init()
+                    if opts.scene.obj?
+                        scene.add_3dgraphics_obj obj : opts.scene.obj
+                    scene.init_done()
+                    cb()
+            # create the 3d renderer
+            opts.scene.opts ?= {}
+            opts.scene.opts.cb = init
+
+            obj = new Math3dThreeJS opts.element, opts.scene.opts
+    ], (err) ->
+        opts.cb? err, scene_obj
+    )
+
+if $?
+    # jQuery plugin for making a DOM object into a 3d renderer
+    $.fn.math3d = (opts = {}) ->
+        @each ->
+            new Math3dThreeJS @, opts


Initial working

Initial working

--- entec.coffee
+++ entec.coffee
@@ -0,0 +1,53 @@
+
+fs = require "fs"
+{jspack} = require "jspack"
+_  = require 'underscore'
+
+console.log new Buffer jspack.Pack "<BBBB", [0x7e, 6, 0, 2]
+
+devicePath = process.argv[2] or "/dev/serial/by-id/usb-FTDI_FT245R_USB_FIFO_ENP9D7H7-if00-port0"
+
+{SerialPort} = require "serialport",
+    baudrate: 57600
+
+device = new SerialPort devicePath
+
+fs.readFile __dirname + "/on.dat", (err, data) ->
+  throw err if err
+  console.log "example", data.length, data
+  # device.write data
+
+  e = new Entec device
+
+  # e.setLamp 2, 0, 0, 0
+  e.setLamp 2, 10, 255, 0
+  e.commit()
+  # console.log "EQ", _.isEqual e.buffer, data
+
+
+
+
+class Entec
+
+
+  constructor: (@device) ->
+
+    @buffer = new Buffer 517
+
+    # Format buffer with zeros
+    for v, k in @buffer
+      @buffer.write "\0", k, "utf8"
+
+
+  setLamp: (dmxPOS, r, g, b) ->
+    jspack.PackTo "<BBBBB", @buffer, dmxPOS+4, [0, r, g, b, 0]
+
+  commit: ->
+    jspack.PackTo "<BBBB", @buffer, 0, [0x7e, 6, 0, 2]
+    jspack.PackTo "<B", @buffer, @buffer.length-1, [0xe7]
+    console.log "mine", @buffer.length, @buffer
+    @device.write @buffer
+
+
+
+


Closes #10; Added strokes to the time.area chart.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/kidaa/epoch.git/'


spec: clipboard.readImage()

spec: clipboard.readImage()

--- spec/api-clipboard-spec.coffee
+++ spec/api-clipboard-spec.coffee
@@ -1,7 +1,18 @@
 assert = require 'assert'
 clipboard = require 'clipboard'
+nativeImage = require 'native-image'
+path = require 'path'

 describe 'clipboard module', ->
+  fixtures = path.resolve __dirname, 'fixtures'
+
+  describe 'clipboard.readImage()', ->
+    it 'returns NativeImage intance', ->
+      p = path.join fixtures, 'assets', 'logo.png'
+      i = nativeImage.createFromPath p
+      clipboard.writeImage p
+      assert.equal clipboard.readImage().toDataUrl(), i.toDataUrl()
+
   describe 'clipboard.readText()', ->
     it 'returns unicode string correctly', ->
       text = '千江有水千江月，万里无云万里天'


Adding back in

Adding back in

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/petermblair/libemail.git/'


Update git specs for latest libgit2 build

--- spec/app/git-spec.coffee
+++ spec/app/git-spec.coffee
@@ -3,15 +3,11 @@
 describe "Git", ->

   describe "getPath()", ->
-    it "returns the repository path for a working directory file path", ->
-      repo = new Git(require.resolve('fixtures/git/nohead.git/HEAD'))
-      expect(repo.getPath()).toBe require.resolve('fixtures/git/nohead.git') + '/'
+    it "returns the repository path for a .git directory path", ->
       repo = new Git(require.resolve('fixtures/git/master.git/HEAD'))
       expect(repo.getPath()).toBe require.resolve('fixtures/git/master.git') + '/'

     it "returns the repository path for a repository path", ->
-      repo = new Git(require.resolve('fixtures/git/nohead.git'))
-      expect(repo.getPath()).toBe require.resolve('fixtures/git/nohead.git') + '/'
       repo = new Git(require.resolve('fixtures/git/master.git'))
       expect(repo.getPath()).toBe require.resolve('fixtures/git/master.git') + '/'


Constants changes

Constants changes

--- app/constants.coffee
+++ app/constants.coffee
@@ -2,7 +2,6 @@

     app_name: 'singularity'
     config_server_base: '/singularity'
-    api_base: 'singularity2/v1'
-    kumonga_api_base: 'kumonga/v2'
+    api_base: 'singularity/v1'

 module.exports = constants