Servo, edge connector and button pair simulator support (#782)
* add common packages dep * inlining microservo * moving more sim * updated * bump pxt
This commit is contained in:
200
sim/state/buttonpairsim.ts
Normal file
200
sim/state/buttonpairsim.ts
Normal file
@ -0,0 +1,200 @@
|
||||
namespace pxsim.visuals {
|
||||
export function mkBtnSvg(xy: Coord): SVGAndSize<SVGGElement> {
|
||||
let [innerCls, outerCls] = ["sim-button", "sim-button-outer"];
|
||||
const tabSize = PIN_DIST / 2.5;
|
||||
const pegR = PIN_DIST / 5;
|
||||
const btnR = PIN_DIST * .8;
|
||||
const pegMargin = PIN_DIST / 8;
|
||||
const plateR = PIN_DIST / 12;
|
||||
|
||||
const pegOffset = pegMargin + pegR;
|
||||
let [x, y] = xy;
|
||||
const left = x - tabSize / 2;
|
||||
const top = y - tabSize / 2;
|
||||
const plateH = 3 * PIN_DIST - tabSize;
|
||||
const plateW = 2 * PIN_DIST + tabSize;
|
||||
const plateL = left;
|
||||
const plateT = top + tabSize;
|
||||
const btnCX = plateL + plateW / 2;
|
||||
const btnCY = plateT + plateH / 2;
|
||||
|
||||
let btng = <SVGGElement>svg.elt("g");
|
||||
//tabs
|
||||
const mkTab = (x: number, y: number) => {
|
||||
svg.child(btng, "rect", { class: "sim-button-tab", x: x, y: y, width: tabSize, height: tabSize})
|
||||
}
|
||||
mkTab(left, top);
|
||||
mkTab(left + 2 * PIN_DIST, top);
|
||||
mkTab(left, top + 3 * PIN_DIST);
|
||||
mkTab(left + 2 * PIN_DIST, top + 3 * PIN_DIST);
|
||||
|
||||
//plate
|
||||
svg.child(btng, "rect", { class: outerCls, x: plateL, y: plateT, rx: plateR, ry: plateR, width: plateW, height: plateH });
|
||||
|
||||
//pegs
|
||||
const mkPeg = (x: number, y: number) => {
|
||||
svg.child(btng, "circle", { class: "sim-button-nut", cx: x, cy: y, r: pegR });
|
||||
}
|
||||
mkPeg(plateL + pegOffset, plateT + pegOffset)
|
||||
mkPeg(plateL + plateW - pegOffset, plateT + pegOffset)
|
||||
mkPeg(plateL + pegOffset, plateT + plateH - pegOffset)
|
||||
mkPeg(plateL + plateW - pegOffset, plateT + plateH - pegOffset)
|
||||
|
||||
//inner btn
|
||||
let innerBtn = svg.child(btng, "circle", { class: innerCls, cx: btnCX, cy: btnCY, r: btnR });
|
||||
|
||||
//return
|
||||
return { el: btng, y: top, x: left, w: plateW, h: plateH + 2 * tabSize };
|
||||
}
|
||||
export const BUTTON_PAIR_STYLE = `
|
||||
.sim-button {
|
||||
pointer-events: none;
|
||||
fill: #000;
|
||||
}
|
||||
.sim-button-outer:active ~ .sim-button,
|
||||
.sim-button-virtual:active {
|
||||
fill: #FFA500;
|
||||
}
|
||||
.sim-button-outer {
|
||||
cursor: pointer;
|
||||
fill: #979797;
|
||||
}
|
||||
.sim-button-outer:hover {
|
||||
stroke:gray;
|
||||
stroke-width: ${PIN_DIST / 5}px;
|
||||
}
|
||||
.sim-button-nut {
|
||||
fill:#000;
|
||||
pointer-events:none;
|
||||
}
|
||||
.sim-button-nut:hover {
|
||||
stroke:${PIN_DIST / 15}px solid #704A4A;
|
||||
}
|
||||
.sim-button-tab {
|
||||
fill:#FFF;
|
||||
pointer-events:none;
|
||||
}
|
||||
.sim-button-virtual {
|
||||
cursor: pointer;
|
||||
fill: rgba(255, 255, 255, 0.6);
|
||||
stroke: rgba(255, 255, 255, 1);
|
||||
stroke-width: ${PIN_DIST / 5}px;
|
||||
}
|
||||
.sim-button-virtual:hover {
|
||||
stroke: rgba(128, 128, 128, 1);
|
||||
}
|
||||
.sim-text-virtual {
|
||||
fill: #000;
|
||||
pointer-events:none;
|
||||
}
|
||||
`;
|
||||
export class ButtonPairView implements IBoardPart<ButtonPairState> {
|
||||
public element: SVGElement;
|
||||
public defs: SVGElement[];
|
||||
public style = BUTTON_PAIR_STYLE;
|
||||
private state: ButtonPairState;
|
||||
private bus: EventBus;
|
||||
private aBtn: SVGGElement;
|
||||
private bBtn: SVGGElement;
|
||||
private abBtn: SVGGElement;
|
||||
|
||||
public init(bus: EventBus, state: ButtonPairState) {
|
||||
this.state = state;
|
||||
this.bus = bus;
|
||||
this.defs = [];
|
||||
this.element = this.mkBtns();
|
||||
this.updateState();
|
||||
this.attachEvents();
|
||||
}
|
||||
|
||||
public moveToCoord(xy: Coord) {
|
||||
let btnWidth = PIN_DIST * 3;
|
||||
let [x, y] = xy;
|
||||
translateEl(this.aBtn, [x, y])
|
||||
translateEl(this.bBtn, [x + btnWidth, y])
|
||||
translateEl(this.abBtn, [x + PIN_DIST * 1.5, y + PIN_DIST * 4])
|
||||
}
|
||||
|
||||
public updateState() {
|
||||
let stateBtns = [this.state.aBtn, this.state.bBtn, this.state.abBtn];
|
||||
let svgBtns = [this.aBtn, this.bBtn, this.abBtn];
|
||||
|
||||
if (this.state.usesButtonAB && this.abBtn.style.visibility != "visible") {
|
||||
this.abBtn.style.visibility = "visible";
|
||||
}
|
||||
}
|
||||
|
||||
public updateTheme() {}
|
||||
|
||||
private mkBtns() {
|
||||
this.aBtn = mkBtnSvg([0, 0]).el;
|
||||
this.bBtn = mkBtnSvg([0, 0]).el;
|
||||
|
||||
const mkVirtualBtn = () => {
|
||||
const numPins = 2;
|
||||
const w = PIN_DIST * 2.8;
|
||||
const offset = (w - (numPins * PIN_DIST)) / 2;
|
||||
const corner = PIN_DIST / 2;
|
||||
const cx = 0 - offset + w / 2;
|
||||
const cy = cx;
|
||||
const txtSize = PIN_DIST * 1.3;
|
||||
const x = -offset;
|
||||
const y = -offset;
|
||||
const txtXOff = PIN_DIST / 7;
|
||||
const txtYOff = PIN_DIST / 10;
|
||||
|
||||
let btng = <SVGGElement>svg.elt("g");
|
||||
let btn = svg.child(btng, "rect", { class: "sim-button-virtual", x: x, y: y, rx: corner, ry: corner, width: w, height: w});
|
||||
let btnTxt = mkTxt(cx + txtXOff, cy + txtYOff, txtSize, 0, "A+B");
|
||||
svg.addClass(btnTxt, "sim-text")
|
||||
svg.addClass(btnTxt, "sim-text-virtual");
|
||||
btng.appendChild(btnTxt);
|
||||
|
||||
return btng;
|
||||
}
|
||||
|
||||
this.abBtn = mkVirtualBtn();
|
||||
this.abBtn.style.visibility = "hidden";
|
||||
|
||||
let el = svg.elt("g");
|
||||
svg.addClass(el, "sim-buttonpair")
|
||||
el.appendChild(this.aBtn);
|
||||
el.appendChild(this.bBtn);
|
||||
el.appendChild(this.abBtn);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
private attachEvents() {
|
||||
let btnStates = [this.state.aBtn, this.state.bBtn];
|
||||
let btnSvgs = [this.aBtn, this.bBtn];
|
||||
btnSvgs.forEach((btn, index) => {
|
||||
pxsim.pointerEvents.down.forEach(evid => btn.addEventListener(evid, ev => {
|
||||
btnStates[index].pressed = true;
|
||||
}))
|
||||
btn.addEventListener(pointerEvents.leave, ev => {
|
||||
btnStates[index].pressed = false;
|
||||
})
|
||||
btn.addEventListener(pointerEvents.up, ev => {
|
||||
btnStates[index].pressed = false;
|
||||
this.bus.queue(btnStates[index].id, this.state.props.BUTTON_EVT_UP);
|
||||
this.bus.queue(btnStates[index].id, this.state.props.BUTTON_EVT_CLICK);
|
||||
})
|
||||
})
|
||||
let updateBtns = (s: boolean) => {
|
||||
btnStates.forEach(b => b.pressed = s)
|
||||
};
|
||||
pxsim.pointerEvents.down.forEach(evid => this.abBtn.addEventListener(evid, ev => {
|
||||
updateBtns(true);
|
||||
}));
|
||||
this.abBtn.addEventListener(pointerEvents.leave, ev => {
|
||||
updateBtns(false);
|
||||
})
|
||||
this.abBtn.addEventListener(pointerEvents.up, ev => {
|
||||
updateBtns(false);
|
||||
this.bus.queue(this.state.abBtn.id, this.state.props.BUTTON_EVT_UP);
|
||||
this.bus.queue(this.state.abBtn.id, this.state.props.BUTTON_EVT_CLICK);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
86
sim/state/edgeconnectorsim.ts
Normal file
86
sim/state/edgeconnectorsim.ts
Normal file
@ -0,0 +1,86 @@
|
||||
namespace pxsim {
|
||||
export enum PinFlags {
|
||||
Unused = 0,
|
||||
Digital = 0x0001,
|
||||
Analog = 0x0002,
|
||||
Input = 0x0004,
|
||||
Output = 0x0008,
|
||||
Touch = 0x0010
|
||||
}
|
||||
|
||||
export class Pin {
|
||||
constructor(public id: number) { }
|
||||
touched = false;
|
||||
value = 0;
|
||||
period = 0;
|
||||
servoAngle = 0;
|
||||
mode = PinFlags.Unused;
|
||||
pitch = false;
|
||||
pull = 0; // PullDown
|
||||
|
||||
digitalReadPin(): number {
|
||||
this.mode = PinFlags.Digital | PinFlags.Input;
|
||||
return this.value > 100 ? 1 : 0;
|
||||
}
|
||||
|
||||
digitalWritePin(value: number) {
|
||||
this.mode = PinFlags.Digital | PinFlags.Output;
|
||||
this.value = value > 0 ? 200 : 0;
|
||||
runtime.queueDisplayUpdate();
|
||||
}
|
||||
|
||||
setPull(pull: number) {
|
||||
this.pull = pull;
|
||||
}
|
||||
|
||||
analogReadPin(): number {
|
||||
this.mode = PinFlags.Analog | PinFlags.Input;
|
||||
return this.value || 0;
|
||||
}
|
||||
|
||||
analogWritePin(value: number) {
|
||||
this.mode = PinFlags.Analog | PinFlags.Output;
|
||||
this.value = Math.max(0, Math.min(1023, value));
|
||||
runtime.queueDisplayUpdate();
|
||||
}
|
||||
|
||||
analogSetPeriod(micros: number) {
|
||||
this.mode = PinFlags.Analog | PinFlags.Output;
|
||||
this.period = micros;
|
||||
runtime.queueDisplayUpdate();
|
||||
}
|
||||
|
||||
servoWritePin(value: number) {
|
||||
this.analogSetPeriod(20000);
|
||||
this.servoAngle = Math.max(0, Math.min(180, value));
|
||||
runtime.queueDisplayUpdate();
|
||||
}
|
||||
|
||||
servoSetPulse(pinId: number, micros: number) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
isTouched(): boolean {
|
||||
this.mode = PinFlags.Touch | PinFlags.Analog | PinFlags.Input;
|
||||
return this.touched;
|
||||
}
|
||||
}
|
||||
|
||||
export interface EdgeConnectorProps {
|
||||
pins: number[];
|
||||
servos?: { [name: string]: number; }
|
||||
}
|
||||
|
||||
export class EdgeConnectorState {
|
||||
pins: Pin[];
|
||||
|
||||
constructor(public props: EdgeConnectorProps) {
|
||||
this.pins = props.pins.map(id => id != undefined ? new Pin(id) : null);
|
||||
}
|
||||
|
||||
public getPin(id: number) {
|
||||
return this.pins.filter(p => p && p.id == id)[0] || null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
83
sim/state/microservo.ts
Normal file
83
sim/state/microservo.ts
Normal file
@ -0,0 +1,83 @@
|
||||
namespace pxsim.visuals {
|
||||
function createMicroServoElement() {
|
||||
return svg.parseString(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="svg2" width="112.188" height="299.674">
|
||||
<g id="layer1" stroke-linecap="round" stroke-linejoin="round" transform="scale(0.8)">
|
||||
<path id="path8212" fill="#0061ff" stroke-width="6.6" d="M.378 44.61v255.064h112.188V44.61H.378z"/>
|
||||
<path id="crankbase" fill="#00f" stroke-width="6.6" d="M56.57 88.047C25.328 88.047 0 113.373 0 144.615c.02 22.352 11.807 42.596 32.238 51.66.03 3.318.095 5.24.088 7.938 0 13.947 11.307 25.254 25.254 25.254 13.947 0 25.254-11.307 25.254-25.254-.006-2.986-.415-5.442-.32-8.746 19.487-9.45 30.606-29.195 30.625-50.852 0-31.24-25.33-56.568-56.57-56.568z"/>
|
||||
<path id="lowertip" fill="#00a2ff" stroke-width="2" d="M.476 260.78v38.894h53.82v-10.486a6.82 6.566 0 0 1-4.545-6.182 6.82 6.566 0 0 1 6.82-6.566 6.82 6.566 0 0 1 6.82 6.566 6.82 6.566 0 0 1-4.545 6.182v10.486h53.82V260.78H.475z"/>
|
||||
<path id="uppertip" fill="#00a2ff" stroke-width="2" d="M112.566 83.503V44.61h-53.82v10.487a6.82 6.566 0 0 1 4.544 6.18 6.82 6.566 0 0 1-6.818 6.568 6.82 6.566 0 0 1-6.82-6.567 6.82 6.566 0 0 1 4.546-6.18V44.61H.378v38.893h112.188z"/>
|
||||
<path id="VCC" fill="red" stroke-width="2" d="M53.72 21.93h5.504v22.627H53.72z"/>
|
||||
<path id="LOGIC" fill="#fc0" stroke-width="2" d="M47.3 21.93h5.503v22.627H47.3z"/>
|
||||
<path id="GND" fill="#a02c2c" stroke-width="2" d="M60.14 21.93h5.505v22.627H60.14z"/>
|
||||
<path id="connector" stroke-width="2" d="M45.064 0a1.488 1.488 0 0 0-1.488 1.488v24.5a1.488 1.488 0 0 0 1.488 1.487h22.71a1.488 1.488 0 0 0 1.49-1.488v-24.5A1.488 1.488 0 0 0 67.774 0h-22.71z"/>
|
||||
<g id="crank" transform="translate(0 -752.688)">
|
||||
<path id="arm" fill="#ececec" stroke="#000" stroke-width="1.372" d="M47.767 880.88c-4.447 1.162-8.412 8.278-8.412 18.492s3.77 18.312 8.412 18.494c8.024.314 78.496 5.06 78.51-16.952.012-22.013-74.377-21.117-78.51-20.035z"/>
|
||||
<circle id="path8216" cx="56.661" cy="899.475" r="8.972" fill="gray" stroke-width="2"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
`).firstElementChild as SVGGElement;
|
||||
}
|
||||
|
||||
export function mkMicroServoPart(xy: Coord = [0, 0]): SVGElAndSize {
|
||||
return { el: createMicroServoElement(), x: xy[0], y: xy[1], w: 112.188, h: 299.674 };
|
||||
}
|
||||
|
||||
export class MicroServoView implements IBoardPart<EdgeConnectorState> {
|
||||
public style: string = "";
|
||||
public overElement: SVGElement = undefined;
|
||||
public element: SVGElement;
|
||||
public defs: SVGElement[] = [];
|
||||
public state: EdgeConnectorState;
|
||||
public bus: EventBus;
|
||||
private currentAngle = 0;
|
||||
private targetAngle = 0;
|
||||
private lastAngleTime = 0;
|
||||
private pin: number;
|
||||
|
||||
private crankEl: SVGGElement;
|
||||
private crankTransform: string;
|
||||
|
||||
public init(bus: EventBus, state: EdgeConnectorState, svgEl: SVGSVGElement, otherParams: Map<string>) {
|
||||
this.state = state;
|
||||
this.pin = this.state.props.servos[
|
||||
pxsim.readPin(otherParams["name"] || otherParams["pin"])
|
||||
];
|
||||
this.bus = bus;
|
||||
this.defs = [];
|
||||
this.initDom();
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
initDom() {
|
||||
this.element = createMicroServoElement();
|
||||
this.crankEl = this.element.querySelector("#crank") as SVGGElement;
|
||||
this.crankTransform = this.crankEl.getAttribute("transform");
|
||||
}
|
||||
|
||||
moveToCoord(xy: visuals.Coord): void {
|
||||
let [x, y] = xy;
|
||||
translateEl(this.element, [x, y])
|
||||
}
|
||||
updateState(): void {
|
||||
this.targetAngle = 180.0 - this.state.getPin(this.pin).servoAngle;
|
||||
if (this.targetAngle != this.currentAngle) {
|
||||
const now = U.now();
|
||||
const cx = 56.661;
|
||||
const cy = 899.475;
|
||||
const speed = 300; // 0.1s/60 degree
|
||||
const dt = Math.min(now - this.lastAngleTime, 50) / 1000;
|
||||
const delta = this.targetAngle - this.currentAngle;
|
||||
this.currentAngle += Math.min(Math.abs(delta), speed * dt) * (delta > 0 ? 1 : -1);
|
||||
this.crankEl.setAttribute("transform", this.crankTransform
|
||||
+ ` rotate(${this.currentAngle}, ${cx}, ${cy})`)
|
||||
this.lastAngleTime = now;
|
||||
setTimeout(() => runtime.updateDisplay(), 20);
|
||||
}
|
||||
}
|
||||
updateTheme(): void {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user