Initial board SVG and basic simulator
This commit is contained in:
197
sim/dalboard.ts
Normal file
197
sim/dalboard.ts
Normal file
@ -0,0 +1,197 @@
|
||||
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||
/// <reference path="../node_modules/pxt-core/localtypings/pxtarget.d.ts"/>
|
||||
/// <reference path="../built/common-sim.d.ts"/>
|
||||
|
||||
namespace pxsim {
|
||||
export enum CPlayPinName {
|
||||
A0,
|
||||
A1,
|
||||
A2,
|
||||
A3,
|
||||
A4,
|
||||
A5,
|
||||
A6,
|
||||
A7,
|
||||
A8,
|
||||
A9,
|
||||
D4,
|
||||
D5,
|
||||
D6,
|
||||
D7,
|
||||
D8,
|
||||
D13
|
||||
}
|
||||
|
||||
export class DalBoard extends CoreBoard implements
|
||||
AccelerometerBoard,
|
||||
CommonBoard,
|
||||
LightBoard,
|
||||
LightSensorBoard,
|
||||
MicrophoneBoard,
|
||||
MusicBoard,
|
||||
SlideSwitchBoard,
|
||||
TemperatureBoard,
|
||||
InfraredBoard,
|
||||
CapTouchBoard {
|
||||
// state & update logic for component services
|
||||
neopixelState: CommonNeoPixelState;
|
||||
buttonState: EV3ButtonState;
|
||||
slideSwitchState: SlideSwitchState;
|
||||
lightSensorState: AnalogSensorState;
|
||||
thermometerState: AnalogSensorState;
|
||||
thermometerUnitState: number;
|
||||
microphoneState: AnalogSensorState;
|
||||
edgeConnectorState: EdgeConnectorState;
|
||||
capacitiveSensorState: CapacitiveSensorState;
|
||||
accelerometerState: AccelerometerState;
|
||||
audioState: AudioState;
|
||||
touchButtonState: TouchButtonState;
|
||||
irState: InfraredState;
|
||||
lightState: EV3LightState;
|
||||
|
||||
view: SVGSVGElement;
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.bus.setNotify(DAL.DEVICE_ID_NOTIFY, DAL.DEVICE_ID_NOTIFY_ONE);
|
||||
|
||||
//components
|
||||
|
||||
this.builtinParts["buttons"] = this.buttonState = new EV3ButtonState();
|
||||
this.builtinParts["light"] = this.lightState = new EV3LightState();
|
||||
/*this.builtinParts["neopixel"] = this.neopixelState = new CommonNeoPixelState();
|
||||
this.builtinParts["buttonpair"] = this.buttonState = new CommonButtonState();
|
||||
|
||||
this.builtinParts["switch"] = this.slideSwitchState = new SlideSwitchState();
|
||||
this.builtinParts["audio"] = this.audioState = new AudioState();
|
||||
this.builtinParts["lightsensor"] = this.lightSensorState = new AnalogSensorState(DAL.DEVICE_ID_LIGHT_SENSOR, 0, 255);
|
||||
this.builtinParts["thermometer"] = this.thermometerState = new AnalogSensorState(DAL.DEVICE_ID_THERMOMETER, -5, 50);
|
||||
this.builtinParts["soundsensor"] = this.microphoneState = new AnalogSensorState(DAL.DEVICE_ID_TOUCH_SENSOR + 1, 0, 255);
|
||||
this.builtinParts["capacitivesensor"] = this.capacitiveSensorState = new CapacitiveSensorState({
|
||||
0: 0,
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 3,
|
||||
6: 4,
|
||||
9: 5,
|
||||
10: 6,
|
||||
12: 7
|
||||
});
|
||||
|
||||
this.builtinParts["accelerometer"] = this.accelerometerState = new AccelerometerState(runtime);
|
||||
this.builtinParts["edgeconnector"] = this.edgeConnectorState = new EdgeConnectorState({
|
||||
pins: [
|
||||
pxsim.CPlayPinName.A0,
|
||||
pxsim.CPlayPinName.A1,
|
||||
pxsim.CPlayPinName.A2,
|
||||
pxsim.CPlayPinName.A3,
|
||||
pxsim.CPlayPinName.A4,
|
||||
pxsim.CPlayPinName.A5,
|
||||
pxsim.CPlayPinName.A6,
|
||||
pxsim.CPlayPinName.A7,
|
||||
pxsim.CPlayPinName.A8,
|
||||
pxsim.CPlayPinName.A9,
|
||||
pxsim.CPlayPinName.D4,
|
||||
pxsim.CPlayPinName.D5,
|
||||
pxsim.CPlayPinName.D6,
|
||||
pxsim.CPlayPinName.D7,
|
||||
pxsim.CPlayPinName.D8,
|
||||
pxsim.CPlayPinName.D13
|
||||
]
|
||||
});
|
||||
this.builtinParts["microservo"] = this.edgeConnectorState;
|
||||
|
||||
this.builtinVisuals["microservo"] = () => new visuals.MicroServoView();
|
||||
this.builtinPartVisuals["microservo"] = (xy: visuals.Coord) => visuals.mkMicroServoPart(xy);
|
||||
this.touchButtonState = new TouchButtonState([
|
||||
pxsim.CPlayPinName.A1,
|
||||
pxsim.CPlayPinName.A2,
|
||||
pxsim.CPlayPinName.A3,
|
||||
pxsim.CPlayPinName.A4,
|
||||
pxsim.CPlayPinName.A5,
|
||||
pxsim.CPlayPinName.A6,
|
||||
pxsim.CPlayPinName.A7
|
||||
]);
|
||||
|
||||
this.builtinParts["ir"] = this.irState = new InfraredState();*/
|
||||
}
|
||||
|
||||
receiveMessage(msg: SimulatorMessage) {
|
||||
if (!runtime || runtime.dead) return;
|
||||
|
||||
switch (msg.type || "") {
|
||||
case "eventbus": {
|
||||
let ev = <SimulatorEventBusMessage>msg;
|
||||
this.bus.queue(ev.id, ev.eventid, ev.value);
|
||||
break;
|
||||
}
|
||||
case "serial": {
|
||||
let data = (<SimulatorSerialMessage>msg).data || "";
|
||||
// TODO
|
||||
break;
|
||||
}
|
||||
case "irpacket": {
|
||||
let ev = <SimulatorInfraredPacketMessage>msg;
|
||||
this.irState.receive(new RefBuffer(ev.packet));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initAsync(msg: SimulatorRunMessage): Promise<void> {
|
||||
super.initAsync(msg);
|
||||
|
||||
const options = (msg.options || {}) as pxt.RuntimeOptions;
|
||||
|
||||
const boardDef = msg.boardDefinition;
|
||||
const cmpsList = msg.parts;
|
||||
const cmpDefs = msg.partDefinitions || {};
|
||||
const fnArgs = msg.fnArgs;
|
||||
|
||||
const opts: visuals.BoardHostOpts = {
|
||||
state: this,
|
||||
boardDef: boardDef,
|
||||
partsList: cmpsList,
|
||||
partDefs: cmpDefs,
|
||||
fnArgs: fnArgs,
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
};
|
||||
const viewHost = new visuals.BoardHost(pxsim.visuals.mkBoardView({
|
||||
visual: boardDef.visual
|
||||
}), opts);
|
||||
|
||||
document.body.innerHTML = ""; // clear children
|
||||
document.body.appendChild(this.view = viewHost.getView() as SVGSVGElement);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
screenshot(): string {
|
||||
return svg.toDataUri(new XMLSerializer().serializeToString(this.view));
|
||||
}
|
||||
|
||||
defaultNeopixelPin() {
|
||||
return this.edgeConnectorState.getPin(CPlayPinName.D8);
|
||||
}
|
||||
|
||||
getDefaultPitchPin() {
|
||||
return this.edgeConnectorState.getPin(CPlayPinName.D6);
|
||||
}
|
||||
}
|
||||
|
||||
export function initRuntimeWithDalBoard() {
|
||||
U.assert(!runtime.board);
|
||||
let b = new DalBoard();
|
||||
runtime.board = b;
|
||||
runtime.postError = (e) => {
|
||||
// TODO
|
||||
runtime.updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
if (!pxsim.initCurrentRuntime) {
|
||||
pxsim.initCurrentRuntime = initRuntimeWithDalBoard;
|
||||
}
|
||||
}
|
92
sim/instructions.ts
Normal file
92
sim/instructions.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/// <reference path="../node_modules/pxt-core/typings/globals/bluebird/index.d.ts"/>
|
||||
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||
/// <reference path="../node_modules/pxt-core/built/pxtrunner.d.ts"/>
|
||||
|
||||
//HACK: allows instructions.html to access pxtblocks without requiring simulator.html to import blocks as well
|
||||
if (!(<any>window).pxt) (<any>window).pxt = {};
|
||||
import pxtrunner = pxt.runner;
|
||||
import pxtdocs = pxt.docs;
|
||||
|
||||
namespace pxsim.instructions {
|
||||
export function drawInstructions() {
|
||||
pxsim.visuals.mkBoardView = (opts: pxsim.visuals.BoardViewOptions): pxsim.visuals.BoardView => {
|
||||
return new visuals.EV3BoardSvg({
|
||||
runtime: runtime,
|
||||
theme: visuals.randomTheme(),
|
||||
disableTilt: false,
|
||||
wireframe: opts.wireframe,
|
||||
});
|
||||
}
|
||||
|
||||
let getQsVal = parseQueryString();
|
||||
|
||||
//project name
|
||||
let name = getQsVal("name") || "Untitled";
|
||||
|
||||
// board def
|
||||
const boardDef = JSON.parse(getQsVal("board")) as pxsim.BoardDefinition;
|
||||
|
||||
//parts list
|
||||
let parts = (getQsVal("parts") || "").split(" ");
|
||||
parts.sort();
|
||||
|
||||
// parts definitions
|
||||
let partDefinitions = JSON.parse(getQsVal("partdefs") || "{}") as pxsim.Map<PartDefinition>
|
||||
|
||||
//fn args
|
||||
let fnArgs = JSON.parse((getQsVal("fnArgs") || "{}"));
|
||||
|
||||
//project code
|
||||
let tsCode = getQsVal("code");
|
||||
let tsPackage = getQsVal("package") || "";
|
||||
let codeSpinnerDiv = document.getElementById("proj-code-spinner");
|
||||
let codeContainerDiv = document.getElementById("proj-code-container");
|
||||
if (tsCode) {
|
||||
//we use the docs renderer to decompile the code to blocks and render it
|
||||
//TODO: render the blocks code directly
|
||||
let md =
|
||||
`\`\`\`blocks
|
||||
${tsCode}
|
||||
\`\`\`
|
||||
\`\`\`package
|
||||
${tsPackage}
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
pxtdocs.requireMarked = function () { return (<any>window).marked; }
|
||||
pxtrunner.renderMarkdownAsync(codeContainerDiv, md)
|
||||
.done(function () {
|
||||
let codeSvg = $("#proj-code-container svg");
|
||||
if (codeSvg.length > 0) {
|
||||
//code rendered successfully as blocks
|
||||
codeSvg.css("width", "inherit");
|
||||
codeSvg.css("height", "inherit");
|
||||
//takes the svg out of the wrapper markdown
|
||||
codeContainerDiv.innerHTML = "";
|
||||
codeContainerDiv.appendChild(codeSvg[0]);
|
||||
} else {
|
||||
//code failed to convert to blocks, display as typescript instead
|
||||
codeContainerDiv.innerText = tsCode;
|
||||
}
|
||||
$(codeContainerDiv).show();
|
||||
$(codeSpinnerDiv).hide();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (name)
|
||||
$("#proj-title").text(name);
|
||||
|
||||
//init runtime
|
||||
if (!pxsim.initCurrentRuntime)
|
||||
pxsim.initCurrentRuntime = initRuntimeWithDalBoard;
|
||||
|
||||
renderParts({
|
||||
name,
|
||||
boardDef,
|
||||
parts,
|
||||
partDefinitions,
|
||||
fnArgs
|
||||
})
|
||||
}
|
||||
}
|
5
sim/public/parts/.gitignore
vendored
Normal file
5
sim/public/parts/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# don't check in until OSS request is approved
|
||||
sparkfun-*
|
||||
raspberrypi-*
|
||||
arduino-*
|
||||
max6675*
|
10
sim/public/sim.manifest
Normal file
10
sim/public/sim.manifest
Normal file
@ -0,0 +1,10 @@
|
||||
CACHE MANIFEST
|
||||
|
||||
CACHE:
|
||||
/cdn/bluebird.min.js
|
||||
/cdn/pxtsim.js
|
||||
/sim/common-sim.js
|
||||
/sim/sim.js
|
||||
|
||||
NETWORK:
|
||||
*
|
201
sim/public/siminstructions.html
Normal file
201
sim/public/siminstructions.html
Normal file
@ -0,0 +1,201 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-framework="typescript">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Assembly Instructions</title>
|
||||
<style>
|
||||
|
||||
svg {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.blocklyText, .ace_editor {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace !important;
|
||||
}
|
||||
|
||||
.blocklyText, .ace_editor {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
.blocklyTreeLabel {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.blocklyCheckbox {
|
||||
fill: #ff3030 !important;
|
||||
text-shadow: 0px 0px 6px #f00;
|
||||
font-size: 17pt !important;
|
||||
}
|
||||
|
||||
.ui.card .blocklyPreview {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(100% - 1em);
|
||||
max-height: calc(100% - 1em);
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
code.lang-config, code.lang-package { display:none; }
|
||||
|
||||
code.lang-blocks::before,
|
||||
code.lang-sig::before,
|
||||
code.lang-block::before,
|
||||
code.lang-shuffle::before,
|
||||
code.lang-sim::before,
|
||||
code.lang-cards::before,
|
||||
code.lang-namespaces::before,
|
||||
code.lang-codecard::before {
|
||||
content: "...";
|
||||
position: absolute;
|
||||
top: calc(50% - 0.5em);
|
||||
left: calc(50% - 5em);
|
||||
}
|
||||
|
||||
code.lang-blocks,
|
||||
code.lang-sig,
|
||||
code.lang-block,
|
||||
code.lang-shuffle,
|
||||
code.lang-sim,
|
||||
code.lang-cards,
|
||||
code.lang-namespaces,
|
||||
code.lang-codecard {
|
||||
color: transparent;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@import "/cdn/semantic.css";
|
||||
@import "/cdn/icons.css";
|
||||
</style>
|
||||
<style>
|
||||
html {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
}
|
||||
|
||||
div {
|
||||
/*undo semantic UI*/
|
||||
box-sizing: content-box;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
/*TODO: Share CSS with main webpage*/
|
||||
|
||||
.organization {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
font-weight: normal;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
font-family: 'Segoe UI', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#front-panel .board-svg {
|
||||
position: absolute;
|
||||
left: 2rem;
|
||||
width: 300px;
|
||||
top: 16rem;
|
||||
}
|
||||
|
||||
#proj-title {
|
||||
width: 100%;
|
||||
font-size: 70px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#proj-code {
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
top: 16rem;
|
||||
}
|
||||
#proj-code-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 4px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
}
|
||||
#proj-code-spinner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.back-panel svg {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
left: inherit;
|
||||
bottom: -7px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='loading' class="ui active inverted dimmer">
|
||||
<div class="ui large loader"></div>
|
||||
</div>
|
||||
<script>
|
||||
// This line gets patched up by the cloud
|
||||
var pxtConfig = null;
|
||||
</script>
|
||||
<script type="text/javascript" src="/cdn/lzma/lzma_worker-min.js"></script>
|
||||
<script type="text/javascript" src="/cdn/marked/marked.min.js"></script>
|
||||
<script type="text/javascript" src="/cdn/jquery.js"></script>
|
||||
<script type="text/javascript" src="/cdn/typescript.js"></script>
|
||||
<script type="text/javascript" src="/cdn/blockly/blockly_compressed.js"></script>
|
||||
<script type="text/javascript" src="/cdn/blockly/blocks_compressed.js"></script>
|
||||
<script type="text/javascript" src="/cdn/blockly/msg/js/en.js"></script>
|
||||
<script type="text/javascript" src="/cdn/pxtlib.js"></script>
|
||||
<script type="text/javascript" src="/cdn/pxtblocks.js"></script>
|
||||
<script type="text/javascript" src="/cdn/pxtsim.js"></script>
|
||||
<script type="text/javascript" src="/cdn/pxtrunner.js"></script>
|
||||
<script type="text/javascript" src="/cdn/semantic.js"></script>
|
||||
<script type="text/javascript" src="/embed.js"></script>
|
||||
<script type="text/javascript" src="/sim/common-sim.js"></script>
|
||||
<script type="text/javascript" src="/sim/sim.js"></script>
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
ksRunnerReady(function() {
|
||||
var orgLogo = pxt.appTarget.appTheme.organizationLogo;
|
||||
if (orgLogo)
|
||||
$('#front-panel').append(
|
||||
$('<img/>').attr('class', 'organization').attr('src', orgLogo)
|
||||
);
|
||||
var loading = document.getElementById('loading');
|
||||
pxsim.instructions.drawInstructions();
|
||||
$(loading).hide();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div id="front-panel" class="instr-panel">
|
||||
<h1 id="proj-title"></h1>
|
||||
|
||||
<!--TODO: extract real code snapshot from PXT -->
|
||||
<div id="proj-code">
|
||||
<i id="proj-code-spinner" class="spinner loading icon"></i>
|
||||
<div id="proj-code-container">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
29
sim/public/simulator.html
Normal file
29
sim/public/simulator.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-manifest="" data-framework="typescript">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>EV3 Simulator</title>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
width:100%;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script src="/cdn/bluebird.min.js"></script>
|
||||
<script src="/cdn/pxtsim.js"></script>
|
||||
<script src="/sim/common-sim.js"></script>
|
||||
<script src="/sim/sim.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
18
sim/state/buttons.ts
Normal file
18
sim/state/buttons.ts
Normal file
@ -0,0 +1,18 @@
|
||||
namespace pxsim {
|
||||
|
||||
export class EV3ButtonState extends CommonButtonState{
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.buttons = [
|
||||
new CommonButton(DAL.BUTTON_ID_UP),
|
||||
new CommonButton(DAL.BUTTON_ID_ENTER),
|
||||
new CommonButton(DAL.BUTTON_ID_DOWN),
|
||||
new CommonButton(DAL.BUTTON_ID_RIGHT),
|
||||
new CommonButton(DAL.BUTTON_ID_LEFT),
|
||||
new CommonButton(DAL.BUTTON_ID_ESCAPE),
|
||||
new CommonButton(DAL.BUTTON_ID_ALL)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
9
sim/state/control.ts
Normal file
9
sim/state/control.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/// <reference path="../../libs/core/enums.d.ts"/>
|
||||
|
||||
namespace pxsim.control {
|
||||
|
||||
export function mmap(filename: string, size: number, offset: number): void {
|
||||
|
||||
}
|
||||
|
||||
}
|
19
sim/state/light.ts
Normal file
19
sim/state/light.ts
Normal file
@ -0,0 +1,19 @@
|
||||
namespace pxsim {
|
||||
|
||||
export class EV3LightState {
|
||||
lightPattern: number;
|
||||
|
||||
constructor() {
|
||||
this.lightPattern = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace pxsim.output {
|
||||
|
||||
export function setLights(pattern: number){
|
||||
const lightState = (board() as DalBoard).lightState;
|
||||
lightState.lightPattern = pattern;
|
||||
runtime.queueDisplayUpdate();
|
||||
}
|
||||
}
|
12
sim/tsconfig.json
Normal file
12
sim/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"declaration": true,
|
||||
"out": "../built/sim.js",
|
||||
"rootDir": ".",
|
||||
"newLine": "LF",
|
||||
"sourceMap": false
|
||||
}
|
||||
}
|
1178
sim/visuals/board.svg
Normal file
1178
sim/visuals/board.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 74 KiB |
803
sim/visuals/board.ts
Normal file
803
sim/visuals/board.ts
Normal file
@ -0,0 +1,803 @@
|
||||
namespace pxsim.visuals {
|
||||
const MB_STYLE = `
|
||||
svg.sim {
|
||||
margin-bottom:1em;
|
||||
}
|
||||
svg.sim.grayscale {
|
||||
-moz-filter: grayscale(1);
|
||||
-webkit-filter: grayscale(1);
|
||||
filter: grayscale(1);
|
||||
}
|
||||
.sim-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.sim-button:hover {
|
||||
stroke-width: 2px !important;
|
||||
stroke: white !important;
|
||||
}
|
||||
|
||||
.sim-systemled {
|
||||
fill:#333;
|
||||
stroke:#555;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.sim-light-level-button {
|
||||
stroke:#f1c40f;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.sim-pin-level-button {
|
||||
stroke:darkorange;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.sim-sound-level-button {
|
||||
stroke:#7f8c8d;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.sim-antenna {
|
||||
stroke:#555;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.sim-text {
|
||||
font-family:"Lucida Console", Monaco, monospace;
|
||||
font-size:8px;
|
||||
fill:#fff;
|
||||
pointer-events: none; user-select: none;
|
||||
}
|
||||
.sim-text.small {
|
||||
font-size:6px;
|
||||
}
|
||||
.sim-text.inverted {
|
||||
fill:#000;
|
||||
}
|
||||
|
||||
.sim-text-pin {
|
||||
font-family:"Lucida Console", Monaco, monospace;
|
||||
font-size:5px;
|
||||
fill:#fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sim-thermometer {
|
||||
stroke:#aaa;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
#rgbledcircle:hover {
|
||||
r:8px;
|
||||
}
|
||||
|
||||
#SLIDE_HOVER {
|
||||
cursor: pointer;
|
||||
}
|
||||
.sim-slide-switch:hover #SLIDE_HOVER {
|
||||
stroke:orange !important;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.sim-slide-switch-inner.on {
|
||||
fill:#ff0000 !important;
|
||||
}
|
||||
|
||||
/* animations */
|
||||
.sim-theme-glow {
|
||||
animation-name: sim-theme-glow-animation;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-direction: alternate;
|
||||
animation-iteration-count: infinite;
|
||||
animation-duration: 1.25s;
|
||||
}
|
||||
@keyframes sim-theme-glow-animation {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.75; }
|
||||
}
|
||||
|
||||
.sim-flash {
|
||||
animation-name: sim-flash-animation;
|
||||
animation-duration: 0.1s;
|
||||
}
|
||||
|
||||
@keyframes sim-flash-animation {
|
||||
from { fill: yellow; }
|
||||
to { fill: default; }
|
||||
}
|
||||
|
||||
.sim-flash-stroke {
|
||||
z-index: 0;
|
||||
animation-name: sim-flash-stroke-animation;
|
||||
animation-duration: 0.4s;
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
@keyframes sim-flash-stroke-animation {
|
||||
from { stroke: yellow; }
|
||||
to { stroke: default; }
|
||||
}
|
||||
|
||||
|
||||
.sim-sound-stroke {
|
||||
animation-name: sim-sound-stroke-animation;
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes sim-sound-stroke-animation {
|
||||
from { stroke: yellow; }
|
||||
to { stroke: default; }
|
||||
}
|
||||
|
||||
/* wireframe */
|
||||
.sim-wireframe * {
|
||||
fill: none;
|
||||
stroke: black;
|
||||
}
|
||||
.sim-wireframe .sim-display,
|
||||
.sim-wireframe .sim-led,
|
||||
.sim-wireframe .sim-led-back,
|
||||
.sim-wireframe .sim-head,
|
||||
.sim-wireframe .sim-theme,
|
||||
.sim-wireframe .sim-button-group,
|
||||
.sim-wireframe .sim-button-label,
|
||||
.sim-wireframe .sim-button,
|
||||
.sim-wireframe .sim-text-pin
|
||||
{
|
||||
visibility: hidden;
|
||||
}
|
||||
.sim-wireframe .sim-label
|
||||
{
|
||||
stroke: none;
|
||||
fill: #777;
|
||||
}
|
||||
.sim-wireframe .sim-board {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
const pinNames: { 'name': string, 'touch': number, 'text': any, 'id'?: number, tooltip?: string }[] = [
|
||||
{ 'name': "PIN_A0", 'touch': 0, 'text': null, 'id': pxsim.CPlayPinName.A0, tooltip: "A0 - Speaker" },
|
||||
{ 'name': "PIN_A1", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A1, tooltip: "~A1" },
|
||||
{ 'name': "PIN_A2", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A2, tooltip: "~A2" },
|
||||
{ 'name': "PIN_A3", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A3, tooltip: "~A3" },
|
||||
{ 'name': "PIN_A4", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A4, tooltip: "A4 - SCL" },
|
||||
{ 'name': "PIN_A5", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A5, tooltip: "A5 - SDA" },
|
||||
{ 'name': "PIN_A6", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A6, tooltip: "A6 - RX" },
|
||||
{ 'name': "PIN_A7", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A7, tooltip: "A7 - TX" },
|
||||
{ 'name': "GND_0", 'touch': 0, 'text': null, tooltip: "Ground" },
|
||||
{ 'name': "GND_1", 'touch': 0, 'text': null, tooltip: "Ground" },
|
||||
{ 'name': "GND_2", 'touch': 0, 'text': null, tooltip: "Ground" },
|
||||
{ 'name': "VBATT", 'touch': 0, 'text': null, tooltip: "Battery power" },
|
||||
{ 'name': "PWR_0", 'touch': 0, 'text': null, tooltip: "+3.3V" },
|
||||
{ 'name': "PWR_1", 'touch': 0, 'text': null, tooltip: "+3.3V" },
|
||||
{ 'name': "PWR_2", 'touch': 0, 'text': null, tooltip: "+3.3V" }
|
||||
];
|
||||
const MB_WIDTH = 169.82979;
|
||||
const MB_HEIGHT = 259.11862;
|
||||
export interface IBoardTheme {
|
||||
accent?: string;
|
||||
display?: string;
|
||||
pin?: string;
|
||||
pinTouched?: string;
|
||||
pinActive?: string;
|
||||
ledOn?: string;
|
||||
ledOff?: string;
|
||||
buttonOuter?: string;
|
||||
buttonUps: string[];
|
||||
buttonDown?: string;
|
||||
virtualButtonOuter?: string;
|
||||
virtualButtonUp?: string;
|
||||
virtualButtonDown?: string;
|
||||
lightLevelOn?: string;
|
||||
lightLevelOff?: string;
|
||||
soundLevelOn?: string;
|
||||
soundLevelOff?: string;
|
||||
gestureButtonOn?: string;
|
||||
gestureButtonOff?: string;
|
||||
}
|
||||
|
||||
export var themes: IBoardTheme[] = ["#3ADCFE"].map(accent => {
|
||||
return {
|
||||
accent: accent,
|
||||
pin: "#D4AF37",
|
||||
pinTouched: "#FFA500",
|
||||
pinActive: "#FF5500",
|
||||
ledOn: "#ff7777",
|
||||
ledOff: "#fff",
|
||||
buttonOuter: "#979797",
|
||||
buttonUps: ["#FFF", "#4D4D4D", "#FFF", "#FFF", "#FFF", "#FFF", '#FFF'],
|
||||
buttonDown: "#000",
|
||||
virtualButtonDown: "#FFA500",
|
||||
virtualButtonOuter: "#333",
|
||||
virtualButtonUp: "#FFF",
|
||||
lightLevelOn: "yellow",
|
||||
lightLevelOff: "#555",
|
||||
soundLevelOn: "#7f8c8d",
|
||||
soundLevelOff: "#555",
|
||||
gestureButtonOn: "#FFA500",
|
||||
gestureButtonOff: "#B4009E"
|
||||
}
|
||||
});
|
||||
|
||||
export function randomTheme(): IBoardTheme {
|
||||
return themes[Math.floor(Math.random() * themes.length)];
|
||||
}
|
||||
|
||||
export interface IBoardProps {
|
||||
runtime?: pxsim.Runtime;
|
||||
theme?: IBoardTheme;
|
||||
disableTilt?: boolean;
|
||||
wireframe?: boolean;
|
||||
}
|
||||
|
||||
export class EV3BoardSvg implements BoardView {
|
||||
public element: SVGSVGElement;
|
||||
private style: SVGStyleElement;
|
||||
private defs: SVGDefsElement;
|
||||
private g: SVGGElement;
|
||||
|
||||
private buttons: SVGElement[];
|
||||
private buttonABText: SVGTextElement;
|
||||
private light: SVGElement;
|
||||
private pins: SVGElement[];
|
||||
private pinControls: { [index: number]: AnalogPinControl };
|
||||
private systemLed: SVGCircleElement;
|
||||
private irReceiver: SVGElement;
|
||||
private irTransmitter: SVGElement;
|
||||
private redLED: SVGRectElement;
|
||||
private slideSwitch: SVGGElement;
|
||||
private lightLevelButton: SVGCircleElement;
|
||||
private lightLevelGradient: SVGLinearGradientElement;
|
||||
private lightLevelText: SVGTextElement;
|
||||
private soundLevelButton: SVGCircleElement;
|
||||
private soundLevelGradient: SVGLinearGradientElement;
|
||||
private soundLevelText: SVGTextElement;
|
||||
private thermometerGradient: SVGLinearGradientElement;
|
||||
private thermometer: SVGRectElement;
|
||||
private thermometerText: SVGTextElement;
|
||||
private antenna: SVGPolylineElement;
|
||||
private shakeButtonGroup: SVGElement;
|
||||
private shakeText: SVGTextElement;
|
||||
public board: pxsim.DalBoard;
|
||||
private pinNmToCoord: Map<Coord> = {
|
||||
};
|
||||
|
||||
constructor(public props: IBoardProps) {
|
||||
this.buildDom();
|
||||
if (props && props.wireframe)
|
||||
svg.addClass(this.element, "sim-wireframe");
|
||||
|
||||
/*
|
||||
if (props && props.theme)
|
||||
this.updateTheme();
|
||||
*/
|
||||
if (props && props.runtime) {
|
||||
this.board = this.props.runtime.board as pxsim.DalBoard;
|
||||
this.board.updateSubscribers.push(() => this.updateState());
|
||||
this.updateState();
|
||||
this.attachEvents();
|
||||
}
|
||||
}
|
||||
|
||||
public getView(): SVGAndSize<SVGSVGElement> {
|
||||
return {
|
||||
el: this.element,
|
||||
y: 0,
|
||||
x: 0,
|
||||
w: MB_WIDTH,
|
||||
h: MB_HEIGHT
|
||||
};
|
||||
}
|
||||
|
||||
public getCoord(pinNm: string): Coord {
|
||||
return this.pinNmToCoord[pinNm];
|
||||
}
|
||||
|
||||
public highlightPin(pinNm: string): void {
|
||||
//TODO: for instructions
|
||||
}
|
||||
|
||||
public getPinDist(): number {
|
||||
return 10;
|
||||
}
|
||||
|
||||
private recordPinCoords() {
|
||||
pinNames.forEach((pin, i) => {
|
||||
const nm = pin.name;
|
||||
const p = this.pins[i];
|
||||
const r = p.getBoundingClientRect();
|
||||
this.pinNmToCoord[nm] = [r.left + r.width / 2, r.top + r.height / 2];
|
||||
});
|
||||
console.log(JSON.stringify(this.pinNmToCoord, null, 2))
|
||||
}
|
||||
|
||||
private updateTheme() {
|
||||
let theme = this.props.theme;
|
||||
|
||||
svg.fill(this.buttons[0], theme.buttonUps[0]);
|
||||
svg.fill(this.buttons[1], theme.buttonUps[1]);
|
||||
svg.fill(this.buttons[2], theme.buttonUps[2]);
|
||||
|
||||
if (this.shakeButtonGroup) {
|
||||
svg.fill(this.shakeButtonGroup, this.props.theme.gestureButtonOff);
|
||||
}
|
||||
|
||||
svg.setGradientColors(this.lightLevelGradient, theme.lightLevelOn, theme.lightLevelOff);
|
||||
|
||||
svg.setGradientColors(this.thermometerGradient, theme.ledOff, theme.ledOn);
|
||||
svg.setGradientColors(this.soundLevelGradient, theme.soundLevelOn, theme.soundLevelOff);
|
||||
|
||||
for (const id in this.pinControls) {
|
||||
this.pinControls[id].updateTheme();
|
||||
}
|
||||
}
|
||||
|
||||
public updateState() {
|
||||
let state = this.board;
|
||||
if (!state) return;
|
||||
let theme = this.props.theme;
|
||||
|
||||
let bpState = state.buttonState;
|
||||
let buttons = bpState.buttons;
|
||||
this.buttons.forEach((button, i) => {
|
||||
svg.fill(button, buttons[i].pressed ? theme.buttonDown : theme.buttonUps[i]);
|
||||
})
|
||||
|
||||
this.updateLight();
|
||||
/*
|
||||
|
||||
this.updatePins();
|
||||
this.updateTilt();
|
||||
this.updateNeoPixels();
|
||||
this.updateSwitch();
|
||||
this.updateSound();
|
||||
this.updateLightLevel();
|
||||
this.updateSoundLevel();
|
||||
this.updateButtonAB();
|
||||
this.updateGestures();
|
||||
this.updateTemperature();
|
||||
this.updateInfrared();
|
||||
*/
|
||||
|
||||
if (!runtime || runtime.dead) svg.addClass(this.element, "grayscale");
|
||||
else svg.removeClass(this.element, "grayscale");
|
||||
}
|
||||
|
||||
private lastFlashTime: number = 0;
|
||||
private flashSystemLed() {
|
||||
/*
|
||||
if (!this.systemLed)
|
||||
this.systemLed = <SVGCircleElement>svg.child(this.g, "circle", { class: "sim-systemled", cx: 75, cy: MB_HEIGHT - 171, r: 2 })
|
||||
let now = Date.now();
|
||||
if (now - this.lastFlashTime > 150) {
|
||||
this.lastFlashTime = now;
|
||||
svg.animate(this.systemLed, "sim-flash")
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private lastIrReceiverFlash: number = 0;
|
||||
public flashIrReceiver() {
|
||||
/*
|
||||
if (!this.irReceiver)
|
||||
this.irReceiver = this.element.getElementById("path2054") as SVGElement;
|
||||
let now = Date.now();
|
||||
if (now - this.lastIrReceiverFlash > 200) {
|
||||
this.lastIrReceiverFlash = now;
|
||||
svg.animate(this.irReceiver, 'sim-flash-stroke')
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private lastIrTransmitterFlash: number = 0;
|
||||
public flashIrTransmitter() {
|
||||
/*
|
||||
if (!this.irTransmitter)
|
||||
this.irTransmitter = this.element.getElementById("path2062") as SVGElement;
|
||||
let now = Date.now();
|
||||
if (now - this.lastIrTransmitterFlash > 200) {
|
||||
this.lastIrTransmitterFlash = now;
|
||||
svg.animate(this.irTransmitter, 'sim-flash-stroke')
|
||||
}*/
|
||||
}
|
||||
|
||||
private updateInfrared() {
|
||||
const state = this.board;
|
||||
if (!state) return;
|
||||
|
||||
if (state.irState.packetReceived) {
|
||||
state.irState.packetReceived = false;
|
||||
this.flashIrReceiver();
|
||||
}
|
||||
}
|
||||
|
||||
private lastLightPattern: number = -1;
|
||||
private updateLight() {
|
||||
let state = this.board;
|
||||
if (!state || !state.lightState) return;
|
||||
|
||||
const lightPattern = state.lightState.lightPattern;
|
||||
if (lightPattern == this.lastLightPattern) return;
|
||||
this.lastLightPattern = lightPattern;
|
||||
switch(lightPattern) {
|
||||
case 0: // LED_BLACK
|
||||
svg.fill(this.light, "#FFF");
|
||||
break;
|
||||
case 1: // LED_GREEN
|
||||
svg.fill(this.light, "#00ff00");
|
||||
break;
|
||||
case 2: // LED_RED
|
||||
svg.fill(this.light, "#ff0000");
|
||||
break;
|
||||
case 3: // LED_ORANGE
|
||||
svg.fill(this.light, "#ffff00");
|
||||
break;
|
||||
case 4: // LED_GREEN_FLASH
|
||||
break;
|
||||
case 5: // LED_RED_FLASH
|
||||
break;
|
||||
case 6: // LED_ORANGE_FLASH
|
||||
break;
|
||||
case 7: // LED_GREEN_PULSE
|
||||
break;
|
||||
case 8: // LED_RED_PULSE
|
||||
break;
|
||||
case 9: // LED_ORANGE_PULSE
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private updateNeoPixels() {
|
||||
let state = this.board;
|
||||
if (!state || !state.neopixelState) return;
|
||||
let neopixels = state.neopixelState.getNeoPixels();
|
||||
for (let i = 0; i < state.neopixelState.NUM_PIXELS; i++) {
|
||||
let rgb = neopixels[i];
|
||||
let p_inner = this.element.getElementById(`LED${i}`) as SVGPathElement;
|
||||
|
||||
if (!rgb || (rgb.length == 3 && rgb[0] == 0 && rgb[1] == 0 && rgb[2] == 0)) {
|
||||
// Clear the pixel
|
||||
svg.fill(p_inner, `rgb(200,200,200)`);
|
||||
svg.filter(p_inner, null);
|
||||
p_inner.style.stroke = `none`
|
||||
continue;
|
||||
}
|
||||
|
||||
let hsl = visuals.rgbToHsl(rgb);
|
||||
let [h, s, l] = hsl;
|
||||
let lx = Math.max(l * 1.3, 85);
|
||||
// at least 10% luminosity
|
||||
l = l * 90 / 100 + 10;
|
||||
if (p_inner) {
|
||||
p_inner.style.stroke = `hsl(${h}, ${s}%, ${Math.min(l * 3, 75)}%)`
|
||||
p_inner.style.strokeWidth = "1.5";
|
||||
svg.fill(p_inner, `hsl(${h}, ${s}%, ${lx}%)`)
|
||||
}
|
||||
if (p_inner) svg.filter(p_inner, `url(#neopixelglow)`);
|
||||
}
|
||||
}
|
||||
|
||||
private updateSound() {
|
||||
let state = this.board;
|
||||
if (!state || !state.audioState) return;
|
||||
let audioState = state.audioState;
|
||||
|
||||
// FIXME
|
||||
// let soundBoard = this.element.getElementById('g4656') as SVGGElement;
|
||||
// if (audioState.isPlaying()) {
|
||||
// svg.addClass(soundBoard, "sim-sound-stroke");
|
||||
// } else {
|
||||
// svg.removeClass(soundBoard, "sim-sound-stroke");
|
||||
// }
|
||||
}
|
||||
|
||||
private updatePins() {
|
||||
let state = this.board;
|
||||
if (!state || !state.edgeConnectorState || !state.capacitiveSensorState) return;
|
||||
state.edgeConnectorState.pins.forEach((pin, i) => this.updatePin(pin, i));
|
||||
}
|
||||
|
||||
private updatePin(pin: Pin, index: number) {
|
||||
if (!pin || !this.pins[index]) return;
|
||||
|
||||
if ((pin as pins.CommonPin).used) {
|
||||
if (this.pinControls[pin.id] === undefined) {
|
||||
const pinName = pinNames.filter((a) => a.id === pin.id)[0];
|
||||
if (pinName) {
|
||||
this.pinControls[pin.id] = new AnalogPinControl(this, this.defs, pin.id, pinName.name);
|
||||
}
|
||||
else {
|
||||
// TODO: Surface pin controls for sensor pins in some way?
|
||||
this.pinControls[pin.id] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.pinControls[pin.id]) {
|
||||
this.pinControls[pin.id].updateValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateLightLevel() {
|
||||
let state = this.board;
|
||||
if (!state || !state.lightSensorState.sensorUsed) return;
|
||||
|
||||
if (!this.lightLevelButton) {
|
||||
let gid = "gradient-light-level";
|
||||
this.lightLevelGradient = svg.linearGradient(this.defs, gid)
|
||||
let cy = 15;
|
||||
let r = 10;
|
||||
this.lightLevelButton = svg.child(this.g, "circle", {
|
||||
cx: `12px`, cy: `${cy}px`, r: `${r}px`,
|
||||
class: 'sim-light-level-button',
|
||||
fill: `url(#${gid})`
|
||||
}) as SVGCircleElement;
|
||||
let pt = this.element.createSVGPoint();
|
||||
svg.buttonEvents(this.lightLevelButton,
|
||||
(ev) => {
|
||||
let pos = svg.cursorPoint(pt, this.element, ev);
|
||||
let rs = r / 2;
|
||||
let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255)));
|
||||
if (level != this.board.lightSensorState.getLevel()) {
|
||||
this.board.lightSensorState.setLevel(level);
|
||||
this.applyLightLevel();
|
||||
}
|
||||
}, ev => { },
|
||||
ev => { })
|
||||
this.lightLevelText = svg.child(this.g, "text", { x: 23, y: cy + r - 15, text: '', class: 'sim-text' }) as SVGTextElement;
|
||||
this.updateTheme();
|
||||
}
|
||||
|
||||
svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(state.lightSensorState.getLevel() * 100 / 255))) + '%')
|
||||
this.lightLevelText.textContent = state.lightSensorState.getLevel().toString();
|
||||
}
|
||||
|
||||
private applyLightLevel() {
|
||||
let lv = this.board.lightSensorState.getLevel();
|
||||
svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(lv * 100 / 255))) + '%')
|
||||
this.lightLevelText.textContent = lv.toString();
|
||||
}
|
||||
|
||||
private updateSoundLevel() {
|
||||
let state = this.board;
|
||||
if (!state || !state.microphoneState.sensorUsed) return;
|
||||
|
||||
if (!this.soundLevelButton) {
|
||||
let gid = "gradient-sound-level";
|
||||
this.soundLevelGradient = svg.linearGradient(this.defs, gid)
|
||||
let cy = 165;
|
||||
let r = 10;
|
||||
this.soundLevelButton = svg.child(this.g, "circle", {
|
||||
cx: `12px`, cy: `${cy}px`, r: `${r}px`,
|
||||
class: 'sim-sound-level-button',
|
||||
fill: `url(#${gid})`
|
||||
}) as SVGCircleElement;
|
||||
|
||||
let pt = this.element.createSVGPoint();
|
||||
svg.buttonEvents(this.soundLevelButton,
|
||||
(ev) => {
|
||||
let pos = svg.cursorPoint(pt, this.element, ev);
|
||||
let rs = r / 2;
|
||||
let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255)));
|
||||
if (level != this.board.microphoneState.getLevel()) {
|
||||
this.board.microphoneState.setLevel(255 - level);
|
||||
this.applySoundLevel();
|
||||
}
|
||||
}, ev => { },
|
||||
ev => { })
|
||||
this.soundLevelText = svg.child(this.g, "text", { x: 23, y: cy + r - 3, text: '', class: 'sim-text' }) as SVGTextElement;
|
||||
this.updateTheme();
|
||||
}
|
||||
|
||||
svg.setGradientValue(this.soundLevelGradient, Math.min(100, Math.max(0, Math.floor((255 - state.microphoneState.getLevel()) * 100 / 255))) + '%')
|
||||
this.soundLevelText.textContent = state.microphoneState.getLevel().toString();
|
||||
}
|
||||
|
||||
private applySoundLevel() {
|
||||
let lv = this.board.microphoneState.getLevel();
|
||||
svg.setGradientValue(this.soundLevelGradient, Math.min(100, Math.max(0, Math.floor((255 - lv) * 100 / 255))) + '%')
|
||||
this.soundLevelText.textContent = lv.toString();
|
||||
}
|
||||
|
||||
private updateTemperature() {
|
||||
let state = this.board;
|
||||
if (!state || !state.thermometerState || !state.thermometerState.sensorUsed) return;
|
||||
|
||||
// Celsius
|
||||
let tmin = -5;
|
||||
let tmax = 50;
|
||||
if (!this.thermometer) {
|
||||
let gid = "gradient-thermometer";
|
||||
this.thermometerGradient = svg.linearGradient(this.defs, gid);
|
||||
this.thermometer = <SVGRectElement>svg.child(this.g, "rect", {
|
||||
class: "sim-thermometer",
|
||||
x: 170,
|
||||
y: 3,
|
||||
width: 7,
|
||||
height: 32,
|
||||
rx: 2, ry: 2,
|
||||
fill: `url(#${gid})`
|
||||
});
|
||||
this.thermometerText = svg.child(this.g, "text", { class: 'sim-text', x: 148, y: 10 }) as SVGTextElement;
|
||||
this.updateTheme();
|
||||
|
||||
let pt = this.element.createSVGPoint();
|
||||
svg.buttonEvents(this.thermometer,
|
||||
(ev) => {
|
||||
let cur = svg.cursorPoint(pt, this.element, ev);
|
||||
let t = Math.max(0, Math.min(1, (35 - cur.y) / 30))
|
||||
state.thermometerState.setLevel(Math.floor(tmin + t * (tmax - tmin)));
|
||||
this.updateTemperature();
|
||||
}, ev => { }, ev => { })
|
||||
}
|
||||
|
||||
let t = Math.max(tmin, Math.min(tmax, state.thermometerState.getLevel()))
|
||||
let per = Math.floor((state.thermometerState.getLevel() - tmin) / (tmax - tmin) * 100)
|
||||
svg.setGradientValue(this.thermometerGradient, 100 - per + "%");
|
||||
|
||||
let unit = "°C";
|
||||
if (state.thermometerUnitState == pxsim.TemperatureUnit.Fahrenheit) {
|
||||
unit = "°F";
|
||||
t = ((t * 18) / 10 + 32) >> 0;
|
||||
}
|
||||
this.thermometerText.textContent = t + unit;
|
||||
}
|
||||
|
||||
private updateButtonAB() {
|
||||
let state = this.board;
|
||||
if (state.buttonState.usesButtonAB) {
|
||||
(<any>this.buttons[2]).style.visibility = "visible";
|
||||
this.updateTheme();
|
||||
}
|
||||
}
|
||||
|
||||
private updateGestures() {
|
||||
let state = this.board;
|
||||
if (state.accelerometerState.useShake && !this.shakeButtonGroup) {
|
||||
const btnr = 2;
|
||||
const width = 22;
|
||||
const height = 10;
|
||||
|
||||
let btng = svg.child(this.g, "g", { class: "sim-button-group" });
|
||||
this.shakeButtonGroup = btng;
|
||||
this.shakeText = svg.child(this.g, "text", { x: 81, y: 32, class: "sim-text small" }) as SVGTextElement;
|
||||
this.shakeText.textContent = "SHAKE"
|
||||
|
||||
svg.child(btng, "rect", { class: "sim-button", x: 79, y: 25, rx: btnr, ry: btnr, width, height });
|
||||
svg.fill(btng, this.props.theme.gestureButtonOff);
|
||||
this.shakeButtonGroup.addEventListener(pointerEvents.down, ev => {
|
||||
let state = this.board;
|
||||
svg.fill(btng, this.props.theme.gestureButtonOn);
|
||||
svg.addClass(this.shakeText, "inverted");
|
||||
})
|
||||
this.shakeButtonGroup.addEventListener(pointerEvents.leave, ev => {
|
||||
let state = this.board;
|
||||
svg.fill(btng, this.props.theme.gestureButtonOff);
|
||||
svg.removeClass(this.shakeText, "inverted");
|
||||
})
|
||||
this.shakeButtonGroup.addEventListener(pointerEvents.up, ev => {
|
||||
let state = this.board;
|
||||
svg.fill(btng, this.props.theme.gestureButtonOff);
|
||||
//this.board.bus.queue(DAL.DEVICE_ID_GESTURE, 11); // GESTURE_SHAKE
|
||||
svg.removeClass(this.shakeText, "inverted");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private updateTilt() {
|
||||
if (this.props.disableTilt) return;
|
||||
let state = this.board;
|
||||
if (!state || !state.accelerometerState.accelerometer.isActive) return;
|
||||
|
||||
const x = state.accelerometerState.accelerometer.getX();
|
||||
const y = state.accelerometerState.accelerometer.getY();
|
||||
const af = 8 / 1023;
|
||||
const s = 1 - Math.min(0.1, Math.pow(Math.max(Math.abs(x), Math.abs(y)) / 1023, 2) / 35);
|
||||
|
||||
this.element.style.transform = `perspective(30em) rotateX(${y * af}deg) rotateY(${x * af}deg) scale(${s}, ${s})`
|
||||
this.element.style.perspectiveOrigin = "50% 50% 50%";
|
||||
this.element.style.perspective = "30em";
|
||||
}
|
||||
|
||||
private buildDom() {
|
||||
this.element = new DOMParser().parseFromString(BOARD_SVG, "image/svg+xml").querySelector("svg") as SVGSVGElement;
|
||||
svg.hydrate(this.element, {
|
||||
"version": "1.0",
|
||||
"viewBox": `0 0 ${MB_WIDTH} ${MB_HEIGHT}`,
|
||||
"class": "sim",
|
||||
"x": "0px",
|
||||
"y": "0px",
|
||||
"width": MB_WIDTH + "px",
|
||||
"height": MB_HEIGHT + "px",
|
||||
});
|
||||
this.style = <SVGStyleElement>svg.child(this.element, "style", {});
|
||||
this.style.textContent = MB_STYLE;
|
||||
|
||||
this.defs = <SVGDefsElement>svg.child(this.element, "defs", {});
|
||||
this.g = <SVGGElement>svg.elt("g");
|
||||
this.element.appendChild(this.g);
|
||||
|
||||
const btnids = ["BTN_1", "BTN_2", "BTN_3", "BTN_4", "BTN_5", "BTN_BACK"];
|
||||
this.buttons = btnids.map(n => this.element.getElementById(n) as SVGElement);
|
||||
this.buttons.forEach(b => svg.addClass(b, "sim-button"));
|
||||
|
||||
this.light = this.element.getElementById("BOARD_Light") as SVGElement;
|
||||
}
|
||||
|
||||
private attachEvents() {
|
||||
Runtime.messagePosted = (msg) => {
|
||||
switch (msg.type || "") {
|
||||
case "serial": this.flashSystemLed(); break;
|
||||
case "irpacket": this.flashIrTransmitter(); break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
let tiltDecayer = 0;
|
||||
this.element.addEventListener(pointerEvents.move, (ev: MouseEvent) => {
|
||||
let state = this.board;
|
||||
if (!state.accelerometerState.accelerometer.isActive) return;
|
||||
|
||||
if (tiltDecayer) {
|
||||
clearInterval(tiltDecayer);
|
||||
tiltDecayer = 0;
|
||||
}
|
||||
|
||||
let bbox = this.element.getBoundingClientRect();
|
||||
let ax = (ev.clientX - bbox.width / 2) / (bbox.width / 3);
|
||||
let ay = (ev.clientY - bbox.height / 2) / (bbox.height / 3);
|
||||
|
||||
let x = - Math.max(- 1023, Math.min(1023, Math.floor(ax * 1023)));
|
||||
let y = Math.max(- 1023, Math.min(1023, Math.floor(ay * 1023)));
|
||||
let z2 = 1023 * 1023 - x * x - y * y;
|
||||
let z = Math.floor((z2 > 0 ? -1 : 1) * Math.sqrt(Math.abs(z2)));
|
||||
|
||||
state.accelerometerState.accelerometer.update(x, y, z);
|
||||
this.updateTilt();
|
||||
}, false);
|
||||
this.element.addEventListener(pointerEvents.leave, (ev: MouseEvent) => {
|
||||
let state = this.board;
|
||||
if (!state.accelerometerState.accelerometer.isActive) return;
|
||||
|
||||
if (!tiltDecayer) {
|
||||
tiltDecayer = setInterval(() => {
|
||||
let accx = state.accelerometerState.accelerometer.getX(MicroBitCoordinateSystem.RAW);
|
||||
accx = Math.floor(Math.abs(accx) * 0.85) * (accx > 0 ? 1 : -1);
|
||||
let accy = state.accelerometerState.accelerometer.getY(MicroBitCoordinateSystem.RAW);
|
||||
accy = Math.floor(Math.abs(accy) * 0.85) * (accy > 0 ? 1 : -1);
|
||||
let accz = -Math.sqrt(Math.max(0, 1023 * 1023 - accx * accx - accy * accy));
|
||||
if (Math.abs(accx) <= 24 && Math.abs(accy) <= 24) {
|
||||
clearInterval(tiltDecayer);
|
||||
tiltDecayer = 0;
|
||||
accx = 0;
|
||||
accy = 0;
|
||||
accz = -1023;
|
||||
}
|
||||
state.accelerometerState.accelerometer.update(accx, accy, accz);
|
||||
this.updateTilt();
|
||||
}, 50)
|
||||
}
|
||||
}, false);
|
||||
*/
|
||||
let bpState = this.board.buttonState;
|
||||
let stateButtons = bpState.buttons;
|
||||
this.buttons.forEach((btn, index) => {
|
||||
let button = stateButtons[index];
|
||||
|
||||
btn.addEventListener(pointerEvents.down, ev => {
|
||||
button.setPressed(true);
|
||||
svg.fill(this.buttons[index], this.props.theme.buttonDown);
|
||||
})
|
||||
btn.addEventListener(pointerEvents.leave, ev => {
|
||||
button.setPressed(false);
|
||||
svg.fill(this.buttons[index], this.props.theme.buttonUps[index]);
|
||||
})
|
||||
btn.addEventListener(pointerEvents.up, ev => {
|
||||
button.setPressed(false);
|
||||
svg.fill(this.buttons[index], this.props.theme.buttonUps[index]);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
1178
sim/visuals/boardsvg.ts
Normal file
1178
sim/visuals/boardsvg.ts
Normal file
File diff suppressed because one or more lines are too long
10
sim/visuals/boardview.ts
Normal file
10
sim/visuals/boardview.ts
Normal file
@ -0,0 +1,10 @@
|
||||
namespace pxsim.visuals {
|
||||
mkBoardView = (opts: BoardViewOptions): BoardView => {
|
||||
return new visuals.EV3BoardSvg({
|
||||
runtime: runtime,
|
||||
theme: visuals.randomTheme(),
|
||||
disableTilt: false,
|
||||
wireframe: opts.wireframe,
|
||||
});
|
||||
}
|
||||
}
|
92
sim/visuals/pincontrol.ts
Normal file
92
sim/visuals/pincontrol.ts
Normal file
@ -0,0 +1,92 @@
|
||||
namespace pxsim.visuals {
|
||||
export class AnalogPinControl {
|
||||
private outerElement: SVGElement;
|
||||
|
||||
private innerCircle: SVGCircleElement;
|
||||
private gradient: SVGLinearGradientElement;
|
||||
private currentValue: number;
|
||||
private pin: Pin;
|
||||
|
||||
constructor(private parent: EV3BoardSvg, private defs: SVGDefsElement, private id: CPlayPinName, name: string) {
|
||||
this.pin = board().edgeConnectorState.getPin(this.id);
|
||||
|
||||
// Init the button events
|
||||
this.outerElement = parent.element.getElementById(name) as SVGElement;
|
||||
svg.addClass(this.outerElement, "sim-pin-touch");
|
||||
this.addButtonEvents();
|
||||
|
||||
|
||||
// Init the gradient controls
|
||||
// const gid = `gradient-${CPlayPinName[id]}-level`;
|
||||
// this.innerCircle = parent.element.getElementById("PIN_CONNECTOR_" + CPlayPinName[id]) as SVGCircleElement;
|
||||
// this.gradient = svg.linearGradient(this.defs, gid);
|
||||
// this.innerCircle.setAttribute("fill", `url(#${gid})`);
|
||||
// this.innerCircle.setAttribute("class", "sim-light-level-button")
|
||||
// this.addLevelControlEvents()
|
||||
|
||||
this.updateTheme();
|
||||
}
|
||||
|
||||
public updateTheme() {
|
||||
const theme = this.parent.props.theme;
|
||||
svg.setGradientColors(this.gradient, theme.lightLevelOff, 'darkorange');
|
||||
}
|
||||
|
||||
public updateValue() {
|
||||
const value = this.pin.value;
|
||||
|
||||
if (value === this.currentValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentValue = value;
|
||||
|
||||
// svg.setGradientValue(this.gradient, 100 - Math.min(100, Math.max(0, Math.floor(value * 100 / 1023))) + '%')
|
||||
// if (this.innerCircle.childNodes.length) {
|
||||
// this.innerCircle.removeChild(this.innerCircle.childNodes[0])
|
||||
// }
|
||||
|
||||
svg.title(this.outerElement, value.toString());
|
||||
}
|
||||
|
||||
private addButtonEvents() {
|
||||
this.outerElement.addEventListener(pointerEvents.down, ev => {
|
||||
this.pin.touched = true;
|
||||
svg.addClass(this.outerElement, "touched");
|
||||
|
||||
(pxtcore.getTouchButton(this.id - 1) as CommonButton).setPressed(true);
|
||||
})
|
||||
this.outerElement.addEventListener(pointerEvents.leave, ev => {
|
||||
this.pin.touched = false;
|
||||
svg.removeClass(this.outerElement, "touched");
|
||||
|
||||
(pxtcore.getTouchButton(this.id - 1) as CommonButton).setPressed(false);
|
||||
})
|
||||
this.outerElement.addEventListener(pointerEvents.up, ev => {
|
||||
this.pin.touched = false;
|
||||
svg.removeClass(this.outerElement, "touched");
|
||||
|
||||
(pxtcore.getTouchButton(this.id - 1) as CommonButton).setPressed(false);
|
||||
})
|
||||
}
|
||||
|
||||
private addLevelControlEvents() {
|
||||
const cy = parseFloat(this.innerCircle.getAttribute("cy"));
|
||||
const r = parseFloat(this.innerCircle.getAttribute("r"));
|
||||
const pt = this.parent.element.createSVGPoint();
|
||||
|
||||
svg.buttonEvents(this.innerCircle,
|
||||
(ev) => {
|
||||
const pos = svg.cursorPoint(pt, this.parent.element, ev);
|
||||
const rs = r / 2;
|
||||
const level = Math.max(0, Math.min(1023, Math.floor((1 - (pos.y - (cy - rs)) / (2 * rs)) * 1023)));
|
||||
|
||||
if (level != this.pin.value) {
|
||||
this.pin.value = level;
|
||||
this.updateValue();
|
||||
}
|
||||
}, ev => { },
|
||||
ev => { });
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user