moving wiring to pxt
This commit is contained in:
parent
da30afb121
commit
3858b0a0a0
672
sim/allocator.ts
672
sim/allocator.ts
@ -1,672 +0,0 @@
|
|||||||
namespace pxsim {
|
|
||||||
const GROUND_COLOR = "blue";
|
|
||||||
const POWER_COLOR = "red";
|
|
||||||
|
|
||||||
export interface AllocatorOpts {
|
|
||||||
boardDef: BoardDefinition,
|
|
||||||
partDefs: Map<PartDefinition>,
|
|
||||||
partsList: string[]
|
|
||||||
fnArgs: any,
|
|
||||||
// Used for finding the nearest available power pins
|
|
||||||
getBBCoord: (loc: BBLoc) => visuals.Coord,
|
|
||||||
};
|
|
||||||
export interface AllocatorResult {
|
|
||||||
partsAndWires: PartAndWiresInst[],
|
|
||||||
}
|
|
||||||
export interface PartInst {
|
|
||||||
name: string,
|
|
||||||
simulationBehavior?: string,
|
|
||||||
visual: PartVisualDefinition,
|
|
||||||
bbFit: PartBBFit,
|
|
||||||
startColumnIdx: number,
|
|
||||||
startRowIdx: number,
|
|
||||||
breadboardConnections: BBLoc[],
|
|
||||||
params: Map<string>,
|
|
||||||
}
|
|
||||||
export interface WireInst {
|
|
||||||
start: Loc,
|
|
||||||
end: Loc,
|
|
||||||
color: string,
|
|
||||||
};
|
|
||||||
export interface AssemblyStep {
|
|
||||||
part?: boolean,
|
|
||||||
wireIndices?: number[],
|
|
||||||
}
|
|
||||||
export interface PartAndWiresInst {
|
|
||||||
part?: PartInst,
|
|
||||||
wires?: WireInst[],
|
|
||||||
assembly: AssemblyStep[],
|
|
||||||
}
|
|
||||||
export interface PartBBFit {
|
|
||||||
xOffset: number,
|
|
||||||
yOffset: number,
|
|
||||||
rowCount: number,
|
|
||||||
colCount: number,
|
|
||||||
}
|
|
||||||
interface PinBBFit {
|
|
||||||
partRelativeColIdx: number,
|
|
||||||
partRelativeRowIdx: number,
|
|
||||||
xOffset: number,
|
|
||||||
yOffset: number,
|
|
||||||
}
|
|
||||||
interface PinIR {
|
|
||||||
loc: XY,
|
|
||||||
def: PartPinDefinition,
|
|
||||||
target: PinTarget,
|
|
||||||
bbFit: PinBBFit,
|
|
||||||
}
|
|
||||||
interface PartIR {
|
|
||||||
name: string,
|
|
||||||
def: PartDefinition,
|
|
||||||
partParams: Map<string>,
|
|
||||||
pins: PinIR[],
|
|
||||||
bbFit: PartBBFit,
|
|
||||||
};
|
|
||||||
interface PartPlacement extends PartIR {
|
|
||||||
startColumnIdx: number,
|
|
||||||
startRowIdx: number,
|
|
||||||
};
|
|
||||||
type WireIRLoc = PinTarget | BBLoc;
|
|
||||||
interface WireIR {
|
|
||||||
pinIdx: number,
|
|
||||||
start: WireIRLoc,
|
|
||||||
end: WireIRLoc,
|
|
||||||
color: string,
|
|
||||||
}
|
|
||||||
interface PartIRAndWireIRs extends PartPlacement {
|
|
||||||
wires: WireIR[],
|
|
||||||
};
|
|
||||||
interface PowerUsage {
|
|
||||||
topGround: boolean,
|
|
||||||
topThreeVolt: boolean,
|
|
||||||
bottomGround: boolean,
|
|
||||||
bottomThreeVolt: boolean,
|
|
||||||
singleGround: boolean,
|
|
||||||
singleThreeVolt: boolean,
|
|
||||||
}
|
|
||||||
interface AllocLocOpts {
|
|
||||||
referenceBBPin?: BBLoc,
|
|
||||||
};
|
|
||||||
interface AllocWireOpts {
|
|
||||||
//TODO: port
|
|
||||||
startColumn: number,
|
|
||||||
partGPIOPins: string[],
|
|
||||||
}
|
|
||||||
function isOnBreadboardBottom(location: WireIRLoc) {
|
|
||||||
let isBot = false;
|
|
||||||
if (typeof location !== "string" && (<BBLoc>location).type === "breadboard") {
|
|
||||||
let bbLoc = <BBLoc>location;
|
|
||||||
let row = bbLoc.row;
|
|
||||||
isBot = 0 <= ["a", "b", "c", "d", "e"].indexOf(row);
|
|
||||||
}
|
|
||||||
return isBot;
|
|
||||||
}
|
|
||||||
const arrCount = (a: boolean[]) => a.reduce((p, n) => p + (n ? 1 : 0), 0);
|
|
||||||
const arrAny = (a: boolean[]) => arrCount(a) > 0;
|
|
||||||
function computePowerUsage(wire: WireIR): PowerUsage {
|
|
||||||
let ends = [wire.start, wire.end];
|
|
||||||
let endIsGround = ends.map(e => e === "ground");
|
|
||||||
let endIsThreeVolt = ends.map(e => e === "threeVolt");
|
|
||||||
let endIsBot = ends.map(e => isOnBreadboardBottom(e));
|
|
||||||
let hasGround = arrAny(endIsGround);
|
|
||||||
let hasThreeVolt = arrAny(endIsThreeVolt);
|
|
||||||
let hasBot = arrAny(endIsBot);
|
|
||||||
return {
|
|
||||||
topGround: hasGround && !hasBot,
|
|
||||||
topThreeVolt: hasThreeVolt && !hasBot,
|
|
||||||
bottomGround: hasGround && hasBot,
|
|
||||||
bottomThreeVolt: hasThreeVolt && hasBot,
|
|
||||||
singleGround: hasGround,
|
|
||||||
singleThreeVolt: hasThreeVolt
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function mergePowerUsage(powerUsages: PowerUsage[]) {
|
|
||||||
let finalPowerUsage = powerUsages.reduce((p, n) => ({
|
|
||||||
topGround: p.topGround || n.topGround,
|
|
||||||
topThreeVolt: p.topThreeVolt || n.topThreeVolt,
|
|
||||||
bottomGround: p.bottomGround || n.bottomGround,
|
|
||||||
bottomThreeVolt: p.bottomThreeVolt || n.bottomThreeVolt,
|
|
||||||
singleGround: n.singleGround ? p.singleGround === null : p.singleGround,
|
|
||||||
singleThreeVolt: n.singleThreeVolt ? p.singleThreeVolt === null : p.singleThreeVolt,
|
|
||||||
}), {
|
|
||||||
topGround: false,
|
|
||||||
topThreeVolt: false,
|
|
||||||
bottomGround: false,
|
|
||||||
bottomThreeVolt: false,
|
|
||||||
singleGround: null,
|
|
||||||
singleThreeVolt: null,
|
|
||||||
});
|
|
||||||
if (finalPowerUsage.singleGround)
|
|
||||||
finalPowerUsage.topGround = finalPowerUsage.bottomGround = false;
|
|
||||||
if (finalPowerUsage.singleThreeVolt)
|
|
||||||
finalPowerUsage.topThreeVolt = finalPowerUsage.bottomThreeVolt = false;
|
|
||||||
return finalPowerUsage;
|
|
||||||
}
|
|
||||||
function copyDoubleArray(a: string[][]) {
|
|
||||||
return a.map(b => b.map(p => p));
|
|
||||||
}
|
|
||||||
function merge2<A, B>(a: A, b: B): A & B {
|
|
||||||
let res: any = {};
|
|
||||||
for (let aKey in a)
|
|
||||||
res[aKey] = (<any>a)[aKey];
|
|
||||||
for (let bKey in b)
|
|
||||||
res[bKey] = (<any>b)[bKey];
|
|
||||||
return <A & B>res;
|
|
||||||
}
|
|
||||||
function merge3<A, B, C>(a: A, b: B, c: C): A & B & C {
|
|
||||||
return merge2(merge2(a, b), c);
|
|
||||||
}
|
|
||||||
function readPin(arg: string): MicrobitPin {
|
|
||||||
U.assert(!!arg, "Invalid pin: " + arg);
|
|
||||||
let pin = arg.split("DigitalPin.")[1];
|
|
||||||
return <MicrobitPin>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;
|
|
||||||
}
|
|
||||||
function isConnectedToBB(pin: PartPinDefinition): boolean {
|
|
||||||
return pin.orientation === "-Z" && pin.style === "male";
|
|
||||||
}
|
|
||||||
class Allocator {
|
|
||||||
//TODO: better handling of allocation errors
|
|
||||||
private opts: AllocatorOpts;
|
|
||||||
private availablePowerPins = {
|
|
||||||
top: {
|
|
||||||
threeVolt: mkRange(26, 51).map(n => <BBLoc>{type: "breadboard", row: "+", col: `${n}`}),
|
|
||||||
ground: mkRange(26, 51).map(n => <BBLoc>{type: "breadboard", row: "-", col: `${n}`}),
|
|
||||||
},
|
|
||||||
bottom: {
|
|
||||||
threeVolt: mkRange(1, 26).map(n => <BBLoc>{type: "breadboard", row: "+", col: `${n}`}),
|
|
||||||
ground: mkRange(1, 26).map(n => <BBLoc>{type: "breadboard", row: "-", col: `${n}`}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
private powerUsage: PowerUsage;
|
|
||||||
private availableWireColors: string[];
|
|
||||||
|
|
||||||
constructor(opts: AllocatorOpts) {
|
|
||||||
this.opts = opts;
|
|
||||||
}
|
|
||||||
|
|
||||||
private allocPartIRs(def: PartDefinition, name: string, bbFit: PartBBFit): PartIR[] {
|
|
||||||
let partIRs: PartIR[] = [];
|
|
||||||
let mkIR = (def: PartDefinition, name: string, instPins?: PinTarget[], partParams?: Map<string>): PartIR => {
|
|
||||||
let pinIRs: PinIR[] = [];
|
|
||||||
for (let i = 0; i < def.numberOfPins; i++) {
|
|
||||||
let pinDef = def.pinDefinitions[i];
|
|
||||||
let pinTarget: PinTarget;
|
|
||||||
if (typeof pinDef.target === "string") {
|
|
||||||
pinTarget = <PinTarget>pinDef.target;
|
|
||||||
} else {
|
|
||||||
let instIdx = (<PinInstantiationIdx>pinDef.target).pinInstantiationIdx;
|
|
||||||
U.assert(!!instPins && instPins[instIdx] !== undefined,
|
|
||||||
`No pin found for PinInstantiationIdx: ${instIdx}. (Is the part missing an ArguementRole or "trackArgs=" annotations?)`);
|
|
||||||
pinTarget = instPins[instIdx];
|
|
||||||
}
|
|
||||||
let pinLoc = def.visual.pinLocations[i];
|
|
||||||
let adjustedY = bbFit.yOffset + pinLoc.y;
|
|
||||||
let relativeRowIdx = Math.round(adjustedY / def.visual.pinDistance);
|
|
||||||
let relativeYOffset = adjustedY - relativeRowIdx * def.visual.pinDistance;
|
|
||||||
let adjustedX = bbFit.xOffset + pinLoc.x;
|
|
||||||
let relativeColIdx = Math.round(adjustedX / def.visual.pinDistance);
|
|
||||||
let relativeXOffset = adjustedX - relativeColIdx * def.visual.pinDistance;
|
|
||||||
let pinBBFit: PinBBFit = {
|
|
||||||
partRelativeRowIdx: relativeRowIdx,
|
|
||||||
partRelativeColIdx: relativeColIdx,
|
|
||||||
xOffset: relativeXOffset,
|
|
||||||
yOffset: relativeYOffset
|
|
||||||
};
|
|
||||||
pinIRs.push({
|
|
||||||
def: pinDef,
|
|
||||||
loc: pinLoc,
|
|
||||||
target: pinTarget,
|
|
||||||
bbFit: pinBBFit,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: name,
|
|
||||||
def: def,
|
|
||||||
pins: pinIRs,
|
|
||||||
partParams: partParams || {},
|
|
||||||
bbFit: bbFit
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (def.instantiation.kind === "singleton") {
|
|
||||||
partIRs.push(mkIR(def, name));
|
|
||||||
} else if (def.instantiation.kind === "function") {
|
|
||||||
let fnAlloc = def.instantiation as PartFunctionDefinition;
|
|
||||||
let fnNm = fnAlloc.fullyQualifiedName;
|
|
||||||
let callsitesTrackedArgs = <string[]>this.opts.fnArgs[fnNm];
|
|
||||||
U.assert(!!callsitesTrackedArgs && !!callsitesTrackedArgs.length, "Failed to read pin(s) from callsite for: " + fnNm);
|
|
||||||
callsitesTrackedArgs.forEach(fnArgsStr => {
|
|
||||||
let fnArgsSplit = fnArgsStr.split(",");
|
|
||||||
U.assert(fnArgsSplit.length === fnAlloc.argumentRoles.length,
|
|
||||||
`Mismatch between number of arguments at callsite (function name: ${fnNm}) vs number of argument roles in part definition (part: ${name}).`);
|
|
||||||
let instPins: PinTarget[] = [];
|
|
||||||
let paramArgs: Map<string> = {};
|
|
||||||
fnArgsSplit.forEach((arg, idx) => {
|
|
||||||
let role = fnAlloc.argumentRoles[idx];
|
|
||||||
if (role.partParameter !== undefined) {
|
|
||||||
paramArgs[role.partParameter] = arg;
|
|
||||||
}
|
|
||||||
if (role.pinInstantiationIdx !== undefined) {
|
|
||||||
let instIdx = role.pinInstantiationIdx;
|
|
||||||
let pin = readPin(arg);
|
|
||||||
instPins[instIdx] = pin;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
partIRs.push(mkIR(def, name, instPins, paramArgs));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return partIRs;
|
|
||||||
}
|
|
||||||
private computePartDimensions(def: PartDefinition, name: string): PartBBFit {
|
|
||||||
let pinLocs = def.visual.pinLocations;
|
|
||||||
let pinDefs = def.pinDefinitions;
|
|
||||||
let numPins = def.numberOfPins;
|
|
||||||
U.assert(pinLocs.length === numPins, `Mismatch between "numberOfPins" and length of "visual.pinLocations" for "${name}"`);
|
|
||||||
U.assert(pinDefs.length === numPins, `Mismatch between "numberOfPins" and length of "pinDefinitions" for "${name}"`);
|
|
||||||
U.assert(numPins > 0, `Part "${name}" has no pins`);
|
|
||||||
let pins = pinLocs.map((loc, idx) => merge3({idx: idx}, loc, pinDefs[idx]));
|
|
||||||
let bbPins = pins.filter(p => p.orientation === "-Z");
|
|
||||||
let hasBBPins = bbPins.length > 0;
|
|
||||||
let pinDist = def.visual.pinDistance;
|
|
||||||
let xOff: number;
|
|
||||||
let yOff: number;
|
|
||||||
let colCount: number;
|
|
||||||
let rowCount: number;
|
|
||||||
if (hasBBPins) {
|
|
||||||
let refPin = bbPins[0];
|
|
||||||
let refPinColIdx = Math.ceil(refPin.x / pinDist);
|
|
||||||
let refPinRowIdx = Math.ceil(refPin.y / pinDist);
|
|
||||||
xOff = refPinColIdx * pinDist - refPin.x;
|
|
||||||
yOff = refPinRowIdx * pinDist - refPin.y;
|
|
||||||
colCount = Math.ceil((xOff + def.visual.width) / pinDist) + 1;
|
|
||||||
rowCount = Math.ceil((yOff + def.visual.height) / pinDist) + 1;
|
|
||||||
} else {
|
|
||||||
colCount = Math.ceil(def.visual.width / pinDist);
|
|
||||||
rowCount = Math.ceil(def.visual.height / pinDist);
|
|
||||||
xOff = colCount * pinDist - def.visual.width;
|
|
||||||
yOff = rowCount * pinDist - def.visual.height;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
xOffset: xOff,
|
|
||||||
yOffset: yOff,
|
|
||||||
rowCount: rowCount,
|
|
||||||
colCount: colCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
private allocColumns(colCounts: {colCount: number}[]): number[] {
|
|
||||||
let partsCount = colCounts.length;
|
|
||||||
const totalColumnsCount = visuals.BREADBOARD_MID_COLS; //TODO allow multiple breadboards
|
|
||||||
let totalSpaceNeeded = colCounts.map(d => d.colCount).reduce((p, n) => p + n, 0);
|
|
||||||
let extraSpace = totalColumnsCount - totalSpaceNeeded;
|
|
||||||
if (extraSpace <= 0) {
|
|
||||||
console.log("Not enough breadboard space!");
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
let padding = Math.floor(extraSpace / (partsCount - 1 + 2));
|
|
||||||
let partSpacing = padding; //Math.floor(extraSpace/(partsCount-1));
|
|
||||||
let totalPartPadding = extraSpace - partSpacing * (partsCount - 1);
|
|
||||||
let leftPadding = Math.floor(totalPartPadding / 2);
|
|
||||||
let rightPadding = Math.ceil(totalPartPadding / 2);
|
|
||||||
let nextAvailableCol = 1 + leftPadding;
|
|
||||||
let partStartCol = colCounts.map(part => {
|
|
||||||
let col = nextAvailableCol;
|
|
||||||
nextAvailableCol += part.colCount + partSpacing;
|
|
||||||
return col;
|
|
||||||
});
|
|
||||||
return partStartCol;
|
|
||||||
}
|
|
||||||
private placeParts(parts: PartIR[]): PartPlacement[] {
|
|
||||||
const totalRowsCount = visuals.BREADBOARD_MID_ROWS + 2; // 10 letters + 2 for the middle gap
|
|
||||||
let startColumnIndices = this.allocColumns(parts.map(p => p.bbFit));
|
|
||||||
let startRowIndicies = parts.map(p => {
|
|
||||||
let extraRows = totalRowsCount - p.bbFit.rowCount;
|
|
||||||
let topPad = Math.floor(extraRows / 2);
|
|
||||||
let startIdx = topPad;
|
|
||||||
if (startIdx > 4)
|
|
||||||
startIdx = 4;
|
|
||||||
if (startIdx < 1)
|
|
||||||
startIdx = 1;
|
|
||||||
return startIdx;
|
|
||||||
});
|
|
||||||
let placements = parts.map((p, idx) => {
|
|
||||||
let row = startRowIndicies[idx];
|
|
||||||
let col = startColumnIndices[idx];
|
|
||||||
return merge2({startColumnIdx: col, startRowIdx: row}, p);
|
|
||||||
});
|
|
||||||
return placements;
|
|
||||||
}
|
|
||||||
private nextColor(): string {
|
|
||||||
if (!this.availableWireColors || this.availableWireColors.length <= 0) {
|
|
||||||
this.availableWireColors = visuals.GPIO_WIRE_COLORS.map(c => c);
|
|
||||||
}
|
|
||||||
return this.availableWireColors.pop();
|
|
||||||
}
|
|
||||||
private allocWireIRs(part: PartPlacement): PartIRAndWireIRs {
|
|
||||||
let groupToColor: string[] = [];
|
|
||||||
let wires: WireIR[] = part.pins.map((pin, pinIdx) => {
|
|
||||||
let end = pin.target;
|
|
||||||
let start: WireIRLoc;
|
|
||||||
let colIdx = part.startColumnIdx + pin.bbFit.partRelativeColIdx;
|
|
||||||
let colName = visuals.getColumnName(colIdx);
|
|
||||||
let pinRowIdx = part.startRowIdx + pin.bbFit.partRelativeRowIdx;
|
|
||||||
if (pinRowIdx >= 7) //account for middle gap
|
|
||||||
pinRowIdx -= 2;
|
|
||||||
if (isConnectedToBB(pin.def)) {
|
|
||||||
//make a wire from bb top or bottom to target
|
|
||||||
let connectedToTop = pinRowIdx < 5;
|
|
||||||
let rowName = connectedToTop ? "j" : "a";
|
|
||||||
start = {
|
|
||||||
type: "breadboard",
|
|
||||||
row: rowName,
|
|
||||||
col: colName,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
//make a wire directly from pin to target
|
|
||||||
let rowName = visuals.getRowName(pinRowIdx);
|
|
||||||
start = {
|
|
||||||
type: "breadboard",
|
|
||||||
row: rowName,
|
|
||||||
col: colName,
|
|
||||||
xOffset: pin.bbFit.xOffset / part.def.visual.pinDistance,
|
|
||||||
yOffset: pin.bbFit.yOffset / part.def.visual.pinDistance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let color: string;
|
|
||||||
if (end === "ground") {
|
|
||||||
color = GROUND_COLOR;
|
|
||||||
} else if (end === "threeVolt") {
|
|
||||||
color = POWER_COLOR;
|
|
||||||
} else if (typeof pin.def.colorGroup === "number") {
|
|
||||||
if (groupToColor[pin.def.colorGroup]) {
|
|
||||||
color = groupToColor[pin.def.colorGroup];
|
|
||||||
} else {
|
|
||||||
color = groupToColor[pin.def.colorGroup] = this.nextColor();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
color = this.nextColor()
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
start: start,
|
|
||||||
end: end,
|
|
||||||
color: color,
|
|
||||||
pinIdx: pinIdx,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return merge2(part, {wires: wires});
|
|
||||||
}
|
|
||||||
private allocLocation(location: WireIRLoc, opts: AllocLocOpts): Loc {
|
|
||||||
if (location === "ground" || location === "threeVolt") {
|
|
||||||
//special case if there is only a single ground or three volt pin in the whole build
|
|
||||||
if (location === "ground" && this.powerUsage.singleGround) {
|
|
||||||
let boardGroundPin = this.getBoardGroundPin();
|
|
||||||
return {type: "dalboard", pin: boardGroundPin};
|
|
||||||
} else if (location === "threeVolt" && this.powerUsage.singleThreeVolt) {
|
|
||||||
let boardThreeVoltPin = this.getBoardThreeVoltPin();
|
|
||||||
return {type: "dalboard", pin: boardThreeVoltPin};
|
|
||||||
}
|
|
||||||
|
|
||||||
U.assert(!!opts.referenceBBPin);
|
|
||||||
let nearestCoord = this.opts.getBBCoord(opts.referenceBBPin);
|
|
||||||
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 barPins: BBLoc[];
|
|
||||||
if (nearTop) {
|
|
||||||
if (location === "ground") {
|
|
||||||
barPins = this.availablePowerPins.top.ground;
|
|
||||||
} else if (location === "threeVolt") {
|
|
||||||
barPins = this.availablePowerPins.top.threeVolt;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (location === "ground") {
|
|
||||||
barPins = this.availablePowerPins.bottom.ground;
|
|
||||||
} else if (location === "threeVolt") {
|
|
||||||
barPins = this.availablePowerPins.bottom.threeVolt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let pinCoords = barPins.map(rowCol => {
|
|
||||||
return this.opts.getBBCoord(rowCol);
|
|
||||||
});
|
|
||||||
let closestPinIdx = visuals.findClosestCoordIdx(nearestCoord, pinCoords);
|
|
||||||
let pin = barPins[closestPinIdx];
|
|
||||||
if (nearTop) {
|
|
||||||
this.availablePowerPins.top.ground.splice(closestPinIdx, 1);
|
|
||||||
this.availablePowerPins.top.threeVolt.splice(closestPinIdx, 1);
|
|
||||||
} else {
|
|
||||||
this.availablePowerPins.bottom.ground.splice(closestPinIdx, 1);
|
|
||||||
this.availablePowerPins.bottom.threeVolt.splice(closestPinIdx, 1);
|
|
||||||
}
|
|
||||||
return pin;
|
|
||||||
} else if ((<BBLoc>location).type === "breadboard") {
|
|
||||||
return <BBLoc>location;
|
|
||||||
} else if (location === "MOSI" || location === "MISO" || location === "SCK") {
|
|
||||||
if (!this.opts.boardDef.spiPins)
|
|
||||||
console.debug("No SPI pin mappings found!");
|
|
||||||
let pin = (<any>this.opts.boardDef.spiPins)[location as string] as string;
|
|
||||||
return {type: "dalboard", pin: pin};
|
|
||||||
} else if (location === "SDA" || location === "SCL") {
|
|
||||||
if (!this.opts.boardDef.i2cPins)
|
|
||||||
console.debug("No I2C pin mappings found!");
|
|
||||||
let pin = (<any>this.opts.boardDef.i2cPins)[location as string] as string;
|
|
||||||
return {type: "dalboard", pin: pin};
|
|
||||||
} else {
|
|
||||||
//it must be a MicrobitPin
|
|
||||||
U.assert(typeof location === "string", "Unknown location type: " + location);
|
|
||||||
let mbPin = <MicrobitPin>location;
|
|
||||||
let boardPin = this.opts.boardDef.gpioPinMap[mbPin];
|
|
||||||
U.assert(!!boardPin, "Unknown pin: " + location);
|
|
||||||
return {type: "dalboard", pin: boardPin};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private getBoardGroundPin(): string {
|
|
||||||
let boardGround = this.opts.boardDef.groundPins[0] || null;
|
|
||||||
if (!boardGround) {
|
|
||||||
console.log("No available ground pin on board!");
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
return boardGround;
|
|
||||||
}
|
|
||||||
private getBoardThreeVoltPin(): string {
|
|
||||||
let threeVoltPin = this.opts.boardDef.threeVoltPins[0] || null;
|
|
||||||
if (!threeVoltPin) {
|
|
||||||
console.log("No available 3.3V pin on board!");
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
return threeVoltPin;
|
|
||||||
}
|
|
||||||
private allocPowerWires(powerUsage: PowerUsage): PartAndWiresInst {
|
|
||||||
let boardGroundPin = this.getBoardGroundPin();
|
|
||||||
let threeVoltPin = this.getBoardThreeVoltPin();
|
|
||||||
const topLeft: BBLoc = {type: "breadboard", row: "-", col: "26"};
|
|
||||||
const botLeft: BBLoc = {type: "breadboard", row: "-", col: "1"};
|
|
||||||
const topRight: BBLoc = {type: "breadboard", row: "-", col: "50"};
|
|
||||||
const botRight: BBLoc = {type: "breadboard", row: "-", col: "25"};
|
|
||||||
let top: BBLoc, bot: BBLoc;
|
|
||||||
if (this.opts.boardDef.attachPowerOnRight) {
|
|
||||||
top = topRight;
|
|
||||||
bot = botRight;
|
|
||||||
} else {
|
|
||||||
top = topLeft;
|
|
||||||
bot = botLeft;
|
|
||||||
}
|
|
||||||
let groundWires: WireInst[] = [];
|
|
||||||
let threeVoltWires: WireInst[] = [];
|
|
||||||
if (powerUsage.bottomGround && powerUsage.topGround) {
|
|
||||||
//bb top - <==> bb bot -
|
|
||||||
groundWires.push({
|
|
||||||
start: this.allocLocation("ground", {referenceBBPin: top}),
|
|
||||||
end: this.allocLocation("ground", {referenceBBPin: bot}),
|
|
||||||
color: GROUND_COLOR,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (powerUsage.topGround) {
|
|
||||||
//board - <==> bb top -
|
|
||||||
groundWires.push({
|
|
||||||
start: this.allocLocation("ground", {referenceBBPin: top}),
|
|
||||||
end: {type: "dalboard", pin: boardGroundPin},
|
|
||||||
color: GROUND_COLOR,
|
|
||||||
});
|
|
||||||
} else if (powerUsage.bottomGround) {
|
|
||||||
//board - <==> bb bot -
|
|
||||||
groundWires.push({
|
|
||||||
start: this.allocLocation("ground", {referenceBBPin: bot}),
|
|
||||||
end: {type: "dalboard", pin: boardGroundPin},
|
|
||||||
color: GROUND_COLOR,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (powerUsage.bottomThreeVolt && powerUsage.bottomGround) {
|
|
||||||
//bb top + <==> bb bot +
|
|
||||||
threeVoltWires.push({
|
|
||||||
start: this.allocLocation("threeVolt", {referenceBBPin: top}),
|
|
||||||
end: this.allocLocation("threeVolt", {referenceBBPin: bot}),
|
|
||||||
color: POWER_COLOR,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (powerUsage.topThreeVolt) {
|
|
||||||
//board + <==> bb top +
|
|
||||||
threeVoltWires.push({
|
|
||||||
start: this.allocLocation("threeVolt", {referenceBBPin: top}),
|
|
||||||
end: {type: "dalboard", pin: threeVoltPin},
|
|
||||||
color: POWER_COLOR,
|
|
||||||
});
|
|
||||||
} else if (powerUsage.bottomThreeVolt) {
|
|
||||||
//board + <==> bb bot +
|
|
||||||
threeVoltWires.push({
|
|
||||||
start: this.allocLocation("threeVolt", {referenceBBPin: bot}),
|
|
||||||
end: {type: "dalboard", pin: threeVoltPin},
|
|
||||||
color: POWER_COLOR,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let assembly: AssemblyStep[] = [];
|
|
||||||
if (groundWires.length > 0)
|
|
||||||
assembly.push({wireIndices: groundWires.map((w, i) => i)});
|
|
||||||
let numGroundWires = groundWires.length;
|
|
||||||
if (threeVoltWires.length > 0)
|
|
||||||
assembly.push({wireIndices: threeVoltWires.map((w, i) => i + numGroundWires)});
|
|
||||||
return {
|
|
||||||
wires: groundWires.concat(threeVoltWires),
|
|
||||||
assembly: assembly
|
|
||||||
};
|
|
||||||
}
|
|
||||||
private allocWire(wireIR: WireIR): WireInst {
|
|
||||||
let ends = [wireIR.start, wireIR.end];
|
|
||||||
let endIsPower = ends.map(e => e === "ground" || e === "threeVolt");
|
|
||||||
//allocate non-power first so we know the nearest pin for the power end
|
|
||||||
let endInsts = ends.map((e, idx) => !endIsPower[idx] ? this.allocLocation(e, {}) : null)
|
|
||||||
//allocate power pins closest to the other end of the wire
|
|
||||||
endInsts = endInsts.map((e, idx) => {
|
|
||||||
if (e)
|
|
||||||
return e;
|
|
||||||
let locInst = <BBLoc>endInsts[1 - idx]; // non-power end
|
|
||||||
let l = this.allocLocation(ends[idx], {
|
|
||||||
referenceBBPin: locInst,
|
|
||||||
});
|
|
||||||
return l;
|
|
||||||
});
|
|
||||||
return {start: endInsts[0], end: endInsts[1], color: wireIR.color};
|
|
||||||
}
|
|
||||||
private allocPart(ir: PartPlacement): PartInst {
|
|
||||||
let bbConnections = ir.pins
|
|
||||||
.filter(p => isConnectedToBB(p.def))
|
|
||||||
.map(p => {
|
|
||||||
let rowIdx = ir.startRowIdx + p.bbFit.partRelativeRowIdx;
|
|
||||||
if (rowIdx >= 7) //account for middle gap
|
|
||||||
rowIdx -= 2;
|
|
||||||
let rowName = visuals.getRowName(rowIdx);
|
|
||||||
let colIdx = ir.startColumnIdx + p.bbFit.partRelativeColIdx;
|
|
||||||
let colName = visuals.getColumnName(colIdx);
|
|
||||||
return <BBLoc>{
|
|
||||||
type: "breadboard",
|
|
||||||
row: rowName,
|
|
||||||
col: colName,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let part: PartInst = {
|
|
||||||
name: ir.name,
|
|
||||||
visual: ir.def.visual,
|
|
||||||
bbFit: ir.bbFit,
|
|
||||||
startColumnIdx: ir.startColumnIdx,
|
|
||||||
startRowIdx: ir.startRowIdx,
|
|
||||||
breadboardConnections: bbConnections,
|
|
||||||
params: ir.partParams,
|
|
||||||
simulationBehavior: ir.def.simulationBehavior
|
|
||||||
}
|
|
||||||
return part;
|
|
||||||
}
|
|
||||||
public allocAll(): AllocatorResult {
|
|
||||||
let partNmAndDefs = this.opts.partsList
|
|
||||||
.map(partName => {return {name: partName, def: this.opts.partDefs[partName]}})
|
|
||||||
.filter(d => !!d.def);
|
|
||||||
if (partNmAndDefs.length > 0) {
|
|
||||||
let partNmsList = partNmAndDefs.map(p => p.name);
|
|
||||||
let partDefsList = partNmAndDefs.map(p => p.def);
|
|
||||||
let dimensions = partNmAndDefs.map(nmAndPart => this.computePartDimensions(nmAndPart.def, nmAndPart.name));
|
|
||||||
let partIRs: PartIR[] = [];
|
|
||||||
partNmAndDefs.forEach((nmAndDef, idx) => {
|
|
||||||
let dims = dimensions[idx];
|
|
||||||
let irs = this.allocPartIRs(nmAndDef.def, nmAndDef.name, dims);
|
|
||||||
partIRs = partIRs.concat(irs);
|
|
||||||
})
|
|
||||||
let partPlacements = this.placeParts(partIRs);
|
|
||||||
let partsAndWireIRs = partPlacements.map(p => this.allocWireIRs(p));
|
|
||||||
let allWireIRs = partsAndWireIRs.map(p => p.wires).reduce((p, n) => p.concat(n), []);
|
|
||||||
let allPowerUsage = allWireIRs.map(w => computePowerUsage(w));
|
|
||||||
this.powerUsage = mergePowerUsage(allPowerUsage);
|
|
||||||
let basicWires = this.allocPowerWires(this.powerUsage);
|
|
||||||
let partsAndWires: PartAndWiresInst[] = partsAndWireIRs.map((irs, idx) => {
|
|
||||||
let part = this.allocPart(irs);
|
|
||||||
let wires = irs.wires.map(w => this.allocWire(w));
|
|
||||||
let pinIdxToWireIdx: number[] = [];
|
|
||||||
irs.wires.forEach((wIR, idx) => {
|
|
||||||
pinIdxToWireIdx[wIR.pinIdx] = idx;
|
|
||||||
});
|
|
||||||
let assembly: AssemblyStep[] = irs.def.assembly.map(stepDef => {
|
|
||||||
return {
|
|
||||||
part: stepDef.part,
|
|
||||||
wireIndices: (stepDef.pinIndices || []).map(i => pinIdxToWireIdx[i])
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
part: part,
|
|
||||||
wires: wires,
|
|
||||||
assembly: assembly
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let all = [basicWires].concat(partsAndWires);
|
|
||||||
return {
|
|
||||||
partsAndWires: all
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
partsAndWires: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function allocateDefinitions(opts: AllocatorOpts): AllocatorResult {
|
|
||||||
return new Allocator(opts).allocAll();
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ namespace pxsim {
|
|||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
// the bus
|
// the bus
|
||||||
bus: EventBus;
|
bus: pxsim.EventBus;
|
||||||
|
|
||||||
// state & update logic for component services
|
// state & update logic for component services
|
||||||
ledMatrixState: LedMatrixState;
|
ledMatrixState: LedMatrixState;
|
||||||
@ -25,7 +25,7 @@ namespace pxsim {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.id = "b" + Math_.random(2147483647);
|
this.id = "b" + Math_.random(2147483647);
|
||||||
this.bus = new EventBus(runtime);
|
this.bus = new pxsim.EventBus(runtime);
|
||||||
|
|
||||||
// components
|
// components
|
||||||
this.ledMatrixState = new LedMatrixState(runtime);
|
this.ledMatrixState = new LedMatrixState(runtime);
|
||||||
@ -97,4 +97,37 @@ namespace pxsim {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function initRuntimeWithDalBoard() {
|
||||||
|
U.assert(!runtime.board);
|
||||||
|
let b = new DalBoard();
|
||||||
|
runtime.board = b;
|
||||||
|
runtime.postError = (e) => {
|
||||||
|
led.setBrightness(255);
|
||||||
|
let img = board().ledMatrixState.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pxsim.initCurrentRuntime) {
|
||||||
|
pxsim.initCurrentRuntime = initRuntimeWithDalBoard;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function board() {
|
||||||
|
return runtime.board as DalBoard;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,8 +1,6 @@
|
|||||||
/// <reference path="../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
/// <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/pxtsim.d.ts"/>
|
||||||
/// <reference path="../node_modules/pxt-core/built/pxtrunner.d.ts"/>
|
/// <reference path="../node_modules/pxt-core/built/pxtrunner.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
|
//HACK: allows instructions.html to access pxtblocks without requiring simulator.html to import blocks as well
|
||||||
if (!(<any>window).pxt) (<any>window).pxt = {};
|
if (!(<any>window).pxt) (<any>window).pxt = {};
|
||||||
|
@ -226,7 +226,14 @@ namespace pxsim.visuals {
|
|||||||
wireframe?: boolean;
|
wireframe?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointerEvents = !!(window as any).PointerEvent ? {
|
export interface IPointerEvents {
|
||||||
|
up: string,
|
||||||
|
down: string,
|
||||||
|
move: string,
|
||||||
|
leave: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pointerEvents = !!(window as any).PointerEvent ? {
|
||||||
up: "pointerup",
|
up: "pointerup",
|
||||||
down: "pointerdown",
|
down: "pointerdown",
|
||||||
move: "pointermove",
|
move: "pointermove",
|
||||||
|
271
sim/simlib.ts
271
sim/simlib.ts
@ -1,271 +0,0 @@
|
|||||||
/// <reference path="../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
|
||||||
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
|
||||||
|
|
||||||
namespace pxsim {
|
|
||||||
export type BoardPin = string;
|
|
||||||
export interface BBLoc {
|
|
||||||
type: "breadboard",
|
|
||||||
row: string,
|
|
||||||
col: string
|
|
||||||
xOffset?: number,
|
|
||||||
yOffset?: number
|
|
||||||
};
|
|
||||||
export interface BoardLoc {
|
|
||||||
type: "dalboard",
|
|
||||||
pin: BoardPin
|
|
||||||
};
|
|
||||||
export type Loc = BBLoc | BoardLoc;
|
|
||||||
|
|
||||||
export function initRuntimeWithDalBoard() {
|
|
||||||
U.assert(!runtime.board);
|
|
||||||
let b = new DalBoard();
|
|
||||||
runtime.board = b;
|
|
||||||
runtime.postError = (e) => {
|
|
||||||
led.setBrightness(255);
|
|
||||||
let img = board().ledMatrixState.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!pxsim.initCurrentRuntime) {
|
|
||||||
pxsim.initCurrentRuntime = initRuntimeWithDalBoard;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function board() {
|
|
||||||
return runtime.board as DalBoard;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mkRange(a: number, b: number): number[] {
|
|
||||||
let res: number[] = [];
|
|
||||||
for (; a < b; a++)
|
|
||||||
res.push(a);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace pxsim.visuals {
|
|
||||||
export interface IPointerEvents {
|
|
||||||
up: string,
|
|
||||||
down: string,
|
|
||||||
move: string,
|
|
||||||
leave: string
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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?: string,
|
|
||||||
maxHeight?: string,
|
|
||||||
}
|
|
||||||
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: string, h: string) => {
|
|
||||||
if (w)
|
|
||||||
svg.hydrate(e, { width: w });
|
|
||||||
if (h)
|
|
||||||
svg.hydrate(e, { height: h });
|
|
||||||
}
|
|
||||||
let setWHpx = (e: SVGSVGElement, w: number, h: number) => svg.hydrate(e, { width: `${w}px`, height: `${h}px` });
|
|
||||||
let scaleUnit = opts.scaleUnit2;
|
|
||||||
let aScalar = opts.scaleUnit2 / opts.scaleUnit1;
|
|
||||||
let bScalar = 1.0;
|
|
||||||
let aw = a.w * aScalar;
|
|
||||||
let ah = a.h * aScalar;
|
|
||||||
setWHpx(a.el, aw, ah);
|
|
||||||
let bw = b.w * bScalar;
|
|
||||||
let bh = b.h * bScalar;
|
|
||||||
setWHpx(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 function mkScaleFn(originUnit: number, targetUnit: number): (n: number) => number {
|
|
||||||
return (n: number) => n * (targetUnit / originUnit);
|
|
||||||
}
|
|
||||||
export interface MkImageOpts {
|
|
||||||
image: string,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
imageUnitDist: number,
|
|
||||||
targetUnitDist: number
|
|
||||||
}
|
|
||||||
export function mkImageSVG(opts: MkImageOpts): SVGAndSize<SVGImageElement> {
|
|
||||||
let scaleFn = mkScaleFn(opts.imageUnitDist, opts.targetUnitDist);
|
|
||||||
let w = scaleFn(opts.width);
|
|
||||||
let h = scaleFn(opts.height);
|
|
||||||
let img = <SVGImageElement>svg.elt("image", {
|
|
||||||
width: w,
|
|
||||||
height: h,
|
|
||||||
"href": `${opts.image}`
|
|
||||||
});
|
|
||||||
return { el: img, w: w, h: h, x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
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 IBoardPart<T> {
|
|
||||||
style: string,
|
|
||||||
element: SVGElement,
|
|
||||||
overElement?: SVGElement,
|
|
||||||
defs: SVGElement[],
|
|
||||||
init(bus: EventBus, state: T, svgEl: SVGSVGElement, otherParams: Map<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" | "pink";
|
|
||||||
export const GPIO_WIRE_COLORS = ["pink", "green", "purple", "orange", "yellow"];
|
|
||||||
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",
|
|
||||||
pink: "#ff80fa"
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
|
|
||||||
export interface BoardView {
|
|
||||||
getView(): SVGAndSize<SVGSVGElement>;
|
|
||||||
getCoord(pinNm: string): Coord;
|
|
||||||
getPinDist(): number;
|
|
||||||
highlightPin(pinNm: string): void;
|
|
||||||
}
|
|
||||||
}
|
|
@ -77,25 +77,6 @@ namespace pxsim {
|
|||||||
export interface RuntimeOptions {
|
export interface RuntimeOptions {
|
||||||
theme: string;
|
theme: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventBus {
|
|
||||||
private queues: Map<EventQueue<number>> = {};
|
|
||||||
|
|
||||||
constructor(private runtime: Runtime) { }
|
|
||||||
|
|
||||||
listen(id: number, evid: number, handler: RefAction) {
|
|
||||||
let k = id + ":" + evid;
|
|
||||||
let queue = this.queues[k];
|
|
||||||
if (!queue) queue = this.queues[k] = new EventQueue<number>(this.runtime);
|
|
||||||
queue.handler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
queue(id: number, evid: number, value: number = 0) {
|
|
||||||
let k = id + ":" + evid;
|
|
||||||
let queue = this.queues[k];
|
|
||||||
if (queue) queue.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace pxsim.basic {
|
namespace pxsim.basic {
|
||||||
|
@ -1,653 +0,0 @@
|
|||||||
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;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.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:none;
|
|
||||||
stroke: #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
|
|
||||||
export const BREADBOARD_MID_ROWS = 10;
|
|
||||||
export const BREADBOARD_MID_COLS = 30;
|
|
||||||
const MID_ROW_GAPS = [4, 4];
|
|
||||||
const MID_ROW_AND_GAPS = BREADBOARD_MID_ROWS + MID_ROW_GAPS.length;
|
|
||||||
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 * (BREADBOARD_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 = (BREADBOARD_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;
|
|
||||||
// Row and column helpers
|
|
||||||
const alphabet = "abcdefghij".split("").reverse();
|
|
||||||
export function getColumnName(colIdx: number): string { return `${colIdx + 1}` };
|
|
||||||
export function getRowName(rowIdx: number): string { return alphabet[rowIdx] };
|
|
||||||
|
|
||||||
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 interface BreadboardOpts {
|
|
||||||
wireframe?: boolean,
|
|
||||||
}
|
|
||||||
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(opts: BreadboardOpts) {
|
|
||||||
this.buildDom();
|
|
||||||
|
|
||||||
if (opts.wireframe)
|
|
||||||
svg.addClass(this.bb, "sim-bb-outline");
|
|
||||||
}
|
|
||||||
|
|
||||||
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: BBLoc): Coord {
|
|
||||||
let {row, col, xOffset, yOffset} = rowCol;
|
|
||||||
let pin = this.getPin(row, col);
|
|
||||||
if (!pin)
|
|
||||||
return null;
|
|
||||||
let xOff = (xOffset || 0) * PIN_DIST;
|
|
||||||
let yOff = (yOffset || 0) * PIN_DIST;
|
|
||||||
return [pin.cx + xOff, pin.cy + yOff];
|
|
||||||
}
|
|
||||||
|
|
||||||
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 < BREADBOARD_MID_ROWS / 2.0 ? "b" : "t";
|
|
||||||
const getBarTopOrBot = (colIdx: number) => colIdx < POWER_COLS / 2.0 ? "b" : "t";
|
|
||||||
const getMidGroupName = (rowIdx: number, colIdx: number) => {
|
|
||||||
let botOrTop = getMidTopOrBot(rowIdx);
|
|
||||||
let colNm = getColumnName(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: BREADBOARD_MID_ROWS,
|
|
||||||
colCount: BREADBOARD_MID_COLS,
|
|
||||||
pinDist: PIN_DIST,
|
|
||||||
mkPin: mkBBPin,
|
|
||||||
mkHoverPin: mkBBHoverPin,
|
|
||||||
getRowName: getRowName,
|
|
||||||
getColName: getColumnName,
|
|
||||||
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: getColumnName,
|
|
||||||
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: getColumnName,
|
|
||||||
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({type: "breadboard", row: row, col: col});
|
|
||||||
let [cx, cy] = loc;
|
|
||||||
let t = mkBBLabel(cx + xOffset, cy + yOffset, size, rotation, txt, group);
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
//columns
|
|
||||||
for (let colIdx = 0; colIdx < BREADBOARD_MID_COLS; colIdx++) {
|
|
||||||
let colNm = getColumnName(colIdx);
|
|
||||||
//top
|
|
||||||
let rowTIdx = 0;
|
|
||||||
let rowTNm = getRowName(rowTIdx);
|
|
||||||
let groupT = getMidGroupName(rowTIdx, colIdx);
|
|
||||||
let lblT = mkBBLabelAtPin(rowTNm, colNm, 0, -PIN_DIST, colNm, groupT);
|
|
||||||
this.allLabels.push(lblT);
|
|
||||||
//bottom
|
|
||||||
let rowBIdx = BREADBOARD_MID_ROWS - 1;
|
|
||||||
let rowBNm = getRowName(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 < BREADBOARD_MID_ROWS; rowIdx++) {
|
|
||||||
let rowNm = getRowName(rowIdx);
|
|
||||||
//top
|
|
||||||
let colTIdx = 0;
|
|
||||||
let colTNm = getColumnName(colTIdx);
|
|
||||||
let lblT = mkBBLabelAtPin(rowNm, colTNm, -PIN_DIST, 0, rowNm);
|
|
||||||
this.allLabels.push(lblT);
|
|
||||||
//top
|
|
||||||
let colBIdx = BREADBOARD_MID_COLS - 1;
|
|
||||||
let colBNm = getColumnName(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(rowCol: BBLoc) {
|
|
||||||
let {row, col} = rowCol;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,305 +0,0 @@
|
|||||||
/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
|
||||||
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
|
||||||
|
|
||||||
namespace pxsim.visuals {
|
|
||||||
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 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
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 interface GenericBoardProps {
|
|
||||||
visualDef: BoardImageDefinition;
|
|
||||||
wireframe?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextBoardId = 0;
|
|
||||||
export class GenericBoardSvg implements BoardView {
|
|
||||||
private element: SVGSVGElement;
|
|
||||||
private style: SVGStyleElement;
|
|
||||||
private defs: SVGDefsElement;
|
|
||||||
private g: SVGGElement;
|
|
||||||
private background: SVGElement;
|
|
||||||
private width: number;
|
|
||||||
private height: number;
|
|
||||||
private id: number;
|
|
||||||
|
|
||||||
// pins & labels
|
|
||||||
//(truth)
|
|
||||||
private allPins: GridPin[] = [];
|
|
||||||
private allLabels: GridLabel[] = [];
|
|
||||||
//(cache)
|
|
||||||
private pinNmToLbl: Map<GridLabel> = {};
|
|
||||||
private pinNmToPin: Map<GridPin> = {};
|
|
||||||
|
|
||||||
constructor(public props: GenericBoardProps) {
|
|
||||||
//TODO: handle wireframe mode
|
|
||||||
this.id = nextBoardId++;
|
|
||||||
let visDef = props.visualDef;
|
|
||||||
let imgHref = props.wireframe ? visDef.outlineImage : visDef.image;
|
|
||||||
let boardImgAndSize = mkImageSVG({
|
|
||||||
image: imgHref,
|
|
||||||
width: visDef.width,
|
|
||||||
height: visDef.height,
|
|
||||||
imageUnitDist: visDef.pinDist,
|
|
||||||
targetUnitDist: PIN_DIST
|
|
||||||
});
|
|
||||||
let scaleFn = mkScaleFn(visDef.pinDist, PIN_DIST);
|
|
||||||
this.width = boardImgAndSize.w;
|
|
||||||
this.height = boardImgAndSize.h;
|
|
||||||
let img = boardImgAndSize.el;
|
|
||||||
this.element = <SVGSVGElement>svg.elt("svg");
|
|
||||||
svg.hydrate(this.element, {
|
|
||||||
"version": "1.0",
|
|
||||||
"viewBox": `0 0 ${this.width} ${this.height}`,
|
|
||||||
"class": `sim sim-board-id-${this.id}`,
|
|
||||||
"x": "0px",
|
|
||||||
"y": "0px"
|
|
||||||
});
|
|
||||||
if (props.wireframe)
|
|
||||||
svg.addClass(this.element, "sim-board-outline")
|
|
||||||
this.style = <SVGStyleElement>svg.child(this.element, "style", {});
|
|
||||||
this.style.textContent += BOARD_SYTLE;
|
|
||||||
this.defs = <SVGDefsElement>svg.child(this.element, "defs", {});
|
|
||||||
this.g = <SVGGElement>svg.elt("g");
|
|
||||||
this.element.appendChild(this.g);
|
|
||||||
|
|
||||||
// main board
|
|
||||||
this.g.appendChild(img);
|
|
||||||
this.background = img;
|
|
||||||
svg.hydrate(img, { class: "sim-board" });
|
|
||||||
let backgroundCover = this.mkGrayCover(0, 0, this.width, this.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 = scaleFn(pinBlock.x) + PIN_DIST / 2.0;
|
|
||||||
let yOffset = 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 = visDef.pinBlocks.map(mkPinBlockGrid);
|
|
||||||
let pinToBlockDef: PinBlockDefinition[] = [];
|
|
||||||
pinBlocks.forEach((blk, blkIdx) => blk.allPins.forEach((p, pIdx) => {
|
|
||||||
this.allPins.push(p);
|
|
||||||
pinToBlockDef.push(visDef.pinBlocks[blkIdx]);
|
|
||||||
}));
|
|
||||||
//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, pos: "above" | "below"): SVGTextElement => {
|
|
||||||
//TODO: extract constants
|
|
||||||
let lblY: number;
|
|
||||||
let lblX: number;
|
|
||||||
|
|
||||||
if (pos === "below") {
|
|
||||||
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, pos: "above" | "below"): GridLabel => {
|
|
||||||
let el = mkLabelTxtEl(pinX, pinY, PIN_LBL_SIZE, txt, pos);
|
|
||||||
svg.addClass(el, "sim-board-pin-lbl");
|
|
||||||
let hoverEl = mkLabelTxtEl(pinX, pinY, PIN_LBL_HOVER_SIZE, txt, pos);
|
|
||||||
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, pIdx) => {
|
|
||||||
let blk = pinToBlockDef[pIdx];
|
|
||||||
return mkLabel(p.cx, p.cy, p.col, blk.labelPosition);
|
|
||||||
});
|
|
||||||
//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 getCoord(pinNm: string): Coord {
|
|
||||||
let pin = this.pinNmToPin[pinNm];
|
|
||||||
if (!pin)
|
|
||||||
return null;
|
|
||||||
return [pin.cx, pin.cy];
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public getView(): SVGAndSize<SVGSVGElement> {
|
|
||||||
return {el: this.element, w: this.width, h: this.height, x: 0, y: 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getPinDist() {
|
|
||||||
return PIN_DIST;
|
|
||||||
}
|
|
||||||
|
|
||||||
public highlightPin(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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
namespace pxsim.visuals {
|
|
||||||
export function mkGenericPartSVG(partVisual: PartVisualDefinition): SVGAndSize<SVGImageElement> {
|
|
||||||
let imgAndSize = mkImageSVG({
|
|
||||||
image: partVisual.image,
|
|
||||||
width: partVisual.width,
|
|
||||||
height: partVisual.height,
|
|
||||||
imageUnitDist: partVisual.pinDistance,
|
|
||||||
targetUnitDist: PIN_DIST
|
|
||||||
});
|
|
||||||
return imgAndSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GenericPart implements IBoardPart<any> {
|
|
||||||
public style: string = "";
|
|
||||||
public element: SVGElement;
|
|
||||||
defs: SVGElement[] = [];
|
|
||||||
|
|
||||||
constructor(partVisual: PartVisualDefinition) {
|
|
||||||
let imgAndSize = mkGenericPartSVG(partVisual);
|
|
||||||
let img = imgAndSize.el;
|
|
||||||
this.element = svg.elt("g");
|
|
||||||
this.element.appendChild(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
moveToCoord(xy: Coord): void {
|
|
||||||
translateEl(this.element, xy);
|
|
||||||
}
|
|
||||||
|
|
||||||
//unused
|
|
||||||
init(bus: EventBus, state: any, svgEl: SVGSVGElement): void { }
|
|
||||||
updateState(): void { }
|
|
||||||
updateTheme(): void { }
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,8 +2,6 @@
|
|||||||
/// <reference path="../../libs/microbit/dal.d.ts"/>
|
/// <reference path="../../libs/microbit/dal.d.ts"/>
|
||||||
/// <reference path="../../libs/microbit/shims.d.ts"/>
|
/// <reference path="../../libs/microbit/shims.d.ts"/>
|
||||||
/// <reference path="../../libs/microbit/enums.d.ts"/>
|
/// <reference path="../../libs/microbit/enums.d.ts"/>
|
||||||
/// <reference path="../state/neopixel.ts"/>
|
|
||||||
/// <reference path="../simlib.ts"/>
|
|
||||||
|
|
||||||
//TODO move to utils
|
//TODO move to utils
|
||||||
namespace pxsim.visuals {
|
namespace pxsim.visuals {
|
||||||
|
@ -1,470 +0,0 @@
|
|||||||
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-ends-g:not(.highlight) .sim-bb-wire-end {
|
|
||||||
stroke: #777;
|
|
||||||
fill: #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, croc: boolean = false): 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: SVGElAndSize;
|
|
||||||
if (croc)
|
|
||||||
e1 = mkCrocEnd(p1, true, clr);
|
|
||||||
else
|
|
||||||
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 mkSmallMBPinEnd(p: [number, number], top: boolean, clr: string): visuals.SVGElAndSize {
|
|
||||||
//HACK
|
|
||||||
//TODO: merge with mkOpenJumperEnd()
|
|
||||||
let k = visuals.PIN_DIST * 0.24;
|
|
||||||
let plasticLength = k * 4;
|
|
||||||
let plasticWidth = k * 1.2;
|
|
||||||
let metalLength = k * 10;
|
|
||||||
let metalWidth = k;
|
|
||||||
const strokeWidth = visuals.PIN_DIST / 4.0;
|
|
||||||
let [cx, cy] = p;
|
|
||||||
let yOffset = 10;
|
|
||||||
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 + yOffset - (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 + yOffset + 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, smallPin: boolean = false): 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: SVGElAndSize;
|
|
||||||
if (smallPin)
|
|
||||||
end2AndSize = mkSmallMBPinEnd(endCoord2, true, color);
|
|
||||||
else
|
|
||||||
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") {
|
|
||||||
let boardPin = (<BoardLoc>end).pin;
|
|
||||||
if (boardPin == "P0" || boardPin == "P1" || boardPin == "P2" || boardPin == "GND" || boardPin == "+3v3" ) {
|
|
||||||
//HACK
|
|
||||||
wireEls = this.drawWireWithCrocs(startLoc, endLoc, color);
|
|
||||||
} else {
|
|
||||||
wireEls = this.drawWireWithCrocs(startLoc, endLoc, color, true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
wireEls = this.drawWire(startLoc, endLoc, color);
|
|
||||||
}
|
|
||||||
return wireEls;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user