8bitworkshop/src/ide/waveform.ts

376 lines
11 KiB
TypeScript

import { Toolbar } from "../common/emu";
import { VirtualList } from "../common/vlist";
const BUILTIN_INPUT_PORTS = [
'clk', 'reset',
];
export interface WaveformMeta {
label : string;
len : number;
//name : string;
//ofs : number;
//wordlen : number;
input : boolean;
output : boolean;
}
export interface WaveformProvider {
getSignalMetadata() : WaveformMeta[];
getSignalData(index:number, start:number, len:number) : number[];
setSignalValue(index:number, value:number);
}
export class WaveformView {
parent : HTMLElement;
wfp : WaveformProvider;
toolbar : Toolbar;
clklabel : HTMLElement;
wavelist;
meta : WaveformMeta[];
lines : HTMLCanvasElement[] = [];
zoom : number = 8;
t0 : number = 0;
tsel : number = -1;
tnow : number = -1;
pageWidth : number;
clocksPerPage : number;
clockMax : number;
hexformat : boolean = true;
scrollbarWidth = 12;
constructor(parent:HTMLElement, wfp:WaveformProvider) {
this.parent = parent;
this.wfp = wfp;
this.recreate();
}
wtimer;
recreate() {
clearTimeout(this.wtimer);
this.wtimer = setTimeout(() => {
this.destroy();
// create new thing
this._recreate();
}, 0);
}
destroy() {
// remove old thing
if (this.wavelist) {
$(this.wavelist.container).remove();
this.wavelist = null;
}
if (this.toolbar) {
this.toolbar.destroy();
this.toolbar = null;
}
if (this.clklabel) {
this.clklabel.remove();
this.clklabel = null;
}
}
_recreate() {
this.meta = this.wfp.getSignalMetadata();
if (!this.meta) return;
var width = $(this.parent).width();
this.pageWidth = width - this.scrollbarWidth;
var rowHeight = 40; // TODO
this.setClocksPerPage();
this.clockMax = 0;
this.wavelist = new VirtualList({
w: width,
h: $(this.parent).height(),
itemHeight: rowHeight,
totalRows: this.meta.length+1,
generatorFn: (row : number) => {
var metarow = this.meta[row]; // TODO: why null?
//var s = metarow != null ? metarow.label : "";
let linediv = document.createElement("div");
let canvas = document.createElement("canvas");
canvas.width = width - 12; // room for scrollbar
canvas.height = rowHeight;
linediv.appendChild(canvas); //document.createTextNode(s));
linediv.classList.add('waverow');
this.lines[row] = canvas;
this.refreshRow(row);
// click to change input
if (metarow && metarow.input && BUILTIN_INPUT_PORTS.indexOf(metarow.label) < 0) {
linediv.onmousedown = (e) => {
var meta = this.meta[row];
if (meta && meta.input) {
this.changeInputValue(row);
}
};
linediv.classList.add('editable');
linediv.style.cursor = 'grab';
}
return linediv;
}
});
var wlc = this.wavelist.container;
wlc.tabIndex = -1; // make it focusable
//wlc.style = "overflow-x: hidden"; // TODO?
this.toolbar = new Toolbar(this.parent, this.parent);
this.toolbar.span.css('display','inline-block');
this.clklabel = document.createElement('span');
this.clklabel.innerText = "-";
$(this.parent).append(this.clklabel);
$(this.parent).append(wlc);
var down = false;
var selfn = (e) => {
this.setSelTime(e.offsetX / this.zoom + this.t0 - 0.5);
};
$(wlc).mousedown( (e) => {
down = true;
selfn(e);
//if (e['pointerId']) e.target.setPointerCapture(e['pointerId']);
});
$(wlc).mousemove( (e) => {
if (down) selfn(e);
});
$(wlc).mouseup( (e) => {
down = false;
//if (e['pointerId']) e.target.releasePointerCapture(e['pointerId']);
});
// scroll left/right
$(wlc).on('wheel', (event:any) => {
if (Math.abs(event.originalEvent.deltaX) > Math.abs(event.originalEvent.deltaY)) {
var xdelta = Math.max(-1000, Math.min(1000, event.originalEvent.deltaX));
if (xdelta) this.setOrgTime(this.t0 + xdelta);
}
});
this.toolbar.add('=', 'Zoom In', 'glyphicon-zoom-in', (e,combo) => {
this.setZoom(this.zoom * 2);
});
this.toolbar.add('+', 'Zoom In', null, (e,combo) => {
this.setZoom(this.zoom * 2);
});
this.toolbar.add('-', 'Zoom Out', 'glyphicon-zoom-out', (e,combo) => {
this.setZoom(this.zoom / 2);
});
this.toolbar.add('ctrl+shift+left', 'Move to beginning', 'glyphicon-backward', (e,combo) => {
this.setSelTime(0);
this.setOrgTime(0);
});
this.toolbar.add('shift+left', 'Move left 1/4 page', 'glyphicon-fast-backward', (e,combo) => {
this.setSelTime(this.tsel - this.clocksPerPage/4);
});
this.toolbar.add('left', 'Move left 1 clock', 'glyphicon-step-backward', (e,combo) => {
this.setSelTime(this.tsel - 1);
});
this.toolbar.add('right', 'Move right 1 clock', 'glyphicon-step-forward', (e,combo) => {
this.setSelTime(this.tsel + 1);
});
this.toolbar.add('shift+right', 'Move right 1/4 page', 'glyphicon-fast-forward', (e,combo) => {
this.setSelTime(this.tsel + this.clocksPerPage/4);
});
this.toolbar.add('space', 'Go to current time', 'glyphicon-flash', (e,combo) => {
this.setOrgTime(this.tnow);
this.setSelTime(this.tnow);
});
this.toolbar.add('h', 'Switch between hex/decimal format', 'glyphicon-barcode', (e,combo) => {
this.hexformat = !this.hexformat;
this.refresh();
});
$(window).resize(() => {
this.recreate();
}); // TODO: remove?
}
roundT(t : number) {
t = Math.round(t);
t = Math.max(0, t); // make sure >= 0
t = Math.min(this.clockMax + this.clocksPerPage/2, t); // make sure <= end
return t;
}
setOrgTime(t : number) {
this.t0 = this.roundT(t);
this.refresh();
}
setCurrentTime(t : number) {
this.tnow = this.roundT(t);
this.refresh();
}
setSelTime(t : number) {
t = this.roundT(t);
if (t >= this.t0 + this.clocksPerPage - 1)
this.t0 += this.clocksPerPage / 4;
if (t <= this.t0 + 2)
this.t0 -= this.clocksPerPage / 4;
this.tsel = t;
this.setOrgTime(this.t0);
this.clklabel.innerText = " clk " + this.tsel;
}
setClocksPerPage() {
this.clocksPerPage = Math.floor(this.pageWidth/this.zoom) - 1;
}
setZoom(zoom : number) {
this.zoom = Math.max(1/16, Math.min(64, zoom));
this.setClocksPerPage();
this.t0 = Math.max(0, Math.round(this.tsel - this.clocksPerPage / 2));
this.refresh();
}
refresh() {
if (!this.meta) this.recreate();
if (!this.meta) return;
for (var i=0; i<this.meta.length; i++) {
this.refreshRow(i);
}
}
value2str(val: number, meta: WaveformMeta) {
var radix = this.hexformat ? 16 : 10;
var txt = val.toString(radix);
if (radix == 16 && meta && meta.len > 3)
txt = `${meta.len}'h${txt}`;
//else if (radix == 10 && meta.len > 3)
//txt = `${meta.len}'d${txt}`;
return txt;
}
refreshRow(row : number) {
var canvas = this.lines[row];
var meta = this.meta[row];
if (!canvas || !meta) return;
var isclk = (meta.label == 'clk');
var w = canvas.width;
var h = canvas.height;
var ctx = canvas.getContext("2d");
var fontbig = "14px Andale Mono, Lucida Console, monospace";
var fontsml = "10px Andale Mono, Lucida Console, monospace";
ctx.font = fontbig;
// clear to black
ctx.clearRect(0, 0, canvas.width, canvas.height);
// highlighted?
var tags = [];
if (meta.input && BUILTIN_INPUT_PORTS.indexOf(meta.label) < 0) tags.push('input');
//if (meta.output) tags.push('output');
// draw waveform
var fh = 12;
var b1 = fh+4;
var b2 = 4;
var h2 = h-b1-b2;
var yrange = meta.len == 32 ? 4294967296.0 : ((1<<meta.len)-1) || 0;
var data = this.wfp.getSignalData(row, this.t0, Math.ceil(w/this.zoom));
this.clockMax = Math.max(this.clockMax, this.t0 + data.length);
var printvals = meta.len > 1 && this.zoom >= 32;
var ycen = b1+h2-4;
ctx.fillStyle = "#336633";
ctx.fillRect(0, fh/2, 3, b1+h2-fh/2); // draw left tag
const COLOR_LINE = ctx.strokeStyle = "#33dd33";
const COLOR_HILITE = ctx.fillStyle = "#66ffff";
const COLOR_NAME = "#dddddd";
// draw waveform
ctx.beginPath();
var x = 0;
var y = 0;
var lastval = -1;
for (var i=0; i<data.length; i++) {
var val = data[i];
if (printvals && val != lastval && x < w-100) { // close to right edge? omit
var ytext = ycen;
var txt = this.value2str(val, null);
if (txt.length > 4)
ctx.font = fontsml;
ctx.fillText(txt, x+this.zoom/4, ytext);
}
if (i>0)
ctx.lineTo(x,y);
y = b1 + (1.0 - val/yrange) * h2;
if (!isclk)
x += this.zoom*(1/8);
if (i==0)
ctx.moveTo(x,y);
else
ctx.lineTo(x,y);
if (this.zoom > 0.75 && lastval != val && Math.abs(lastval - val) < yrange*0.1)
ctx.fillRect(x,y,1+this.zoom/4,1);
if (isclk)
x += this.zoom;
else
x += this.zoom*(7/8);
lastval = val;
}
ctx.stroke();
// draw selection thingie
ctx.font = fontbig;
if (this.tsel >= this.t0) {
ctx.strokeStyle = ctx.fillStyle = "#ff66ff";
ctx.beginPath();
x = (this.tsel - this.t0)*this.zoom + this.zoom/2;
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
ctx.stroke();
// print value
var val = data[this.tsel - this.t0];
ctx.textAlign = 'right';
if (val !== undefined) {
var s = this.value2str(val, meta);
var x = w-fh;
var dims = ctx.measureText(s);
ctx.fillStyle = 'black';
ctx.fillRect(x-dims.width-2, ycen-13, dims.width+4, 17);
ctx.fillStyle = "#ff66ff";
ctx.fillText(s, x, ycen);
}
}
// draw current line
if (this.tnow >= this.t0) {
ctx.strokeStyle = ctx.fillStyle = "#6666cc";
ctx.beginPath();
x = (this.tnow - this.t0)*this.zoom + this.zoom/2;
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
ctx.stroke();
}
// draw labels
ctx.fillStyle = COLOR_NAME;
ctx.textAlign = "left";
var lbl = meta.label;
if (tags.length > 0) { lbl += " (" + tags.join(', ') + ")"; }
ctx.fillText(lbl, 5, fh);
}
changeInputValue(row : number) {
var meta = this.meta[row];
if (!meta) return;
var data = this.wfp.getSignalData(row, this.t0, 1);
var oldValue = data[0] || 0;
var min = 0;
var max = (1 << meta.len) - 1;
//console.log(meta, oldValue, min, max);
if (max == 1) {
this.wfp.setSignalValue(row, oldValue > 0 ? 0 : 1);
} else {
var rangestr = `${min} to ${max}`;
bootbox.prompt({
value: oldValue+"",
inputType: "number",
//min: 0,
//max: meta.len-1,
//placeholder: rangestr,
title: `Enter new value for "${meta.label}" (${rangestr}):`,
callback: (result) => {
if (result != null) {
var value = parseInt(result);
if (value >= min && value <= max) {
this.wfp.setSignalValue(row, parseInt(result));
}
}
}
});
}
}
}