pxt-calliope/sim/instructions/instructions.ts
2016-09-01 19:07:01 -07:00

679 lines
25 KiB
TypeScript

/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
/// <reference path="../../node_modules/pxt-core/built/pxtrunner.d.ts"/>
/// <reference path="../../libs/microbit/dal.d.ts"/>
/// <reference path="../visuals/genericboard.ts"/>
/// <reference path="../visuals/wiring.ts"/>
//HACK: allows instructions.html to access pxtblocks without requiring simulator.html to import blocks as well
if (!(<any>window).pxt) (<any>window).pxt = {};
import pxtrunner = pxt.runner;
import pxtdocs = pxt.docs;
namespace pxsim.instructions {
const LOC_LBL_SIZE = 10;
const QUANT_LBL_SIZE = 30;
const QUANT_LBL = (q: number) => `${q}x`;
const WIRE_QUANT_LBL_SIZE = 20;
const LBL_VERT_PAD = 3;
const LBL_RIGHT_PAD = 5;
const LBL_LEFT_PAD = 5;
const REQ_WIRE_HEIGHT = 45;
const REQ_CMP_HEIGHT = 55;
const REQ_CMP_SCALE = 0.5 * 4;
type Orientation = "landscape" | "portrait";
const ORIENTATION: Orientation = "portrait";
const PPI = 96.0;
const PAGE_SCALAR = 0.95;
const [FULL_PAGE_WIDTH, FULL_PAGE_HEIGHT]
= (ORIENTATION == "portrait" ? [PPI * 8.5 * PAGE_SCALAR, PPI * 11.0 * PAGE_SCALAR] : [PPI * 11.0 * PAGE_SCALAR, PPI * 8.5 * PAGE_SCALAR]);
const PAGE_MARGIN = PPI * 0.45;
const PAGE_WIDTH = FULL_PAGE_WIDTH - PAGE_MARGIN * 2;
const PAGE_HEIGHT = FULL_PAGE_HEIGHT - PAGE_MARGIN * 2;
const BORDER_COLOR = "gray";
const BORDER_RADIUS = 5 * 4;
const BORDER_WIDTH = 2 * 2;
const [PANEL_ROWS, PANEL_COLS] = [1, 1];
const PANEL_MARGIN = 20;
const PANEL_PADDING = 8 * 3;
const PANEL_WIDTH = PAGE_WIDTH / PANEL_COLS - (PANEL_MARGIN + PANEL_PADDING + BORDER_WIDTH) * PANEL_COLS;
const PANEL_HEIGHT = PAGE_HEIGHT / PANEL_ROWS - (PANEL_MARGIN + PANEL_PADDING + BORDER_WIDTH) * PANEL_ROWS;
const BOARD_WIDTH = 465;
const BOARD_LEFT = (PANEL_WIDTH - BOARD_WIDTH) / 2.0 + PANEL_PADDING;
const BOARD_BOT = PANEL_PADDING;
const NUM_BOX_SIZE = 120;
const NUM_FONT = 80;
const NUM_MARGIN = 10;
const FRONT_PAGE_BOARD_WIDTH = 400;
const PART_SCALAR = 2.3
const PARTS_BOARD_SCALE = 0.17;
const PARTS_BB_SCALE = 0.25;
const PARTS_CMP_SCALE = 0.3;
const PARTS_WIRE_SCALE = 0.23;
const BACK_PAGE_BOARD_WIDTH = PANEL_WIDTH - PANEL_PADDING * 1.5;
const STYLE = `
.instr-panel {
margin: ${PANEL_MARGIN}px;
padding: ${PANEL_PADDING}px;
border-width: ${BORDER_WIDTH}px;
border-color: ${BORDER_COLOR};
border-style: solid;
border-radius: ${BORDER_RADIUS}px;
display: block;
width: ${PANEL_WIDTH}px;
height: ${PANEL_HEIGHT}px;
position: relative;
overflow: hidden;
page-break-inside: avoid;
}
.board-svg {
margin: 0 auto;
display: block;
position: absolute;
bottom: ${BOARD_BOT}px;
left: ${BOARD_LEFT}px;
}
.panel-num-outer {
position: absolute;
left: ${-BORDER_WIDTH}px;
top: ${-BORDER_WIDTH}px;
width: ${NUM_BOX_SIZE}px;
height: ${NUM_BOX_SIZE}px;
border-width: ${BORDER_WIDTH}px;
border-style: solid;
border-color: ${BORDER_COLOR};
border-radius: ${BORDER_RADIUS}px 0 ${BORDER_RADIUS}px 0;
}
.panel-num {
margin: ${NUM_MARGIN}px 0;
text-align: center;
font-size: ${NUM_FONT}px;
}
.cmp-div {
display: inline-block;
}
.reqs-div {
margin-left: ${PANEL_PADDING + NUM_BOX_SIZE}px;
margin-top: 5px;
}
.partslist-wire,
.partslist-cmp {
margin: 10px;
}
.partslist-wire {
display: inline-block;
}
`;
function addClass(el: HTMLElement, cls: string) {
//TODO move to library
if (el.classList) el.classList.add(cls);
//BUG: won't work if element has class that is prefix of new class
//TODO: make github issue (same issue exists svg.addClass)
else if (!el.className.indexOf(cls)) el.className += " " + cls;
}
function mkTxt(p: [number, number], txt: string, size: number) {
let el = svg.elt("text")
let [x, y] = p;
svg.hydrate(el, { x: x, y: y, style: `font-size:${size}px;` });
el.textContent = txt;
return el;
}
type mkCmpDivOpts = {
top?: string,
topSize?: number,
right?: string,
rightSize?: number,
left?: string,
leftSize?: number,
bot?: string,
botSize?: number,
wireClr?: string,
cmpWidth?: number,
cmpHeight?: number,
cmpScale?: number
};
function mkBoardImgSvg(def: string | BoardImageDefinition): visuals.SVGElAndSize {
let boardView: visuals.BoardView;
if (def === "microbit") {
boardView = new visuals.MicrobitBoardSvg({
theme: visuals.randomTheme()
})
} else {
boardView = new visuals.GenericBoardSvg({
visualDef: <BoardImageDefinition>def
})
}
return boardView.getView();
}
function mkBBSvg(): visuals.SVGElAndSize {
let bb = new visuals.Breadboard({});
return bb.getSVGAndSize();
}
function wrapSvg(el: visuals.SVGElAndSize, opts: mkCmpDivOpts): HTMLElement {
//TODO: Refactor this function; it is too complicated. There is a lot of error-prone math being done
// to scale and place all elements which could be simplified with more forethought.
let svgEl = <SVGSVGElement>document.createElementNS("http://www.w3.org/2000/svg", "svg");
let dims = {l: 0, t: 0, w: 0, h: 0};
let cmpSvgEl = <SVGSVGElement>document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl.appendChild(cmpSvgEl);
cmpSvgEl.appendChild(el.el);
let cmpSvgAtts = {
"viewBox": `${el.x} ${el.y} ${el.w} ${el.h}`,
"preserveAspectRatio": "xMidYMid",
};
dims.w = el.w;
dims.h = el.h;
let scale = (scaler: number) => {
dims.h *= scaler;
dims.w *= scaler;
(<any>cmpSvgAtts).width = dims.w;
(<any>cmpSvgAtts).height = dims.h;
}
if (opts.cmpScale) {
scale(opts.cmpScale)
}
if (opts.cmpWidth && opts.cmpWidth < dims.w) {
scale(opts.cmpWidth / dims.w);
} else if (opts.cmpHeight && opts.cmpHeight < dims.h) {
scale(opts.cmpHeight / dims.h)
}
svg.hydrate(cmpSvgEl, cmpSvgAtts);
let elDims = {l: dims.l, t: dims.t, w: dims.w, h: dims.h};
let updateL = (newL: number) => {
if (newL < dims.l) {
let extraW = dims.l - newL;
dims.l = newL;
dims.w += extraW;
}
}
let updateR = (newR: number) => {
let oldR = dims.l + dims.w;
if (oldR < newR) {
let extraW = newR - oldR;
dims.w += extraW;
}
}
let updateT = (newT: number) => {
if (newT < dims.t) {
let extraH = dims.t - newT;
dims.t = newT;
dims.h += extraH;
}
}
let updateB = (newB: number) => {
let oldB = dims.t + dims.h;
if (oldB < newB) {
let extraH = newB - oldB;
dims.h += extraH;
}
}
//labels
let [xOff, yOff] = [-0.3, 0.3]; //HACK: these constants tweak the way "mkTxt" knows how to center the text
const txtAspectRatio = [1.4, 1.0];
if (opts && opts.top) {
let size = opts.topSize;
let txtW = size / txtAspectRatio[0];
let txtH = size / txtAspectRatio[1];
let [cx, y] = [elDims.l + elDims.w / 2, elDims.t - LBL_VERT_PAD - txtH / 2];
let lbl = visuals.mkTxt(cx, y, size, 0, opts.top, xOff, yOff);
svg.addClass(lbl, "cmp-lbl");
svgEl.appendChild(lbl);
let len = txtW * opts.top.length;
updateT(y - txtH / 2);
updateL(cx - len / 2);
updateR(cx + len / 2);
}
if (opts && opts.bot) {
let size = opts.botSize;
let txtW = size / txtAspectRatio[0];
let txtH = size / txtAspectRatio[1];
let [cx, y] = [elDims.l + elDims.w / 2, elDims.t + elDims.h + LBL_VERT_PAD + txtH / 2];
let lbl = visuals.mkTxt(cx, y, size, 0, opts.bot, xOff, yOff);
svg.addClass(lbl, "cmp-lbl");
svgEl.appendChild(lbl);
let len = txtW * opts.bot.length;
updateB(y + txtH / 2);
updateL(cx - len / 2);
updateR(cx + len / 2);
}
if (opts && opts.right) {
let size = opts.rightSize;
let txtW = size / txtAspectRatio[0];
let txtH = size / txtAspectRatio[1];
let len = txtW * opts.right.length;
let [cx, cy] = [elDims.l + elDims.w + LBL_RIGHT_PAD + len / 2, elDims.t + elDims.h / 2];
let lbl = visuals.mkTxt(cx, cy, size, 0, opts.right, xOff, yOff);
svg.addClass(lbl, "cmp-lbl");
svgEl.appendChild(lbl);
updateT(cy - txtH / 2);
updateR(cx + len / 2);
updateB(cy + txtH / 2);
}
if (opts && opts.left) {
let size = opts.leftSize;
let txtW = size / txtAspectRatio[0];
let txtH = size / txtAspectRatio[1];
let len = txtW * opts.left.length;
let [cx, cy] = [elDims.l - LBL_LEFT_PAD - len / 2, elDims.t + elDims.h / 2];
let lbl = visuals.mkTxt(cx, cy, size, 0, opts.left, xOff, yOff);
svg.addClass(lbl, "cmp-lbl");
svgEl.appendChild(lbl);
updateT(cy - txtH / 2);
updateL(cx - len / 2);
updateB(cy + txtH / 2);
}
let svgAtts = {
"viewBox": `${dims.l} ${dims.t} ${dims.w} ${dims.h}`,
"width": dims.w * PART_SCALAR,
"height": dims.h * PART_SCALAR,
"preserveAspectRatio": "xMidYMid",
};
svg.hydrate(svgEl, svgAtts);
let div = document.createElement("div");
div.appendChild(svgEl);
return div;
}
function mkCmpDiv(cmp: "wire" | string | PartVisualDefinition, opts: mkCmpDivOpts): HTMLElement {
let el: visuals.SVGElAndSize;
if (cmp == "wire") {
//TODO: support non-croc wire parts
el = visuals.mkWirePart([0, 0], opts.wireClr || "red", true);
} else if (typeof cmp == "string") {
let builtinVis = <string>cmp;
let cnstr = builtinComponentPartVisual[builtinVis];
el = cnstr([0, 0]);
} else {
let partVis = <PartVisualDefinition> cmp;
el = visuals.mkGenericPartSVG(partVis);
}
return wrapSvg(el, opts);
}
type BoardProps = {
boardDef: BoardDefinition,
cmpDefs: Map<PartDefinition>,
fnArgs: any,
allAlloc: AllocatorResult,
stepToWires: WireInst[][],
stepToCmps: CmpInst[][]
allWires: WireInst[],
allCmps: CmpInst[],
lastStep: number,
colorToWires: Map<WireInst[]>,
allWireColors: string[],
};
function mkBoardProps(allocOpts: AllocatorOpts): BoardProps {
let allocRes = allocateDefinitions(allocOpts);
let {powerWires, components} = allocRes;
let stepToWires: WireInst[][] = [];
let stepToCmps: CmpInst[][] = [];
powerWires.forEach(w => {
let step = w.assemblyStep + 1;
(stepToWires[step] || (stepToWires[step] = [])).push(w)
});
let getMaxStep = (ns: {assemblyStep: number}[]) => ns.reduce((m, n) => Math.max(m, n.assemblyStep), 0);
let stepOffset = getMaxStep(powerWires) + 2;
components.forEach(cAndWs => {
let {component, wires} = cAndWs;
let cStep = component.assemblyStep + stepOffset;
let arr = stepToCmps[cStep] || (stepToCmps[cStep] = []);
arr.push(component);
let wSteps = wires.map(w => w.assemblyStep + stepOffset);
wires.forEach((w, i) => {
let wStep = wSteps[i];
let arr = stepToWires[wStep] || (stepToWires[wStep] = []);
arr.push(w);
})
stepOffset = Math.max(cStep, wSteps.reduce((m, n) => Math.max(m, n), 0)) + 1;
});
let lastStep = stepOffset - 1;
let allCmps = components.map(p => p.component);
let allWires = powerWires.concat(components.map(p => p.wires).reduce((p, n) => p.concat(n), []));
let colorToWires: Map<WireInst[]> = {}
let allWireColors: string[] = [];
allWires.forEach(w => {
if (!colorToWires[w.color]) {
colorToWires[w.color] = [];
allWireColors.push(w.color);
}
colorToWires[w.color].push(w);
});
return {
boardDef: allocOpts.boardDef,
cmpDefs: allocOpts.cmpDefs,
fnArgs: allocOpts.fnArgs,
allAlloc: allocRes,
stepToWires: stepToWires,
stepToCmps: stepToCmps,
allWires: allWires,
allCmps: allCmps,
lastStep: lastStep,
colorToWires: colorToWires,
allWireColors: allWireColors,
};
}
function mkBlankBoardAndBreadboard(boardDef: BoardDefinition, cmpDefs: Map<PartDefinition>, fnArgs: any, width: number, buildMode: boolean = false): visuals.BoardHost {
let state = runtime.board as pxsim.DalBoard;
let boardHost = new visuals.BoardHost({
state: state,
boardDef: boardDef,
forceBreadboard: true,
cmpDefs: cmpDefs,
maxWidth: `${width}px`,
fnArgs: fnArgs,
wireframe: buildMode,
});
let view = boardHost.getView();
svg.addClass(view, "board-svg");
//set smiley
//HACK
// let img = board.board.displayCmp.image;
// img.set(1, 0, 255);
// img.set(3, 0, 255);
// img.set(0, 2, 255);
// img.set(1, 3, 255);
// img.set(2, 3, 255);
// img.set(3, 3, 255);
// img.set(4, 2, 255);
// board.updateState();
return boardHost;
}
function drawSteps(board: visuals.BoardHost, step: number, props: BoardProps) {
let view = board.getView();
if (step > 0) {
svg.addClass(view, "grayed");
}
for (let i = 0; i <= step; i++) {
let wires = props.stepToWires[i];
if (wires) {
wires.forEach(w => {
let wire = board.addWire(w)
//last step
if (i === step) {
//location highlights
if (w.start.type == "breadboard") {
let lbls = board.highlightBreadboardPin((<BBLoc>w.start).rowCol);
} else {
board.highlightBoardPin((<BoardLoc>w.start).pin);
}
if (w.end.type == "breadboard") {
let [row, col] = (<BBLoc>w.end).rowCol;
let lbls = board.highlightBreadboardPin((<BBLoc>w.end).rowCol);
} else {
board.highlightBoardPin((<BoardLoc>w.end).pin);
}
//highlight wire
board.highlightWire(wire);
}
});
}
let cmps = props.stepToCmps[i];
if (cmps) {
cmps.forEach(cmpInst => {
let cmp = board.addComponent(cmpInst)
let colOffset = (<any>cmpInst.visual).breadboardStartColIdx || 0;
let rowCol: BBRowCol = [`${cmpInst.breadboardStartRow}`, `${colOffset + cmpInst.breadboardStartColumn}`];
//last step
if (i === step) {
board.highlightBreadboardPin(rowCol);
if (cmpInst.visual === "buttonpair") {
//TODO: don't specialize this
let rowCol2: BBRowCol = [`${cmpInst.breadboardStartRow}`, `${cmpInst.breadboardStartColumn + 3}`];
board.highlightBreadboardPin(rowCol2);
}
svg.addClass(cmp.element, "notgrayed");
}
});
}
}
}
function mkPanel() {
//panel
let panel = document.createElement("div");
addClass(panel, "instr-panel");
return panel;
}
function mkPartsPanel(props: BoardProps) {
let panel = mkPanel();
// board and breadboard
let boardImg = mkBoardImgSvg(props.boardDef.visual);
let board = wrapSvg(boardImg, {left: QUANT_LBL(1), leftSize: QUANT_LBL_SIZE, cmpScale: PARTS_BOARD_SCALE});
panel.appendChild(board);
let bbRaw = mkBBSvg();
let bb = wrapSvg(bbRaw, {left: QUANT_LBL(1), leftSize: QUANT_LBL_SIZE, cmpScale: PARTS_BB_SCALE});
panel.appendChild(bb);
// components
let cmps = props.allCmps;
cmps.forEach(c => {
let quant = 1;
// TODO: don't special case this
if (c.visual === "buttonpair") {
quant = 2;
}
let cmp = mkCmpDiv(c.visual, {
left: QUANT_LBL(quant),
leftSize: QUANT_LBL_SIZE,
cmpScale: PARTS_CMP_SCALE,
});
addClass(cmp, "partslist-cmp");
panel.appendChild(cmp);
});
// wires
props.allWireColors.forEach(clr => {
let quant = props.colorToWires[clr].length;
let cmp = mkCmpDiv("wire", {
left: QUANT_LBL(quant),
leftSize: WIRE_QUANT_LBL_SIZE,
wireClr: clr,
cmpScale: PARTS_WIRE_SCALE
})
addClass(cmp, "partslist-wire");
panel.appendChild(cmp);
})
return panel;
}
function mkStepPanel(step: number, props: BoardProps) {
let panel = mkPanel();
//board
let board = mkBlankBoardAndBreadboard(props.boardDef, props.cmpDefs, props.fnArgs, BOARD_WIDTH, true)
drawSteps(board, step, props);
panel.appendChild(board.getView());
//number
let numDiv = document.createElement("div");
addClass(numDiv, "panel-num-outer");
addClass(numDiv, "noselect");
panel.appendChild(numDiv)
let num = document.createElement("div");
addClass(num, "panel-num");
num.textContent = (step + 1) + "";
numDiv.appendChild(num)
// add requirements
let reqsDiv = document.createElement("div");
addClass(reqsDiv, "reqs-div")
panel.appendChild(reqsDiv);
let wires = (props.stepToWires[step] || []);
let mkLabel = (loc: Loc) => {
if (loc.type === "breadboard") {
let [row, col] = (<BBLoc>loc).rowCol;
return `(${row},${col})`
} else
return (<BoardLoc>loc).pin;
};
wires.forEach(w => {
let cmp = mkCmpDiv("wire", {
top: mkLabel(w.end),
topSize: LOC_LBL_SIZE,
bot: mkLabel(w.start),
botSize: LOC_LBL_SIZE,
wireClr: w.color,
cmpHeight: REQ_WIRE_HEIGHT
})
addClass(cmp, "cmp-div");
reqsDiv.appendChild(cmp);
});
let cmps = (props.stepToCmps[step] || []);
cmps.forEach(c => {
let l: BBRowCol = [`${c.breadboardStartRow}`, `${c.breadboardStartColumn}`];
let locs = [l];
if (c.visual === "buttonpair") {
//TODO: don't special case this
let l2: BBRowCol = [`${c.breadboardStartRow}`, `${c.breadboardStartColumn + 3}`];
locs.push(l2);
}
locs.forEach((l, i) => {
let [row, col] = l;
let cmp = mkCmpDiv(c.visual, {
top: `(${row},${col})`,
topSize: LOC_LBL_SIZE,
cmpHeight: REQ_CMP_HEIGHT,
cmpScale: REQ_CMP_SCALE
})
addClass(cmp, "cmp-div");
reqsDiv.appendChild(cmp);
});
});
return panel;
}
function updateFrontPanel(props: BoardProps): [HTMLElement, BoardProps] {
let panel = document.getElementById("front-panel");
let board = mkBlankBoardAndBreadboard(props.boardDef, props.cmpDefs, props.fnArgs, FRONT_PAGE_BOARD_WIDTH, false);
board.addAll(props.allAlloc);
panel.appendChild(board.getView());
return [panel, props];
}
function mkFinalPanel(props: BoardProps) {
let panel = mkPanel();
addClass(panel, "back-panel");
let board = mkBlankBoardAndBreadboard(props.boardDef, props.cmpDefs, props.fnArgs, BACK_PAGE_BOARD_WIDTH, false)
board.addAll(props.allAlloc);
panel.appendChild(board.getView());
return panel;
}
export function drawInstructions() {
let getQsVal = parseQueryString();
//project name
let name = getQsVal("name") || "Untitled";
if (name) {
$("#proj-title").text(name);
}
//project code
let tsCode = getQsVal("code");
let tsPackage = getQsVal("package") || "";
let codeSpinnerDiv = document.getElementById("proj-code-spinner");
let codeContainerDiv = document.getElementById("proj-code-container");
if (tsCode) {
//we use the docs renderer to decompile the code to blocks and render it
//TODO: render the blocks code directly
let md =
`\`\`\`blocks
${tsCode}
\`\`\`
\`\`\`package
${tsPackage}
\`\`\`
`
pxtdocs.requireMarked = function() { return (<any>window).marked; }
pxtrunner.renderMarkdownAsync(codeContainerDiv, md)
.done(function() {
let codeSvg = $("#proj-code-container svg");
if (codeSvg.length > 0) {
//code rendered successfully as blocks
codeSvg.css("width", "inherit");
codeSvg.css("height", "inherit");
//takes the svg out of the wrapper markdown
codeContainerDiv.innerHTML = "";
codeContainerDiv.appendChild(codeSvg[0]);
} else {
//code failed to convert to blocks, display as typescript instead
codeContainerDiv.innerText = tsCode;
}
$(codeContainerDiv).show();
$(codeSpinnerDiv).hide();
});
}
//parts list
let parts = (getQsVal("parts") || "").split(" ");
parts.sort();
// parts definitions
let partDefinitions = JSON.parse(getQsVal("partdefs") || "{}") as pxsim.Map<PartDefinition>
//fn args
let fnArgs = JSON.parse((getQsVal("fnArgs") || "{}"));
//init runtime
const COMP_CODE = "";
if (!pxsim.initCurrentRuntime)
pxsim.initCurrentRuntime = initRuntimeWithDalBoard;
pxsim.runtime = new Runtime(COMP_CODE);
pxsim.runtime.board = null;
pxsim.initCurrentRuntime();
let style = document.createElement("style");
document.head.appendChild(style);
style.textContent += STYLE;
const boardDef = CURRENT_BOARD;
const cmpDefs = partDefinitions;
//props
let dummyBreadboard = new visuals.Breadboard({});
let onboardCmps = boardDef.onboardComponents || [];
let activeComponents = (parts || []).filter(c => onboardCmps.indexOf(c) < 0);
activeComponents.sort();
let props = mkBoardProps({
boardDef: boardDef,
cmpDefs: cmpDefs,
cmpList: activeComponents,
fnArgs: fnArgs,
getBBCoord: dummyBreadboard.getCoord.bind(dummyBreadboard)
});
//front page
let frontPanel = updateFrontPanel(props);
//all required parts
let partsPanel = mkPartsPanel(props);
document.body.appendChild(partsPanel);
//steps
for (let s = 0; s <= props.lastStep; s++) {
let p = mkStepPanel(s, props);
document.body.appendChild(p);
}
//final
let finalPanel = mkFinalPanel(props);
document.body.appendChild(finalPanel);
}
}