/// /// /// //HACK: allows instructions.html to access pxtblocks without requiring simulator.html to import blocks as well if (!(window).pxt) (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 * 3; 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: inline-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, crocClips?: boolean }; 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: 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 = document.createElementNS("http://www.w3.org/2000/svg", "svg"); let dims = { l: 0, t: 0, w: 0, h: 0 }; let cmpSvgEl = 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; (cmpSvgAtts).width = dims.w; (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" | PartVisualDefinition, opts: mkCmpDivOpts): HTMLElement { let state = runtime.board as pxsim.CoreBoard; let el: visuals.SVGElAndSize; if (cmp == "wire") { el = visuals.mkWirePart([0, 0], opts.wireClr || "red", opts.crocClips); } else { let partVis = cmp; if (typeof partVis.builtIn == "string") { let cnstr = state.builtinPartVisuals[partVis.builtIn]; el = cnstr([0, 0]); } else { el = visuals.mkGenericPartSVG(partVis); } } return wrapSvg(el, opts); } type BoardProps = { boardDef: BoardDefinition, cmpDefs: Map, fnArgs: any, allAlloc: AllocatorResult, stepToWires: WireInst[][], stepToCmps: PartInst[][] allWires: WireInst[], allCmps: PartInst[], lastStep: number, colorToWires: Map, allWireColors: string[], }; function mkBoardProps(allocOpts: AllocatorOpts): BoardProps { let allocRes = allocateDefinitions(allocOpts); let stepToWires: WireInst[][] = []; let stepToCmps: PartInst[][] = []; let stepOffset = 0; allocRes.partsAndWires.forEach(cAndWs => { let part = cAndWs.part; let wires = cAndWs.wires; cAndWs.assembly.forEach((step, idx) => { if (step.part && part) stepToCmps[stepOffset + idx] = [part] if (step.wireIndices && step.wireIndices.length > 0 && wires) stepToWires[stepOffset + idx] = step.wireIndices.map(i => wires[i]) }) stepOffset += cAndWs.assembly.length; }); let numSteps = stepOffset; let lastStep = numSteps - 1; let allCmps = allocRes.partsAndWires.map(r => r.part).filter(p => !!p); let allWires = allocRes.partsAndWires.map(r => r.wires || []).reduce((p, n) => p.concat(n), []); let colorToWires: Map = {} 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.partDefs, 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, fnArgs: any, width: number, buildMode: boolean = false): visuals.BoardHost { const state = runtime.board as pxsim.CoreBoard; const opts : visuals.BoardHostOpts = { state: state, boardDef: boardDef, forceBreadboard: true, partDefs: cmpDefs, maxWidth: `${width}px`, fnArgs: fnArgs, wireframe: buildMode, }; let boardHost = new visuals.BoardHost(pxsim.visuals.mkBoardView(opts), opts); 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 cmps = props.stepToCmps[i]; if (cmps) { cmps.forEach(partInst => { let cmp = board.addPart(partInst) //last step if (i === step) { //highlight locations pins partInst.breadboardConnections.forEach(bbLoc => board.highlightBreadboardPin(bbLoc)); svg.addClass(cmp.element, "notgrayed"); } }); } 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((w.start)); } else { board.highlightBoardPin((w.start).pin); } if (w.end.type == "breadboard") { let lbls = board.highlightBreadboardPin((w.end)); } else { board.highlightBoardPin((w.end).pin); } //highlight wire board.highlightWire(wire); } }); } } } 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.builtIn === "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, crocClips: props.boardDef.useCrocClips }) 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} = (loc); return `(${row},${col})` } else return (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, crocClips: props.boardDef.useCrocClips }) addClass(cmp, "cmp-div"); reqsDiv.appendChild(cmp); }); let cmps = (props.stepToCmps[step] || []); cmps.forEach(c => { let locs: BBLoc[]; if (c.visual.builtIn === "buttonpair") { //TODO: don't special case this locs = [c.breadboardConnections[0], c.breadboardConnections[2]] } else { locs = [c.breadboardConnections[0]]; } locs.forEach((l, i) => { let topLbl: string; if (l) { let {row, col} = l; topLbl = `(${row},${col})`; } else { topLbl = ""; } let cmp = mkCmpDiv(c.visual, { top: topLbl, 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 (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(); }); } // board def const boardDef = JSON.parse(getQsVal("board")) as pxsim.BoardDefinition; //parts list let parts = (getQsVal("parts") || "").split(" "); parts.sort(); // parts definitions let partDefinitions = JSON.parse(getQsVal("partdefs") || "{}") as pxsim.Map //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 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, partDefs: cmpDefs, partsList: 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); } }