moves all of pxt-arduino breadboarding here...

... see pxt-arduino history starting here: acd49bb795
This commit is contained in:
darzu 2016-08-30 11:55:00 -07:00
parent 89e899cc79
commit a65e71f3b1
15 changed files with 4467 additions and 963 deletions

View File

@ -75,7 +75,7 @@
},
"simulator": {
"autoRun": true,
"aspectRatio": 1.22
"aspectRatio": 0.69
},
"compileService": {
"yottaTarget": "bbc-microbit-classic-gcc",

432
sim/allocator.ts Normal file
View File

@ -0,0 +1,432 @@
namespace pxsim {
export interface AllocatorOpts {
boardDef: BoardDefinition,
cmpDefs: Map<ComponentDefinition>,
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 => <BBRowCol>["+", `${n}`]),
ground: mkRange(26, 51).map(n => <BBRowCol>["-", `${n}`]),
},
bottom: {
threeVolt: mkRange(1, 26).map(n => <BBRowCol>["+", `${n}`]),
ground: mkRange(1, 26).map(n => <BBRowCol>["-", `${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 = <string>location[1];
let col = (<number>location[2] + opts.startColumn).toString();
return {type: "breadboard", rowCol: [row, col]}
} else if (location[0] === "GPIO") {
U.assert(!!opts.cmpGPIOPins);
let idx = <number>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 = <BBLoc>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 = (<PredefinedPinAlloc>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 = (<FactoryFunctionPinAlloc>def.pinAllocation);
let fnNm = fnPinAlloc.functionName;
let fnsAndArgs = <string[]>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 = (<AutoPinAlloc>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 = <number[]>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();
}
}

97
sim/dalboard.ts Normal file
View File

@ -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 = <SimulatorEventBusMessage>msg;
this.bus.queue(ev.id, ev.eventid, ev.value);
break;
case "serial":
let data = (<SimulatorSerialMessage>msg).data || "";
this.serialState.recieveData(data);
break;
case "radiopacket":
let packet = <SimulatorRadioPacketMessage>msg;
this.radioState.recievePacket(packet);
break;
}
}
kill() {
super.kill();
AudioContextManager.stop();
}
initAsync(msg: SimulatorRunMessage): Promise<void> {
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();
}
}
}

321
sim/definitions.ts Normal file
View File

@ -0,0 +1,321 @@
/// <reference path="../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
/// <reference path="../libs/microbit/dal.d.ts"/>
/// <reference path="./visuals/neopixel.ts"/>
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<ComponentDefinition> = {
"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<any>> = {
"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),
};
}

View File

@ -0,0 +1,675 @@
/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
/// <reference path="../../node_modules/pxt-core/built/pxtrunner.d.ts"/>
/// <reference path="../../libs/microbit/dal.d.ts"/>
/// <reference path="../visuals/genericboard.ts"/>
/// <reference path="../visuals/wiring.ts"/>
//HACK: allows instructions.html to access pxtblocks without requiring simulator.html to import blocks as well
if (!(<any>window).pxt) (<any>window).pxt = {};
import pxtrunner = pxt.runner;
import pxtdocs = pxt.docs;
namespace pxsim.instructions {
const LOC_LBL_SIZE = 10;
const QUANT_LBL_SIZE = 30;
const QUANT_LBL = (q: number) => `${q}x`;
const WIRE_QUANT_LBL_SIZE = 20;
const LBL_VERT_PAD = 3;
const LBL_RIGHT_PAD = 5;
const LBL_LEFT_PAD = 5;
const REQ_WIRE_HEIGHT = 45;
const REQ_CMP_HEIGHT = 55;
const REQ_CMP_SCALE = 0.5;
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 = <SVGSVGElement>document.createElementNS("http://www.w3.org/2000/svg", "svg");
let dims = {l: 0, t: 0, w: 0, h: 0};
let cmpSvgEl = <SVGSVGElement>document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgEl.appendChild(cmpSvgEl);
cmpSvgEl.appendChild(el.el);
let cmpSvgAtts = {
"viewBox": `${el.x} ${el.y} ${el.w} ${el.h}`,
"preserveAspectRatio": "xMidYMid",
};
dims.w = el.w;
dims.h = el.h;
let scale = (scaler: number) => {
dims.h *= scaler;
dims.w *= scaler;
(<any>cmpSvgAtts).width = dims.w;
(<any>cmpSvgAtts).height = dims.h;
}
if (opts.cmpScale) {
scale(opts.cmpScale)
}
if (opts.cmpWidth && opts.cmpWidth < dims.w) {
scale(opts.cmpWidth / dims.w);
} else if (opts.cmpHeight && opts.cmpHeight < dims.h) {
scale(opts.cmpHeight / dims.h)
}
svg.hydrate(cmpSvgEl, cmpSvgAtts);
let elDims = {l: dims.l, t: dims.t, w: dims.w, h: dims.h};
let updateL = (newL: number) => {
if (newL < dims.l) {
let extraW = dims.l - newL;
dims.l = newL;
dims.w += extraW;
}
}
let updateR = (newR: number) => {
let oldR = dims.l + dims.w;
if (oldR < newR) {
let extraW = newR - oldR;
dims.w += extraW;
}
}
let updateT = (newT: number) => {
if (newT < dims.t) {
let extraH = dims.t - newT;
dims.t = newT;
dims.h += extraH;
}
}
let updateB = (newB: number) => {
let oldB = dims.t + dims.h;
if (oldB < newB) {
let extraH = newB - oldB;
dims.h += extraH;
}
}
//labels
let [xOff, yOff] = [-0.3, 0.3]; //HACK: these constants tweak the way "mkTxt" knows how to center the text
const txtAspectRatio = [1.4, 1.0];
if (opts && opts.top) {
let size = opts.topSize;
let txtW = size / txtAspectRatio[0];
let txtH = size / txtAspectRatio[1];
let [cx, y] = [elDims.l + elDims.w / 2, elDims.t - LBL_VERT_PAD - txtH / 2];
let lbl = visuals.mkTxt(cx, y, size, 0, opts.top, xOff, yOff);
svg.addClass(lbl, "cmp-lbl");
svgEl.appendChild(lbl);
let len = txtW * opts.top.length;
updateT(y - txtH / 2);
updateL(cx - len / 2);
updateR(cx + len / 2);
}
if (opts && opts.bot) {
let size = opts.botSize;
let txtW = size / txtAspectRatio[0];
let txtH = size / txtAspectRatio[1];
let [cx, y] = [elDims.l + elDims.w / 2, elDims.t + elDims.h + LBL_VERT_PAD + txtH / 2];
let lbl = visuals.mkTxt(cx, y, size, 0, opts.bot, xOff, yOff);
svg.addClass(lbl, "cmp-lbl");
svgEl.appendChild(lbl);
let len = txtW * opts.bot.length;
updateB(y + txtH / 2);
updateL(cx - len / 2);
updateR(cx + len / 2);
}
if (opts && opts.right) {
let size = opts.rightSize;
let txtW = size / txtAspectRatio[0];
let txtH = size / txtAspectRatio[1];
let len = txtW * opts.right.length;
let [cx, cy] = [elDims.l + elDims.w + LBL_RIGHT_PAD + len / 2, elDims.t + elDims.h / 2];
let lbl = visuals.mkTxt(cx, cy, size, 0, opts.right, xOff, yOff);
svg.addClass(lbl, "cmp-lbl");
svgEl.appendChild(lbl);
updateT(cy - txtH / 2);
updateR(cx + len / 2);
updateB(cy + txtH / 2);
}
if (opts && opts.left) {
let size = opts.leftSize;
let txtW = size / txtAspectRatio[0];
let txtH = size / txtAspectRatio[1];
let len = txtW * opts.left.length;
let [cx, cy] = [elDims.l - LBL_LEFT_PAD - len / 2, elDims.t + elDims.h / 2];
let lbl = visuals.mkTxt(cx, cy, size, 0, opts.left, xOff, yOff);
svg.addClass(lbl, "cmp-lbl");
svgEl.appendChild(lbl);
updateT(cy - txtH / 2);
updateL(cx - len / 2);
updateB(cy + txtH / 2);
}
let svgAtts = {
"viewBox": `${dims.l} ${dims.t} ${dims.w} ${dims.h}`,
"width": dims.w,
"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<ComponentDefinition>,
allAlloc: AllocatorResult,
stepToWires: WireInst[][],
stepToCmps: CmpInst[][]
allWires: WireInst[],
allCmps: CmpInst[],
lastStep: number,
colorToWires: Map<WireInst[]>,
allWireColors: string[],
};
function mkBoardProps(allocOpts: AllocatorOpts): BoardProps {
let allocRes = allocateDefinitions(allocOpts);
let {powerWires, components} = allocRes;
let stepToWires: WireInst[][] = [];
let stepToCmps: CmpInst[][] = [];
powerWires.forEach(w => {
let step = w.assemblyStep + 1;
(stepToWires[step] || (stepToWires[step] = [])).push(w)
});
let getMaxStep = (ns: {assemblyStep: number}[]) => ns.reduce((m, n) => Math.max(m, n.assemblyStep), 0);
let stepOffset = getMaxStep(powerWires) + 2;
components.forEach(cAndWs => {
let {component, wires} = cAndWs;
let cStep = component.assemblyStep + stepOffset;
let arr = stepToCmps[cStep] || (stepToCmps[cStep] = []);
arr.push(component);
let wSteps = wires.map(w => w.assemblyStep + stepOffset);
wires.forEach((w, i) => {
let wStep = wSteps[i];
let arr = stepToWires[wStep] || (stepToWires[wStep] = []);
arr.push(w);
})
stepOffset = Math.max(cStep, wSteps.reduce((m, n) => Math.max(m, n), 0)) + 1;
});
let lastStep = stepOffset - 1;
let allCmps = components.map(p => p.component);
let allWires = powerWires.concat(components.map(p => p.wires).reduce((p, n) => p.concat(n), []));
let colorToWires: Map<WireInst[]> = {}
let allWireColors: string[] = [];
allWires.forEach(w => {
if (!colorToWires[w.color]) {
colorToWires[w.color] = [];
allWireColors.push(w.color);
}
colorToWires[w.color].push(w);
});
return {
boardDef: allocOpts.boardDef,
cmpDefs: allocOpts.cmpDefs,
allAlloc: allocRes,
stepToWires: stepToWires,
stepToCmps: stepToCmps,
allWires: allWires,
allCmps: allCmps,
lastStep: lastStep,
colorToWires: colorToWires,
allWireColors: allWireColors,
};
}
function mkBoard(boardDef: BoardDefinition, cmpDefs: Map<ComponentDefinition>, 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": `${(<BoardImageDefinition>boardDef.visual).outlineImage}`
})
svg.addClass(board.hostElement, "sim-board-outline")
let bb = board.breadboard.bb;
svg.addClass(bb, "sim-bb-outline")
let style = <SVGStyleElement>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] = (<BBLoc>w.start).rowCol;
let lbls = board.breadboard.highlightLoc(row, col);
} else {
board.highlightLoc((<BoardLoc>w.start).pin);
}
if (w.end.type == "breadboard") {
let [row, col] = (<BBLoc>w.end).rowCol;
let lbls = board.breadboard.highlightLoc(row, col);
} else {
board.highlightLoc((<BoardLoc>w.end).pin);
}
//highlight wire
board.highlightWire(wire);
}
});
}
let cmps = props.stepToCmps[i];
if (cmps) {
cmps.forEach(cmpInst => {
let cmp = board.addComponent(cmpInst)
let [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(<BoardImageDefinition>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 = <string>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] = (<BBLoc>loc).rowCol;
return `(${row},${col})`
} else
return (<BoardLoc>loc).pin;
};
wires.forEach(w => {
let cmp = mkCmpDiv("wire", {
top: mkLabel(w.end),
topSize: LOC_LBL_SIZE,
bot: mkLabel(w.start),
botSize: LOC_LBL_SIZE,
wireClr: w.color,
cmpHeight: REQ_WIRE_HEIGHT
})
addClass(cmp, "cmp-div");
reqsDiv.appendChild(cmp);
});
let cmps = (props.stepToCmps[step] || []);
cmps.forEach(c => {
let l: BBRowCol = [`${c.breadboardStartRow}`, `${c.breadboardStartColumn}`];
let locs = [l];
if (c.visual === "buttonpair") {
//TODO: don't special case this
let l2: BBRowCol = [`${c.breadboardStartRow}`, `${c.breadboardStartColumn + 3}`];
locs.push(l2);
}
locs.forEach((l, i) => {
let [row, col] = l;
if (typeof c.visual === "string") {
let builtinVisual = <string>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 (<any>window).marked; }
pxtrunner.renderMarkdownAsync(codeContainerDiv, md)
.done(function() {
let codeSvg = $("#proj-code-container svg");
if (codeSvg.length > 0) {
//code rendered successfully as blocks
codeSvg.css("width", "inherit");
codeSvg.css("height", "inherit");
//takes the svg out of the wrapper markdown
codeContainerDiv.innerHTML = "";
codeContainerDiv.appendChild(codeSvg[0]);
} else {
//code failed to convert to blocks, display as typescript instead
codeContainerDiv.innerText = tsCode;
}
$(codeContainerDiv).show();
$(codeSpinnerDiv).hide();
});
}
//parts list
let parts = (getQsVal("parts") || "").split(" ");
parts.sort();
//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);
}
}

View File

@ -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<any>[] = [];
private pinNmToCoord: Map<Coord> = {};
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<ComponentDefinition> = 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 = (<BBLoc>loc).rowCol;
coord = this.getBBCoord(rowCol);
} else {
let pinNm = (<BoardLoc>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<any> {
let cmp: IBoardComponent<any> = 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 = <BBRowCol>[`${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 = (<SVGStylable><any>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) {
(<any>this.buttonsOuter[2]).style.visibility = "visible";
(<any>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 = <SVGSVGElement>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 = <SVGStyleElement>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 = <SVGDefsElement>svg.child(this.element, "defs", {});
this.g = svg.elt("g");
this.g = <SVGGElement>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 => <SVGTextElement>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);
})
}
}

File diff suppressed because one or more lines are too long

View File

@ -3,761 +3,203 @@
/// <reference path="../libs/microbit/dal.d.ts"/>
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<boolean>((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 {
(<any>window).AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
if ((<any>window).AudioContext) {
try {
// this call my crash.
// SyntaxError: audio resources unavailable for AudioContext construction
return new (<any>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 <string>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<SVGSVGElement>,
scaleUnit1: number,
el2: SVGAndSize<SVGSVGElement>,
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 = <SVGSVGElement>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 = <SVGGElement>svg.child(host, "g");
host.appendChild(a.el);
host.appendChild(b.el);
let over = <SVGGElement>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<T> {
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 = <SVGTextElement>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<string> = {
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<T extends SVGElement> {
el: T,
y: number,
x: number,
w: number,
h: number
};
export type SVGElAndSize = SVGAndSize<SVGElement>;
export const PIN_DIST = 15;
}

640
sim/visuals/breadboard.ts Normal file
View File

@ -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 = <SVGGElement>svg.elt("g");
let colIdxOffset = opts.colStartIdx || 0;
let rowIdxOffset = opts.rowStartIdx || 0;
let copyArr = <T>(arr: T[]): T[] => arr ? arr.slice(0, arr.length) : [];
let removeAll = <T>(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<Map<GridPin>> = {};
private rowColToLbls: Map<Map<GridLabel[]>> = {};
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 = <SVGSVGElement>svg.elt("svg", {
"version": "1.0",
"viewBox": `0 0 ${WIDTH} ${HEIGHT}`,
"class": `sim-bb`,
"width": WIDTH + "px",
"height": HEIGHT + "px",
});
this.styleEl = <SVGStyleElement>svg.child(this.bb, "style", {});
this.styleEl.textContent += BREADBOARD_CSS;
this.defs = <SVGDefsElement>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 = <SVGLinearGradientElement>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<GridLabel[]> = {};
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 = <SVGRectElement>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 = <SVGGElement>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<SVGGElement> = {};
allGrpNms.forEach((g, i) => grpNmToGroup[g] = groups[i]);
//group pins and add connecting wire
let grpNmToPins: Map<GridPin[]> = {};
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 = <SVGGElement>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<SVGSVGElement> {
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);
}
}
}

204
sim/visuals/buttonpair.ts Normal file
View File

@ -0,0 +1,204 @@
/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
/// <reference path="../../libs/microbit/dal.d.ts"/>
namespace pxsim.visuals {
export function mkBtnSvg(xy: Coord): SVGAndSize<SVGGElement> {
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 = <SVGGElement>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<ButtonPairState> {
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 = <SVGGElement>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);
})
}
}
}

515
sim/visuals/genericboard.ts Normal file
View File

@ -0,0 +1,515 @@
/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
/// <reference path="../../libs/microbit/dal.d.ts"/>
namespace pxsim.visuals {
const svg = pxsim.svg;
export interface IBoardSvgProps {
runtime: pxsim.Runtime;
boardDef: BoardDefinition;
disableTilt?: boolean;
activeComponents: string[];
fnArgs?: any;
componentDefinitions: Map<ComponentDefinition>;
}
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<any>[];
public breadboard: Breadboard;
private underboard: SVGGElement;
public boardDef: BoardDefinition;
private boardDim: ComputedBoardDimensions;
public componentDefs: Map<ComponentDefinition>;
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<GridLabel> = {};
private pinNmToPin: Map<GridPin> = {};
constructor(public props: IBoardSvgProps) {
this.id = nextBoardId++;
this.boardDef = props.boardDef;
this.boardDim = getBoardDimensions(<BoardImageDefinition>this.boardDef.visual);
this.board = this.props.runtime.board as pxsim.DalBoard;
this.board.updateView = () => this.updateState();
this.hostElement = <SVGSVGElement>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 = <SVGStyleElement>svg.child(this.hostElement, "style", {});
this.style.textContent += BOARD_SYTLE;
this.defs = <SVGDefsElement>svg.child(this.hostElement, "defs", {});
this.g = <SVGGElement>svg.elt("g");
this.hostElement.appendChild(this.g);
this.underboard = <SVGGElement>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 = (<BBLoc>loc).rowCol;
coord = this.getBBCoord(rowCol);
} else {
let pinNm = (<BoardLoc>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 = <SVGRectElement>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<any> {
let cmp: IBoardComponent<any> = 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 = <BBRowCol>[`${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": `${(<BoardImageDefinition>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 = (<BoardImageDefinition>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 => {
(<any>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");
});
}
}
}

View File

@ -0,0 +1,16 @@
namespace pxsim.visuals {
export class GenericComponentView implements IBoardComponent<any> {
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 {
}
}
}

130
sim/visuals/ledmatrix.ts Normal file
View File

@ -0,0 +1,130 @@
/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
/// <reference path="../../libs/microbit/dal.d.ts"/>
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 = <SVGGElement>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 => (<SVGStylable><any>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<LedMatrixState> {
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 = (<SVGStylable><any>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;
}
}
}

256
sim/visuals/neopixel.ts Normal file
View File

@ -0,0 +1,256 @@
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
/// <reference path="../../libs/microbit/dal.d.ts"/>
/// <reference path="../../libs/microbit/shims.d.ts"/>
/// <reference path="../../libs/microbit/enums.d.ts"/>
/// <reference path="../state/neopixel.ts"/>
/// <reference path="../simlib.ts"/>
//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 = <SVGImageElement>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<SVGCircleElement> {
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 = <SVGCircleElement>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 = <SVGSVGElement>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 = <SVGRectElement>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> = {
"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<NeoPixelState> {
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 = <SVGGElement>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 { }
}
}

420
sim/visuals/wiring.ts Normal file
View File

@ -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<SVGGElement> {
let g = <SVGGElement>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 = <SVGPathElement>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<SVGPathElement> {
//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 = <SVGPathElement>svg.mkPath("sim-bb-wire", `M${coordStr(p1)} C${coordStr(c1)} ${coordStr(c2)} ${coordStr(p2)}`);
(<any>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 = <SVGPathElement>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}`);
(<any>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"});
(<any>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"});
(<any>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"});
(<any>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 = <SVGGElement>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 = <SVGGElement>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;
}
}
}