Spaces:
Sleeping
Sleeping
const Config = require("./config.js"); | |
const Node = require("./node.js"); | |
class Navigator { | |
constructor (criterion, sample, options = {}) { | |
this.criterion = criterion; | |
this.sample = sample; | |
this.getCursorOffset = options.getCursorOffset || (() => null); | |
this.outOfPage = options.outOfPage; | |
this.bestNode = null; | |
this.fineCursor = null; | |
this.breakingSI = sample.notes.length - 1; | |
this.zeroNode = Node.zero(); | |
this.zeroNode.offset = this.getCursorOffset() || 0; | |
this.relocationThreshold = options.relocationThreshold || Config.RelocationThreshold; | |
} | |
step (index) { | |
//console.log("step:", this.zeroNode.offset); | |
const note = this.sample.notes[index]; | |
if (note.matches.length > 0) { | |
//console.log("zeroNode.offset:", index, this.zeroNode.offset); | |
note.matches.forEach(node => { | |
node.evaluatePrev(this.zeroNode); | |
//console.log("node:", node, node.evaluatePrevCost(this.zeroNode), node.offset, this.zeroNode.offset); | |
for (let si = index - 1; si >= Math.max(this.breakingSI + 1, index - Config.SkipDeep); --si) { | |
//const skipCost = Config.SkipCost * (index - 1 - si); | |
const prevNote = this.sample.notes[si]; | |
console.assert(prevNote, "prevNote is null:", si, index, this.sample.notes); | |
prevNote.matches.forEach(prevNode => { | |
const bias = node.offset - prevNode.offset; | |
if (/*prevNode.totalCost + skipCost < node.totalCost | |
&&*/ (bias < 2 / Config.LagOffsetCost && bias > -2 / Config.LeadOffsetCost)) | |
node.evaluatePrev(prevNode); | |
}); | |
} | |
node.prior = node.totalCost > 1.99 ? -1 : node.priorByOffset(this.zeroNode.offset); | |
if (node.prior > 0 && this.outOfPage) { | |
const tick = this.criterion.notes[node.ci].startTick; | |
if (this.outOfPage(tick)) | |
node.prior -= 0.7; | |
} | |
}); | |
note.matches.sort((c1, c2) => c2.prior - c1.prior); | |
this.cursors = note.matches; | |
//console.log("navigator cursors:", this.cursors); | |
let fineCursor = null; | |
const nullLength = this.nullSteps(index); | |
const cursor = this.cursors[0]; | |
if (cursor && cursor.totalCost < 1) { | |
//console.log("nullLength:", nullLength, nullLength * Math.log(cursor.value / 4)); | |
if (cursor.prior > 0 || (cursor.totalCost < 0.4 && Math.log(Math.max(nullLength * cursor.value, 1e-3)) > this.relocationThreshold)) { | |
this.zeroNode.offset = cursor.offset; | |
fineCursor = cursor; | |
if (!this.bestNode || cursor.value > this.bestNode.value) | |
this.bestNode = cursor; | |
} | |
} | |
if (fineCursor) | |
this.fineCursor = fineCursor; | |
else { | |
if (!this.resetCursor(index, {breaking: false/*nullLength > Config.SkipDeep*/})) { | |
this.zeroNode.offset += note.deltaSi * Math.tanh(nullLength); | |
console.assert(!Number.isNaN(this.zeroNode.offset), "zeroNode.offset is NaN.", note.deltaSi, nullLength); | |
} | |
} | |
} | |
else | |
this.cursors = []; | |
} | |
path ({fromIndex = 0, toIndex = this.sample.notes.length - 1} = {}) { | |
const path = []; | |
let offset = null; | |
for (let si = toIndex; si >= fromIndex;) { | |
const note = this.sample.notes[si]; | |
if (!note.matches.length || note.matches[0].prior < -0.01 || note.matches[0].totalCost >= 1) { | |
//if (note.matches.length) | |
// console.log("path -1:", si, note.matches[0].prior, note.matches[0].totalCost); | |
path[si] = -1; | |
--si; | |
continue; | |
} | |
// sort nodes by backwards heuristic offset | |
if (offset != null) { | |
note.matches.forEach(node => node.backPrior = (node.totalCost < 1.99 ? node.priorByOffset(offset) : -1)); | |
note.matches.sort((n1, n2) => n2.backPrior - n1.backPrior); | |
} | |
const node = note.matches[0]; | |
node.path.forEach((ci, si) => path[si] = ci); | |
//console.log("node path:", si, node.path); | |
offset = node.root.offset; | |
si = node.rootSi - 1; | |
} | |
console.assert(path.length == toIndex + 1, "path length error:", path, fromIndex, toIndex + 1, | |
this.sample.notes.length, this.sample.notes.length ? this.sample.notes[this.sample.notes.length - 1].index : null); | |
return path; | |
} | |
nullSteps (index) { | |
return index - (this.fineCursor ? this.fineCursor.si : -1) - 1; | |
} | |
resetCursor (index, {breaking = true} = {}) { | |
if (breaking) | |
this.breakingSI = index; | |
const cursorOffset = this.getCursorOffset(); | |
if (cursorOffset != null) { | |
//console.log("cursorOffset:", cursorOffset); | |
this.zeroNode.offset = cursorOffset; | |
//this.breaking = this.nullSteps(index) > Config.SkipDeep; | |
//if (this.breaking) // trivial zero node si resets result in focus path interruption | |
this.zeroNode.si = index; | |
this.fineCursor = null; | |
console.assert(!Number.isNaN(this.zeroNode.offset), "zeroNode.offset is NaN.", cursorOffset); | |
//console.log("cursor offset reset:", cursorOffset); | |
return true; | |
} | |
return false; | |
} | |
get relocationTendency () { | |
const cursor = this.cursors && this.cursors[0]; | |
if (!cursor) | |
return null; | |
const nullLength = this.nullSteps(cursor.si); | |
if (nullLength <= 0) | |
return 0; | |
return Math.log(Math.max(nullLength * cursor.value, 1e-3)) / this.relocationThreshold; | |
} | |
}; | |
module.exports = Navigator; | |