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; | |