From a65e71f3b1ff6c491a2b61f3c985936b161146ff Mon Sep 17 00:00:00 2001 From: darzu Date: Tue, 30 Aug 2016 11:55:00 -0700 Subject: [PATCH] moves all of pxt-arduino breadboarding here... ... see pxt-arduino history starting here: https://github.com/Microsoft/pxt-arduino/commit/acd49bb79512b95e8c65182ae94f3548494238ab --- pxtarget.json | 2 +- sim/allocator.ts | 432 ++++++++++++++ sim/dalboard.ts | 97 ++++ sim/definitions.ts | 321 +++++++++++ sim/instructions/instructions.ts | 675 ++++++++++++++++++++++ sim/microbit.ts | 590 ++++++++++++------- sim/public/instructions.html | 200 +++++++ sim/simlib.ts | 932 +++++++------------------------ sim/visuals/breadboard.ts | 640 +++++++++++++++++++++ sim/visuals/buttonpair.ts | 204 +++++++ sim/visuals/genericboard.ts | 515 +++++++++++++++++ sim/visuals/genericpart.ts | 16 + sim/visuals/ledmatrix.ts | 130 +++++ sim/visuals/neopixel.ts | 256 +++++++++ sim/visuals/wiring.ts | 420 ++++++++++++++ 15 files changed, 4467 insertions(+), 963 deletions(-) create mode 100644 sim/allocator.ts create mode 100644 sim/dalboard.ts create mode 100644 sim/definitions.ts create mode 100644 sim/instructions/instructions.ts create mode 100644 sim/public/instructions.html create mode 100644 sim/visuals/breadboard.ts create mode 100644 sim/visuals/buttonpair.ts create mode 100644 sim/visuals/genericboard.ts create mode 100644 sim/visuals/genericpart.ts create mode 100644 sim/visuals/ledmatrix.ts create mode 100644 sim/visuals/neopixel.ts create mode 100644 sim/visuals/wiring.ts diff --git a/pxtarget.json b/pxtarget.json index c14da7b5..a71fec58 100644 --- a/pxtarget.json +++ b/pxtarget.json @@ -75,7 +75,7 @@ }, "simulator": { "autoRun": true, - "aspectRatio": 1.22 + "aspectRatio": 0.69 }, "compileService": { "yottaTarget": "bbc-microbit-classic-gcc", diff --git a/sim/allocator.ts b/sim/allocator.ts new file mode 100644 index 00000000..a29fe4e6 --- /dev/null +++ b/sim/allocator.ts @@ -0,0 +1,432 @@ + +namespace pxsim { + export interface AllocatorOpts { + boardDef: BoardDefinition, + cmpDefs: Map, + fnArgs: any, + getBBCoord: (loc: BBRowCol) => visuals.Coord, + cmpList: string[] + }; + export interface AllocatorResult { + powerWires: WireInst[], + components: CmpAndWireInst[] + } + + export interface CmpAndWireInst { + component: CmpInst, + wires: WireInst[] + } + export interface CmpInst { + name: string, + breadboardStartColumn: number, + breadboardStartRow: string, + assemblyStep: number, + visual: string | ComponentVisualDefinition, + microbitPins: string[], + otherArgs?: string[], + } + export interface WireInst { + start: Loc, + end: Loc, + color: string, + assemblyStep: number + }; + interface PartialCmpAlloc { + name: string, + def: ComponentDefinition, + pinsAssigned: string[], + pinsNeeded: number | number[], + breadboardColumnsNeeded: number, + otherArgs?: string[], + } + + interface AllocLocOpts { + nearestBBPin?: BBRowCol, + startColumn?: number, + cmpGPIOPins?: string[], + }; + interface AllocWireOpts { + startColumn: number, + cmpGPIOPins: string[], + } + interface AllocBlock { + cmpIdx: number, + cmpBlkIdx: number, + gpioNeeded: number, + gpioAssigned: string[] + } + function copyDoubleArray(a: string[][]) { + return a.map(b => b.map(p => p)); + } + function readPin(arg: string): string { + U.assert(!!arg, "Invalid pin: " + arg); + let pin = arg.split("DigitalPin.")[1]; + return pin; + } + function mkReverseMap(map: {[key: string]: string}) { + let origKeys: string[] = []; + let origVals: string[] = []; + for (let key in map) { + origKeys.push(key); + origVals.push(map[key]); + } + let newMap: {[key: string]: string} = {}; + for (let i = 0; i < origKeys.length; i++) { + let newKey = origVals[i]; + let newVal = origKeys[i]; + newMap[newKey] = newVal; + } + return newMap; + } + class Allocator { + private opts: AllocatorOpts; + private availablePowerPins = { + top: { + threeVolt: mkRange(26, 51).map(n => ["+", `${n}`]), + ground: mkRange(26, 51).map(n => ["-", `${n}`]), + }, + bottom: { + threeVolt: mkRange(1, 26).map(n => ["+", `${n}`]), + ground: mkRange(1, 26).map(n => ["-", `${n}`]), + }, + }; + + constructor(opts: AllocatorOpts) { + this.opts = opts; + } + + private allocateLocation(location: LocationDefinition, opts: AllocLocOpts): Loc { + if (location === "ground" || location === "threeVolt") { + U.assert(!!opts.nearestBBPin); + let nearLoc = opts.nearestBBPin; + let nearestCoord = this.opts.getBBCoord(nearLoc); + let firstTopAndBot = [ + this.availablePowerPins.top.ground[0] || this.availablePowerPins.top.threeVolt[0], + this.availablePowerPins.bottom.ground[0] || this.availablePowerPins.bottom.threeVolt[0] + ].map(loc => { + return this.opts.getBBCoord(loc); + }); + if (!firstTopAndBot[0] || !firstTopAndBot[1]) { + console.debug(`No more available "${location}" locations!`); + //TODO + } + let nearTop = visuals.findClosestCoordIdx(nearestCoord, firstTopAndBot) == 0; + let pins: BBRowCol[]; + if (nearTop) { + if (location === "ground") { + pins = this.availablePowerPins.top.ground; + } else if (location === "threeVolt") { + pins = this.availablePowerPins.top.threeVolt; + } + } else { + if (location === "ground") { + pins = this.availablePowerPins.bottom.ground; + } else if (location === "threeVolt") { + pins = this.availablePowerPins.bottom.threeVolt; + } + } + let pinCoords = pins.map(rowCol => { + return this.opts.getBBCoord(rowCol); + }); + let pinIdx = visuals.findClosestCoordIdx(nearestCoord, pinCoords); + let pin = pins[pinIdx]; + if (nearTop) { + this.availablePowerPins.top.ground.splice(pinIdx, 1); + this.availablePowerPins.top.threeVolt.splice(pinIdx, 1); + } else { + this.availablePowerPins.bottom.ground.splice(pinIdx, 1); + this.availablePowerPins.bottom.threeVolt.splice(pinIdx, 1); + } + return {type: "breadboard", rowCol: pin}; + } else if (location[0] === "breadboard") { + U.assert(!!opts.startColumn); + let row = location[1]; + let col = (location[2] + opts.startColumn).toString(); + return {type: "breadboard", rowCol: [row, col]} + } else if (location[0] === "GPIO") { + U.assert(!!opts.cmpGPIOPins); + let idx = location[1]; + let pin = opts.cmpGPIOPins[idx]; + return {type: "dalboard", pin: pin}; + } else { + //TODO + U.assert(false); + return null; + } + } + private allocatePowerWires(): WireInst[] { + let boardGround = this.opts.boardDef.groundPins[0] || null; + if (!boardGround) { + console.log("No available ground pin on board!"); + //TODO + } + let threeVoltPin = this.opts.boardDef.threeVoltPins[0] || null; + if (!threeVoltPin) { + console.log("No available 3.3V pin on board!"); + //TODO + } + let topLeft: BBRowCol = ["-", "26"]; + let botLeft: BBRowCol = ["-", "1"]; + let topRight: BBRowCol = ["-", "50"]; + let botRight: BBRowCol = ["-", "25"]; + let top: BBRowCol, bot: BBRowCol; + if (this.opts.boardDef.attachPowerOnRight) { + top = topRight; + bot = botRight; + } else { + top = topLeft; + bot = botLeft; + } + const GROUND_COLOR = "blue"; + const POWER_COLOR = "red"; + const wires: WireInst[] = [ + {start: this.allocateLocation("ground", {nearestBBPin: top}), + end: this.allocateLocation("ground", {nearestBBPin: bot}), + color: GROUND_COLOR, assemblyStep: 0}, + {start: this.allocateLocation("ground", {nearestBBPin: top}), + end: {type: "dalboard", pin: boardGround}, + color: GROUND_COLOR, assemblyStep: 0}, + {start: this.allocateLocation("threeVolt", {nearestBBPin: top}), + end: this.allocateLocation("threeVolt", {nearestBBPin: bot}), + color: POWER_COLOR, assemblyStep: 1}, + {start: this.allocateLocation("threeVolt", {nearestBBPin: top}), + end: {type: "dalboard", pin: threeVoltPin}, + color: POWER_COLOR, assemblyStep: 1}, + ]; + return wires; + } + private allocateWire(wireDef: WireDefinition, opts: AllocWireOpts): WireInst { + let ends = [wireDef.start, wireDef.end]; + let endIsPower = ends.map(e => e === "ground" || e === "threeVolt"); + let endInsts = ends.map((e, idx) => !endIsPower[idx] ? this.allocateLocation(e, opts) : null) + endInsts = endInsts.map((e, idx) => { + if (e) + return e; + let locInst = endInsts[1 - idx]; + let l = this.allocateLocation(ends[idx], { + nearestBBPin: locInst.rowCol, + startColumn: opts.startColumn, + cmpGPIOPins: opts.cmpGPIOPins + }); + return l; + }); + return {start: endInsts[0], end: endInsts[1], color: wireDef.color, assemblyStep: wireDef.assemblyStep}; + } + private allocatePartialCmps(): PartialCmpAlloc[] { + let cmpNmAndDefs = this.opts.cmpList.map(cmpName => <[string, ComponentDefinition]>[cmpName, this.opts.cmpDefs[cmpName]]).filter(d => !!d[1]); + let cmpNmsList = cmpNmAndDefs.map(p => p[0]); + let cmpDefsList = cmpNmAndDefs.map(p => p[1]); + let partialCmps: PartialCmpAlloc[] = []; + cmpDefsList.forEach((def, idx) => { + let nm = cmpNmsList[idx]; + if (def.pinAllocation.type === "predefined") { + let mbPins = (def.pinAllocation).pins; + let pinsAssigned = mbPins.map(p => this.opts.boardDef.gpioPinMap[p]); + partialCmps.push({ + name: nm, + def: def, + pinsAssigned: pinsAssigned, + pinsNeeded: 0, + breadboardColumnsNeeded: def.breadboardColumnsNeeded, + }); + } else if (def.pinAllocation.type === "factoryfunction") { + let fnPinAlloc = (def.pinAllocation); + let fnNm = fnPinAlloc.functionName; + let fnsAndArgs = this.opts.fnArgs[fnNm]; + let success = false; + if (fnsAndArgs && fnsAndArgs.length) { + let pinArgPoses = fnPinAlloc.pinArgPositions; + let otherArgPoses = fnPinAlloc.otherArgPositions || []; + fnsAndArgs.forEach(fnArgsStr => { + let fnArgsSplit = fnArgsStr.split(","); + let pinArgs: string[] = []; + pinArgPoses.forEach(i => { + pinArgs.push(fnArgsSplit[i]); + }); + let mbPins = pinArgs.map(arg => readPin(arg)); + let otherArgs: string[] = []; + otherArgPoses.forEach(i => { + otherArgs.push(fnArgsSplit[i]); + }); + let pinsAssigned = mbPins.map(p => this.opts.boardDef.gpioPinMap[p]); + partialCmps.push({ + name: nm, + def: def, + pinsAssigned: pinsAssigned, + pinsNeeded: 0, + breadboardColumnsNeeded: def.breadboardColumnsNeeded, + otherArgs: otherArgs.length ? otherArgs : null, + }); + }); + } else { + // failed to find pin allocation from callsites + console.debug("Failed to read pin(s) from callsite for: " + fnNm); + let pinsNeeded = fnPinAlloc.pinArgPositions.length; + partialCmps.push({ + name: nm, + def: def, + pinsAssigned: [], + pinsNeeded: pinsNeeded, + breadboardColumnsNeeded: def.breadboardColumnsNeeded, + }); + } + } else if (def.pinAllocation.type === "auto") { + let pinsNeeded = (def.pinAllocation).gpioPinsNeeded; + partialCmps.push({ + name: nm, + def: def, + pinsAssigned: [], + pinsNeeded: pinsNeeded, + breadboardColumnsNeeded: def.breadboardColumnsNeeded, + }); + } + }); + return partialCmps; + } + private allocateGPIOPins(partialCmps: PartialCmpAlloc[]): string[][] { + let availableGPIOBlocks = copyDoubleArray(this.opts.boardDef.gpioPinBlocks); + let sortAvailableGPIOBlocks = () => availableGPIOBlocks.sort((a, b) => a.length - b.length); //smallest blocks first + // determine blocks needed + let blockAssignments: AllocBlock[] = []; + let preassignedPins: string[] = []; + partialCmps.forEach((cmp, idx) => { + if (cmp.pinsAssigned && cmp.pinsAssigned.length) { + //already assigned + blockAssignments.push({cmpIdx: idx, cmpBlkIdx: 0, gpioNeeded: 0, gpioAssigned: cmp.pinsAssigned}); + preassignedPins = preassignedPins.concat(cmp.pinsAssigned); + } else if (cmp.pinsNeeded) { + if (typeof cmp.pinsNeeded === "number") { + //individual pins + for (let i = 0; i < cmp.pinsNeeded; i++) { + blockAssignments.push( + {cmpIdx: idx, cmpBlkIdx: 0, gpioNeeded: 1, gpioAssigned: []}); + } + } else { + //blocks of pins + let blocks = cmp.pinsNeeded; + blocks.forEach((numNeeded, blkIdx) => { + blockAssignments.push( + {cmpIdx: idx, cmpBlkIdx: blkIdx, gpioNeeded: numNeeded, gpioAssigned: []}); + }); + } + } + }); + // remove assigned blocks + availableGPIOBlocks.forEach(blks => { + for (let i = blks.length - 1; 0 <= i; i--) { + let pin = blks[i]; + if (0 <= preassignedPins.indexOf(pin)) { + blks.splice(i, 1); + } + } + }); + // sort by size of blocks + let sortBlockAssignments = () => blockAssignments.sort((a, b) => b.gpioNeeded - a.gpioNeeded); //largest blocks first + // allocate each block + if (0 < blockAssignments.length && 0 < availableGPIOBlocks.length) { + do { + sortBlockAssignments(); + sortAvailableGPIOBlocks(); + let assignment = blockAssignments[0]; + let smallestAvailableBlockThatFits: string[]; + for (let j = 0; j < availableGPIOBlocks.length; j++) { + smallestAvailableBlockThatFits = availableGPIOBlocks[j]; + if (assignment.gpioNeeded <= availableGPIOBlocks[j].length) { + break; + } + } + if (!smallestAvailableBlockThatFits || smallestAvailableBlockThatFits.length <= 0) { + break; // out of pins + } + while (0 < assignment.gpioNeeded && 0 < smallestAvailableBlockThatFits.length) { + assignment.gpioNeeded--; + let pin = smallestAvailableBlockThatFits[0]; + smallestAvailableBlockThatFits.splice(0, 1); + assignment.gpioAssigned.push(pin); + } + sortBlockAssignments(); + } while (0 < blockAssignments[0].gpioNeeded); + } + if (0 < blockAssignments.length && 0 < blockAssignments[0].gpioNeeded) { + console.debug("Not enough GPIO pins!"); + return null; + } + let cmpGPIOPinBlocks: string[][][] = partialCmps.map((def, cmpIdx) => { + if (!def) + return null; + let assignments = blockAssignments.filter(a => a.cmpIdx === cmpIdx); + let gpioPins: string[][] = []; + for (let i = 0; i < assignments.length; i++) { + let a = assignments[i]; + let blk = gpioPins[a.cmpBlkIdx] || (gpioPins[a.cmpBlkIdx] = []); + a.gpioAssigned.forEach(p => blk.push(p)); + } + return gpioPins; + }); + let cmpGPIOPins = cmpGPIOPinBlocks.map(blks => blks.reduce((p, n) => p.concat(n), [])); + return cmpGPIOPins; + } + private allocateColumns(partialCmps: PartialCmpAlloc[]): number[] { + let componentsCount = partialCmps.length; + let totalAvailableSpace = 30; //TODO allow multiple breadboards + let totalSpaceNeeded = partialCmps.map(d => d.breadboardColumnsNeeded).reduce((p, n) => p + n, 0); + let extraSpace = totalAvailableSpace - totalSpaceNeeded; + if (extraSpace <= 0) { + console.log("Not enough breadboard space!"); + //TODO + } + let padding = Math.floor(extraSpace / (componentsCount - 1 + 2)); + let componentSpacing = padding; //Math.floor(extraSpace/(componentsCount-1)); + let totalCmpPadding = extraSpace - componentSpacing * (componentsCount - 1); + let leftPadding = Math.floor(totalCmpPadding / 2); + let rightPadding = Math.ceil(totalCmpPadding / 2); + let nextAvailableCol = 1 + leftPadding; + let cmpStartCol = partialCmps.map(cmp => { + let col = nextAvailableCol; + nextAvailableCol += cmp.breadboardColumnsNeeded + componentSpacing; + return col; + }); + return cmpStartCol; + } + private allocateComponent(partialCmp: PartialCmpAlloc, startColumn: number, microbitPins: string[]): CmpInst { + return { + name: partialCmp.name, + breadboardStartColumn: startColumn, + breadboardStartRow: partialCmp.def.breadboardStartRow, + assemblyStep: partialCmp.def.assemblyStep, + visual: partialCmp.def.visual, + microbitPins: microbitPins, + otherArgs: partialCmp.otherArgs, + }; + } + public allocateAll(): AllocatorResult { + let cmpList = this.opts.cmpList; + let basicWires: WireInst[] = []; + let cmpsAndWires: CmpAndWireInst[] = []; + if (cmpList.length > 0) { + basicWires = this.allocatePowerWires(); + let partialCmps = this.allocatePartialCmps(); + let cmpGPIOPins = this.allocateGPIOPins(partialCmps); + let reverseMap = mkReverseMap(this.opts.boardDef.gpioPinMap); + let cmpMicrobitPins = cmpGPIOPins.map(pins => pins.map(p => reverseMap[p])); + let cmpStartCol = this.allocateColumns(partialCmps); + let cmps = partialCmps.map((c, idx) => this.allocateComponent(c, cmpStartCol[idx], cmpMicrobitPins[idx])); + let wires = partialCmps.map((c, idx) => c.def.wires.map(d => this.allocateWire(d, { + cmpGPIOPins: cmpGPIOPins[idx], + startColumn: cmpStartCol[idx], + }))); + cmpsAndWires = cmps.map((c, idx) => { + return {component: c, wires: wires[idx]} + }); + } + return { + powerWires: basicWires, + components: cmpsAndWires + }; + } + } + + export function allocateDefinitions(opts: AllocatorOpts): AllocatorResult { + return new Allocator(opts).allocateAll(); + } +} \ No newline at end of file diff --git a/sim/dalboard.ts b/sim/dalboard.ts new file mode 100644 index 00000000..a407a385 --- /dev/null +++ b/sim/dalboard.ts @@ -0,0 +1,97 @@ +namespace pxsim { + export class DalBoard extends BaseBoard { + id: string; + + // the bus + bus: EventBus; + + // state & update logic for component services + ledMatrixState: LedMatrixState; + edgeConnectorState: EdgeConnectorState; + serialState: SerialState; + accelerometerState: AccelerometerState; + compassState: CompassState; + thermometerState: ThermometerState; + lightSensorState: LightSensorState; + buttonPairState: ButtonPairState; + radioState: RadioState; + neopixelState: NeoPixelState; + + constructor() { + super() + this.id = "b" + Math_.random(2147483647); + this.bus = new EventBus(runtime); + + // components + this.ledMatrixState = new LedMatrixState(runtime); + this.buttonPairState = new ButtonPairState(); + this.edgeConnectorState = new EdgeConnectorState(); + this.radioState = new RadioState(runtime); + this.accelerometerState = new AccelerometerState(runtime); + this.serialState = new SerialState(); + this.thermometerState = new ThermometerState(); + this.lightSensorState = new LightSensorState(); + this.compassState = new CompassState(); + this.neopixelState = new NeoPixelState(); + } + + receiveMessage(msg: SimulatorMessage) { + if (!runtime || runtime.dead) return; + + switch (msg.type || "") { + case "eventbus": + let ev = msg; + this.bus.queue(ev.id, ev.eventid, ev.value); + break; + case "serial": + let data = (msg).data || ""; + this.serialState.recieveData(data); + break; + case "radiopacket": + let packet = msg; + this.radioState.recievePacket(packet); + break; + } + } + + kill() { + super.kill(); + AudioContextManager.stop(); + } + + initAsync(msg: SimulatorRunMessage): Promise { + let options = (msg.options || {}) as RuntimeOptions; + + let boardDef = ARDUINO_ZERO; //TODO: read from pxt.json/pxttarget.json + let cmpsList = msg.parts; + cmpsList.sort(); + let cmpDefs = COMPONENT_DEFINITIONS; //TODO: read from pxt.json/pxttarget.json + let fnArgs = msg.fnArgs; + + let mb = true; + let view: visuals.GenericBoardSvg | visuals.MicrobitBoardSvg; + if (mb) { + view = new visuals.MicrobitBoardSvg({ + runtime: runtime, + theme: visuals.randomTheme(), + activeComponents: cmpsList, + fnArgs: fnArgs, + disableTilt: false + }); + } else { + view = new visuals.GenericBoardSvg({ + boardDef: boardDef, + activeComponents: cmpsList, + componentDefinitions: cmpDefs, + runtime: runtime, + fnArgs: fnArgs + }) + } + + document.body.innerHTML = ""; // clear children + document.body.appendChild(view.hostElement); + + return Promise.resolve(); + } + } +} \ No newline at end of file diff --git a/sim/definitions.ts b/sim/definitions.ts new file mode 100644 index 00000000..acddfe2f --- /dev/null +++ b/sim/definitions.ts @@ -0,0 +1,321 @@ +/// +/// +/// +/// + +namespace pxsim { + export interface PinBlockDefinition { + x: number, + y: number, + labels: string[] + } + export interface BoardImageDefinition { + image?: string, + outlineImage?: string, + width: number, + height: number, + pinDist: number, + pinBlocks: PinBlockDefinition[], + }; + export interface BoardDefinition { + visual: BoardImageDefinition | string, + gpioPinBlocks?: string[][], + gpioPinMap: {[pin: string]: string}, + groundPins: string[], + threeVoltPins: string[], + attachPowerOnRight?: boolean, + } + export interface FactoryFunctionPinAlloc { + type: "factoryfunction", + functionName: string, + pinArgPositions: number[], + otherArgPositions?: number[], + } + export interface PredefinedPinAlloc { + type: "predefined", + pins: string[], + } + export interface AutoPinAlloc { + type: "auto", + gpioPinsNeeded: number | number[], + } + export interface ComponentVisualDefinition { + } + export interface ComponentDefinition { + visual: string | ComponentVisualDefinition, + breadboardColumnsNeeded: number, + breadboardStartRow: string, + wires: WireDefinition[], + assemblyStep: number, + pinAllocation: FactoryFunctionPinAlloc | PredefinedPinAlloc | AutoPinAlloc, + } + export interface WireDefinition { + start: LocationDefinition, + end: LocationDefinition, + color: string, + assemblyStep: number + }; + export type LocationDefinition = + ["breadboard", string, number] | ["GPIO", number] | "ground" | "threeVolt"; + + export const MICROBIT_DEF: BoardDefinition = { + visual: "microbit", + gpioPinBlocks: [ + ["P0"], ["P1"], ["P2"], + ["P3"], + ["P4", "P5", "P6", "P7"], + ["P8", "P9", "P10", "P11", "P12"], + ["P13", "P14", "P15", "P16"], + ["P19", "P20"], + ], + gpioPinMap: { + "P0": "P0", + "P1": "P1", + "P2": "P2", + "P3": "P3", + "P4": "P4", + "P5": "P5", + "P6": "P6", + "P7": "P7", + "P8": "P8", + "P9": "P9", + "P10": "P10", + "P11": "P11", + "P12": "P12", + "P13": "P13", + "P14": "P14", + "P15": "P15", + "P16": "P16", + "P19": "P19", + "P20": "P20", + }, + groundPins: ["GND"], + threeVoltPins: ["+3v3"], + attachPowerOnRight: true, + } + export const RASPBERRYPI_MODELB: BoardDefinition = { + visual: { + image: "/static/hardware/raspberrypi-model-b.svg", + outlineImage: "/static/hardware/raspberrypi-model-b-outline.svg", + width: 331, + height: 230, + pinDist: 9, + pinBlocks: [ + { x: 5, y: 31, labels: ["3V3", "SDA", "SCL", "#4", "--", "#17", "#21", "#22", "--", "MOSI", "MISO", "SCLK", "--"]}, + { x: 5, y: 39, labels: ["5V", "--", "GND", "TXD", "RXD", "#18", "--", "#23", "#24", "--", "#25", "CS0", "CS1"]} + ], + }, + gpioPinBlocks: [ + ["SDA", "SCL", "#4"], + ["#17", "#21", "#22"], + ["MOSI", "MISO", "SCLK"], + ["TXD", "RXD", "#18"], + ["#23", "#24"], + ["#25", "CS0", "CS1"], + ], + gpioPinMap: { + "P0": "SDA", + "P1": "SCL", + "P2": "#4", + "P3": "MOSI", + "P4": "MISO", + "P5": "SCLK", + "P6": "TXD", + "P7": "RXD", + "P8": "#18", + "P9": "#23", + "P10": "#24", + "P11": "#25", + "P12": "CS0", + "P13": "CS1", + }, + groundPins: ["GND"], + threeVoltPins: ["3V3"], + } + export const SPARKFUN_PHOTON: BoardDefinition = { + visual: { + image: "/static/hardware/sparkfun-photon.svg", + outlineImage: "/static/hardware/sparkfun-photon-outline.svg", + width: 264.4, + height: 202.4, + pinDist: 9.5, + pinBlocks: [ + {x: 72, y: 6, labels: ["~SCL/D1", "~SDA/D0", " ", "GND0", "SCK/A3", "~MISO/A4", "~MOSI/A5", "SS/A2", "~WKP", "DAC"]}, + {x: 174, y: 6, labels: ["D7", "D6", "D5", "D4", "~D3", "~D2", "~TX", "~RX"]}, + {x: 107, y: 188, labels: [" ", " ", "RESET", "3.3V", "V-USB", "GND1", "GND2", "VIN"]}, + {x: 193, y: 188, labels: ["A0", "A1", "A2", "A3", "A4", "A5"]}, + ], + }, + gpioPinBlocks: [ + ["~SCL/D1", "~SDA/D0", "SCK/A3", "~MISO/A4", "~MOSI/A5", "SS/A2"], + ["D7", "D6", "D5", "D4", "~D3", "~D2", "~TX", "~RX"], + ["A0", "A1", "A2", "A3", "A4", "A5"], + ], + gpioPinMap: { + "P0": "A0", + "P1": "A1", + "P2": "A2", + "P3": "A3", + "P4": "A4", + "P5": "A5", + "P6": "~SDA/D0", + "P7": "~SCL/D1", + "P8": "~D2", + "P9": "~D3", + "P10": "D4", + "P11": "D5", + "P12": "D6", + "P13": "D7", + "P14": "SS/A2", + "P15": "SCK/A3", + "P16": "~MISO/A4", + "P19": "~MOSI/A5", + }, + groundPins: ["GND0", "GND1", "GND2"], + threeVoltPins: ["3.3V"], + } + export const ARDUINO_ZERO: BoardDefinition = { + visual: { + image: "/static/hardware/arduino-zero.png", + outlineImage: "/static/hardware/arduino-zero-outline.svg", + width: 1000, + height: 762, + pinDist: 35.5, + pinBlocks: [ + {x: 276.8, y: 17.8, labels: ["SCL", "SDA", "AREF", "GND0", "~13", "~12", "~11", "~10", "~9", "~8"]}, + {x: 655.5, y: 17.8, labels: ["7", "~6", "~5", "~4", "~3", "2", "TX->1", "RX<-0"]}, + {x: 411.7, y: 704.6, labels: ["ATN", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN"]}, + {x: 732.9, y: 704.6, labels: ["A0", "A1", "A2", "A3", "A4", "A5"]}, + ], + }, + gpioPinBlocks: [ + ["A0", "A1", "A2", "A3", "A4", "A5"], + ["~13", "~12", "~11", "~10", "~9", "~8"], + ["7", "~6", "~5", "~4", "~3", "2", "TX->1", "RX<-0"], + ], + gpioPinMap: { + "P0": "A0", + "P1": "A1", + "P2": "A2", + "P3": "A3", + "P4": "A4", + "P5": "A5", + "P6": "RX<-0", + "P7": "TX->1", + "P8": "2", + "P9": "~3", + "P10": "~4", + "P11": "~5", + "P12": "~6", + "P13": "7", + "P14": "~8", + "P15": "~9", + "P16": "~10", + "P19": "~11", + "P20": "~12", + }, + groundPins: ["GND0", "GND1", "GND2"], + threeVoltPins: ["3.3V"], + } + export const COMPONENT_DEFINITIONS: Map = { + "ledmatrix": { + visual: "ledmatrix", + breadboardColumnsNeeded: 8, + breadboardStartRow: "h", + pinAllocation: { + type: "auto", + gpioPinsNeeded: [5, 5], + }, + assemblyStep: 0, + wires: [ + {start: ["breadboard", `j`, 0], end: ["GPIO", 5], color: "purple", assemblyStep: 1}, + {start: ["breadboard", `j`, 1], end: ["GPIO", 6], color: "purple", assemblyStep: 1}, + {start: ["breadboard", `j`, 2], end: ["GPIO", 7], color: "purple", assemblyStep: 1}, + {start: ["breadboard", `j`, 3], end: ["GPIO", 8], color: "purple", assemblyStep: 1}, + {start: ["breadboard", `a`, 7], end: ["GPIO", 9], color: "purple", assemblyStep: 1}, + {start: ["breadboard", `a`, 0], end: ["GPIO", 0], color: "green", assemblyStep: 2}, + {start: ["breadboard", `a`, 1], end: ["GPIO", 1], color: "green", assemblyStep: 2}, + {start: ["breadboard", `a`, 2], end: ["GPIO", 2], color: "green", assemblyStep: 2}, + {start: ["breadboard", `a`, 3], end: ["GPIO", 3], color: "green", assemblyStep: 2}, + {start: ["breadboard", `j`, 4], end: ["GPIO", 4], color: "green", assemblyStep: 2}, + ] + }, + "buttonpair": { + visual: "buttonpair", + breadboardColumnsNeeded: 6, + breadboardStartRow: "f", + pinAllocation: { + type: "predefined", + pins: ["P13", "P12"], + }, + assemblyStep: 0, + wires: [ + {start: ["breadboard", "j", 0], end: ["GPIO", 0], color: "yellow", assemblyStep: 1}, + {start: ["breadboard", "a", 2], end: "ground", color: "blue", assemblyStep: 1}, + {start: ["breadboard", "j", 3], end: ["GPIO", 1], color: "orange", assemblyStep: 2}, + {start: ["breadboard", "a", 5], end: "ground", color: "blue", assemblyStep: 2}, + ], + }, + "neopixel": { + visual: "neopixel", + breadboardColumnsNeeded: 5, + breadboardStartRow: "h", + pinAllocation: { + type: "factoryfunction", + functionName: "neopixel.create", + pinArgPositions: [0], + otherArgPositions: [1], + }, + assemblyStep: 0, + wires: [ + {start: ["breadboard", "j", 1], end: "ground", color: "blue", assemblyStep: 1}, + {start: ["breadboard", "j", 2], end: "threeVolt", color: "red", assemblyStep: 2}, + {start: ["breadboard", "j", 3], end: ["GPIO", 0], color: "green", assemblyStep: 2}, + ], + }, + "speaker": { + visual: { + image: "/static/hardware/speaker.svg", + width: 500, + height: 500, + left: -180, + top: -135, + pinDist: 70, + }, + breadboardColumnsNeeded: 5, + breadboardStartRow: "f", + pinAllocation: { + type: "auto", + gpioPinsNeeded: 1, + }, + assemblyStep: 0, + wires: [ + {start: ["breadboard", "j", 1], end: ["GPIO", 0], color: "white", assemblyStep: 1}, + {start: ["breadboard", "j", 3], end: "ground", color: "white", assemblyStep: 1}, + ], + }, + } + + export const builtinComponentSimVisual: Map<() => visuals.IBoardComponent> = { + "buttonpair": () => new visuals.ButtonPairView(), + "ledmatrix": () => new visuals.LedMatrixView(), + "neopixel": () => new visuals.NeoPixelView(), + }; + export const builtinComponentSimState: Map<(d: DalBoard) => any> = { + "buttonpair": (d: DalBoard) => d.buttonPairState, + "ledmatrix": (d: DalBoard) => d.ledMatrixState, + "edgeconnector": (d: DalBoard) => d.edgeConnectorState, + "serial": (d: DalBoard) => d.serialState, + "radio": (d: DalBoard) => d.radioState, + "thermometer": (d: DalBoard) => d.thermometerState, + "accelerometer": (d: DalBoard) => d.accelerometerState, + "compass": (d: DalBoard) => d.compassState, + "lightsensor": (d: DalBoard) => d.lightSensorState, + "neopixel": (d: DalBoard) => d.neopixelState, + }; + export const builtinComponentPartVisual: Map<(xy: visuals.Coord) => visuals.SVGElAndSize> = { + "buttonpair": (xy: visuals.Coord) => visuals.mkBtnSvg(xy), + "ledmatrix": (xy: visuals.Coord) => visuals.mkLedMatrixSvg(xy, 8, 8), + "neopixel": (xy: visuals.Coord) => visuals.mkNeoPixelPart(xy), + }; +} \ No newline at end of file diff --git a/sim/instructions/instructions.ts b/sim/instructions/instructions.ts new file mode 100644 index 00000000..cb1d1f36 --- /dev/null +++ b/sim/instructions/instructions.ts @@ -0,0 +1,675 @@ +/// +/// +/// +/// +/// +/// + +//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; + type Orientation = "landscape" | "portrait"; + const ORIENTATION: Orientation = "portrait"; + const PPI = 96.0; + const [FULL_PAGE_WIDTH, FULL_PAGE_HEIGHT] + = (ORIENTATION == "portrait" ? [PPI * 8.5, PPI * 11.0] : [PPI * 11.0, PPI * 8.5]); + 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; + const BORDER_WIDTH = 2; + const [PANEL_ROWS, PANEL_COLS] = [2, 2]; + const PANEL_MARGIN = 20; + const PANEL_PADDING = 8; + 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 = 240; + const BOARD_LEFT = (PANEL_WIDTH - BOARD_WIDTH) / 2.0 + PANEL_PADDING; + const BOARD_BOT = PANEL_PADDING; + const NUM_BOX_SIZE = 60; + const NUM_FONT = 40; + const NUM_MARGIN = 5; + const FRONT_PAGE_BOARD_WIDTH = 200; + const STYLE = ` + ${visuals.BOARD_SYTLE} + .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; + } + .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; + } + .partslist-wire, + .partslist-cmp { + margin: 5px; + } + .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: BoardImageDefinition): visuals.SVGElAndSize { + let img = svg.elt( "image"); + let [l, t] = [0, 0]; + let w = def.width; + let h = def.height; + svg.hydrate(img, { + class: "sim-board", + x: l, + y: t, + width: def.width, + height: def.height, + "href": `${def.image}`}); + + return {el: img, w: w, h: h, x: l, y: t}; + } + 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, + "height": dims.h, + "preserveAspectRatio": "xMidYMid", + }; + svg.hydrate(svgEl, svgAtts); + let div = document.createElement("div"); + div.appendChild(svgEl); + return div; + } + function mkCmpDiv(type: "wire" | string, opts: mkCmpDivOpts): HTMLElement { + let el: visuals.SVGElAndSize; + if (type == "wire") { + el = visuals.mkWirePart([0, 0], opts.wireClr || "red"); + } else { + let cnstr = builtinComponentPartVisual[type]; + el = cnstr([0, 0]); + } + return wrapSvg(el, opts); + } + type BoardProps = { + boardDef: BoardDefinition, + cmpDefs: Map, + allAlloc: AllocatorResult, + stepToWires: WireInst[][], + stepToCmps: CmpInst[][] + allWires: WireInst[], + allCmps: CmpInst[], + lastStep: number, + colorToWires: Map, + 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 = {} + 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, + allAlloc: allocRes, + stepToWires: stepToWires, + stepToCmps: stepToCmps, + allWires: allWires, + allCmps: allCmps, + lastStep: lastStep, + colorToWires: colorToWires, + allWireColors: allWireColors, + }; + } + function mkBoard(boardDef: BoardDefinition, cmpDefs: Map, width: number, buildMode: boolean = false): visuals.GenericBoardSvg { + let board = new visuals.GenericBoardSvg({ + runtime: pxsim.runtime, + boardDef: boardDef, + activeComponents: [], + componentDefinitions: cmpDefs, + }) + svg.hydrate(board.hostElement, { + "width": width, + }); + svg.addClass(board.hostElement, "board-svg"); + if (buildMode) { + svg.hydrate(board.background, { + "href": `${(boardDef.visual).outlineImage}` + }) + svg.addClass(board.hostElement, "sim-board-outline") + let bb = board.breadboard.bb; + svg.addClass(bb, "sim-bb-outline") + let style = svg.child(bb, "style", {}); + } + + board.updateState(); + + //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 board; + } + function drawSteps(board: visuals.GenericBoardSvg, step: number, props: BoardProps) { + if (step > 0) { + svg.addClass(board.hostElement, "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 [row, col] = (w.start).rowCol; + let lbls = board.breadboard.highlightLoc(row, col); + } else { + board.highlightLoc((w.start).pin); + } + if (w.end.type == "breadboard") { + let [row, col] = (w.end).rowCol; + let lbls = board.breadboard.highlightLoc(row, col); + } else { + board.highlightLoc((w.end).pin); + } + //highlight wire + board.highlightWire(wire); + } + }); + } + let cmps = props.stepToCmps[i]; + if (cmps) { + cmps.forEach(cmpInst => { + let cmp = board.addComponent(cmpInst) + let [row, col]: BBRowCol = [`${cmpInst.breadboardStartRow}`, `${cmpInst.breadboardStartColumn}`]; + //last step + if (i === step) { + board.breadboard.highlightLoc(row, col); + if (cmpInst.visual === "buttonpair") { + //TODO: don't specialize this + let [row2, col2]: BBRowCol = [`${cmpInst.breadboardStartRow}`, `${cmpInst.breadboardStartColumn + 3}`]; + board.breadboard.highlightLoc(row2, col2); + } + 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(); + + const BOARD_SCALE = 0.1; + const BB_SCALE = 0.25; + const CMP_SCALE = 0.3; + const WIRE_SCALE = 0.23; + + // board and breadboard + let boardImg = mkBoardImgSvg(props.boardDef.visual); + let board = wrapSvg(boardImg, {left: QUANT_LBL(1), leftSize: QUANT_LBL_SIZE, cmpScale: BOARD_SCALE}); + panel.appendChild(board); + let bbRaw = mkBBSvg(); + let bb = wrapSvg(bbRaw, {left: QUANT_LBL(1), leftSize: QUANT_LBL_SIZE, cmpScale: 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; + } + if (typeof c.visual === "string") { + let builtinVisual = c.visual; + let cmp = mkCmpDiv(builtinVisual, { + left: QUANT_LBL(quant), + leftSize: QUANT_LBL_SIZE, + cmpScale: CMP_SCALE, + }); + addClass(cmp, "partslist-cmp"); + panel.appendChild(cmp); + } else { + //TODO: handle generic components + } + }); + + // 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: WIRE_SCALE + }) + addClass(cmp, "partslist-wire"); + panel.appendChild(cmp); + }) + + return panel; + } + function mkStepPanel(step: number, props: BoardProps) { + let panel = mkPanel(); + + //board + let board = mkBoard(props.boardDef, props.cmpDefs, BOARD_WIDTH, true) + drawSteps(board, step, props); + panel.appendChild(board.hostElement); + + //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).rowCol; + 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 + }) + 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; + if (typeof c.visual === "string") { + let builtinVisual = c.visual; + let cmp = mkCmpDiv(builtinVisual, { + top: `(${row},${col})`, + topSize: LOC_LBL_SIZE, + cmpHeight: REQ_CMP_HEIGHT, + cmpScale: REQ_CMP_SCALE + }) + addClass(cmp, "cmp-div"); + reqsDiv.appendChild(cmp); + } else { + //TODO: generic component + } + }); + }); + + return panel; + } + function updateFrontPanel(props: BoardProps): [HTMLElement, BoardProps] { + let panel = document.getElementById("front-panel"); + + let board = mkBoard(props.boardDef, props.cmpDefs, FRONT_PAGE_BOARD_WIDTH, false); + board.addAll(props.allAlloc); + panel.appendChild(board.hostElement); + + return [panel, props]; + } + function mkFinalPanel(props: BoardProps) { + const BACK_PAGE_BOARD_WIDTH = PANEL_WIDTH - 20; + + let panel = mkPanel(); + addClass(panel, "back-panel"); + let board = mkBoard(props.boardDef, props.cmpDefs, BACK_PAGE_BOARD_WIDTH, false) + board.addAll(props.allAlloc); + panel.appendChild(board.hostElement); + + 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 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\n" + tsCode + "```" + 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(); + }); + } + + //parts list + let parts = (getQsVal("parts") || "").split(" "); + parts.sort(); + + //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; + + let boardDef = ARDUINO_ZERO; + let cmpDefs = COMPONENT_DEFINITIONS; + + //props + let dummyBreadboard = new visuals.Breadboard(); + let props = mkBoardProps({ + boardDef: boardDef, + cmpDefs: cmpDefs, + cmpList: parts, + 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); + } +} \ No newline at end of file diff --git a/sim/microbit.ts b/sim/microbit.ts index 9dc6fdf9..b7fddd0a 100644 --- a/sim/microbit.ts +++ b/sim/microbit.ts @@ -1,6 +1,46 @@ -namespace pxsim.micro_bit { - const svg = pxsim.svg; - +namespace pxsim.visuals { + const pins4onXs = [66.7, 79.1, 91.4, 103.7, 164.3, 176.6, 188.9, 201.3, 213.6, 275.2, 287.5, 299.8, 312.1, 324.5, 385.1, 397.4, 409.7, 422]; + const pins4onMids = pins4onXs.map(x => x + 5); + const littlePinDist = pins4onMids[1] - pins4onMids[0]; + const bigPinWidth = pins4onMids[4] - pins4onMids[3]; + const pin0mid = pins4onXs[0] - bigPinWidth / 2.0; + const pin3mid = pin0mid - bigPinWidth / 2.0; + const pin1mid = pins4onMids[3] + bigPinWidth / 2.0; + const pin2mid = pins4onMids[8] + bigPinWidth / 2.0; + const pin3Vmid = pins4onMids[13] + bigPinWidth / 2.0; + const pinGNDmid = pins4onMids[pins4onMids.length - 1] + bigPinWidth / 2.0; + const pinGND2mid = pinGNDmid + bigPinWidth / 2.0; + const pinMids = [pin0mid, pin1mid, pin2mid, pin3mid].concat(pins4onXs).concat([pinGNDmid, pin3Vmid, pinGND2mid]); + const pinNames = [ + "P0", "P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9", "P10", + "P11", "P12", "P13", "P14", "P15", "P16", "P17", "P18", "P19", "P20", + "GND0", "GND", "+3v3", "GND1"]; + const pinTitles = [ + "P0, ANALOG IN", + "P1, ANALOG IN", + "P2, ANALOG IN", + "P3, ANALOG IN, LED Col 1", + "P4, ANALOG IN, LED Col 2", + "P5, BUTTON A", + "P6, LED Col 9", + "P7, LED Col 8", + "P8", + "P9, LED Col 7", + "P10, ANALOG IN, LED Col 3", + "P11, BUTTON B", + "P12, RESERVED ACCESSIBILITY", + "P13, SPI - SCK", + "P14, SPI - MISO", + "P15, SPI - MOSI", + "P16, SPI - Chip Select", + "P17, +3v3", + "P18, +3v3", + "P19, I2C - SCL", + "P20, I2C - SDA", + "GND", "GND", "+3v3", "GND" + ]; + const MB_WIDTH = 498; + const MB_HEIGHT = 406; export interface IBoardTheme { accent?: string; display?: string; @@ -46,6 +86,8 @@ namespace pxsim.micro_bit { runtime: pxsim.Runtime; theme?: IBoardTheme; disableTilt?: boolean; + activeComponents: string[]; + fnArgs?: any; } const pointerEvents = !!(window as any).PointerEvent ? { @@ -61,10 +103,11 @@ namespace pxsim.micro_bit { }; export class MicrobitBoardSvg { + public hostElement: SVGSVGElement; public element: SVGSVGElement; private style: SVGStyleElement; private defs: SVGDefsElement; - private g: SVGElement; + private g: SVGGElement; private logos: SVGElement[]; private head: SVGGElement; private headInitialized = false; @@ -88,17 +131,140 @@ namespace pxsim.micro_bit { private thermometerText: SVGTextElement; private shakeButton: SVGCircleElement; private shakeText: SVGTextElement; - public board: pxsim.Board; + public state: pxsim.DalBoard; + + //EXPERIMENTAl + private wireFactory: WireFactory; + private breadboard: Breadboard; + private components: IBoardComponent[] = []; + private pinNmToCoord: Map = {}; + private fromBBCoord: (xy: Coord) => Coord; + private fromMBCoord: (xy: Coord) => Coord; constructor(public props: IBoardProps) { - this.board = this.props.runtime.board as pxsim.Board; - this.board.updateView = () => this.updateState(); + this.state = this.props.runtime.board as pxsim.DalBoard; + this.state.updateView = () => this.updateState(); + + //EXPERIMENTAl + let boardDef = MICROBIT_DEF; + let cmpsDef: Map = COMPONENT_DEFINITIONS; + this.breadboard = new Breadboard(); this.buildDom(); + this.hostElement = this.element; + this.recordPinCoords(); + let cmps = props.activeComponents.filter(a => a === "neopixel"); + if (0 < cmps.length) { + let compRes = composeSVG({ + el1: {el: this.element, y: 0, x: 0, w: MB_WIDTH, h: MB_HEIGHT}, + scaleUnit1: littlePinDist * 1.7, + el2: this.breadboard.getSVGAndSize(), + scaleUnit2: this.breadboard.getPinDist(), + margin: [0, 0, 10, 0], + middleMargin: 80, + maxWidth: 299, + maxHeight: 433, + }); + let under = compRes.under; + let over = compRes.over; + this.hostElement = compRes.host; + let edges = compRes.edges; + this.fromMBCoord = compRes.toHostCoord1; + this.fromBBCoord = compRes.toHostCoord2; + let pinDist = compRes.scaleUnit; + + this.wireFactory = new WireFactory(under, over, edges, this.style, this.getLocCoord.bind(this)); + let allocRes = allocateDefinitions({ + boardDef: boardDef, + cmpDefs: cmpsDef, + fnArgs: this.props.fnArgs, + getBBCoord: this.getBBCoord.bind(this), + cmpList: cmps, + }); + this.addAll(allocRes); + } else { + svg.hydrate(this.hostElement, { + width: 299, + height: 433, + }); + } + this.updateTheme(); this.updateState(); this.attachEvents(); } + //EXPERIMENTAl + private getBoardPinCoord(pinNm: string): Coord { + let coord = this.pinNmToCoord[pinNm]; + return this.fromMBCoord(coord); + } + private getBBCoord(rowCol: BBRowCol): Coord { + let bbCoord = this.breadboard.getCoord(rowCol); + if (!bbCoord) + return null; + return this.fromBBCoord(bbCoord); + } + public getLocCoord(loc: Loc): Coord { + let coord: Coord; + if (loc.type === "breadboard") { + let rowCol = (loc).rowCol; + coord = this.getBBCoord(rowCol); + } else { + let pinNm = (loc).pin; + coord = this.getBoardPinCoord(pinNm); + } + if (!coord) { + console.error("Unknown location: " + name) + return [0, 0]; + } + return coord; + } + public addWire(inst: WireInst): Wire { + return this.wireFactory.addWire(inst.start, inst.end, inst.color, true); + } + public addAll(basicWiresAndCmpsAndWires: AllocatorResult) { + let {powerWires, components} = basicWiresAndCmpsAndWires; + powerWires.forEach(w => this.addWire(w)); + components.forEach((cAndWs, idx) => { + let {component, wires} = cAndWs; + wires.forEach(w => this.addWire(w)); + this.addComponent(component); + }); + } + public addComponent(cmpDesc: CmpInst): IBoardComponent { + let cmp: IBoardComponent = null; + if (typeof cmpDesc.visual === "string") { + let builtinVisual = cmpDesc.visual as string; + let cnstr = builtinComponentSimVisual[builtinVisual]; + let stateFn = builtinComponentSimState[builtinVisual]; + let cmp = cnstr(); + cmp.init(this.state.bus, stateFn(this.state), this.element, cmpDesc.microbitPins, cmpDesc.otherArgs); + this.components.push(cmp); + this.hostElement.appendChild(cmp.element); + if (cmp.defs) + cmp.defs.forEach(d => this.defs.appendChild(d)); + this.style.textContent += cmp.style || ""; + let rowCol = [`${cmpDesc.breadboardStartRow}`, `${cmpDesc.breadboardStartColumn}`]; + let coord = this.getBBCoord(rowCol); + cmp.moveToCoord(coord); + let getCmpClass = (type: string) => `sim-${type}-cmp`; + let cls = getCmpClass(name); + svg.addClass(cmp.element, cls); + svg.addClass(cmp.element, "sim-cmp"); + cmp.updateTheme(); + cmp.updateState(); + } else { + } + return cmp; + } + public recordPinCoords() { + const pinsY = 356.7 + 40; + pinNames.forEach((nm, i) => { + let x = pinMids[i]; + this.pinNmToCoord[nm] = [x, pinsY]; + }); + } + private updateTheme() { let theme = this.props.theme; @@ -119,16 +285,18 @@ namespace pxsim.micro_bit { } public updateState() { - let state = this.board; + let state = this.state; if (!state) return; let theme = this.props.theme; - state.buttons.forEach((btn, index) => { + let bpState = state.buttonPairState; + let buttons = [bpState.aBtn, bpState.bBtn, bpState.abBtn]; + buttons.forEach((btn, index) => { svg.fill(this.buttons[index], btn.pressed ? theme.buttonDown : theme.buttonUp); }); - let bw = state.displayMode == pxsim.DisplayMode.bw - let img = state.image; + let bw = state.ledMatrixState.displayMode == pxsim.DisplayMode.bw + let img = state.ledMatrixState.image; this.leds.forEach((led, i) => { let sel = (led) sel.style.opacity = ((bw ? img.data[i] > 0 ? 255 : 0 : img.data[i]) / 255.0) + ""; @@ -143,25 +311,28 @@ namespace pxsim.micro_bit { if (!runtime || runtime.dead) svg.addClass(this.element, "grayscale"); else svg.removeClass(this.element, "grayscale"); + + //EXPERIMENTAl + this.components.forEach(c => c.updateState()); } private updateGestures() { - let state = this.board; - if (state.useShake && !this.shakeButton) { + let state = this.state; + if (state.accelerometerState.useShake && !this.shakeButton) { this.shakeButton = svg.child(this.g, "circle", { cx: 380, cy: 100, r: 16.5 }) as SVGCircleElement; svg.fill(this.shakeButton, this.props.theme.virtualButtonUp) this.shakeButton.addEventListener(pointerEvents.down, ev => { - let state = this.board; + let state = this.state; svg.fill(this.shakeButton, this.props.theme.buttonDown); }) this.shakeButton.addEventListener(pointerEvents.leave, ev => { - let state = this.board; + let state = this.state; svg.fill(this.shakeButton, this.props.theme.virtualButtonUp); }) this.shakeButton.addEventListener(pointerEvents.up, ev => { - let state = this.board; + let state = this.state; svg.fill(this.shakeButton, this.props.theme.virtualButtonUp); - this.board.bus.queue(DAL.MICROBIT_ID_GESTURE, 11); // GESTURE_SHAKE + this.state.bus.queue(DAL.MICROBIT_ID_GESTURE, 11); // GESTURE_SHAKE }) this.shakeText = svg.child(this.g, "text", { x: 400, y: 110, class: "sim-text" }) as SVGTextElement; this.shakeText.textContent = "SHAKE" @@ -169,8 +340,8 @@ namespace pxsim.micro_bit { } private updateButtonAB() { - let state = this.board; - if (state.usesButtonAB && !this.buttonABText) { + let state = this.state; + if (state.buttonPairState.usesButtonAB && !this.buttonABText) { (this.buttonsOuter[2]).style.visibility = "visible"; (this.buttons[2]).style.visibility = "visible"; this.buttonABText = svg.child(this.g, "text", { class: "sim-text", x: 370, y: 272 }) as SVGTextElement; @@ -202,8 +373,8 @@ namespace pxsim.micro_bit { } private updateTemperature() { - let state = this.board; - if (!state || !state.usesTemperature) return; + let state = this.state; + if (!state || !state.thermometerState.usesTemperature) return; let tmin = -5; let tmax = 50; @@ -227,13 +398,13 @@ namespace pxsim.micro_bit { (ev) => { let cur = svg.cursorPoint(pt, this.element, ev); let t = Math.max(0, Math.min(1, (260 - cur.y) / 140)) - state.temperature = Math.floor(tmin + t * (tmax - tmin)); + state.thermometerState.temperature = Math.floor(tmin + t * (tmax - tmin)); this.updateTemperature(); }, ev => { }, ev => { }) } - let t = Math.max(tmin, Math.min(tmax, state.temperature)) - let per = Math.floor((state.temperature - tmin) / (tmax - tmin) * 100) + let t = Math.max(tmin, Math.min(tmax, state.thermometerState.temperature)) + let per = Math.floor((state.thermometerState.temperature - tmin) / (tmax - tmin) * 100) svg.setGradientValue(this.thermometerGradient, 100 - per + "%"); this.thermometerText.textContent = t + "°C"; } @@ -241,8 +412,8 @@ namespace pxsim.micro_bit { private updateHeading() { let xc = 258; let yc = 75; - let state = this.board; - if (!state || !state.usesHeading) return; + let state = this.state; + if (!state || !state.compassState.usesHeading) return; if (!this.headInitialized) { let p = this.head.firstChild.nextSibling as SVGPathElement; p.setAttribute("d", "m269.9,50.134647l0,0l-39.5,0l0,0c-14.1,0.1 -24.6,10.7 -24.6,24.8c0,13.9 10.4,24.4 24.3,24.7l0,0l39.6,0c14.2,0 40.36034,-22.97069 40.36034,-24.85394c0,-1.88326 -26.06034,-24.54606 -40.16034,-24.64606m-0.2,39l0,0l-39.3,0c-7.7,-0.1 -14,-6.4 -14,-14.2c0,-7.8 6.4,-14.2 14.2,-14.2l39.1,0c7.8,0 14.2,6.4 14.2,14.2c0,7.9 -6.4,14.2 -14.2,14.2l0,0l0,0z"); @@ -252,16 +423,16 @@ namespace pxsim.micro_bit { this.head, (ev: MouseEvent) => { let cur = svg.cursorPoint(pt, this.element, ev); - state.heading = Math.floor(Math.atan2(cur.y - yc, cur.x - xc) * 180 / Math.PI + 90); - if (state.heading < 0) state.heading += 360; + state.compassState.heading = Math.floor(Math.atan2(cur.y - yc, cur.x - xc) * 180 / Math.PI + 90); + if (state.compassState.heading < 0) state.compassState.heading += 360; this.updateHeading(); }); this.headInitialized = true; } - let txt = state.heading.toString() + "°"; + let txt = state.compassState.heading.toString() + "°"; if (txt != this.headText.textContent) { - svg.rotateElement(this.head, xc, yc, state.heading + 180); + svg.rotateElement(this.head, xc, yc, state.compassState.heading + 180); this.headText.textContent = txt; } } @@ -294,15 +465,15 @@ namespace pxsim.micro_bit { } private updatePins() { - let state = this.board; + let state = this.state; if (!state) return; - state.pins.forEach((pin, i) => this.updatePin(pin, i)); + state.edgeConnectorState.pins.forEach((pin, i) => this.updatePin(pin, i)); } private updateLightLevel() { - let state = this.board; - if (!state || !state.usesLightLevel) return; + let state = this.state; + if (!state || !state.lightSensorState.usesLightLevel) return; if (!this.lightLevelButton) { let gid = "gradient-light-level"; @@ -320,8 +491,8 @@ namespace pxsim.micro_bit { let pos = svg.cursorPoint(pt, this.element, ev); let rs = r / 2; let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255))); - if (level != this.board.lightLevel) { - this.board.lightLevel = level; + if (level != this.state.lightSensorState.lightLevel) { + this.state.lightSensorState.lightLevel = level; this.applyLightLevel(); } }, ev => { }, @@ -330,23 +501,23 @@ namespace pxsim.micro_bit { this.updateTheme(); } - svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(state.lightLevel * 100 / 255))) + '%') - this.lightLevelText.textContent = state.lightLevel.toString(); + svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(state.lightSensorState.lightLevel * 100 / 255))) + '%') + this.lightLevelText.textContent = state.lightSensorState.lightLevel.toString(); } private applyLightLevel() { - let lv = this.board.lightLevel; + let lv = this.state.lightSensorState.lightLevel; svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(lv * 100 / 255))) + '%') this.lightLevelText.textContent = lv.toString(); } private updateTilt() { if (this.props.disableTilt) return; - let state = this.board; - if (!state || !state.accelerometer.isActive) return; + let state = this.state; + if (!state || !state.accelerometerState.accelerometer.isActive) return; - let x = state.accelerometer.getX(); - let y = state.accelerometer.getY(); + let x = state.accelerometerState.accelerometer.getX(); + let y = state.accelerometerState.accelerometer.getY(); let af = 8 / 1023; this.element.style.transform = "perspective(30em) rotateX(" + y * af + "deg) rotateY(" + x * af + "deg)" @@ -358,129 +529,129 @@ namespace pxsim.micro_bit { this.element = svg.elt("svg") svg.hydrate(this.element, { "version": "1.0", - "viewBox": "0 0 498 406", - "enable-background": "new 0 0 498 406", + "viewBox": `0 0 ${MB_WIDTH} ${MB_HEIGHT}`, "class": "sim", "x": "0px", - "y": "0px" + "y": "0px", + "width": MB_WIDTH + "px", + "height": MB_HEIGHT + "px", }); this.style = svg.child(this.element, "style", {}); this.style.textContent = ` -svg.sim { - margin-bottom:1em; -} -svg.sim.grayscale { - -moz-filter: grayscale(1); - -webkit-filter: grayscale(1); - filter: grayscale(1); -} -.sim-button { - pointer-events: none; -} + svg.sim { + margin-bottom:1em; + } + svg.sim.grayscale { + -moz-filter: grayscale(1); + -webkit-filter: grayscale(1); + filter: grayscale(1); + } + .sim-button { + pointer-events: none; + } -.sim-button-outer:hover { - stroke:grey; - stroke-width: 3px; -} -.sim-button-nut { - fill:#704A4A; - pointer-events:none; -} -.sim-button-nut:hover { - stroke:1px solid #704A4A; -} -.sim-pin:hover { - stroke:#D4AF37; - stroke-width:2px; -} + .sim-button-outer:hover { + stroke:grey; + stroke-width: 3px; + } + .sim-button-nut { + fill:#704A4A; + pointer-events:none; + } + .sim-button-nut:hover { + stroke:1px solid #704A4A; + } + .sim-pin:hover { + stroke:#D4AF37; + stroke-width:2px; + } -.sim-pin-touch.touched:hover { - stroke:darkorange; -} + .sim-pin-touch.touched:hover { + stroke:darkorange; + } -.sim-led-back:hover { - stroke:#a0a0a0; - stroke-width:3px; -} -.sim-led:hover { - stroke:#ff7f7f; - stroke-width:3px; -} + .sim-led-back:hover { + stroke:#a0a0a0; + stroke-width:3px; + } + .sim-led:hover { + stroke:#ff7f7f; + stroke-width:3px; + } -.sim-systemled { - fill:#333; - stroke:#555; - stroke-width: 1px; -} + .sim-systemled { + fill:#333; + stroke:#555; + stroke-width: 1px; + } -.sim-light-level-button { - stroke:#fff; - stroke-width: 3px; -} + .sim-light-level-button { + stroke:#fff; + stroke-width: 3px; + } -.sim-antenna { - stroke:#555; - stroke-width: 2px; -} + .sim-antenna { + stroke:#555; + stroke-width: 2px; + } -.sim-text { - font-family:"Lucida Console", Monaco, monospace; - font-size:25px; - fill:#fff; - pointer-events: none; -} + .sim-text { + font-family:"Lucida Console", Monaco, monospace; + font-size:25px; + fill:#fff; + pointer-events: none; + } -.sim-text-pin { - font-family:"Lucida Console", Monaco, monospace; - font-size:20px; - fill:#fff; - pointer-events: none; -} + .sim-text-pin { + font-family:"Lucida Console", Monaco, monospace; + font-size:20px; + fill:#fff; + pointer-events: none; + } -.sim-thermometer { - stroke:#aaa; - stroke-width: 3px; -} + .sim-thermometer { + stroke:#aaa; + stroke-width: 3px; + } -/* animations */ -.sim-theme-glow { - animation-name: sim-theme-glow-animation; - animation-timing-function: ease-in-out; - animation-direction: alternate; - animation-iteration-count: infinite; - animation-duration: 1.25s; -} -@keyframes sim-theme-glow-animation { - from { opacity: 1; } - to { opacity: 0.75; } -} + /* animations */ + .sim-theme-glow { + animation-name: sim-theme-glow-animation; + animation-timing-function: ease-in-out; + animation-direction: alternate; + animation-iteration-count: infinite; + animation-duration: 1.25s; + } + @keyframes sim-theme-glow-animation { + from { opacity: 1; } + to { opacity: 0.75; } + } -.sim-flash { - animation-name: sim-flash-animation; - animation-duration: 0.1s; -} + .sim-flash { + animation-name: sim-flash-animation; + animation-duration: 0.1s; + } -@keyframes sim-flash-animation { - from { fill: yellow; } - to { fill: default; } -} + @keyframes sim-flash-animation { + from { fill: yellow; } + to { fill: default; } + } -.sim-flash-stroke { - animation-name: sim-flash-stroke-animation; - animation-duration: 0.4s; - animation-timing-function: ease-in; -} + .sim-flash-stroke { + animation-name: sim-flash-stroke-animation; + animation-duration: 0.4s; + animation-timing-function: ease-in; + } -@keyframes sim-flash-stroke-animation { - from { stroke: yellow; } - to { stroke: default; } -} + @keyframes sim-flash-stroke-animation { + from { stroke: yellow; } + to { stroke: default; } + } `; - this.defs = svg.child(this.element, "defs", {}); - this.g = svg.elt("g"); + this.g = svg.elt("g"); this.element.appendChild(this.g); // filters @@ -530,38 +701,19 @@ svg.sim.grayscale { "M16.5,341.2c0,0.4-0.1,0.9-0.1,1.3v60.7c4.1,1.7,8.6,2.7,12.9,2.7h34.4v-64.7h0.3c0,0,0-0.1,0-0.1c0-13-10.6-23.6-23.7-23.6C27.2,317.6,16.5,328.1,16.5,341.2z M21.2,341.6c0-10.7,8.7-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3c0,10.7-8.6,19.3-19.3,19.3C29.9,360.9,21.2,352.2,21.2,341.6z", "M139.1,317.3c-12.8,0-22.1,10.3-23.1,23.1V406h46.2v-65.6C162.2,327.7,151.9,317.3,139.1,317.3zM139.3,360.1c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C158.6,351.5,150,360.1,139.3,360.1z", "M249,317.3c-12.8,0-22.1,10.3-23.1,23.1V406h46.2v-65.6C272.1,327.7,261.8,317.3,249,317.3z M249.4,360.1c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C268.7,351.5,260.1,360.1,249.4,360.1z" - ].map((p, pi) => svg.path(this.g, "sim-pin sim-pin-touch", p, `P${pi}, ANALOG IN`)); + ].map((p, pi) => svg.path(this.g, "sim-pin sim-pin-touch", p)); // P3 - this.pins.push(svg.path(this.g, "sim-pin", "M0,357.7v19.2c0,10.8,6.2,20.2,14.4,25.2v-44.4H0z", "P3, ANALOG IN, LED Col 1")); + this.pins.push(svg.path(this.g, "sim-pin", "M0,357.7v19.2c0,10.8,6.2,20.2,14.4,25.2v-44.4H0z")); - [66.7, 79.1, 91.4, 103.7, 164.3, 176.6, 188.9, 201.3, 213.6, 275.2, 287.5, 299.8, 312.1, 324.5, 385.1, 397.4, 409.7, 422].forEach(x => { + pins4onXs.forEach(x => { this.pins.push(svg.child(this.g, "rect", { x: x, y: 356.7, width: 10, height: 50, class: "sim-pin" })); }) - svg.title(this.pins[4], "P4, ANALOG IN, LED Col 2") - svg.title(this.pins[5], "P5, BUTTON A") - svg.title(this.pins[6], "P6, LED Col 9") - svg.title(this.pins[7], "P7, LED Col 8") - svg.title(this.pins[8], "P8") - svg.title(this.pins[9], "P9, LED Col 7") - svg.title(this.pins[10], "P10, ANALOG IN, LED Col 3") - svg.title(this.pins[11], "P11, BUTTON B") - svg.title(this.pins[12], "P12, RESERVED ACCESSIBILITY") - svg.title(this.pins[13], "P13, SPI - SCK") - svg.title(this.pins[14], "P14, SPI - MISO") - svg.title(this.pins[15], "P15, SPI - MOSI") - svg.title(this.pins[16], "P16, SPI - Chip Select") - svg.title(this.pins[17], "P17, +3v3") - svg.title(this.pins[18], "P18, +3v3") - svg.title(this.pins[19], "P19, I2C - SCL") - svg.title(this.pins[20], "P20, I2C - SDA") - svg.title(this.pins[21], "GND") - - this.pins.push(svg.path(this.g, "sim-pin", "M483.6,402c8.2-5,14.4-14.4,14.4-25.1v-19.2h-14.4V402z", "GND")); - - this.pins.push(svg.path(this.g, "sim-pin", "M359.9,317.3c-12.8,0-22.1,10.3-23.1,23.1V406H383v-65.6C383,327.7,372.7,317.3,359.9,317.3z M360,360.1c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C379.3,351.5,370.7,360.1,360,360.1z", "+3v3")); - this.pins.push(svg.path(this.g, "sim-pin", "M458,317.6c-13,0-23.6,10.6-23.6,23.6c0,0,0,0.1,0,0.1h0V406H469c4.3,0,8.4-1,12.6-2.7v-60.7c0-0.4,0-0.9,0-1.3C481.6,328.1,471,317.6,458,317.6z M457.8,360.9c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C477.1,352.2,468.4,360.9,457.8,360.9z", "GND")); + this.pins.push(svg.path(this.g, "sim-pin", "M483.6,402c8.2-5,14.4-14.4,14.4-25.1v-19.2h-14.4V402z")); + this.pins.push(svg.path(this.g, "sim-pin", "M359.9,317.3c-12.8,0-22.1,10.3-23.1,23.1V406H383v-65.6C383,327.7,372.7,317.3,359.9,317.3z M360,360.1c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C379.3,351.5,370.7,360.1,360,360.1z")); + this.pins.push(svg.path(this.g, "sim-pin", "M458,317.6c-13,0-23.6,10.6-23.6,23.6c0,0,0,0.1,0,0.1h0V406H469c4.3,0,8.4-1,12.6-2.7v-60.7c0-0.4,0-0.9,0-1.3C481.6,328.1,471,317.6,458,317.6z M457.8,360.9c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C477.1,352.2,468.4,360.9,457.8,360.9z")); + this.pins.forEach((p, i) => svg.hydrate(p, {title: pinTitles[i]})); this.pinGradients = this.pins.map((pin, i) => { let gid = "gradient-pin-" + i @@ -571,6 +723,7 @@ svg.sim.grayscale { }) this.pinTexts = [67, 165, 275].map(x => svg.child(this.g, "text", { class: "sim-text-pin", x: x, y: 345 })); + this.buttonsOuter = []; this.buttons = []; const outerBtn = (left: number, top: number) => { @@ -619,8 +772,8 @@ svg.sim.grayscale { } let tiltDecayer = 0; this.element.addEventListener(pointerEvents.move, (ev: MouseEvent) => { - let state = this.board; - if (!state.accelerometer.isActive) return; + let state = this.state; + if (!state.accelerometerState.accelerometer.isActive) return; if (tiltDecayer) { clearInterval(tiltDecayer); @@ -635,18 +788,18 @@ svg.sim.grayscale { let z2 = 1023 * 1023 - x * x - y * y; let z = Math.floor((z2 > 0 ? -1 : 1) * Math.sqrt(Math.abs(z2))); - state.accelerometer.update(x, y, z); + state.accelerometerState.accelerometer.update(x, y, z); this.updateTilt(); }, false); this.element.addEventListener(pointerEvents.leave, (ev: MouseEvent) => { - let state = this.board; - if (!state.accelerometer.isActive) return; + let state = this.state; + if (!state.accelerometerState.accelerometer.isActive) return; if (!tiltDecayer) { tiltDecayer = setInterval(() => { - let accx = state.accelerometer.getX(MicroBitCoordinateSystem.RAW); + let accx = state.accelerometerState.accelerometer.getX(MicroBitCoordinateSystem.RAW); accx = Math.floor(Math.abs(accx) * 0.85) * (accx > 0 ? 1 : -1); - let accy = state.accelerometer.getY(MicroBitCoordinateSystem.RAW); + let accy = state.accelerometerState.accelerometer.getY(MicroBitCoordinateSystem.RAW); accy = Math.floor(Math.abs(accy) * 0.85) * (accy > 0 ? 1 : -1); let accz = -Math.sqrt(Math.max(0, 1023 * 1023 - accx * accx - accy * accy)); if (Math.abs(accx) <= 24 && Math.abs(accy) <= 24) { @@ -656,20 +809,20 @@ svg.sim.grayscale { accy = 0; accz = -1023; } - state.accelerometer.update(accx, accy, accz); + state.accelerometerState.accelerometer.update(accx, accy, accz); this.updateTilt(); }, 50) } }, false); this.pins.forEach((pin, index) => { - if (!this.board.pins[index]) return; + if (!this.state.edgeConnectorState.pins[index]) return; let pt = this.element.createSVGPoint(); svg.buttonEvents(pin, // move ev => { - let state = this.board; - let pin = state.pins[index]; + let state = this.state; + let pin = state.edgeConnectorState.pins[index]; let svgpin = this.pins[index]; if (pin.mode & PinFlags.Input) { let cursor = svg.cursorPoint(pt, this.element, ev); @@ -680,8 +833,8 @@ svg.sim.grayscale { }, // start ev => { - let state = this.board; - let pin = state.pins[index]; + let state = this.state; + let pin = state.edgeConnectorState.pins[index]; let svgpin = this.pins[index]; svg.addClass(svgpin, "touched"); if (pin.mode & PinFlags.Input) { @@ -693,8 +846,8 @@ svg.sim.grayscale { }, // stop (ev: MouseEvent) => { - let state = this.board; - let pin = state.pins[index]; + let state = this.state; + let pin = state.edgeConnectorState.pins[index]; let svgpin = this.pins[index]; svg.removeClass(svgpin, "touched"); this.updatePin(pin, index); @@ -703,71 +856,74 @@ svg.sim.grayscale { }) this.pins.slice(0, 3).forEach((btn, index) => { btn.addEventListener(pointerEvents.down, ev => { - let state = this.board; - state.pins[index].touched = true; - this.updatePin(state.pins[index], index); + let state = this.state; + state.edgeConnectorState.pins[index].touched = true; + this.updatePin(state.edgeConnectorState.pins[index], index); }) btn.addEventListener(pointerEvents.leave, ev => { - let state = this.board; - state.pins[index].touched = false; - this.updatePin(state.pins[index], index); + let state = this.state; + state.edgeConnectorState.pins[index].touched = false; + this.updatePin(state.edgeConnectorState.pins[index], index); }) btn.addEventListener(pointerEvents.up, ev => { - let state = this.board; - state.pins[index].touched = false; - this.updatePin(state.pins[index], index); - this.board.bus.queue(state.pins[index].id, DAL.MICROBIT_BUTTON_EVT_UP); - this.board.bus.queue(state.pins[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); + let state = this.state; + state.edgeConnectorState.pins[index].touched = false; + this.updatePin(state.edgeConnectorState.pins[index], index); + this.state.bus.queue(state.edgeConnectorState.pins[index].id, DAL.MICROBIT_BUTTON_EVT_UP); + this.state.bus.queue(state.edgeConnectorState.pins[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); }) }) + + let bpState = this.state.buttonPairState; + let stateButtons = [bpState.aBtn, bpState.bBtn, bpState.abBtn]; this.buttonsOuter.slice(0, 2).forEach((btn, index) => { btn.addEventListener(pointerEvents.down, ev => { - let state = this.board; - state.buttons[index].pressed = true; + let state = this.state; + stateButtons[index].pressed = true; svg.fill(this.buttons[index], this.props.theme.buttonDown); }) btn.addEventListener(pointerEvents.leave, ev => { - let state = this.board; - state.buttons[index].pressed = false; + let state = this.state; + stateButtons[index].pressed = false; svg.fill(this.buttons[index], this.props.theme.buttonUp); }) btn.addEventListener(pointerEvents.up, ev => { - let state = this.board; - state.buttons[index].pressed = false; + let state = this.state; + stateButtons[index].pressed = false; svg.fill(this.buttons[index], this.props.theme.buttonUp); - this.board.bus.queue(state.buttons[index].id, DAL.MICROBIT_BUTTON_EVT_UP); - this.board.bus.queue(state.buttons[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); + this.state.bus.queue(stateButtons[index].id, DAL.MICROBIT_BUTTON_EVT_UP); + this.state.bus.queue(stateButtons[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); }) }) this.buttonsOuter[2].addEventListener(pointerEvents.down, ev => { - let state = this.board; - state.buttons[0].pressed = true; - state.buttons[1].pressed = true; - state.buttons[2].pressed = true; + let state = this.state; + stateButtons[0].pressed = true; + stateButtons[1].pressed = true; + stateButtons[2].pressed = true; svg.fill(this.buttons[0], this.props.theme.buttonDown); svg.fill(this.buttons[1], this.props.theme.buttonDown); svg.fill(this.buttons[2], this.props.theme.buttonDown); }) this.buttonsOuter[2].addEventListener(pointerEvents.leave, ev => { - let state = this.board; - state.buttons[0].pressed = false; - state.buttons[1].pressed = false; - state.buttons[2].pressed = false; + let state = this.state; + stateButtons[0].pressed = false; + stateButtons[1].pressed = false; + stateButtons[2].pressed = false; svg.fill(this.buttons[0], this.props.theme.buttonUp); svg.fill(this.buttons[1], this.props.theme.buttonUp); svg.fill(this.buttons[2], this.props.theme.virtualButtonUp); }) this.buttonsOuter[2].addEventListener(pointerEvents.up, ev => { - let state = this.board; - state.buttons[0].pressed = false; - state.buttons[1].pressed = false; - state.buttons[2].pressed = false; + let state = this.state; + stateButtons[0].pressed = false; + stateButtons[1].pressed = false; + stateButtons[2].pressed = false; svg.fill(this.buttons[0], this.props.theme.buttonUp); svg.fill(this.buttons[1], this.props.theme.buttonUp); svg.fill(this.buttons[2], this.props.theme.virtualButtonUp); - this.board.bus.queue(state.buttons[2].id, DAL.MICROBIT_BUTTON_EVT_UP); - this.board.bus.queue(state.buttons[2].id, DAL.MICROBIT_BUTTON_EVT_CLICK); + this.state.bus.queue(stateButtons[2].id, DAL.MICROBIT_BUTTON_EVT_UP); + this.state.bus.queue(stateButtons[2].id, DAL.MICROBIT_BUTTON_EVT_CLICK); }) } } diff --git a/sim/public/instructions.html b/sim/public/instructions.html new file mode 100644 index 00000000..94683291 --- /dev/null +++ b/sim/public/instructions.html @@ -0,0 +1,200 @@ + + + + + + Arduino Instructions + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + +
+

+ + +
+ +
+
+
+ + +
+ + \ No newline at end of file diff --git a/sim/simlib.ts b/sim/simlib.ts index 7d427d21..e96bd947 100644 --- a/sim/simlib.ts +++ b/sim/simlib.ts @@ -3,761 +3,203 @@ /// namespace pxsim { - pxsim.initCurrentRuntime = () => { + export type BBRowCol = [/*row*/string, /*column*/string]; + export type BoardPin = string; + export interface BBLoc {type: "breadboard", rowCol: BBRowCol}; + export interface BoardLoc {type: "dalboard", pin: BoardPin}; + export type Loc = BBLoc | BoardLoc; + + export function initRuntimeWithDalBoard() { U.assert(!runtime.board); - runtime.board = new Board(); + let b = new DalBoard(); + runtime.board = b; + } + if (!pxsim.initCurrentRuntime) { + pxsim.initCurrentRuntime = initRuntimeWithDalBoard; } export function board() { - return runtime.board as Board; + return runtime.board as DalBoard; } - export interface AnimationOptions { - interval: number; - // false means last frame - frame: () => boolean; - whenDone?: (cancelled: boolean) => void; + export function mkRange(a: number, b: number): number[] { + let res: number[] = []; + for (; a < b; a++) + res.push(a); + return res; } - export class AnimationQueue { - private queue: AnimationOptions[] = []; - private process: () => void; + export function parseQueryString(): (key: string) => string { + let qs = window.location.search.substring(1); + let getQsVal = (key: string) => decodeURIComponent((qs.split(`${key}=`)[1] || "").split("&")[0] || "").replace(/\+/g, " "); + return getQsVal; + } +} - constructor(private runtime: Runtime) { - this.process = () => { - let top = this.queue[0] - if (!top) return - if (this.runtime.dead) return - runtime = this.runtime - let res = top.frame() - runtime.queueDisplayUpdate() - runtime.maybeUpdateDisplay() - if (res === false) { - this.queue.shift(); - // if there is already something in the queue, start processing - if (this.queue[0]) - setTimeout(this.process, this.queue[0].interval) - // this may push additional stuff - top.whenDone(false); - } else { - setTimeout(this.process, top.interval) - } - } - } - - public cancelAll() { - let q = this.queue - this.queue = [] - for (let a of q) { - a.whenDone(true) - } - } - - public cancelCurrent() { - let top = this.queue[0] - if (top) { - this.queue.shift(); - top.whenDone(true); - } - } - - public enqueue(anim: AnimationOptions) { - if (!anim.whenDone) anim.whenDone = () => { }; - this.queue.push(anim) - // we start processing when the queue goes from 0 to 1 - if (this.queue.length == 1) - this.process() - } - - public executeAsync(anim: AnimationOptions) { - U.assert(!anim.whenDone) - return new Promise((resolve, reject) => { - anim.whenDone = resolve - this.enqueue(anim) - }) - } +namespace pxsim.visuals { + export interface IPointerEvents { + up: string, + down: string, + move: string, + leave: string } - /** - * Error codes used in the micro:bit runtime. - */ - export enum PanicCode { - // PANIC Codes. These are not return codes, but are terminal conditions. - // These induce a panic operation, where all code stops executing, and a panic state is - // entered where the panic code is diplayed. - - // Out out memory error. Heap storage was requested, but is not available. - MICROBIT_OOM = 20, - - // Corruption detected in the micro:bit heap space - MICROBIT_HEAP_ERROR = 30, - - // Dereference of a NULL pointer through the ManagedType class, - MICROBIT_NULL_DEREFERENCE = 40, + export const pointerEvents: IPointerEvents = !!(window as any).PointerEvent ? { + up: "pointerup", + down: "pointerdown", + move: "pointermove", + leave: "pointerleave" + } : { + up: "mouseup", + down: "mousedown", + move: "mousemove", + leave: "mouseleave" }; - export function panic(code: number) { - console.log("PANIC:", code) - led.setBrightness(255); - let img = board().image; - img.clear(); - img.set(0, 4, 255); - img.set(1, 3, 255); - img.set(2, 3, 255); - img.set(3, 3, 255); - img.set(4, 4, 255); - img.set(0, 0, 255); - img.set(1, 0, 255); - img.set(0, 1, 255); - img.set(1, 1, 255); - img.set(3, 0, 255); - img.set(4, 0, 255); - img.set(3, 1, 255); - img.set(4, 1, 255); - runtime.updateDisplay(); - - throw new Error("PANIC " + code) - } - - export function getPin(id: number) { - return board().pins.filter(p => p && p.id == id)[0] || null - } - - - export namespace AudioContextManager { - let _context: any; // AudioContext - let _vco: any; // OscillatorNode; - let _vca: any; // GainNode; - - function context(): any { - if (!_context) _context = freshContext(); - return _context; - } - - function freshContext(): any { - (window).AudioContext = (window).AudioContext || (window).webkitAudioContext; - if ((window).AudioContext) { - try { - // this call my crash. - // SyntaxError: audio resources unavailable for AudioContext construction - return new (window).AudioContext(); - } catch (e) { } - } - return undefined; - } - - export function stop() { - if (_vca) _vca.gain.value = 0; - } - - export function tone(frequency: number, gain: number) { - if (frequency <= 0) return; - let ctx = context(); - if (!ctx) return; - - gain = Math.max(0, Math.min(1, gain)); - if (!_vco) { - try { - _vco = ctx.createOscillator(); - _vca = ctx.createGain(); - _vco.connect(_vca); - _vca.connect(ctx.destination); - _vca.gain.value = gain; - _vco.start(0); - } catch (e) { - _vco = undefined; - _vca = undefined; - return; - } - } - - _vco.frequency.value = frequency; - _vca.gain.value = gain; - } - } - - -} - -namespace pxsim.basic { - export var pause = thread.pause; - export var forever = thread.forever; - - export function showNumber(x: number, interval: number) { - if (interval < 0) return; - - let leds = createImageFromString(x.toString()); - if (x < 0 || x >= 10) ImageMethods.scrollImage(leds, 1, interval); - else showLeds(leds, interval * 5); - } - - export function showString(s: string, interval: number) { - if (interval < 0) return; - if (s.length == 0) { - clearScreen(); - pause(interval * 5); - } else { - if (s.length == 1) showLeds(createImageFromString(s + " "), interval * 5) - else ImageMethods.scrollImage(createImageFromString(s + " "), 1, interval); - } - } - - export function showLeds(leds: Image, delay: number): void { - showAnimation(leds, delay); - } - - export function clearScreen() { - board().image.clear(); - runtime.queueDisplayUpdate() - } - - export function showAnimation(leds: Image, interval: number): void { - ImageMethods.scrollImage(leds, 5, interval); - } - - export function plotLeds(leds: Image): void { - ImageMethods.plotImage(leds, 0); - } -} - -namespace pxsim.control { - export var inBackground = thread.runInBackground; - - export function reset() { - U.userError("reset not implemented in simulator yet") - } - - export function waitMicros(micros: number) { - // TODO - } - - export function deviceName(): string { - let b = board(); - return b && b.id - ? b.id.slice(0, 4) - : "abcd"; - } - - export function deviceSerialNumber(): number { - let b = board(); - return parseInt(b && b.id - ? b.id.slice(1) - : "42"); - } - - export function onEvent(id: number, evid: number, handler: RefAction) { - pxt.registerWithDal(id, evid, handler) - } - - export function raiseEvent(id: number, evid: number, mode: number) { - // TODO mode? - board().bus.queue(id, evid) - } -} - -namespace pxsim.pxt { - export function registerWithDal(id: number, evid: number, handler: RefAction) { - board().bus.listen(id, evid, handler); - } -} - -namespace pxsim.input { - export function onButtonPressed(button: number, handler: RefAction): void { - let b = board(); - if (button == DAL.MICROBIT_ID_BUTTON_AB && !board().usesButtonAB) { - b.usesButtonAB = true; - runtime.queueDisplayUpdate(); - } - pxt.registerWithDal(button, DAL.MICROBIT_BUTTON_EVT_CLICK, handler); - } - - export function buttonIsPressed(button: number): boolean { - let b = board(); - if (button == DAL.MICROBIT_ID_BUTTON_AB && !board().usesButtonAB) { - b.usesButtonAB = true; - runtime.queueDisplayUpdate(); - } - let bts = b.buttons; - if (button == DAL.MICROBIT_ID_BUTTON_A) return bts[0].pressed; - if (button == DAL.MICROBIT_ID_BUTTON_B) return bts[1].pressed; - return bts[2].pressed || (bts[0].pressed && bts[1].pressed); - } - - export function onGesture(gesture: number, handler: RefAction) { - let b = board(); - b.accelerometer.activate(); - - if (gesture == 11 && !b.useShake) { // SAKE - b.useShake = true; - runtime.queueDisplayUpdate(); - } - pxt.registerWithDal(DAL.MICROBIT_ID_GESTURE, gesture, handler); - } - - export function onPinPressed(pinId: number, handler: RefAction) { - let pin = getPin(pinId); - if (!pin) return; - pin.isTouched(); - pxt.registerWithDal(pin.id, DAL.MICROBIT_BUTTON_EVT_CLICK, handler); - } - - export function onPinReleased(pinId: number, handler: RefAction) { - let pin = getPin(pinId); - if (!pin) return; - pin.isTouched(); - pxt.registerWithDal(pin.id, DAL.MICROBIT_BUTTON_EVT_UP, handler); - } - - export function pinIsPressed(pinId: number): boolean { - let pin = getPin(pinId); - if (!pin) return false; - return pin.isTouched(); - } - - - - export function compassHeading(): number { - let b = board(); - if (!b.usesHeading) { - b.usesHeading = true; - runtime.queueDisplayUpdate(); - } - return b.heading; - } - - export function temperature(): number { - let b = board(); - if (!b.usesTemperature) { - b.usesTemperature = true; - runtime.queueDisplayUpdate(); - } - return b.temperature; - } - - export function acceleration(dimension: number): number { - let b = board(); - let acc = b.accelerometer; - acc.activate(); - switch (dimension) { - case 0: return acc.getX(); - case 1: return acc.getY(); - case 2: return acc.getZ(); - default: return Math.floor(Math.sqrt(acc.instantaneousAccelerationSquared())); - } - } - - export function rotation(kind: number): number { - let b = board(); - let acc = b.accelerometer; - acc.activate(); - let x = acc.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN); - let y = acc.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN); - let z = acc.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN); - - let roll = Math.atan2(y, z); - let pitch = Math.atan(-x / (y * Math.sin(roll) + z * Math.cos(roll))); - - let r = 0; - switch (kind) { - case 0: r = pitch; break; - case 1: r = roll; break; - } - return Math.floor(r / Math.PI * 180); - } - - export function setAccelerometerRange(range: number) { - let b = board(); - b.accelerometer.setSampleRange(range); - } - - export function lightLevel(): number { - let b = board(); - if (!b.usesLightLevel) { - b.usesLightLevel = true; - runtime.queueDisplayUpdate(); - } - return b.lightLevel; - } - - export function magneticForce(): number { - // TODO - return 0; - } - - export function runningTime(): number { - return runtime.runningTime(); - } - - export function calibrate() { - } -} - -namespace pxsim.led { - export function plot(x: number, y: number) { - board().image.set(x, y, 255); - runtime.queueDisplayUpdate() - } - - export function unplot(x: number, y: number) { - board().image.set(x, y, 0); - runtime.queueDisplayUpdate() - } - - export function point(x: number, y: number): boolean { - return !!board().image.get(x, y); - } - - export function brightness(): number { - return board().brigthness; - } - - export function setBrightness(value: number): void { - board().brigthness = value; - runtime.queueDisplayUpdate() - } - - export function stopAnimation(): void { - board().animationQ.cancelAll(); - } - - export function setDisplayMode(mode: DisplayMode): void { - board().displayMode = mode; - runtime.queueDisplayUpdate() - } - - export function screenshot(): Image { - let img = createImage(5) - board().image.copyTo(0, 5, img, 0); - return img; - } -} - -namespace pxsim.serial { - export function writeString(s: string) { - board().writeSerial(s); - } - - export function readString(): string { - return board().readSerial(); - } - - export function readLine(): string { - return board().readSerial(); - } - - export function onDataReceived(delimiters: string, handler: RefAction) { - let b = board(); - b.bus.listen(DAL.MICROBIT_ID_SERIAL, DAL.MICROBIT_SERIAL_EVT_DELIM_MATCH, handler); - } - - export function redirect(tx: number, rx: number, rate: number) { - // TODO? - } -} - - -namespace pxsim.radio { - export function broadcastMessage(msg: number): void { - board().radio.broadcast(msg); - } - - export function onBroadcastMessageReceived(msg: number, handler: RefAction): void { - pxt.registerWithDal(DAL.MES_BROADCAST_GENERAL_ID, msg, handler); - } - - export function setGroup(id: number): void { - board().radio.setGroup(id); - } - - export function setTransmitPower(power: number): void { - board().radio.setTransmitPower(power); - } - - export function setTransmitSerialNumber(transmit: boolean): void { - board().radio.setTransmitSerialNumber(transmit); - } - - export function sendNumber(value: number): void { - board().radio.datagram.send([value]); - } - - export function sendString(msg: string): void { - board().radio.datagram.send(msg); - } - - export function writeValueToSerial(): void { - let b = board(); - let v = b.radio.datagram.recv().data[0]; - b.writeSerial(`{v:${v}}`); - } - - export function sendValue(name: string, value: number) { - board().radio.datagram.send([value]); - } - - export function receiveNumber(): number { - let buffer = board().radio.datagram.recv().data; - if (buffer instanceof Array) return buffer[0]; - - return 0; - } - - export function receiveString(): string { - let buffer = board().radio.datagram.recv().data; - if (typeof buffer === "string") return buffer; - return ""; - } - - export function receivedNumberAt(index: number): number { - let buffer = board().radio.datagram.recv().data; - if (buffer instanceof Array) return buffer[index] || 0; - - return 0; - } - - export function receivedSignalStrength(): number { - return board().radio.datagram.lastReceived.rssi; - } - - export function onDataReceived(handler: RefAction): void { - pxt.registerWithDal(DAL.MICROBIT_ID_RADIO, DAL.MICROBIT_RADIO_EVT_DATAGRAM, handler); - radio.receiveNumber(); - } -} - -namespace pxsim.pins { - export function onPulsed(name: number, pulse: number, body: RefAction) { - } - - export function pulseDuration(): number { - return 0; - } - - export function createBuffer(sz: number) { - return pxsim.BufferMethods.createBuffer(sz) - } - - export function digitalReadPin(pinId: number): number { - let pin = getPin(pinId); - if (!pin) return; - pin.mode = PinFlags.Digital | PinFlags.Input; - return pin.value > 100 ? 1 : 0; - } - - export function digitalWritePin(pinId: number, value: number) { - let pin = getPin(pinId); - if (!pin) return; - pin.mode = PinFlags.Digital | PinFlags.Output; - pin.value = value > 0 ? 1023 : 0; - runtime.queueDisplayUpdate(); - } - - export function setPull(pinId: number, pull: number) { - let pin = getPin(pinId); - if (!pin) return; - pin.pull = pull; - } - - export function analogReadPin(pinId: number): number { - let pin = getPin(pinId); - if (!pin) return; - pin.mode = PinFlags.Analog | PinFlags.Input; - return pin.value || 0; - } - - export function analogWritePin(pinId: number, value: number) { - let pin = getPin(pinId); - if (!pin) return; - pin.mode = PinFlags.Analog | PinFlags.Output; - pin.value = value ? 1 : 0; - runtime.queueDisplayUpdate(); - } - - export function analogSetPeriod(pinId: number, micros: number) { - let pin = getPin(pinId); - if (!pin) return; - pin.mode = PinFlags.Analog | PinFlags.Output; - pin.period = micros; - runtime.queueDisplayUpdate(); - } - - export function servoWritePin(pinId: number, value: number) { - analogSetPeriod(pinId, 20000); - // TODO - } - - export function servoSetPulse(pinId: number, micros: number) { - let pin = getPin(pinId); - if (!pin) return; - // TODO - } - - export function pulseIn(name: number, value: number, maxDuration: number): number { - let pin = getPin(name); - if (!pin) return 0; - - return 5000; - } - - export function spiWrite(value: number): number { - // TODO - return 0; - } - - export function i2cReadBuffer(address: number, size: number, repeat?: boolean): RefBuffer { - // fake reading zeros - return createBuffer(size) - } - - export function i2cWriteBuffer(address: number, buf: RefBuffer, repeat?: boolean): void { - // fake - noop - } - - export function analogSetPitchPin(pinId: number) { - let pin = getPin(pinId); - if (!pin) return; - board().pins.filter(p => !!p).forEach(p => p.pitch = false); - pin.pitch = true; - } - - export function analogPitch(frequency: number, ms: number) { - // update analog output - let pin = board().pins.filter(pin => !!pin && pin.pitch)[0] || board().pins[0]; - pin.mode = PinFlags.Analog | PinFlags.Output; - if (frequency <= 0) { - pin.value = 0; - pin.period = 0; - } else { - pin.value = 512; - pin.period = 1000000 / frequency; - } - runtime.queueDisplayUpdate(); - - let cb = getResume(); - AudioContextManager.tone(frequency, 1); - if (ms <= 0) cb(); - else { - setTimeout(() => { - AudioContextManager.stop(); - pin.value = 0; - pin.period = 0; - pin.mode = PinFlags.Unused; - runtime.queueDisplayUpdate(); - cb() - }, ms); - } - } - - -} - -namespace pxsim.bluetooth { - export function startIOPinService(): void { - // TODO - } - export function startLEDService(): void { - // TODO - } - export function startTemperatureService(): void { - // TODO - } - export function startMagnetometerService(): void { - // TODO - } - export function startAccelerometerService(): void { - // TODO - } - export function startButtonService(): void { - // TODO - } -} - -namespace pxsim.images { - export function createImage(img: Image) { return img } - export function createBigImage(img: Image) { return img } -} - -namespace pxsim.ImageMethods { - export function showImage(leds: Image, offset: number) { - if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - - leds.copyTo(offset, 5, board().image, 0) - runtime.queueDisplayUpdate() - } - - export function plotImage(leds: Image, offset: number): void { - if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - - leds.copyTo(offset, 5, board().image, 0) - runtime.queueDisplayUpdate() - } - - export function height(leds: Image): number { - if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - return Image.height; - } - - export function width(leds: Image): number { - if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - return leds.width; - } - - export function plotFrame(leds: Image, frame: number) { - ImageMethods.plotImage(leds, frame * Image.height); - } - - export function showFrame(leds: Image, frame: number) { - ImageMethods.showImage(leds, frame * Image.height); - } - - export function pixel(leds: Image, x: number, y: number): number { - if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - return leds.get(x, y); - } - - export function setPixel(leds: Image, x: number, y: number, v: number) { - if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - leds.set(x, y, v); - } - - export function clear(leds: Image) { - if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - - leds.clear(); - } - - export function setPixelBrightness(i: Image, x: number, y: number, b: number) { - if (!i) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - - i.set(x, y, b); - } - - export function pixelBrightness(i: Image, x: number, y: number): number { - if (!i) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - - return i.get(x, y); - } - - export function scrollImage(leds: Image, stride: number, interval: number): void { - if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - if (stride == 0) stride = 1; - - let cb = getResume(); - let off = stride > 0 ? 0 : leds.width - 1; - let display = board().image; - - board().animationQ.enqueue({ - interval: interval, - frame: () => { - //TODO: support right to left. - if (off >= leds.width || off < 0) return false; - stride > 0 ? display.shiftLeft(stride) : display.shiftRight(-stride); - let c = Math.min(stride, leds.width - off); - leds.copyTo(off, c, display, 5 - stride) - off += stride; - return true; - }, - whenDone: cb - }) - } -} + export function translateEl(el: SVGElement, xy: [number, number]) { + //TODO append translation instead of replacing the full transform + svg.hydrate(el, {transform: `translate(${xy[0]} ${xy[1]})`}); + } + + export interface ComposeOpts { + el1: SVGAndSize, + scaleUnit1: number, + el2: SVGAndSize, + scaleUnit2: number, + margin: [number, number, number, number], + middleMargin: number, + maxWidth: number, + maxHeight: number, + } + export interface ComposeResult { + host: SVGSVGElement, + scaleUnit: number, + under: SVGGElement, + over: SVGGElement, + edges: number[], + toHostCoord1: (xy: Coord) => Coord, + toHostCoord2: (xy: Coord) => Coord, + } + export function composeSVG(opts: ComposeOpts): ComposeResult { + let [a, b] = [opts.el1, opts.el2]; + U.assert(a.x == 0 && a.y == 0 && b.x == 0 && b.y == 0, "el1 and el2 x,y offsets not supported"); + let setXY = (e: SVGSVGElement, x: number, y: number) => svg.hydrate(e, {x: x, y: y}); + let setWH = (e: SVGSVGElement, w: number, h: number) => svg.hydrate(e, {width: w, height: h}); + let scaleUnit = opts.scaleUnit2; + let aScalar = opts.scaleUnit2 / opts.scaleUnit1; + let bScalar = 1.0; + let aw = a.w * aScalar; + let ah = a.h * aScalar; + setWH(a.el, aw, ah); + let bw = b.w * bScalar; + let bh = b.h * bScalar; + setWH(b.el, bw, bh); + let [mt, mr, mb, ml] = opts.margin; + let mm = opts.middleMargin; + let innerW = Math.max(aw, bw); + let ax = mr + (innerW - aw) / 2.0; + let ay = mt; + setXY(a.el, ax, ay); + let bx = mr + (innerW - bw) / 2.0; + let by = ay + ah + mm; + setXY(b.el, bx, by); + let edges = [ay, ay + ah, by, by + bh]; + let w = mr + innerW + ml; + let h = mt + ah + mm + bh + mb; + let host = svg.elt("svg", { + "version": "1.0", + "viewBox": `0 0 ${w} ${h}`, + "class": `sim-bb`, + }); + setWH(host, opts.maxWidth, opts.maxHeight); + setXY(host, 0, 0); + let under = svg.child(host, "g"); + host.appendChild(a.el); + host.appendChild(b.el); + let over = svg.child(host, "g"); + let toHostCoord1 = (xy: Coord): Coord => { + let [x, y] = xy; + return [x * aScalar + ax, y * aScalar + ay]; + }; + let toHostCoord2 = (xy: Coord): Coord => { + let [x, y] = xy; + return [x * bScalar + bx, y * bScalar + by]; + }; + return { + under: under, + over: over, + host: host, + edges: edges, + scaleUnit: scaleUnit, + toHostCoord1: toHostCoord1, + toHostCoord2: toHostCoord2, + }; + } + + export type Coord = [number, number]; + export function findDistSqrd(a: Coord, b: Coord): number { + let x = a[0] - b[0]; + let y = a[1] - b[1]; + return x * x + y * y; + } + export function findClosestCoordIdx(a: Coord, bs: Coord[]): number { + let dists = bs.map(b => findDistSqrd(a, b)); + let minIdx = dists.reduce((prevIdx, currDist, currIdx, arr) => { + return currDist < arr[prevIdx] ? currIdx : prevIdx; + }, 0); + return minIdx; + } + + export interface IBoardComponent { + style: string, + element: SVGElement, + defs: SVGElement[], + init(bus: EventBus, state: T, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void, //NOTE: constructors not supported in interfaces + moveToCoord(xy: Coord): void, + updateState(): void, + updateTheme(): void, + } + + export function mkTxt(cx: number, cy: number, size: number, rot: number, txt: string, txtXOffFactor?: number, txtYOffFactor?: number): SVGTextElement { + let el = svg.elt("text") + //HACK: these constants (txtXOffFactor, txtYOffFactor) tweak the way this algorithm knows how to center the text + txtXOffFactor = txtXOffFactor || -0.33333; + txtYOffFactor = txtYOffFactor || 0.3; + const xOff = txtXOffFactor * size * txt.length; + const yOff = txtYOffFactor * size; + svg.hydrate(el, {style: `font-size:${size}px;`, + transform: `translate(${cx} ${cy}) rotate(${rot}) translate(${xOff} ${yOff})` }); + svg.addClass(el, "noselect"); + el.textContent = txt; + return el; + } + + export type WireColor = + "black" | "white" | "gray" | "purple" | "blue" | "green" | "yellow" | "orange" | "red" | "brown"; + export const WIRE_COLOR_MAP: Map = { + black: "#514f4d", + white: "#fcfdfc", + gray: "#acabab", + purple: "#a772a1", + blue: "#01a6e8", + green: "#3cce73", + yellow: "#ece600", + orange: "#fdb262", + red: "#f44f43", + brown: "#c89764", + } + export function mapWireColor(clr: WireColor | string): string { + return WIRE_COLOR_MAP[clr] || clr; + } + + export interface SVGAndSize { + el: T, + y: number, + x: number, + w: number, + h: number + }; + export type SVGElAndSize = SVGAndSize; + + export const PIN_DIST = 15; +} \ No newline at end of file diff --git a/sim/visuals/breadboard.ts b/sim/visuals/breadboard.ts new file mode 100644 index 00000000..02a5a858 --- /dev/null +++ b/sim/visuals/breadboard.ts @@ -0,0 +1,640 @@ +namespace pxsim.visuals { + // The distance between the center of two pins. This is the constant on which everything else is based. + const PIN_DIST = 15; + // CSS styling for the breadboard + const BLUE = "#1AA5D7"; + const RED = "#DD4BA0"; + const BREADBOARD_CSS = ` + /* bread board */ + .sim-bb-background { + fill:#E0E0E0; + } + .sim-bb-pin { + fill:#999; + } + .sim-bb-pin-hover { + visibility: hidden; + pointer-events: all; + stroke-width: ${PIN_DIST / 2}px; + stroke: transparent; + fill: #777; + } + .sim-bb-pin-hover:hover { + visibility: visible; + fill:#444; + } + .sim-bb-group-wire { + stroke: #999; + stroke-width: ${PIN_DIST / 4}px; + visibility: hidden; + } + .sim-bb-pin-group { + pointer-events: all; + } + .sim-bb-label, + .sim-bb-label-hover { + font-family:"Lucida Console", Monaco, monospace; + fill:#555; + pointer-events: all; + stroke-width: 0; + } + .sim-bb-label-hover { + visibility: hidden; + fill:#000; + font-weight: bold; + } + .sim-bb-bar { + stroke-width: 0; + } + .sim-bb-blue { + fill:${BLUE}; + stroke:${BLUE} + } + .sim-bb-red { + fill:${RED}; + stroke:${RED}; + } + .sim-bb-pin-group:hover .sim-bb-pin-hover, + .sim-bb-pin-group:hover .sim-bb-group-wire, + .sim-bb-pin-group:hover .sim-bb-label-hover { + visibility: visible; + } + .sim-bb-pin-group:hover .sim-bb-label { + visibility: hidden; + } + /* outline mode */ + .sim-bb-outline .sim-bb-background { + stroke-width: ${PIN_DIST / 7}px; + fill: #FFF; + stroke: #000; + } + .sim-bb-outline .sim-bb-mid-channel { + fill: #FFF; + stroke: #888; + stroke-width: 1px; + } + /* grayed out */ + .grayed .sim-bb-red, + .grayed .sim-bb-blue { + fill: #BBB; + } + .grayed .sim-bb-pin { + fill: #BBB; + } + .grayed .sim-bb-label { + fill: #BBB; + } + .grayed .sim-bb-background { + stroke: #BBB; + } + .grayed .sim-bb-group-wire { + stroke: #DDD; + } + /* highlighted */ + .sim-bb-label.highlight { + visibility: hidden; + } + .sim-bb-label-hover.highlight { + visibility: visible; + } + .sim-bb-blue.highlight { + fill:${BLUE}; + } + .sim-bb-red.highlight { + fill:${RED}; + } + ` + // Pin rows and coluns + const MID_ROWS = 10; + const MID_ROW_GAPS = [4, 4]; + const MID_ROW_AND_GAPS = MID_ROWS + MID_ROW_GAPS.length; + const MID_COLS = 30; + const BAR_ROWS = 2; + const BAR_COLS = 25; + const POWER_ROWS = BAR_ROWS * 2; + const POWER_COLS = BAR_COLS * 2; + const BAR_COL_GAPS = [4, 9, 14, 19]; + const BAR_COL_AND_GAPS = BAR_COLS + BAR_COL_GAPS.length; + // Essential dimensions + const WIDTH = PIN_DIST * (MID_COLS + 3); + const HEIGHT = PIN_DIST * (MID_ROW_AND_GAPS + POWER_ROWS + 5.5); + const MID_RATIO = 2.0 / 3.0; + const BAR_RATIO = (1.0 - MID_RATIO) * 0.5; + const MID_HEIGHT = HEIGHT * MID_RATIO; + const BAR_HEIGHT = HEIGHT * BAR_RATIO; + // Pin grids + const MID_GRID_WIDTH = (MID_COLS - 1) * PIN_DIST; + const MID_GRID_HEIGHT = (MID_ROW_AND_GAPS - 1) * PIN_DIST; + const MID_GRID_X = (WIDTH - MID_GRID_WIDTH) / 2.0; + const MID_GRID_Y = BAR_HEIGHT + (MID_HEIGHT - MID_GRID_HEIGHT) / 2.0; + const BAR_GRID_HEIGHT = (BAR_ROWS - 1) * PIN_DIST; + const BAR_GRID_WIDTH = (BAR_COL_AND_GAPS - 1) * PIN_DIST; + const BAR_TOP_GRID_X = (WIDTH - BAR_GRID_WIDTH) / 2.0; + const BAR_TOP_GRID_Y = (BAR_HEIGHT - BAR_GRID_HEIGHT) / 2.0; + const BAR_BOT_GRID_X = BAR_TOP_GRID_X; + const BAR_BOT_GRID_Y = BAR_TOP_GRID_Y + BAR_HEIGHT + MID_HEIGHT; + // Individual pins + const PIN_HOVER_SCALAR = 1.3; + const PIN_WIDTH = PIN_DIST / 2.5; + const PIN_ROUNDING = PIN_DIST / 7.5; + // Labels + const PIN_LBL_SIZE = PIN_DIST * 0.7; + const PIN_LBL_HOVER_SCALAR = 1.3; + const PLUS_LBL_SIZE = PIN_DIST * 1.7; + const MINUS_LBL_SIZE = PIN_DIST * 2; + const POWER_LBL_OFFSET = PIN_DIST * 0.8; + const MINUS_LBL_EXTRA_OFFSET = PIN_DIST * 0.07; + const LBL_ROTATION = -90; + // Channels + const CHANNEL_HEIGHT = PIN_DIST * 1.0; + const SMALL_CHANNEL_HEIGHT = PIN_DIST * 0.05; + // Background + const BACKGROUND_ROUNDING = PIN_DIST * 0.3; + + export interface GridPin { + el: SVGElement, + hoverEl: SVGElement, + cx: number, + cy: number, + row: string, + col: string, + group?: string + }; + export interface GridOptions { + xOffset?: number, + yOffset?: number, + rowCount: number, + colCount: number, + rowStartIdx?: number, + colStartIdx?: number, + pinDist: number, + mkPin: () => SVGElAndSize, + mkHoverPin: () => SVGElAndSize, + getRowName: (rowIdx: number) => string, + getColName: (colIdx: number) => string, + getGroupName?: (rowIdx: number, colIdx: number) => string, + rowIdxsWithGap?: number[], + colIdxsWithGap?: number[], + }; + export interface GridResult { + g: SVGGElement, + allPins: GridPin[], + } + export function mkGrid(opts: GridOptions): GridResult { + let xOff = opts.xOffset || 0; + let yOff = opts.yOffset || 0; + let allPins: GridPin[] = []; + let grid = svg.elt("g"); + let colIdxOffset = opts.colStartIdx || 0; + let rowIdxOffset = opts.rowStartIdx || 0; + let copyArr = (arr: T[]): T[] => arr ? arr.slice(0, arr.length) : []; + let removeAll = (arr: T[], e: T): number => { + let res = 0; + let idx: number; + while (0 <= (idx = arr.indexOf(e))) { + arr.splice(idx, 1); + res += 1; + } + return res; + }; + let rowGaps = 0; + let rowIdxsWithGap = copyArr(opts.rowIdxsWithGap) + for (let i = 0; i < opts.rowCount; i++) { + let colGaps = 0; + let colIdxsWithGap = copyArr(opts.colIdxsWithGap) + let cy = yOff + i * opts.pinDist + rowGaps * opts.pinDist; + let rowIdx = i + rowIdxOffset; + for (let j = 0; j < opts.colCount; j++) { + let cx = xOff + j * opts.pinDist + colGaps * opts.pinDist; + let colIdx = j + colIdxOffset; + const addEl = (pin: SVGElAndSize) => { + let pinX = cx - pin.w * 0.5; + let pinY = cy - pin.h * 0.5; + svg.hydrate(pin.el, {x: pinX, y: pinY}); + grid.appendChild(pin.el); + return pin.el; + } + let el = addEl(opts.mkPin()); + let hoverEl = addEl(opts.mkHoverPin()); + let row = opts.getRowName(rowIdx); + let col = opts.getColName(colIdx); + let group = opts.getGroupName ? opts.getGroupName(rowIdx, colIdx) : null; + let gridPin: GridPin = {el: el, hoverEl: hoverEl, cx: cx, cy: cy, row: row, col: col, group: group}; + allPins.push(gridPin); + //column gaps + colGaps += removeAll(colIdxsWithGap, colIdx); + } + //row gaps + rowGaps += removeAll(rowIdxsWithGap, rowIdx); + } + return {g: grid, allPins: allPins}; + } + function mkBBPin(): SVGElAndSize { + let el = svg.elt("rect"); + let width = PIN_WIDTH; + svg.hydrate(el, { + class: "sim-bb-pin", + rx: PIN_ROUNDING, + ry: PIN_ROUNDING, + width: width, + height: width + }); + return {el: el, w: width, h: width, x: 0, y: 0}; + } + function mkBBHoverPin(): SVGElAndSize { + let el = svg.elt("rect"); + let width = PIN_WIDTH * PIN_HOVER_SCALAR; + svg.hydrate(el, { + class: "sim-bb-pin-hover", + rx: PIN_ROUNDING, + ry: PIN_ROUNDING, + width: width, + height: width, + }); + return {el: el, w: width, h: width, x: 0, y: 0}; + } + export interface GridLabel { + el: SVGTextElement, + hoverEl: SVGTextElement, + txt: string, + group?: string, + }; + function mkBBLabel(cx: number, cy: number, size: number, rotation: number, txt: string, group: string, extraClasses?: string[]): GridLabel { + //lbl + let el = mkTxt(cx, cy, size, rotation, txt); + svg.addClass(el, "sim-bb-label"); + if (extraClasses) + extraClasses.forEach(c => svg.addClass(el, c)); + + //hover lbl + let hoverEl = mkTxt(cx, cy, size * PIN_LBL_HOVER_SCALAR, rotation, txt); + svg.addClass(hoverEl, "sim-bb-label-hover"); + if (extraClasses) + extraClasses.forEach(c => svg.addClass(hoverEl, c)); + + let lbl = {el: el, hoverEl: hoverEl, txt: txt, group: group}; + return lbl; + } + interface BBBar { + el: SVGRectElement, + group?: string + }; + export class Breadboard { + public bb: SVGSVGElement; + private styleEl: SVGStyleElement; + private defs: SVGDefsElement; + + //truth + private allPins: GridPin[] = []; + private allLabels: GridLabel[] = []; + private allPowerBars: BBBar[] = []; + //quick lookup caches + private rowColToPin: Map> = {}; + private rowColToLbls: Map> = {}; + + constructor() { + this.buildDom(); + } + + public updateLocation(x: number, y: number) { + svg.hydrate(this.bb, { + x: `${x}px`, + y: `${y}px`, + }); + } + + public getPin(row: string, col: string): GridPin { + let colToPin = this.rowColToPin[row]; + if (!colToPin) + return null; + let pin = colToPin[col]; + if (!pin) + return null; + return pin; + } + public getCoord(rowCol: BBRowCol): Coord { + let [row, col] = rowCol; + let pin = this.getPin(row, col); + if (!pin) + return null; + return [pin.cx, pin.cy]; + } + + public getPinDist() { + return PIN_DIST; + } + + private buildDom() { + this.bb = svg.elt("svg", { + "version": "1.0", + "viewBox": `0 0 ${WIDTH} ${HEIGHT}`, + "class": `sim-bb`, + "width": WIDTH + "px", + "height": HEIGHT + "px", + }); + this.styleEl = svg.child(this.bb, "style", {}); + this.styleEl.textContent += BREADBOARD_CSS; + this.defs = svg.child(this.bb, "defs", {}); + + //background + svg.child(this.bb, "rect", { class: "sim-bb-background", width: WIDTH, height: HEIGHT, rx: BACKGROUND_ROUNDING, ry: BACKGROUND_ROUNDING}); + + //mid channel + let channelGid = "sim-bb-channel-grad"; + let channelGrad = svg.elt("linearGradient") + svg.hydrate(channelGrad, { id: channelGid, x1: "0%", y1: "0%", x2: "0%", y2: "100%" }); + this.defs.appendChild(channelGrad); + let channelDark = "#AAA"; + let channelLight = "#CCC"; + let stop1 = svg.child(channelGrad, "stop", { offset: "0%", style: `stop-color: ${channelDark};` }) + let stop2 = svg.child(channelGrad, "stop", { offset: "20%", style: `stop-color: ${channelLight};` }) + let stop3 = svg.child(channelGrad, "stop", { offset: "80%", style: `stop-color: ${channelLight};` }) + let stop4 = svg.child(channelGrad, "stop", { offset: "100%", style: `stop-color: ${channelDark};` }) + + const mkChannel = (cy: number, h: number, cls?: string) => { + let channel = svg.child(this.bb, "rect", { class: `sim-bb-channel ${cls || ""}`, y: cy - h / 2, width: WIDTH, height: h}); + channel.setAttribute("fill", `url(#${channelGid})`); + return channel; + } + + mkChannel(BAR_HEIGHT + MID_HEIGHT / 2, CHANNEL_HEIGHT, "sim-bb-mid-channel"); + mkChannel(BAR_HEIGHT, SMALL_CHANNEL_HEIGHT); + mkChannel(BAR_HEIGHT + MID_HEIGHT, SMALL_CHANNEL_HEIGHT); + + //-----pins + const getMidTopOrBot = (rowIdx: number) => rowIdx < MID_ROWS / 2.0 ? "b" : "t"; + const getBarTopOrBot = (colIdx: number) => colIdx < POWER_COLS / 2.0 ? "b" : "t"; + const alphabet = "abcdefghij".split("").reverse(); + const getColName = (colIdx: number) => `${colIdx + 1}`; + const getMidRowName = (rowIdx: number) => alphabet[rowIdx]; + const getMidGroupName = (rowIdx: number, colIdx: number) => { + let botOrTop = getMidTopOrBot(rowIdx); + let colNm = getColName(colIdx); + return `${botOrTop}${colNm}`; + }; + const getBarRowName = (rowIdx: number) => rowIdx === 0 ? "-" : "+"; + const getBarGroupName = (rowIdx: number, colIdx: number) => { + let botOrTop = getBarTopOrBot(colIdx); + let rowName = getBarRowName(rowIdx); + return `${rowName}${botOrTop}`; + }; + + //mid grid + let midGridRes = mkGrid({ + xOffset: MID_GRID_X, + yOffset: MID_GRID_Y, + rowCount: MID_ROWS, + colCount: MID_COLS, + pinDist: PIN_DIST, + mkPin: mkBBPin, + mkHoverPin: mkBBHoverPin, + getRowName: getMidRowName, + getColName: getColName, + getGroupName: getMidGroupName, + rowIdxsWithGap: MID_ROW_GAPS, + }); + let midGridG = midGridRes.g; + this.allPins = this.allPins.concat(midGridRes.allPins); + + //bot bar + let botBarGridRes = mkGrid({ + xOffset: BAR_BOT_GRID_X, + yOffset: BAR_BOT_GRID_Y, + rowCount: BAR_ROWS, + colCount: BAR_COLS, + pinDist: PIN_DIST, + mkPin: mkBBPin, + mkHoverPin: mkBBHoverPin, + getRowName: getBarRowName, + getColName: getColName, + getGroupName: getBarGroupName, + colIdxsWithGap: BAR_COL_GAPS, + }); + let botBarGridG = botBarGridRes.g; + this.allPins = this.allPins.concat(botBarGridRes.allPins); + + //top bar + let topBarGridRes = mkGrid({ + xOffset: BAR_TOP_GRID_X, + yOffset: BAR_TOP_GRID_Y, + rowCount: BAR_ROWS, + colCount: BAR_COLS, + colStartIdx: BAR_COLS, + pinDist: PIN_DIST, + mkPin: mkBBPin, + mkHoverPin: mkBBHoverPin, + getRowName: getBarRowName, + getColName: getColName, + getGroupName: getBarGroupName, + colIdxsWithGap: BAR_COL_GAPS.map(g => g + BAR_COLS), + }); + let topBarGridG = topBarGridRes.g; + this.allPins = this.allPins.concat(topBarGridRes.allPins); + + //tooltip + this.allPins.forEach(pin => { + let {el, row, col, hoverEl} = pin + let title = `(${row},${col})`; + svg.hydrate(el, {title: title}); + svg.hydrate(hoverEl, {title: title}); + }) + + //catalog pins + this.allPins.forEach(pin => { + let colToPin = this.rowColToPin[pin.row]; + if (!colToPin) + colToPin = this.rowColToPin[pin.row] = {}; + colToPin[pin.col] = pin; + }) + + //-----labels + const mkBBLabelAtPin = (row: string, col: string, xOffset: number, yOffset: number, txt: string, group?: string): GridLabel => { + let size = PIN_LBL_SIZE; + let rotation = LBL_ROTATION; + let loc = this.getCoord([row, col]); + let [cx, cy] = loc; + let t = mkBBLabel(cx + xOffset, cy + yOffset, size, rotation, txt, group); + return t; + } + + //columns + for (let colIdx = 0; colIdx < MID_COLS; colIdx++) { + let colNm = getColName(colIdx); + //top + let rowTIdx = 0; + let rowTNm = getMidRowName(rowTIdx); + let groupT = getMidGroupName(rowTIdx, colIdx); + let lblT = mkBBLabelAtPin(rowTNm, colNm, 0, -PIN_DIST, colNm, groupT); + this.allLabels.push(lblT); + //bottom + let rowBIdx = MID_ROWS - 1; + let rowBNm = getMidRowName(rowBIdx); + let groupB = getMidGroupName(rowBIdx, colIdx); + let lblB = mkBBLabelAtPin(rowBNm, colNm, 0, +PIN_DIST, colNm, groupB); + this.allLabels.push(lblB); + } + //rows + for (let rowIdx = 0; rowIdx < MID_ROWS; rowIdx++) { + let rowNm = getMidRowName(rowIdx); + //top + let colTIdx = 0; + let colTNm = getColName(colTIdx); + let lblT = mkBBLabelAtPin(rowNm, colTNm, -PIN_DIST, 0, rowNm); + this.allLabels.push(lblT); + //top + let colBIdx = MID_COLS - 1; + let colBNm = getColName(colBIdx); + let lblB = mkBBLabelAtPin(rowNm, colBNm, +PIN_DIST, 0, rowNm); + this.allLabels.push(lblB); + } + + //+- labels + let botPowerLabels = [ + //BL + mkBBLabel(0 + POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, BAR_HEIGHT + MID_HEIGHT + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, 0), [`sim-bb-blue`]), + mkBBLabel(0 + POWER_LBL_OFFSET, BAR_HEIGHT + MID_HEIGHT + BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, 0), [`sim-bb-red`]), + //BR + mkBBLabel(WIDTH - POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, BAR_HEIGHT + MID_HEIGHT + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, BAR_COLS - 1), [`sim-bb-blue`]), + mkBBLabel(WIDTH - POWER_LBL_OFFSET, BAR_HEIGHT + MID_HEIGHT + BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, BAR_COLS - 1), [`sim-bb-red`]), + ]; + this.allLabels = this.allLabels.concat(botPowerLabels); + let topPowerLabels = [ + //TL + mkBBLabel(0 + POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, 0 + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, BAR_COLS), [`sim-bb-blue`]), + mkBBLabel(0 + POWER_LBL_OFFSET, BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, BAR_COLS), [`sim-bb-red`]), + //TR + mkBBLabel(WIDTH - POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, 0 + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, POWER_COLS - 1), [`sim-bb-blue`]), + mkBBLabel(WIDTH - POWER_LBL_OFFSET, BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, POWER_COLS - 1), [`sim-bb-red`]), + ]; + this.allLabels = this.allLabels.concat(topPowerLabels); + + //catalog lbls + let lblNmToLbls: Map = {}; + this.allLabels.forEach(lbl => { + let {el, txt} = lbl; + let lbls = lblNmToLbls[txt] = lblNmToLbls[txt] || [] + lbls.push(lbl); + }); + const isPowerPin = (pin: GridPin) => pin.row === "-" || pin.row === "+"; + this.allPins.forEach(pin => { + let {row, col, group} = pin; + let colToLbls = this.rowColToLbls[row] || (this.rowColToLbls[row] = {}); + let lbls = colToLbls[col] || (colToLbls[col] = []); + if (isPowerPin(pin)) { + //power pins + let isBot = Number(col) <= BAR_COLS; + if (isBot) + botPowerLabels.filter(l => l.group == pin.group).forEach(l => lbls.push(l)); + else + topPowerLabels.filter(l => l.group == pin.group).forEach(l => lbls.push(l)); + } else { + //mid pins + let rowLbls = lblNmToLbls[row]; + rowLbls.forEach(l => lbls.push(l)); + let colLbls = lblNmToLbls[col]; + colLbls.forEach(l => lbls.push(l)); + } + }) + + //-----blue & red lines + const lnLen = BAR_GRID_WIDTH + PIN_DIST * 1.5; + const lnThickness = PIN_DIST / 5.0; + const lnYOff = PIN_DIST * 0.6; + const lnXOff = (lnLen - BAR_GRID_WIDTH) / 2.0; + const mkPowerLine = (x: number, y: number, group: string, cls: string): BBBar => { + let ln = svg.elt("rect"); + svg.hydrate(ln, { + class: `sim-bb-bar ${cls}`, + x: x, + y: y - lnThickness / 2.0, + width: lnLen, + height: lnThickness}); + let bar: BBBar = {el: ln, group: group}; + return bar; + } + let barLines = [ + //top + mkPowerLine(BAR_BOT_GRID_X - lnXOff, BAR_BOT_GRID_Y - lnYOff, getBarGroupName(0, POWER_COLS - 1), "sim-bb-blue"), + mkPowerLine(BAR_BOT_GRID_X - lnXOff, BAR_BOT_GRID_Y + PIN_DIST + lnYOff, getBarGroupName(1, POWER_COLS - 1), "sim-bb-red"), + //bot + mkPowerLine(BAR_TOP_GRID_X - lnXOff, BAR_TOP_GRID_Y - lnYOff, getBarGroupName(0, 0), "sim-bb-blue"), + mkPowerLine(BAR_TOP_GRID_X - lnXOff, BAR_TOP_GRID_Y + PIN_DIST + lnYOff, getBarGroupName(1, 0), "sim-bb-red"), + ]; + this.allPowerBars = this.allPowerBars.concat(barLines); + //attach power bars + this.allPowerBars.forEach(b => this.bb.appendChild(b.el)); + + //-----electrically connected groups + //make groups + let allGrpNms = this.allPins.map(p => p.group).filter((g, i, a) => a.indexOf(g) == i); + let groups: SVGGElement[] = allGrpNms.map(grpNm => { + let g = svg.elt("g"); + return g; + }); + groups.forEach(g => svg.addClass(g, "sim-bb-pin-group")); + groups.forEach((g, i) => svg.addClass(g, `group-${allGrpNms[i]}`)); + let grpNmToGroup: Map = {}; + allGrpNms.forEach((g, i) => grpNmToGroup[g] = groups[i]); + //group pins and add connecting wire + let grpNmToPins: Map = {}; + this.allPins.forEach((p, i) => { + let g = p.group; + let pins = grpNmToPins[g] || (grpNmToPins[g] = []); + pins.push(p); + }); + //connecting wire + allGrpNms.forEach(grpNm => { + let pins = grpNmToPins[grpNm]; + let [xs, ys] = [pins.map(p => p.cx), pins.map(p => p.cy)]; + let minFn = (arr: number[]) => arr.reduce((a, b) => a < b ? a : b); + let maxFn = (arr: number[]) => arr.reduce((a, b) => a > b ? a : b); + let [minX, maxX, minY, maxY] = [minFn(xs), maxFn(xs), minFn(ys), maxFn(ys)]; + let wire = svg.elt("rect"); + let width = Math.max(maxX - minX, 0.0001/*rects with no width aren't displayed*/); + let height = Math.max(maxY - minY, 0.0001); + svg.hydrate(wire, {x: minX, y: minY, width: width, height: height}); + svg.addClass(wire, "sim-bb-group-wire") + let g = grpNmToGroup[grpNm]; + g.appendChild(wire); + }); + //group pins + this.allPins.forEach(p => { + let g = grpNmToGroup[p.group]; + g.appendChild(p.el); + g.appendChild(p.hoverEl); + }) + //group lbls + let miscLblGroup = svg.elt("g"); + svg.hydrate(miscLblGroup, {class: "sim-bb-group-misc"}); + groups.push(miscLblGroup); + this.allLabels.forEach(l => { + if (l.group) { + let g = grpNmToGroup[l.group]; + g.appendChild(l.el); + g.appendChild(l.hoverEl); + } else { + miscLblGroup.appendChild(l.el); + miscLblGroup.appendChild(l.hoverEl); + } + }) + + //attach to bb + groups.forEach(g => this.bb.appendChild(g)); //attach to breadboard + } + + public getSVGAndSize(): SVGAndSize { + return {el: this.bb, y: 0, x: 0, w: WIDTH, h: HEIGHT}; + } + + public highlightLoc(row: string, col: string) { + let pin = this.rowColToPin[row][col]; + let {cx, cy} = pin; + let lbls = this.rowColToLbls[row][col]; + const highlightLbl = (lbl: GridLabel) => { + svg.addClass(lbl.el, "highlight"); + svg.addClass(lbl.hoverEl, "highlight"); + }; + lbls.forEach(highlightLbl); + } + } +} \ No newline at end of file diff --git a/sim/visuals/buttonpair.ts b/sim/visuals/buttonpair.ts new file mode 100644 index 00000000..9ae5e7f4 --- /dev/null +++ b/sim/visuals/buttonpair.ts @@ -0,0 +1,204 @@ +/// +/// +/// + +namespace pxsim.visuals { + export function mkBtnSvg(xy: Coord): SVGAndSize { + let [innerCls, outerCls] = ["sim-button", "sim-button-outer"]; + const tabSize = PIN_DIST / 2.5; + const pegR = PIN_DIST / 5; + const btnR = PIN_DIST * .8; + const pegMargin = PIN_DIST / 8; + const plateR = PIN_DIST / 12; + + const pegOffset = pegMargin + pegR; + let [x, y] = xy; + const left = x - tabSize / 2; + const top = y - tabSize / 2; + const plateH = 3 * PIN_DIST - tabSize; + const plateW = 2 * PIN_DIST + tabSize; + const plateL = left; + const plateT = top + tabSize; + const btnCX = plateL + plateW / 2; + const btnCY = plateT + plateH / 2; + + let btng = svg.elt("g"); + //tabs + const mkTab = (x: number, y: number) => { + svg.child(btng, "rect", { class: "sim-button-tab", x: x, y: y, width: tabSize, height: tabSize}) + } + mkTab(left, top); + mkTab(left + 2 * PIN_DIST, top); + mkTab(left, top + 3 * PIN_DIST); + mkTab(left + 2 * PIN_DIST, top + 3 * PIN_DIST); + + //plate + svg.child(btng, "rect", { class: outerCls, x: plateL, y: plateT, rx: plateR, ry: plateR, width: plateW, height: plateH }); + + //pegs + const mkPeg = (x: number, y: number) => { + svg.child(btng, "circle", { class: "sim-button-nut", cx: x, cy: y, r: pegR }); + } + mkPeg(plateL + pegOffset, plateT + pegOffset) + mkPeg(plateL + plateW - pegOffset, plateT + pegOffset) + mkPeg(plateL + pegOffset, plateT + plateH - pegOffset) + mkPeg(plateL + plateW - pegOffset, plateT + plateH - pegOffset) + + //inner btn + let innerBtn = svg.child(btng, "circle", { class: innerCls, cx: btnCX, cy: btnCY, r: btnR }); + + //return + return { el: btng, y: top, x: left, w: plateW, h: plateH + 2 * tabSize }; + } + export const BUTTON_PAIR_STYLE = ` + .sim-button { + pointer-events: none; + fill: #000; + } + .sim-button-outer:active ~ .sim-button, + .sim-button-virtual:active { + fill: #FFA500; + } + .sim-button-outer { + cursor: pointer; + fill: #979797; + } + .sim-button-outer:hover { + stroke:gray; + stroke-width: ${PIN_DIST / 5}px; + } + .sim-button-nut { + fill:#000; + pointer-events:none; + } + .sim-button-nut:hover { + stroke:${PIN_DIST / 15}px solid #704A4A; + } + .sim-button-tab { + fill:#FFF; + pointer-events:none; + } + .sim-button-virtual { + cursor: pointer; + fill: rgba(255, 255, 255, 0.6); + stroke: rgba(255, 255, 255, 1); + stroke-width: ${PIN_DIST / 5}px; + } + .sim-button-virtual:hover { + stroke: rgba(128, 128, 128, 1); + } + .sim-text-virtual { + fill: #000; + pointer-events:none; + } + `; + export class ButtonPairView implements IBoardComponent { + public element: SVGElement; + public defs: SVGElement[]; + public style = BUTTON_PAIR_STYLE; + private state: ButtonPairState; + private bus: EventBus; + private aBtn: SVGGElement; + private bBtn: SVGGElement; + private abBtn: SVGGElement; + + public init(bus: EventBus, state: ButtonPairState) { + this.state = state; + this.bus = bus; + this.defs = []; + this.element = this.mkBtns(); + this.updateState(); + this.attachEvents(); + } + + public moveToCoord(xy: Coord) { + let btnWidth = PIN_DIST * 3; + let [x, y] = xy; + translateEl(this.aBtn, [x, y]) + translateEl(this.bBtn, [x + btnWidth, y]) + translateEl(this.abBtn, [x + PIN_DIST * 1.5, y + PIN_DIST * 4]) + } + + public updateState() { + let stateBtns = [this.state.aBtn, this.state.bBtn, this.state.abBtn]; + let svgBtns = [this.aBtn, this.bBtn, this.abBtn]; + + if (this.state.usesButtonAB && this.abBtn.style.visibility != "visible") { + this.abBtn.style.visibility = "visible"; + } + } + + public updateTheme() {} + + private mkBtns() { + this.aBtn = mkBtnSvg([0, 0]).el; + this.bBtn = mkBtnSvg([0, 0]).el; + + const mkVirtualBtn = () => { + const numPins = 2; + const w = PIN_DIST * 2.8; + const offset = (w - (numPins * PIN_DIST)) / 2; + const corner = PIN_DIST / 2; + const cx = 0 - offset + w / 2; + const cy = cx; + const txtSize = PIN_DIST * 1.3; + const x = -offset; + const y = -offset; + const txtXOff = PIN_DIST / 7; + const txtYOff = PIN_DIST / 10; + + let btng = svg.elt("g"); + let btn = svg.child(btng, "rect", { class: "sim-button-virtual", x: x, y: y, rx: corner, ry: corner, width: w, height: w}); + let btnTxt = mkTxt(cx + txtXOff, cy + txtYOff, txtSize, 0, "A+B"); + svg.addClass(btnTxt, "sim-text") + svg.addClass(btnTxt, "sim-text-virtual"); + btng.appendChild(btnTxt); + + return btng; + } + + this.abBtn = mkVirtualBtn(); + this.abBtn.style.visibility = "hidden"; + + let el = svg.elt("g"); + svg.addClass(el, "sim-buttonpair") + el.appendChild(this.aBtn); + el.appendChild(this.bBtn); + el.appendChild(this.abBtn); + + return el; + } + + private attachEvents() { + let btnStates = [this.state.aBtn, this.state.bBtn]; + let btnSvgs = [this.aBtn, this.bBtn]; + btnSvgs.forEach((btn, index) => { + btn.addEventListener(pointerEvents.down, ev => { + btnStates[index].pressed = true; + }) + btn.addEventListener(pointerEvents.leave, ev => { + btnStates[index].pressed = false; + }) + btn.addEventListener(pointerEvents.up, ev => { + btnStates[index].pressed = false; + this.bus.queue(btnStates[index].id, DAL.MICROBIT_BUTTON_EVT_UP); + this.bus.queue(btnStates[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); + }) + }) + let updateBtns = (s: boolean) => { + btnStates.forEach(b => b.pressed = s) + }; + this.abBtn.addEventListener(pointerEvents.down, ev => { + updateBtns(true); + }) + this.abBtn.addEventListener(pointerEvents.leave, ev => { + updateBtns(false); + }) + this.abBtn.addEventListener(pointerEvents.up, ev => { + updateBtns(false); + this.bus.queue(this.state.abBtn.id, DAL.MICROBIT_BUTTON_EVT_UP); + this.bus.queue(this.state.abBtn.id, DAL.MICROBIT_BUTTON_EVT_CLICK); + }) + } + } +} \ No newline at end of file diff --git a/sim/visuals/genericboard.ts b/sim/visuals/genericboard.ts new file mode 100644 index 00000000..76ca57b4 --- /dev/null +++ b/sim/visuals/genericboard.ts @@ -0,0 +1,515 @@ +/// +/// +/// + +namespace pxsim.visuals { + const svg = pxsim.svg; + + export interface IBoardSvgProps { + runtime: pxsim.Runtime; + boardDef: BoardDefinition; + disableTilt?: boolean; + activeComponents: string[]; + fnArgs?: any; + componentDefinitions: Map; + } + + export const VIEW_WIDTH = 498; + export const VIEW_HEIGHT = 725; + const TOP_MARGIN = 20; + const MID_MARGIN = 40; + const BOT_MARGIN = 20; + const PIN_LBL_SIZE = PIN_DIST * 0.7; + const PIN_LBL_HOVER_SIZE = PIN_LBL_SIZE * 1.5; + const SQUARE_PIN_WIDTH = PIN_DIST * 0.66666; + const SQUARE_PIN_HOVER_WIDTH = PIN_DIST * 0.66666 + PIN_DIST / 3.0; + + export type ComputedBoardDimensions = { + scaleFn: (n: number) => number, + height: number, + width: number, + xOff: number, + yOff: number + }; + export function getBoardDimensions(vis: BoardImageDefinition): ComputedBoardDimensions { + let scaleFn = (n: number) => n * (PIN_DIST / vis.pinDist); + let width = scaleFn(vis.width); + return { + scaleFn: scaleFn, + height: scaleFn(vis.height), + width: width, + xOff: (VIEW_WIDTH - width) / 2.0, + yOff: TOP_MARGIN + } + } + + export const BOARD_SYTLE = ` + .noselect { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Chrome/Safari/Opera */ + -khtml-user-select: none; /* Konqueror */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + not supported by any browser */ + } + svg.sim.grayscale { + -moz-filter: grayscale(1); + -webkit-filter: grayscale(1); + filter: grayscale(1); + } + + .sim-text { + font-family:"Lucida Console", Monaco, monospace; + font-size:25px; + fill:#fff; + pointer-events: none; + } + + /* animations */ + .sim-theme-glow { + animation-name: sim-theme-glow-animation; + animation-timing-function: ease-in-out; + animation-direction: alternate; + animation-iteration-count: infinite; + animation-duration: 1.25s; + } + @keyframes sim-theme-glow-animation { + from { opacity: 1; } + to { opacity: 0.75; } + } + + .sim-flash { + animation-name: sim-flash-animation; + animation-duration: 0.1s; + } + + @keyframes sim-flash-animation { + from { fill: yellow; } + to { fill: default; } + } + + .sim-flash-stroke { + animation-name: sim-flash-stroke-animation; + animation-duration: 0.4s; + animation-timing-function: ease-in; + } + + @keyframes sim-flash-stroke-animation { + from { stroke: yellow; } + to { stroke: default; } + } + + .sim-board-pin { + fill:#999; + stroke:#000; + stroke-width:${PIN_DIST / 3.0}px; + } + .sim-board-pin-lbl { + fill: #333; + } + .gray-cover { + fill:#FFF; + opacity: 0.7; + stroke-width:0; + visibility: hidden; + } + .sim-board-pin-hover { + visibility: hidden; + pointer-events: all; + stroke-width:${PIN_DIST / 6.0}px; + } + .sim-board-pin-hover:hover { + visibility: visible; + } + .sim-board-pin-lbl { + visibility: hidden; + } + .sim-board-outline .sim-board-pin-lbl { + visibility: visible; + } + .sim-board-pin-lbl { + fill: #555; + } + .sim-board-pin-lbl-hover { + fill: red; + } + .sim-board-outline .sim-board-pin-lbl-hover { + fill: black; + } + .sim-board-pin-lbl, + .sim-board-pin-lbl-hover { + font-family:"Lucida Console", Monaco, monospace; + pointer-events: all; + stroke-width: 0; + } + .sim-board-pin-lbl-hover { + visibility: hidden; + } + .sim-board-outline .sim-board-pin-hover:hover + .sim-board-pin-lbl, + .sim-board-pin-lbl.highlight { + visibility: hidden; + } + .sim-board-outline .sim-board-pin-hover:hover + * + .sim-board-pin-lbl-hover, + .sim-board-pin-lbl-hover.highlight { + visibility: visible; + } + /* Graying out */ + .grayed .sim-board-pin-lbl:not(.highlight) { + fill: #AAA; + } + .grayed .sim-board-pin:not(.highlight) { + fill:#BBB; + stroke:#777; + } + .grayed .gray-cover { + visibility: inherit; + } + .grayed .sim-cmp:not(.notgrayed) { + opacity: 0.3; + } + /* Highlighting */ + .sim-board-pin-lbl.highlight { + fill: #000; + font-weight: bold; + } + .sim-board-pin.highlight { + fill:#999; + stroke:#000; + } + `; + + let nextBoardId = 0; + export class GenericBoardSvg { + public hostElement: SVGSVGElement; + private style: SVGStyleElement; + private defs: SVGDefsElement; + private g: SVGGElement; + public board: pxsim.DalBoard; + public background: SVGElement; + private components: IBoardComponent[]; + public breadboard: Breadboard; + private underboard: SVGGElement; + public boardDef: BoardDefinition; + private boardDim: ComputedBoardDimensions; + public componentDefs: Map; + private boardEdges: number[]; + private id: number; + public bbX: number; + public bbY: number; + private boardTopEdge: number; + private boardBotEdge: number; + private wireFactory: WireFactory; + //truth + private allPins: GridPin[] = []; + private allLabels: GridLabel[] = []; + //cache + private pinNmToLbl: Map = {}; + private pinNmToPin: Map = {}; + + constructor(public props: IBoardSvgProps) { + this.id = nextBoardId++; + this.boardDef = props.boardDef; + this.boardDim = getBoardDimensions(this.boardDef.visual); + this.board = this.props.runtime.board as pxsim.DalBoard; + this.board.updateView = () => this.updateState(); + this.hostElement = svg.elt("svg") + svg.hydrate(this.hostElement, { + "version": "1.0", + "viewBox": `0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`, + "enable-background": `new 0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`, + "class": `sim sim-board-id-${this.id}`, + "x": "0px", + "y": "0px" + }); + this.style = svg.child(this.hostElement, "style", {}); + this.style.textContent += BOARD_SYTLE; + this.defs = svg.child(this.hostElement, "defs", {}); + this.g = svg.elt("g"); + this.hostElement.appendChild(this.g); + this.underboard = svg.child(this.g, "g", {class: "sim-underboard"}); + this.components = []; + this.componentDefs = props.componentDefinitions; + + // breadboard + this.breadboard = new Breadboard() + this.g.appendChild(this.breadboard.bb); + let bbSize = this.breadboard.getSVGAndSize(); + let [bbWidth, bbHeight] = [bbSize.w, bbSize.h]; + const bbX = (VIEW_WIDTH - bbWidth) / 2; + this.bbX = bbX; + const bbY = TOP_MARGIN + this.boardDim.height + MID_MARGIN; + this.bbY = bbY; + this.breadboard.updateLocation(bbX, bbY); + + // edges + this.boardTopEdge = TOP_MARGIN; + this.boardBotEdge = TOP_MARGIN + this.boardDim.height; + this.boardEdges = [this.boardTopEdge, this.boardBotEdge, bbY, bbY + bbHeight] + + this.wireFactory = new WireFactory(this.underboard, this.g, this.boardEdges, this.style, this.getLocCoord.bind(this)); + + this.buildDom(); + + this.updateTheme(); + this.updateState(); + + let cmps = props.activeComponents; + if (cmps.length) { + let allocRes = allocateDefinitions({ + boardDef: this.boardDef, + cmpDefs: this.componentDefs, + fnArgs: this.props.fnArgs, + getBBCoord: this.getBBCoord.bind(this), + cmpList: props.activeComponents, + }); + this.addAll(allocRes); + } + } + + private getBoardPinCoord(pinNm: string): Coord { + let pin = this.pinNmToPin[pinNm]; + if (!pin) + return null; + return [pin.cx, pin.cy]; + } + private getBBCoord(rowCol: BBRowCol): Coord { + let bbCoord = this.breadboard.getCoord(rowCol); + if (!bbCoord) + return null; + let [x, y] = bbCoord; + return [x + this.bbX, y + this.bbY]; + } + + public getLocCoord(loc: Loc): Coord { + let coord: Coord; + if (loc.type === "breadboard") { + let rowCol = (loc).rowCol; + coord = this.getBBCoord(rowCol); + } else { + let pinNm = (loc).pin; + coord = this.getBoardPinCoord(pinNm); + } + if (!coord) { + console.error("Unknown location: " + name) + return [0, 0]; + } + return coord; + } + + private mkGrayCover(x: number, y: number, w: number, h: number) { + let rect = svg.elt("rect"); + svg.hydrate(rect, {x: x, y: y, width: w, height: h, class: "gray-cover"}); + return rect; + } + + private getCmpClass = (type: string) => `sim-${type}-cmp`; + + public addWire(inst: WireInst): Wire { + return this.wireFactory.addWire(inst.start, inst.end, inst.color); + } + public addAll(basicWiresAndCmpsAndWires: AllocatorResult) { + let {powerWires, components} = basicWiresAndCmpsAndWires; + powerWires.forEach(w => this.addWire(w)); + components.forEach((cAndWs, idx) => { + let {component, wires} = cAndWs; + wires.forEach(w => this.addWire(w)); + this.addComponent(component); + }); + } + + public addComponent(cmpDesc: CmpInst): IBoardComponent { + let cmp: IBoardComponent = null; + if (typeof cmpDesc.visual === "string") { + let builtinVisual = cmpDesc.visual as string; + let cnstr = builtinComponentSimVisual[builtinVisual]; + let stateFn = builtinComponentSimState[builtinVisual]; + let state = stateFn(this.board); + cmp = cnstr(); + cmp.init(this.board.bus, state, this.hostElement, cmpDesc.microbitPins, cmpDesc.otherArgs); + this.components.push(cmp); + this.g.appendChild(cmp.element); + if (cmp.defs) + cmp.defs.forEach(d => this.defs.appendChild(d)); + this.style.textContent += cmp.style || ""; + let rowCol = [`${cmpDesc.breadboardStartRow}`, `${cmpDesc.breadboardStartColumn}`]; + let coord = this.getBBCoord(rowCol); + cmp.moveToCoord(coord); + let cls = this.getCmpClass(name); + svg.addClass(cmp.element, cls); + svg.addClass(cmp.element, "sim-cmp"); + cmp.updateTheme(); + cmp.updateState(); + } else { + //TODO: adding generic components + } + return cmp; + } + + private updateTheme() { + this.components.forEach(c => c.updateTheme()); + } + + public updateState() { + let state = this.board; + if (!state) return; + + this.components.forEach(c => c.updateState()); + + if (!runtime || runtime.dead) svg.addClass(this.hostElement, "grayscale"); + else svg.removeClass(this.hostElement, "grayscale"); + } + + private buildDom() { + + // filters + let glow = svg.child(this.defs, "filter", { id: "filterglow", x: "-5%", y: "-5%", width: "120%", height: "120%" }); + svg.child(glow, "feGaussianBlur", { stdDeviation: "5", result: "glow" }); + let merge = svg.child(glow, "feMerge", {}); + for (let i = 0; i < 3; ++i) + svg.child(merge, "feMergeNode", { in: "glow" }) + + // main board + this.background = svg.child(this.g, "image", + { class: "sim-board", x: this.boardDim.xOff, y: this.boardDim.yOff, width: this.boardDim.width, height: this.boardDim.height, + "href": `${(this.boardDef.visual).image}`}); + let backgroundCover = this.mkGrayCover(this.boardDim.xOff, this.boardDim.yOff, this.boardDim.width, this.boardDim.height); + this.g.appendChild(backgroundCover); + + // ----- pins + const mkSquarePin = (): SVGElAndSize => { + let el = svg.elt("rect"); + let width = SQUARE_PIN_WIDTH; + svg.hydrate(el, { + class: "sim-board-pin", + width: width, + height: width, + }); + return {el: el, w: width, h: width, x: 0, y: 0}; + } + const mkSquareHoverPin = (): SVGElAndSize => { + let el = svg.elt("rect"); + let width = SQUARE_PIN_HOVER_WIDTH; + svg.hydrate(el, { + class: "sim-board-pin-hover", + width: width, + height: width + }); + return {el: el, w: width, h: width, x: 0, y: 0}; + } + const mkPinBlockGrid = (pinBlock: PinBlockDefinition, blockIdx: number) => { + let xOffset = this.boardDim.xOff + this.boardDim.scaleFn(pinBlock.x) + PIN_DIST / 2.0; + let yOffset = this.boardDim.yOff + this.boardDim.scaleFn(pinBlock.y) + PIN_DIST / 2.0; + let rowCount = 1; + let colCount = pinBlock.labels.length; + let getColName = (colIdx: number) => pinBlock.labels[colIdx]; + let getRowName = () => `${blockIdx + 1}` + let getGroupName = () => pinBlock.labels.join(" "); + let gridRes = mkGrid({ + xOffset: xOffset, + yOffset: yOffset, + rowCount: rowCount, + colCount: colCount, + pinDist: PIN_DIST, + mkPin: mkSquarePin, + mkHoverPin: mkSquareHoverPin, + getRowName: getRowName, + getColName: getColName, + getGroupName: getGroupName, + }); + let pins = gridRes.allPins; + let pinsG = gridRes.g; + svg.addClass(gridRes.g, "sim-board-pin-group"); + return gridRes; + }; + let pinBlocks = (this.boardDef.visual).pinBlocks.map(mkPinBlockGrid); + pinBlocks.forEach(blk => blk.allPins.forEach(p => { + this.allPins.push(p); + })); + //tooltip + this.allPins.forEach(p => { + let tooltip = p.col; + svg.hydrate(p.el, {title: tooltip}); + svg.hydrate(p.hoverEl, {title: tooltip}); + }); + //attach pins + this.allPins.forEach(p => { + this.g.appendChild(p.el); + this.g.appendChild(p.hoverEl); + }); + //catalog pins + this.allPins.forEach(p => { + this.pinNmToPin[p.col] = p; + }); + + // ----- labels + const mkLabelTxtEl = (pinX: number, pinY: number, size: number, txt: string): SVGTextElement => { + //TODO: extract constants + let lblY: number; + let lblX: number; + let edges = [this.boardTopEdge, this.boardBotEdge]; + let distFromTopBot = edges.map(e => Math.abs(e - pinY)); + let closestEdgeIdx = distFromTopBot.reduce((pi, n, ni) => n < distFromTopBot[pi] ? ni : pi, 0); + let topEdge = closestEdgeIdx == 0; + if (topEdge) { + let lblLen = size * 0.25 * txt.length; + lblX = pinX; + lblY = pinY + 12 + lblLen; + } else { + let lblLen = size * 0.32 * txt.length; + lblX = pinX; + lblY = pinY - 11 - lblLen; + } + let el = mkTxt(lblX, lblY, size, -90, txt); + return el; + }; + const mkLabel = (pinX: number, pinY: number, txt: string): GridLabel => { + let el = mkLabelTxtEl(pinX, pinY, PIN_LBL_SIZE, txt); + svg.addClass(el, "sim-board-pin-lbl"); + let hoverEl = mkLabelTxtEl(pinX, pinY, PIN_LBL_HOVER_SIZE, txt); + svg.addClass(hoverEl, "sim-board-pin-lbl-hover"); + let label: GridLabel = {el: el, hoverEl: hoverEl, txt: txt}; + return label; + } + this.allLabels = this.allPins.map(p => { + return mkLabel(p.cx, p.cy, p.col); + }); + //attach labels + this.allLabels.forEach(l => { + this.g.appendChild(l.el); + this.g.appendChild(l.hoverEl); + }); + //catalog labels + this.allPins.forEach((pin, pinIdx) => { + let lbl = this.allLabels[pinIdx]; + this.pinNmToLbl[pin.col] = lbl; + }); + } + + public highlightLoc(pinNm: string) { + let lbl = this.pinNmToLbl[pinNm]; + let pin = this.pinNmToPin[pinNm]; + if (lbl && pin) { + svg.addClass(lbl.el, "highlight"); + svg.addClass(lbl.hoverEl, "highlight"); + svg.addClass(pin.el, "highlight"); + svg.addClass(pin.hoverEl, "highlight"); + } + } + + public highlightWire(wire: Wire) { + //underboard wires + wire.wires.forEach(e => { + (e).style["visibility"] = "visible"; + }); + + //un greyed out + [wire.end1, wire.end2].forEach(e => { + svg.addClass(e, "highlight"); + }); + wire.wires.forEach(e => { + svg.addClass(e, "highlight"); + }); + } + } +} \ No newline at end of file diff --git a/sim/visuals/genericpart.ts b/sim/visuals/genericpart.ts new file mode 100644 index 00000000..8d342fd2 --- /dev/null +++ b/sim/visuals/genericpart.ts @@ -0,0 +1,16 @@ + +namespace pxsim.visuals { + export class GenericComponentView implements IBoardComponent { + public style: string; + public element: SVGElement; + defs: SVGElement[]; + init(bus: EventBus, state: any, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void { + } + moveToCoord(xy: Coord): void { + } + updateState(): void { + } + updateTheme(): void { + } + } +} \ No newline at end of file diff --git a/sim/visuals/ledmatrix.ts b/sim/visuals/ledmatrix.ts new file mode 100644 index 00000000..89d1e282 --- /dev/null +++ b/sim/visuals/ledmatrix.ts @@ -0,0 +1,130 @@ +/// +/// +/// + +namespace pxsim.visuals { + export function mkLedMatrixSvg(xy: Coord, rows: number, cols: number): + {el: SVGGElement, y: number, x: number, w: number, h: number, leds: SVGElement[], ledsOuter: SVGElement[], background: SVGElement} { + let result: {el: SVGGElement, y: number, x: number, w: number, h: number, leds: SVGElement[], ledsOuter: SVGElement[], background: SVGElement} + = {el: null, y: 0, x: 0, w: 0, h: 0, leds: [], ledsOuter: [], background: null}; + result.el = svg.elt("g"); + let width = cols * PIN_DIST; + let height = rows * PIN_DIST; + let ledRad = Math.round(PIN_DIST * .35); + let spacing = PIN_DIST; + let padding = (spacing - 2 * ledRad) / 2.0; + let [x, y] = xy; + let left = x - (ledRad + padding); + let top = y - (ledRad + padding); + result.x = left; + result.y = top; + result.w = width; + result.h = height; + result.background = svg.child(result.el, "rect", {class: "sim-display", x: left, y: top, width: width, height: height}) + + // ledsOuter + result.leds = []; + result.ledsOuter = []; + let hoverRad = ledRad * 1.2; + for (let i = 0; i < rows; ++i) { + let y = top + ledRad + i * spacing + padding; + for (let j = 0; j < cols; ++j) { + let x = left + ledRad + j * spacing + padding; + result.ledsOuter.push(svg.child(result.el, "circle", { class: "sim-led-back", cx: x, cy: y, r: ledRad })); + result.leds.push(svg.child(result.el, "circle", { class: "sim-led", cx: x, cy: y, r: hoverRad, title: `(${j},${i})` })); + } + } + + //default theme + svg.fill(result.background, defaultLedMatrixTheme.background); + svg.fills(result.leds, defaultLedMatrixTheme.ledOn); + svg.fills(result.ledsOuter, defaultLedMatrixTheme.ledOff); + + //turn off LEDs + result.leds.forEach(l => (l).style.opacity = 0 + ""); + + return result; + } + + export interface ILedMatrixTheme { + background?: string; + ledOn?: string; + ledOff?: string; + } + export var defaultLedMatrixTheme: ILedMatrixTheme = { + background: "#000", + ledOn: "#ff5f5f", + ledOff: "#DDD", + }; + + export const LED_MATRIX_STYLE = ` + .sim-led-back:hover { + stroke:#a0a0a0; + stroke-width:3px; + } + .sim-led:hover { + stroke:#ff7f7f; + stroke-width:3px; + } + ` + + export class LedMatrixView implements IBoardComponent { + private background: SVGElement; + private ledsOuter: SVGElement[]; + private leds: SVGElement[]; + private state: LedMatrixState; + private bus: EventBus; + public element: SVGElement; + public defs: SVGElement[]; + private theme: ILedMatrixTheme; + + private DRAW_SIZE = 8; + private ACTIVE_SIZE = 5; + + public style = LED_MATRIX_STYLE; + + public init(bus: EventBus, state: LedMatrixState) { + this.bus = bus; + this.state = state; + this.theme = defaultLedMatrixTheme; + this.defs = []; + this.element = this.buildDom(); + } + + public moveToCoord(xy: Coord) { + let [x, y] = xy; + translateEl(this.element, [x, y]); + } + + public updateTheme() { + svg.fill(this.background, this.theme.background); + svg.fills(this.leds, this.theme.ledOn); + svg.fills(this.ledsOuter, this.theme.ledOff); + } + + public updateState() { + let bw = this.state.displayMode == pxsim.DisplayMode.bw + let img = this.state.image; + this.leds.forEach((led, i) => { + let sel = (led) + let dx = i % this.DRAW_SIZE; + let dy = (i - dx) / this.DRAW_SIZE; + if (dx < this.ACTIVE_SIZE && dy < this.ACTIVE_SIZE) { + let j = dx + dy * this.ACTIVE_SIZE; + sel.style.opacity = ((bw ? img.data[j] > 0 ? 255 : 0 : img.data[j]) / 255.0) + ""; + } else { + sel.style.opacity = 0 + ""; + } + }) + } + + public buildDom() { + let res = mkLedMatrixSvg([0, 0], this.DRAW_SIZE, this.DRAW_SIZE); + let display = res.el; + this.background = res.background; + this.leds = res.leds; + this.ledsOuter = res.ledsOuter; + return display; + } + } +} \ No newline at end of file diff --git a/sim/visuals/neopixel.ts b/sim/visuals/neopixel.ts new file mode 100644 index 00000000..da09f791 --- /dev/null +++ b/sim/visuals/neopixel.ts @@ -0,0 +1,256 @@ +/// +/// +/// +/// +/// +/// + +//TODO move to utils +namespace pxsim.visuals { + //expects rgb from 0,255, gives h in [0,360], s in [0, 100], l in [0, 100] + export function rgbToHsl(rgb: [number, number, number]): [number, number, number] { + let [r, g, b] = rgb; + let [r$, g$, b$] = [r / 255, g / 255, b / 255]; + let cMin = Math.min(r$, g$, b$); + let cMax = Math.max(r$, g$, b$); + let cDelta = cMax - cMin; + let h: number, s: number, l: number; + let maxAndMin = cMax + cMin; + + //lum + l = (maxAndMin / 2) * 100 + + if (cDelta === 0) + s = h = 0; + else { + //hue + if (cMax === r$) + h = 60 * (((g$ - b$) / cDelta) % 6); + else if (cMax === g$) + h = 60 * (((b$ - r$) / cDelta) + 2); + else if (cMax === b$) + h = 60 * (((r$ - g$) / cDelta) + 4); + + //sat + if (l > 50) + s = 100 * (cDelta / (2 - maxAndMin)); + else + s = 100 * (cDelta / maxAndMin); + } + + return [Math.floor(h), Math.floor(s), Math.floor(l)]; + } +} + +namespace pxsim.visuals { + const PIXEL_SPACING = PIN_DIST * 3; + const PIXEL_RADIUS = PIN_DIST; + const CANVAS_WIDTH = 1.2 * PIN_DIST; + const CANVAS_HEIGHT = 12 * PIN_DIST; + const CANVAS_VIEW_WIDTH = CANVAS_WIDTH; + const CANVAS_VIEW_HEIGHT = CANVAS_HEIGHT; + const CANVAS_VIEW_PADDING = PIN_DIST * 4; + const CANVAS_LEFT = 1.4 * PIN_DIST; + const CANVAS_TOP = PIN_DIST; + + // For the instructions parts list + export function mkNeoPixelPart(xy: Coord = [0, 0]): SVGElAndSize { + const NP_PART_XOFF = -13.5; + const NP_PART_YOFF = -11; + const NP_PART_WIDTH = 87.5; + const NP_PART_HEIGHT = 190; + const NEOPIXEL_PART_IMG = "neopixel-black-60-vert.svg"; + let [x, y] = xy; + let l = x + NP_PART_XOFF; + let t = y + NP_PART_YOFF; + let w = NP_PART_WIDTH; + let h = NP_PART_HEIGHT; + let img = svg.elt("image"); + svg.hydrate(img, {class: "sim-neopixel-strip", x: l, y: t, width: w, height: h, + href: `/static/hardware/${NEOPIXEL_PART_IMG}`}); + return {el: img, x: l, y: t, w: w, h: h}; + } + export class NeoPixel implements SVGAndSize { + public el: SVGCircleElement; + public w: number; + public h: number; + public x: number; + public y: number; + public cx: number; + public cy: number; + + constructor(xy: Coord = [0, 0]) { + let circle = svg.elt("circle"); + let r = PIXEL_RADIUS; + let [cx, cy] = xy; + svg.hydrate(circle, {cx: cx, cy: cy, r: r, class: "sim-neopixel"}); + this.el = circle; + this.w = r * 2; + this.h = r * 2; + this.x = cx - r; + this.y = cy - r; + this.cx = cx; + this.cy = cy; + } + + public setRgb(rgb: [number, number, number]) { + let hsl = rgbToHsl(rgb); + let [h, s, l] = hsl; + //We ignore luminosity since it doesn't map well to real-life brightness + let fill = `hsl(${h}, ${s}%, 70%)`; + this.el.setAttribute("fill", fill); + } + } + + export class NeoPixelCanvas { + public canvas: SVGSVGElement; + public pin: number; + public pixels: NeoPixel[]; + private viewBox: [number, number, number, number]; + private background: SVGRectElement; + + constructor(pin: number) { + this.pixels = []; + this.pin = pin; + let el = svg.elt("svg"); + svg.hydrate(el, { + "class": `sim-neopixel-canvas`, + "x": "0px", + "y": "0px", + "width": `${CANVAS_WIDTH}px`, + "height": `${CANVAS_HEIGHT}px`, + }); + this.canvas = el; + this.background = svg.child(el, "rect", { class: "sim-neopixel-background hidden"}); + this.updateViewBox(-CANVAS_VIEW_WIDTH / 2, 0, CANVAS_VIEW_WIDTH, CANVAS_VIEW_HEIGHT); + } + + private updateViewBox(x: number, y: number, w: number, h: number) { + this.viewBox = [x, y, w, h]; + svg.hydrate(this.canvas, {"viewBox": `${x} ${y} ${w} ${h}`}); + svg.hydrate(this.background, {"x": x, "y": y, "width": w, "height": h}); + } + + public update(colors: RGBW[]) { + if (!colors || colors.length <= 0) + return; + + for (let i = 0; i < colors.length; i++) { + let pixel = this.pixels[i]; + if (!pixel) { + let cxy: Coord = [0, CANVAS_VIEW_PADDING + i * PIXEL_SPACING]; + pixel = this.pixels[i] = new NeoPixel(cxy); + this.canvas.appendChild(pixel.el); + } + let color = colors[i]; + pixel.setRgb(color); + svg.hydrate(pixel.el, {title: `offset: ${i}`}); + } + + //show the canvas if it's hidden + svg.removeClass(this.background, "hidden"); + + //resize if necessary + let [first, last] = [this.pixels[0], this.pixels[this.pixels.length - 1]] + let yDiff = last.cy - first.cy; + let newH = yDiff + CANVAS_VIEW_PADDING * 2; + let [oldX, oldY, oldW, oldH] = this.viewBox; + if (oldH < newH) { + let scalar = newH / oldH; + let newW = oldW * scalar; + this.updateViewBox(-newW / 2, oldY, newW, newH); + } + } + + public setLoc(xy: Coord) { + let [x, y] = xy; + svg.hydrate(this.canvas, {x: x, y: y}); + } + }; + + function gpioPinToPinNumber(gpioPin: string): number { + let pinNumStr = gpioPin.split("P")[1]; + let pinNum = Number(pinNumStr) + 7 /*MICROBIT_ID_IO_P0; TODO: don't hardcode this, import enums.d.ts*/; + return pinNum + } + function parseNeoPixelMode(modeStr: string): NeoPixelMode { + const modeMap: Map = { + "NeoPixelMode.RGB": NeoPixelMode.RGB, + "NeoPixelMode.RGBW": NeoPixelMode.RGBW, + "*": NeoPixelMode.RGB, + }; + let mode: NeoPixelMode = null; + for (let key in modeMap) { + if (key == modeStr) { + mode = modeMap[key]; + break; + } + } + U.assert(mode != null, "Unknown NeoPixelMode: " + modeStr); + return mode; + } + + export class NeoPixelView implements IBoardComponent { + public style: string = ` + .sim-neopixel-canvas { + } + .sim-neopixel-canvas-parent:hover { + transform-origin: center; + transform: scale(4) translateY(-60px); + } + .sim-neopixel-canvas .hidden { + visibility:hidden; + } + .sim-neopixel-background { + fill: rgba(255,255,255,0.9); + } + .sim-neopixel-strip { + } + `; + public element: SVGElement; + public defs: SVGElement[]; + private state: NeoPixelState; + private canvas: NeoPixelCanvas; + private part: SVGElAndSize; + private stripGroup: SVGGElement; + private lastLocation: Coord; + private pin: number; + private mode: NeoPixelMode; + + public init(bus: EventBus, state: NeoPixelState, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void { + U.assert(otherArgs.length === 1, "NeoPixels assumes a RGB vs RGBW mode is passed to it"); + let modeStr = otherArgs[0]; + this.mode = parseNeoPixelMode(modeStr); + this.state = state; + this.stripGroup = svg.elt("g"); + this.element = this.stripGroup; + let pinStr = gpioPins[0]; + this.pin = gpioPinToPinNumber(pinStr); + this.lastLocation = [0, 0]; + let part = mkNeoPixelPart(); + this.part = part; + this.stripGroup.appendChild(part.el); + let canvas = new NeoPixelCanvas(this.pin); + this.canvas = canvas; + let canvasG = svg.child(this.stripGroup, "g", {class: "sim-neopixel-canvas-parent"}); + canvasG.appendChild(canvas.canvas); + this.updateStripLoc(); + } + public moveToCoord(xy: Coord): void { + let [x, y] = xy; + let loc: Coord = [x, y]; + this.lastLocation = loc; + this.updateStripLoc(); + } + private updateStripLoc() { + let [x, y] = this.lastLocation; + this.canvas.setLoc([x + CANVAS_LEFT, y + CANVAS_TOP]); + svg.hydrate(this.part.el, {transform: `translate(${x} ${y})`}); //TODO: update part's l,h, etc. + } + public updateState(): void { + let colors = this.state.getColors(this.pin, this.mode); + this.canvas.update(colors); + } + public updateTheme (): void { } + } +} \ No newline at end of file diff --git a/sim/visuals/wiring.ts b/sim/visuals/wiring.ts new file mode 100644 index 00000000..9303962e --- /dev/null +++ b/sim/visuals/wiring.ts @@ -0,0 +1,420 @@ +namespace pxsim.visuals { + const WIRE_WIDTH = PIN_DIST / 2.5; + const BB_WIRE_SMOOTH = 0.7; + const INSTR_WIRE_SMOOTH = 0.8; + const WIRE_PART_CURVE_OFF = 15; + const WIRE_PART_LENGTH = 100; + export const WIRES_CSS = ` + .sim-bb-wire { + fill:none; + stroke-linecap: round; + stroke-width:${WIRE_WIDTH}px; + pointer-events: none; + } + .sim-bb-wire-end { + stroke:#333; + fill:#333; + } + .sim-bb-wire-bare-end { + fill: #ccc; + } + .sim-bb-wire-hover { + stroke-width: ${WIRE_WIDTH}px; + visibility: hidden; + stroke-dasharray: ${PIN_DIST / 10.0},${PIN_DIST / 1.5}; + /*stroke-opacity: 0.4;*/ + } + .grayed .sim-bb-wire-end:not(.highlight) { + stroke: #777; + } + .grayed .sim-bb-wire:not(.highlight) { + stroke: #CCC; + } + .sim-bb-wire-ends-g:hover .sim-bb-wire-end { + stroke: red; + fill: red; + } + .sim-bb-wire-ends-g:hover .sim-bb-wire-bare-end { + stroke: #FFF; + fill: #FFF; + } + `; + + export interface Wire { + endG: SVGGElement; + end1: SVGElement; + end2: SVGElement; + wires: SVGElement[]; + } + + function cssEncodeColor(color: string): string { + //HACK/TODO: do real CSS encoding. + return color + .replace(/\#/g, "-") + .replace(/\(/g, "-") + .replace(/\)/g, "-") + .replace(/\,/g, "-") + .replace(/\./g, "-") + .replace(/\s/g, ""); + } + export enum WireEndStyle { + BBJumper, + OpenJumper, + Croc, + } + export interface WireOpts { //TODO: use throughout + color?: string, + colorClass?: string, + bendFactor?: number, + } + export function mkWirePart(cp: [number, number], clr: string): visuals.SVGAndSize { + let g = svg.elt("g"); + let [cx, cy] = cp; + let offset = WIRE_PART_CURVE_OFF; + let p1: visuals.Coord = [cx - offset, cy - WIRE_PART_LENGTH / 2]; + let p2: visuals.Coord = [cx + offset, cy + WIRE_PART_LENGTH / 2]; + clr = visuals.mapWireColor(clr); + let e1 = mkOpenJumperEnd(p1, true, clr); + let s = mkWirePartSeg(p1, p2, clr); + let e2 = mkOpenJumperEnd(p2, false, clr); + g.appendChild(s.el); + g.appendChild(e1.el); + g.appendChild(e2.el); + let l = Math.min(e1.x, e2.x); + let r = Math.max(e1.x + e1.w, e2.x + e2.w); + let t = Math.min(e1.y, e2.y); + let b = Math.max(e1.y + e1.h, e2.y + e2.h); + return {el: g, x: l, y: t, w: r - l, h: b - t}; + } + function mkCurvedWireSeg(p1: [number, number], p2: [number, number], smooth: number, clrClass: string): SVGPathElement { + const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`}; + let [x1, y1] = p1; + let [x2, y2] = p2 + let yLen = (y2 - y1); + let c1: [number, number] = [x1, y1 + yLen * smooth]; + let c2: [number, number] = [x2, y2 - yLen * smooth]; + let w = svg.mkPath("sim-bb-wire", `M${coordStr(p1)} C${coordStr(c1)} ${coordStr(c2)} ${coordStr(p2)}`); + svg.addClass(w, `wire-stroke-${clrClass}`); + return w; + } + function mkWirePartSeg(p1: [number, number], p2: [number, number], clr: string): visuals.SVGAndSize { + //TODO: merge with mkCurvedWireSeg + const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`}; + let [x1, y1] = p1; + let [x2, y2] = p2 + let yLen = (y2 - y1); + let c1: [number, number] = [x1, y1 + yLen * .8]; + let c2: [number, number] = [x2, y2 - yLen * .8]; + let e = svg.mkPath("sim-bb-wire", `M${coordStr(p1)} C${coordStr(c1)} ${coordStr(c2)} ${coordStr(p2)}`); + (e).style["stroke"] = clr; + return {el: e, x: Math.min(x1, x2), y: Math.min(y1, y2), w: Math.abs(x1 - x2), h: Math.abs(y1 - y2)}; + } + function mkWireSeg(p1: [number, number], p2: [number, number], clrClass: string): SVGPathElement { + const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`}; + let w = svg.mkPath("sim-bb-wire", `M${coordStr(p1)} L${coordStr(p2)}`); + svg.addClass(w, `wire-stroke-${clrClass}`); + return w; + } + function mkBBJumperEnd(p: [number, number], clrClass: string): SVGElement { + const endW = PIN_DIST / 4; + let w = svg.elt("circle"); + let x = p[0]; + let y = p[1]; + let r = WIRE_WIDTH / 2 + endW / 2; + svg.hydrate(w, {cx: x, cy: y, r: r, class: "sim-bb-wire-end"}); + svg.addClass(w, `wire-fill-${clrClass}`); + (w).style["stroke-width"] = `${endW}px`; + return w; + } + function mkOpenJumperEnd(p: [number, number], top: boolean, clr: string): visuals.SVGElAndSize { + let k = visuals.PIN_DIST * 0.24; + let plasticLength = k * 10; + let plasticWidth = k * 2; + let metalLength = k * 6; + let metalWidth = k; + const strokeWidth = visuals.PIN_DIST / 4.0; + let [cx, cy] = p; + let o = top ? -1 : 1; + let g = svg.elt("g") + + let el = svg.elt("rect"); + let h1 = plasticLength; + let w1 = plasticWidth; + let x1 = cx - w1 / 2; + let y1 = cy - (h1 / 2); + svg.hydrate(el, {x: x1, y: y1, width: w1, height: h1, rx: 0.5, ry: 0.5, class: "sim-bb-wire-end"}); + (el).style["stroke-width"] = `${strokeWidth}px`; + + let el2 = svg.elt("rect"); + let h2 = metalLength; + let w2 = metalWidth; + let cy2 = cy + o * (h1 / 2 + h2 / 2); + let x2 = cx - w2 / 2; + let y2 = cy2 - (h2 / 2); + svg.hydrate(el2, {x: x2, y: y2, width: w2, height: h2, class: "sim-bb-wire-bare-end"}); + (el2).style["fill"] = `#bbb`; + + g.appendChild(el2); + g.appendChild(el); + return {el: g, x: x1 - strokeWidth, y: Math.min(y1, y2), w: w1 + strokeWidth * 2, h: h1 + h2}; + } + function mkCrocEnd(p: [number, number], top: boolean, clr: string): SVGElAndSize { + //TODO: merge with mkOpenJumperEnd() + let k = visuals.PIN_DIST * 0.24; + const plasticWidth = k * 4; + const plasticLength = k * 10.0; + const metalWidth = k * 3.5; + const metalHeight = k * 3.5; + const pointScalar = .15; + const baseScalar = .3; + const taperScalar = .7; + const strokeWidth = visuals.PIN_DIST / 4.0; + let [cx, cy] = p; + let o = top ? -1 : 1; + let g = svg.elt("g") + + let el = svg.elt("polygon"); + let h1 = plasticLength; + let w1 = plasticWidth; + let x1 = cx - w1 / 2; + let y1 = cy - (h1 / 2); + let mkPnt = (xy: Coord) => `${xy[0]},${xy[1]}`; + let mkPnts = (...xys: Coord[]) => xys.map(xy => mkPnt(xy)).join(" "); + const topScalar = top ? pointScalar : baseScalar; + const midScalar = top ? taperScalar : (1 - taperScalar); + const botScalar = top ? baseScalar : pointScalar; + svg.hydrate(el, { + points: mkPnts( + [x1 + w1 * topScalar, y1], //TL + [x1 + w1 * (1 - topScalar), y1], //TR + [x1 + w1, y1 + h1 * midScalar], //MR + [x1 + w1 * (1 - botScalar), y1 + h1], //BR + [x1 + w1 * botScalar, y1 + h1], //BL + [x1, y1 + h1 * midScalar]) //ML + }); + svg.hydrate(el, {rx: 0.5, ry: 0.5, class: "sim-bb-wire-end"}); + (el).style["stroke-width"] = `${strokeWidth}px`; + + let el2 = svg.elt("rect"); + let h2 = metalWidth; + let w2 = metalHeight; + let cy2 = cy + o * (h1 / 2 + h2 / 2); + let x2 = cx - w2 / 2; + let y2 = cy2 - (h2 / 2); + svg.hydrate(el2, {x: x2, y: y2, width: w2, height: h2, class: "sim-bb-wire-bare-end"}); + + g.appendChild(el2); + g.appendChild(el); + return {el: g, x: x1 - strokeWidth, y: Math.min(y1, y2), w: w1 + strokeWidth * 2, h: h1 + h2}; + } + + //TODO: make this stupid class obsolete + export class WireFactory { + private underboard: SVGGElement; + private overboard: SVGGElement; + private boardEdges: number[]; + private getLocCoord: (loc: Loc) => Coord; + public styleEl: SVGStyleElement; + + constructor(underboard: SVGGElement, overboard: SVGGElement, boardEdges: number[], styleEl: SVGStyleElement, getLocCoord: (loc: Loc) => Coord) { + this.styleEl = styleEl; + this.styleEl.textContent += WIRES_CSS; + this.underboard = underboard; + this.overboard = overboard; + this.boardEdges = boardEdges; + this.getLocCoord = getLocCoord; + } + + private indexOfMin(vs: number[]): number { + let minIdx = 0; + let min = vs[0]; + for (let i = 1; i < vs.length; i++) { + if (vs[i] < min) { + min = vs[i]; + minIdx = i; + } + } + return minIdx; + } + private closestEdgeIdx(p: [number, number]): number { + let dists = this.boardEdges.map(e => Math.abs(p[1] - e)); + let edgeIdx = this.indexOfMin(dists); + return edgeIdx; + } + private closestEdge(p: [number, number]): number { + return this.boardEdges[this.closestEdgeIdx(p)]; + } + + private nextWireId = 0; + private drawWire(pin1: Coord, pin2: Coord, color: string): Wire { + let wires: SVGElement[] = []; + let g = svg.child(this.overboard, "g", {class: "sim-bb-wire-group"}); + const closestPointOffBoard = (p: [number, number]): [number, number] => { + const offset = PIN_DIST / 2; + let e = this.closestEdge(p); + let y: number; + if (e - p[1] < 0) + y = e - offset; + else + y = e + offset; + return [p[0], y]; + } + let wireId = this.nextWireId++; + let clrClass = cssEncodeColor(color); + let end1 = mkBBJumperEnd(pin1, clrClass); + let end2 = mkBBJumperEnd(pin2, clrClass); + let endG = svg.child(g, "g", {class: "sim-bb-wire-ends-g"}); + endG.appendChild(end1); + endG.appendChild(end2); + let edgeIdx1 = this.closestEdgeIdx(pin1); + let edgeIdx2 = this.closestEdgeIdx(pin2); + if (edgeIdx1 == edgeIdx2) { + let seg = mkWireSeg(pin1, pin2, clrClass); + g.appendChild(seg); + wires.push(seg); + } else { + let offP1 = closestPointOffBoard(pin1); + let offP2 = closestPointOffBoard(pin2); + let offSeg1 = mkWireSeg(pin1, offP1, clrClass); + let offSeg2 = mkWireSeg(pin2, offP2, clrClass); + let midSeg: SVGElement; + let midSegHover: SVGElement; + let isBetweenMiddleTwoEdges = (edgeIdx1 == 1 || edgeIdx1 == 2) && (edgeIdx2 == 1 || edgeIdx2 == 2); + if (isBetweenMiddleTwoEdges) { + midSeg = mkCurvedWireSeg(offP1, offP2, BB_WIRE_SMOOTH, clrClass); + midSegHover = mkCurvedWireSeg(offP1, offP2, BB_WIRE_SMOOTH, clrClass); + } else { + midSeg = mkWireSeg(offP1, offP2, clrClass); + midSegHover = mkWireSeg(offP1, offP2, clrClass); + } + svg.addClass(midSegHover, "sim-bb-wire-hover"); + g.appendChild(offSeg1); + wires.push(offSeg1); + g.appendChild(offSeg2); + wires.push(offSeg2); + this.underboard.appendChild(midSeg); + wires.push(midSeg); + g.appendChild(midSegHover); + wires.push(midSegHover); + //set hover mechanism + let wireIdClass = `sim-bb-wire-id-${wireId}`; + const setId = (e: SVGElement) => svg.addClass(e, wireIdClass); + setId(endG); + setId(midSegHover); + this.styleEl.textContent += ` + .${wireIdClass}:hover ~ .${wireIdClass}.sim-bb-wire-hover { + visibility: visible; + }` + } + + // wire colors + let colorCSS = ` + .wire-stroke-${clrClass} { + stroke: ${mapWireColor(color)}; + } + .wire-fill-${clrClass} { + fill: ${mapWireColor(color)}; + } + ` + this.styleEl.textContent += colorCSS; + + return {endG: endG, end1: end1, end2: end2, wires: wires}; + } + private drawWireWithCrocs(pin1: Coord, pin2: Coord, color: string): Wire { + //TODO: merge with drawWire() + const PIN_Y_OFF = 40; + const CROC_Y_OFF = -17; + let wires: SVGElement[] = []; + let g = svg.child(this.overboard, "g", {class: "sim-bb-wire-group"}); + const closestPointOffBoard = (p: [number, number]): [number, number] => { + const offset = PIN_DIST / 2; + let e = this.closestEdge(p); + let y: number; + if (e - p[1] < 0) + y = e - offset; + else + y = e + offset; + return [p[0], y]; + } + let wireId = this.nextWireId++; + let clrClass = cssEncodeColor(color); + let end1 = mkBBJumperEnd(pin1, clrClass); + let pin2orig = pin2; + let [x2, y2] = pin2; + pin2 = [x2, y2 + PIN_Y_OFF];//HACK + [x2, y2] = pin2; + let endCoord2: Coord = [x2, y2 + CROC_Y_OFF] + let end2AndSize = mkCrocEnd(endCoord2, true, color); + let end2 = end2AndSize.el; + let endG = svg.child(g, "g", {class: "sim-bb-wire-ends-g"}); + endG.appendChild(end1); + //endG.appendChild(end2); + let edgeIdx1 = this.closestEdgeIdx(pin1); + let edgeIdx2 = this.closestEdgeIdx(pin2orig); + if (edgeIdx1 == edgeIdx2) { + let seg = mkWireSeg(pin1, pin2, clrClass); + g.appendChild(seg); + wires.push(seg); + } else { + let offP1 = closestPointOffBoard(pin1); + //let offP2 = closestPointOffBoard(pin2orig); + let offSeg1 = mkWireSeg(pin1, offP1, clrClass); + //let offSeg2 = mkWireSeg(pin2, offP2, clrClass); + let midSeg: SVGElement; + let midSegHover: SVGElement; + let isBetweenMiddleTwoEdges = (edgeIdx1 == 1 || edgeIdx1 == 2) && (edgeIdx2 == 1 || edgeIdx2 == 2); + if (isBetweenMiddleTwoEdges) { + midSeg = mkCurvedWireSeg(offP1, pin2, BB_WIRE_SMOOTH, clrClass); + midSegHover = mkCurvedWireSeg(offP1, pin2, BB_WIRE_SMOOTH, clrClass); + } else { + midSeg = mkWireSeg(offP1, pin2, clrClass); + midSegHover = mkWireSeg(offP1, pin2, clrClass); + } + svg.addClass(midSegHover, "sim-bb-wire-hover"); + g.appendChild(offSeg1); + wires.push(offSeg1); + // g.appendChild(offSeg2); + // wires.push(offSeg2); + this.underboard.appendChild(midSeg); + wires.push(midSeg); + //g.appendChild(midSegHover); + //wires.push(midSegHover); + //set hover mechanism + let wireIdClass = `sim-bb-wire-id-${wireId}`; + const setId = (e: SVGElement) => svg.addClass(e, wireIdClass); + setId(endG); + setId(midSegHover); + this.styleEl.textContent += ` + .${wireIdClass}:hover ~ .${wireIdClass}.sim-bb-wire-hover { + visibility: visible; + }` + } + endG.appendChild(end2);//HACK + + // wire colors + let colorCSS = ` + .wire-stroke-${clrClass} { + stroke: ${mapWireColor(color)}; + } + .wire-fill-${clrClass} { + fill: ${mapWireColor(color)}; + } + ` + this.styleEl.textContent += colorCSS; + + return {endG: endG, end1: end1, end2: end2, wires: wires}; + } + + public addWire(start: Loc, end: Loc, color: string, withCrocs: boolean = false): Wire { + let startLoc = this.getLocCoord(start); + let endLoc = this.getLocCoord(end); + let wireEls: Wire; + if (withCrocs && end.type == "dalboard") { + wireEls = this.drawWireWithCrocs(startLoc, endLoc, color); + } else { + wireEls = this.drawWire(startLoc, endLoc, color); + } + return wireEls; + } + } +}