664 lines
23 KiB
JavaScript
664 lines
23 KiB
JavaScript
|
/* Lemmings - robot and GUI script.
|
|||
|
*
|
|||
|
* Copyright 2016 Harmen de Weerd
|
|||
|
* Copyright 2017 Johannes Keyser, James Cooke, George Kachergis
|
|||
|
*
|
|||
|
* This program is free software: you can redistribute it and/or modify
|
|||
|
* it under the terms of the GNU General Public License as published by
|
|||
|
* the Free Software Foundation, either version 3 of the License, or
|
|||
|
* (at your option) any later version.
|
|||
|
*
|
|||
|
* This program is distributed in the hope that it will be useful,
|
|||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|||
|
* GNU General Public License for more details.
|
|||
|
*
|
|||
|
* You should have received a copy of the GNU General Public License
|
|||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
*/
|
|||
|
|
|||
|
// Description of robot(s), and attached sensor(s) used by InstantiateRobot()
|
|||
|
RobotInfo = [
|
|||
|
{body: null, // for MatterJS body, added by InstantiateRobot()
|
|||
|
color: [255, 255, 255], // color of the robot shape
|
|||
|
init: {x: 50, y: 50, angle: 0}, // initial position and orientation
|
|||
|
sensors: [ // define an array of sensors on the robot
|
|||
|
// define one sensor
|
|||
|
{sense: senseDistance, // function handle, determines type of sensor
|
|||
|
minVal: 0, // minimum detectable distance, in pixels
|
|||
|
maxVal: 50, // maximum detectable distance, in pixels
|
|||
|
attachAngle: Math.PI/4, // where the sensor is mounted on robot body
|
|||
|
lookAngle: 0, // direction the sensor is looking (relative to center-out)
|
|||
|
id: 'distR', // a unique, arbitrary ID of the sensor, for printing/debugging
|
|||
|
color: [150, 0, 0], // sensor color [in RGB], to distinguish them
|
|||
|
parent: null, // robot object the sensor is attached to, added by InstantiateRobot
|
|||
|
value: null // sensor value, i.e. distance in pixels; updated by sense() function
|
|||
|
},
|
|||
|
// define another sensor
|
|||
|
{sense: senseDistance, minVal: 0, maxVal: 50, attachAngle: -Math.PI/4,
|
|||
|
lookAngle: 0, id: 'distL', color: [0, 150, 0], parent: null, value: null
|
|||
|
}
|
|||
|
]
|
|||
|
}
|
|||
|
];
|
|||
|
|
|||
|
// Simulation settings; please change anything that you think makes sense.
|
|||
|
simInfo = {
|
|||
|
maxSteps: 50000, // maximal number of simulation steps to run
|
|||
|
airDrag: 0.1, // "air" friction of enviroment; 0 is vacuum, 0.9 is molasses
|
|||
|
boxFric: 0.005, // friction between boxes during collisions
|
|||
|
boxMass: 0.01, // mass of boxes
|
|||
|
boxSize: 20, // size of the boxes, in pixels
|
|||
|
robotSize: 13, // approximate robot radius, in pixels (note the SVG gets scaled down)
|
|||
|
robotMass: 0.4, // robot mass (a.u)
|
|||
|
gravity: 0, // constant acceleration in Y-direction
|
|||
|
bayRobot: null, // currently selected robot
|
|||
|
baySensor: null, // currently selected sensor
|
|||
|
bayScale: 3, // scale within 2nd, inset canvas showing robot in it's "bay"
|
|||
|
doContinue: true, // whether to continue simulation, set in HTML
|
|||
|
debugSensors: false, // plot sensor rays and mark detected objects
|
|||
|
debugMouse: false, // allow dragging any object with the mouse
|
|||
|
engine: null, // MatterJS 2D physics engine
|
|||
|
world: null, // world object (composite of all objects in MatterJS engine)
|
|||
|
runner: null, // object for running MatterJS engine
|
|||
|
height: null, // set in HTML file; height of arena (world canvas), in pixels
|
|||
|
width: null, // set in HTML file; width of arena (world canvas), in pixels
|
|||
|
curSteps: 0 // increased by simStep()
|
|||
|
};
|
|||
|
|
|||
|
robots = new Array();
|
|||
|
sensors = new Array();
|
|||
|
|
|||
|
function init() { // called once when loading HTML file
|
|||
|
const robotBay = document.getElementById("bayLemming"),
|
|||
|
arena = document.getElementById("arenaLemming"),
|
|||
|
height = arena.height,
|
|||
|
width = arena.width;
|
|||
|
simInfo.height = height;
|
|||
|
simInfo.width = width;
|
|||
|
|
|||
|
/* Create a MatterJS engine and world. */
|
|||
|
simInfo.engine = Matter.Engine.create();
|
|||
|
simInfo.world = simInfo.engine.world;
|
|||
|
simInfo.world.gravity.y = simInfo.gravity;
|
|||
|
simInfo.engine.timing.timeScale = 1;
|
|||
|
|
|||
|
/* Create walls and boxes, and add them to the world. */
|
|||
|
// note that "roles" are custom properties for rendering (not from MatterJS)
|
|||
|
function getWall(x, y, width, height) {
|
|||
|
return Matter.Bodies.rectangle(x, y, width, height,
|
|||
|
{isStatic: true, role: 'wall',
|
|||
|
color:[150, 150, 150]});
|
|||
|
};
|
|||
|
const wall_lo = getWall(width/2, height-5, width-5, 5),
|
|||
|
wall_hi = getWall(width/2, 5, width-5, 5),
|
|||
|
wall_le = getWall(5, height/2, 5, height-15),
|
|||
|
wall_ri = getWall(width-5, height/2, 5, height-15);
|
|||
|
Matter.World.add(simInfo.world, [wall_lo, wall_hi, wall_le, wall_ri]);
|
|||
|
|
|||
|
/* Add a bunch of boxes in a neat grid. */
|
|||
|
function getBox(x, y) {
|
|||
|
// flip coin for red vs blue and add rgb
|
|||
|
colFlag = Math.round(Math.random()); // random 0,1 variable for box color
|
|||
|
if (colFlag == 1 ){
|
|||
|
color = [0, 0, 200];
|
|||
|
}
|
|||
|
else {
|
|||
|
color = [200, 0, 0];
|
|||
|
}
|
|||
|
box = Matter.Bodies.rectangle(x, y, simInfo.boxSize, simInfo.boxSize,
|
|||
|
{frictionAir: simInfo.airDrag,
|
|||
|
friction: simInfo.boxFric,
|
|||
|
mass: simInfo.boxMass,
|
|||
|
role: 'box',
|
|||
|
color: color});
|
|||
|
return box;
|
|||
|
};
|
|||
|
|
|||
|
const startX = 100, startY = 100,
|
|||
|
nBoxX = 5, nBoxY = 5,
|
|||
|
gapX = 40, gapY = 30,
|
|||
|
stack = Matter.Composites.stack(startX, startY,
|
|||
|
nBoxX, nBoxY,
|
|||
|
gapX, gapY, getBox);
|
|||
|
Matter.World.add(simInfo.world, stack);
|
|||
|
|
|||
|
/* Add debugging mouse control for dragging objects. */
|
|||
|
if (simInfo.debugMouse){
|
|||
|
const mouseConstraint = Matter.MouseConstraint.create(simInfo.engine,
|
|||
|
{mouse: Matter.Mouse.create(arena),
|
|||
|
// spring stiffness mouse ~ object
|
|||
|
constraint: {stiffness: 0.5}});
|
|||
|
Matter.World.add(simInfo.world, mouseConstraint);
|
|||
|
}
|
|||
|
// Add the tracker functions from mouse.js
|
|||
|
addMouseTracker(arena);
|
|||
|
addMouseTracker(robotBay);
|
|||
|
|
|||
|
/* Running the MatterJS physics engine (without rendering). */
|
|||
|
simInfo.runner = Matter.Runner.create({fps: 60, isFixed: false});
|
|||
|
Matter.Runner.start(simInfo.runner, simInfo.engine);
|
|||
|
// register function simStep() as callback to MatterJS's engine events
|
|||
|
Matter.Events.on(simInfo.engine, 'tick', simStep);
|
|||
|
|
|||
|
/* Create robot(s). */
|
|||
|
setRobotNumber(1); // requires defined simInfo.world
|
|||
|
loadBay(robots[0]);
|
|||
|
|
|||
|
};
|
|||
|
|
|||
|
function convrgb(values) {
|
|||
|
return 'rgb(' + values.join(', ') + ')';
|
|||
|
};
|
|||
|
|
|||
|
|
|||
|
function rotate(robot, torque=0) {
|
|||
|
/* Apply a torque to the robot to rotate it.
|
|||
|
*
|
|||
|
* Parameters
|
|||
|
* torque - rotational force to apply to the body.
|
|||
|
* Try values around +/- 0.005.
|
|||
|
*/
|
|||
|
robot.body.torque = torque;
|
|||
|
};
|
|||
|
|
|||
|
function drive(robot, force=0) {
|
|||
|
/* Apply a force to the robot to move it.
|
|||
|
*
|
|||
|
* Parameters
|
|||
|
* force - force to apply to the body.
|
|||
|
* Try values around +/- 0.0005.
|
|||
|
*/
|
|||
|
const orientation = robot.body.angle,
|
|||
|
force_vec = Matter.Vector.create(force, 0),
|
|||
|
move_vec = Matter.Vector.rotate(force_vec, orientation);
|
|||
|
Matter.Body.applyForce(robot.body, robot.body.position , move_vec);
|
|||
|
};
|
|||
|
|
|||
|
|
|||
|
function senseDistance() {
|
|||
|
/* Distance sensor simulation based on ray casting. Called from sensor
|
|||
|
* object, returns nothing, updates a new reading into this.value.
|
|||
|
*
|
|||
|
* Idea: Cast a ray with a certain length from the sensor, and check
|
|||
|
* via collision detection if objects intersect with the ray.
|
|||
|
* To determine distance, run a Binary search on ray length.
|
|||
|
* Note: Sensor ray needs to ignore robot (parts), or start outside of it.
|
|||
|
* The latter is easy with the current circular shape of the robots.
|
|||
|
* Note: Order of tests are optimized by starting with max ray length, and
|
|||
|
* then only testing the maximal number of initially resulting objects.
|
|||
|
* Note: The sensor's "ray" could have any other (convex) shape;
|
|||
|
* currently it's just a very thin rectangle.
|
|||
|
*/
|
|||
|
|
|||
|
const context = document.getElementById('arenaLemming').getContext('2d');
|
|||
|
var bodies = Matter.Composite.allBodies(simInfo.engine.world);
|
|||
|
|
|||
|
const robotAngle = this.parent.body.angle,
|
|||
|
attachAngle = this.attachAngle,
|
|||
|
rayAngle = robotAngle + attachAngle + this.lookAngle;
|
|||
|
|
|||
|
const rPos = this.parent.body.position,
|
|||
|
rSize = simInfo.robotSize,
|
|||
|
startPoint = {x: rPos.x + (rSize+1) * Math.cos(robotAngle + attachAngle),
|
|||
|
y: rPos.y + (rSize+1) * Math.sin(robotAngle + attachAngle)};
|
|||
|
|
|||
|
function getEndpoint(rayLength) {
|
|||
|
return {x: rPos.x + (rSize + rayLength) * Math.cos(rayAngle),
|
|||
|
y: rPos.y + (rSize + rayLength) * Math.sin(rayAngle)};
|
|||
|
};
|
|||
|
|
|||
|
function sensorRay(bodies, rayLength) {
|
|||
|
// Cast ray of supplied length and return the bodies that collide with it.
|
|||
|
const rayWidth = 1e-100,
|
|||
|
endPoint = getEndpoint(rayLength);
|
|||
|
rayX = (endPoint.x + startPoint.x) / 2,
|
|||
|
rayY = (endPoint.y + startPoint.y) / 2,
|
|||
|
rayRect = Matter.Bodies.rectangle(rayX, rayY, rayLength, rayWidth,
|
|||
|
{isSensor: true, isStatic: true,
|
|||
|
angle: rayAngle, role: 'sensor'});
|
|||
|
|
|||
|
var collidedBodies = [];
|
|||
|
for (var bb = 0; bb < bodies.length; bb++) {
|
|||
|
var body = bodies[bb];
|
|||
|
// coarse check on body boundaries, to increase performance:
|
|||
|
if (Matter.Bounds.overlaps(body.bounds, rayRect.bounds)) {
|
|||
|
for (var pp = body.parts.length === 1 ? 0 : 1; pp < body.parts.length; pp++) {
|
|||
|
var part = body.parts[pp];
|
|||
|
// finer, more costly check on actual geometry:
|
|||
|
if (Matter.Bounds.overlaps(part.bounds, rayRect.bounds)) {
|
|||
|
const collision = Matter.SAT.collides(part, rayRect);
|
|||
|
if (collision.collided) {
|
|||
|
collidedBodies.push(body);
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return collidedBodies;
|
|||
|
};
|
|||
|
|
|||
|
// call 1x with full length, and check all bodies in the world;
|
|||
|
// in subsequent calls, only check the bodies resulting here
|
|||
|
var rayLength = this.maxVal;
|
|||
|
bodies = sensorRay(bodies, rayLength);
|
|||
|
// if some collided, search for maximal ray length without collisions
|
|||
|
if (bodies.length > 0) {
|
|||
|
var lo = 0,
|
|||
|
hi = rayLength;
|
|||
|
while (lo < rayLength) {
|
|||
|
if (sensorRay(bodies, rayLength).length > 0) {
|
|||
|
hi = rayLength;
|
|||
|
}
|
|||
|
else {
|
|||
|
lo = rayLength;
|
|||
|
}
|
|||
|
rayLength = Math.floor(lo + (hi-lo)/2);
|
|||
|
}
|
|||
|
}
|
|||
|
// increase length to (barely) touch closest body (if any)
|
|||
|
rayLength += 1;
|
|||
|
bodies = sensorRay(bodies, rayLength);
|
|||
|
|
|||
|
if (simInfo.debugSensors) { // if invisible, check order of object drawing
|
|||
|
// draw the resulting ray
|
|||
|
endPoint = getEndpoint(rayLength);
|
|||
|
context.beginPath();
|
|||
|
context.moveTo(startPoint.x, startPoint.y);
|
|||
|
context.lineTo(endPoint.x, endPoint.y);
|
|||
|
context.strokeStyle = this.parent.info.color;
|
|||
|
context.lineWidth = 0.5;
|
|||
|
context.stroke();
|
|||
|
// mark all objects's lines intersecting with the ray
|
|||
|
for (var bb = 0; bb < bodies.length; bb++) {
|
|||
|
var vertices = bodies[bb].vertices;
|
|||
|
context.moveTo(vertices[0].x, vertices[0].y);
|
|||
|
for (var vv = 1; vv < vertices.length; vv += 1) {
|
|||
|
context.lineTo(vertices[vv].x, vertices[vv].y);
|
|||
|
}
|
|||
|
context.closePath();
|
|||
|
}
|
|||
|
context.stroke();
|
|||
|
}
|
|||
|
|
|||
|
// indicate if the sensor exceeded its maximum length by returning infinity
|
|||
|
if (rayLength > this.maxVal) {
|
|||
|
rayLength = Infinity;
|
|||
|
}
|
|||
|
else {
|
|||
|
// apply mild noise on the sensor reading, and clamp between valid values
|
|||
|
function gaussNoise(sigma=1) {
|
|||
|
const x0 = 1.0 - Math.random();
|
|||
|
const x1 = 1.0 - Math.random();
|
|||
|
return sigma * Math.sqrt(-2 * Math.log(x0)) * Math.cos(2 * Math.PI * x1);
|
|||
|
};
|
|||
|
rayLength = Math.floor(rayLength + gaussNoise(3));
|
|||
|
rayLength = Matter.Common.clamp(rayLength, this.minVal, this.maxVal);
|
|||
|
}
|
|||
|
|
|||
|
this.value = rayLength;
|
|||
|
};
|
|||
|
|
|||
|
function dragSensor(sensor, event) {
|
|||
|
const robotBay = document.getElementById('bayLemming'),
|
|||
|
bCenter = {x: robotBay.width/2,
|
|||
|
y: robotBay.height/2},
|
|||
|
rSize = simInfo.robotSize,
|
|||
|
bScale = simInfo.bayScale,
|
|||
|
sSize = sensor.getWidth(),
|
|||
|
mAngle = Math.atan2( event.mouse.x - bCenter.x,
|
|||
|
-(event.mouse.y - bCenter.y));
|
|||
|
sensor.info.attachAngle = mAngle;
|
|||
|
sensor.x = bCenter.x - sSize - bScale * rSize * Math.sin(-mAngle);
|
|||
|
sensor.y = bCenter.y - sSize - bScale * rSize * Math.cos( mAngle);
|
|||
|
repaintBay();
|
|||
|
}
|
|||
|
|
|||
|
function loadSensor(sensor, event) {
|
|||
|
loadSensorInfo(sensor.sensor);
|
|||
|
}
|
|||
|
|
|||
|
function loadSensorInfo(sensorInfo) {
|
|||
|
simInfo.baySensor = sensorInfo;
|
|||
|
}
|
|||
|
|
|||
|
function loadBay(robot) {
|
|||
|
simInfo.bayRobot = robot;
|
|||
|
sensors = new Array();
|
|||
|
const robotBay = document.getElementById("bayLemming");
|
|||
|
const bCenter = {x: robotBay.width/2,
|
|||
|
y: robotBay.height/2},
|
|||
|
rSize = simInfo.robotSize,
|
|||
|
bScale = simInfo.bayScale;
|
|||
|
|
|||
|
for (var ss = 0; ss < robot.info.sensors.length; ++ss) {
|
|||
|
const curSensor = robot.sensors[ss],
|
|||
|
attachAngle = curSensor.attachAngle;
|
|||
|
// put current sensor into global variable, make mouse-interactive
|
|||
|
sensors[ss] = makeInteractiveElement(new SensorGraphics(curSensor),
|
|||
|
document.getElementById("bayLemming"));
|
|||
|
const sSize = sensors[ss].getWidth();
|
|||
|
sensors[ss].x = bCenter.x - sSize - bScale * rSize * Math.sin(-attachAngle);
|
|||
|
sensors[ss].y = bCenter.y - sSize - bScale * rSize * Math.cos( attachAngle);
|
|||
|
sensors[ss].onDragging = dragSensor;
|
|||
|
sensors[ss].onDrag = loadSensor;
|
|||
|
}
|
|||
|
repaintBay();
|
|||
|
}
|
|||
|
|
|||
|
function SensorGraphics(sensorInfo) {
|
|||
|
this.info = sensorInfo;
|
|||
|
this.plotSensor = plotSensor;
|
|||
|
// add functions getWidth/getHeight for graphics.js & mouse.js,
|
|||
|
// to enable dragging the sensor in the robot bay
|
|||
|
this.getWidth = function() { return 6; };
|
|||
|
this.getHeight = function() { return 6; };
|
|||
|
}
|
|||
|
|
|||
|
function loadFromSVG() {
|
|||
|
var vertexSets = [];
|
|||
|
const svg = document.getElementById('robotbodySVG'),
|
|||
|
data = svg.contentDocument;
|
|||
|
|
|||
|
jQuery(data).find('path').each(function(_, path) {
|
|||
|
var points = Matter.Svg.pathToVertices(path, 30);
|
|||
|
vertexSets.push(Matter.Vertices.scale(points, 0.2, 0.2));
|
|||
|
});
|
|||
|
|
|||
|
return vertexSets;
|
|||
|
};
|
|||
|
|
|||
|
function InstantiateRobot(robotInfo) {
|
|||
|
// load robot's body shape from SVG file
|
|||
|
const bodySVGpoints = loadFromSVG();
|
|||
|
this.body = Matter.Bodies.fromVertices(robotInfo.init.x,
|
|||
|
robotInfo.init.y,
|
|||
|
bodySVGpoints,
|
|||
|
{frictionAir: simInfo.airDrag,
|
|||
|
mass: simInfo.robotMass,
|
|||
|
color: [255, 255, 255],
|
|||
|
role: 'robot'}, true);
|
|||
|
|
|||
|
Matter.World.add(simInfo.world, this.body);
|
|||
|
Matter.Body.setAngle(this.body, robotInfo.init.angle);
|
|||
|
|
|||
|
// instantiate its sensors
|
|||
|
this.sensors = robotInfo.sensors;
|
|||
|
for (var ss = 0; ss < this.sensors.length; ++ss) {
|
|||
|
this.sensors[ss].parent = this;
|
|||
|
}
|
|||
|
|
|||
|
// attach its helper functions
|
|||
|
this.rotate = rotate;
|
|||
|
this.drive = drive;
|
|||
|
this.info = robotInfo;
|
|||
|
this.plotRobot = plotRobot;
|
|||
|
|
|||
|
// add functions getWidth/getHeight for graphics.js & mouse.js,
|
|||
|
// to enable selection by clicking the robot in the arena
|
|||
|
this.getWidth = function() { return 2 * simInfo.robotSize; };
|
|||
|
this.getHeight = function() { return 2 * simInfo.robotSize; };
|
|||
|
}
|
|||
|
|
|||
|
function robotUpdateSensors(robot) {
|
|||
|
// update all sensors of robot; puts new values into sensor.value
|
|||
|
for (var ss = 0; ss < robot.sensors.length; ss++) {
|
|||
|
robot.sensors[ss].sense();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
function getSensorValById(robot, id) {
|
|||
|
for (var ss = 0; ss < robot.sensors.length; ss++) {
|
|||
|
if (robot.sensors[ss].id == id) {
|
|||
|
return robot.sensors[ss].value;
|
|||
|
}
|
|||
|
}
|
|||
|
return undefined; // if not returned yet, id doesn't exist
|
|||
|
};
|
|||
|
|
|||
|
function robotMove(robot) {
|
|||
|
// TODO: Define Lemming program here.
|
|||
|
const distL = getSensorValById(robot, 'distL'),
|
|||
|
distR = getSensorValById(robot, 'distR');
|
|||
|
|
|||
|
robot.rotate(robot, +0.005);
|
|||
|
robot.drive(robot, 0.0005);
|
|||
|
};
|
|||
|
|
|||
|
function plotSensor(context, x = this.x, y = this.y) {
|
|||
|
context.beginPath();
|
|||
|
context.arc(x + this.getWidth()/2,
|
|||
|
y + this.getHeight()/2,
|
|||
|
this.getWidth()/2, 0, 2*Math.PI);
|
|||
|
context.closePath();
|
|||
|
context.fillStyle = 'black';
|
|||
|
context.strokeStyle = 'black';
|
|||
|
context.fill();
|
|||
|
context.stroke();
|
|||
|
}
|
|||
|
|
|||
|
function plotRobot(context,
|
|||
|
xTopLeft = this.body.position.x,
|
|||
|
yTopLeft = this.body.position.y) {
|
|||
|
var x, y, scale, angle, i, half, full,
|
|||
|
rSize = simInfo.robotSize;
|
|||
|
const showInternalEdges = false;
|
|||
|
|
|||
|
if (context.canvas.id == "bayLemming") {
|
|||
|
scale = simInfo.bayScale;
|
|||
|
half = Math.floor(rSize/2*scale);
|
|||
|
full = half * 2;
|
|||
|
x = xTopLeft + full;
|
|||
|
y = yTopLeft + full;
|
|||
|
angle = -Math.PI / 2;
|
|||
|
} else {
|
|||
|
scale = 1;
|
|||
|
half = Math.floor(rSize/2*scale);
|
|||
|
full = half * 2;
|
|||
|
x = xTopLeft;
|
|||
|
y = yTopLeft;
|
|||
|
angle = this.body.angle;
|
|||
|
}
|
|||
|
context.save();
|
|||
|
context.translate(x, y);
|
|||
|
context.rotate(angle);
|
|||
|
|
|||
|
if (context.canvas.id == "arenaLemming") {
|
|||
|
// draw into world canvas without transformations,
|
|||
|
// because MatterJS thinks in world coords...
|
|||
|
context.restore();
|
|||
|
|
|||
|
const body = this.body;
|
|||
|
// handle compound parts
|
|||
|
|
|||
|
context.beginPath();
|
|||
|
for (k = body.parts.length > 1 ? 1 : 0; k < body.parts.length; k++) {
|
|||
|
part = body.parts[k];
|
|||
|
context.moveTo(part.vertices[0].x,
|
|||
|
part.vertices[0].y);
|
|||
|
for (j = 1; j < part.vertices.length; j++) {
|
|||
|
if (!part.vertices[j - 1].isInternal || showInternalEdges) {
|
|||
|
context.lineTo(part.vertices[j].x,
|
|||
|
part.vertices[j].y);
|
|||
|
} else {
|
|||
|
context.moveTo(part.vertices[j].x,
|
|||
|
part.vertices[j].y);
|
|||
|
}
|
|||
|
|
|||
|
if (part.vertices[j].isInternal && !showInternalEdges) {
|
|||
|
context.moveTo(part.vertices[(j + 1) % part.vertices.length].x,
|
|||
|
part.vertices[(j + 1) % part.vertices.length].y);
|
|||
|
}
|
|||
|
}
|
|||
|
context.lineTo(part.vertices[0].x,
|
|||
|
part.vertices[0].y);
|
|||
|
}
|
|||
|
context.strokeStyle = convrgb(body.color);
|
|||
|
context.lineWidth = 1.5;
|
|||
|
context.stroke();
|
|||
|
|
|||
|
// to draw the rest, rotate & translate again
|
|||
|
context.save();
|
|||
|
context.translate(x, y);
|
|||
|
context.rotate(angle);
|
|||
|
}
|
|||
|
|
|||
|
// Plot sensor positions into world canvas.
|
|||
|
if (context.canvas.id == "arenaLemming") {
|
|||
|
for (ss = 0; ss < this.info.sensors.length; ++ss) {
|
|||
|
context.beginPath();
|
|||
|
context.arc(full * Math.cos(this.info.sensors[ss].attachAngle),
|
|||
|
full * Math.sin(this.info.sensors[ss].attachAngle),
|
|||
|
scale, 0, 2*Math.PI);
|
|||
|
context.closePath();
|
|||
|
context.fillStyle = 'black';
|
|||
|
context.strokeStyle = 'black';
|
|||
|
context.fill();
|
|||
|
context.stroke();
|
|||
|
}
|
|||
|
}
|
|||
|
context.restore();
|
|||
|
}
|
|||
|
|
|||
|
function simStep() {
|
|||
|
// advance simulation by one step (except MatterJS engine's physics)
|
|||
|
if (simInfo.curSteps < simInfo.maxSteps) {
|
|||
|
repaintBay();
|
|||
|
drawBoard();
|
|||
|
for (var rr = 0; rr < robots.length; ++rr) {
|
|||
|
robotUpdateSensors(robots[rr]);
|
|||
|
robotMove(robots[rr]);
|
|||
|
// To enable selection by clicking (via mouse.js/graphics.js),
|
|||
|
// the position on the canvas needs to be defined in (x, y):
|
|||
|
const rSize = simInfo.robotSize;
|
|||
|
robots[rr].x = robots[rr].body.position.x - rSize;
|
|||
|
robots[rr].y = robots[rr].body.position.y - rSize;
|
|||
|
}
|
|||
|
// count and display number of steps
|
|||
|
simInfo.curSteps += 1;
|
|||
|
document.getElementById("SimStepLabel").innerHTML =
|
|||
|
padnumber(simInfo.curSteps, 5) +
|
|||
|
' of ' +
|
|||
|
padnumber(simInfo.maxSteps, 5);
|
|||
|
}
|
|||
|
else {
|
|||
|
toggleSimulation();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function drawBoard() {
|
|||
|
var context = document.getElementById('arenaLemming').getContext('2d');
|
|||
|
context.fillStyle = "#444444";
|
|||
|
context.fillRect(0, 0, simInfo.width, simInfo.height);
|
|||
|
|
|||
|
// draw objects within world
|
|||
|
const Composite = Matter.Composite,
|
|||
|
bodies = Composite.allBodies(simInfo.world);
|
|||
|
|
|||
|
for (var bb = 0; bb < bodies.length; bb += 1) {
|
|||
|
var vertices = bodies[bb].vertices,
|
|||
|
vv;
|
|||
|
|
|||
|
// draw all non-robot bodies here (walls and boxes)
|
|||
|
// don't draw robot's bodies here; they're drawn in plotRobot()
|
|||
|
if (bodies[bb].role != 'robot') {
|
|||
|
context.beginPath();
|
|||
|
context.moveTo(vertices[0].x, vertices[0].y);
|
|||
|
for (vv = 1; vv < vertices.length; vv += 1) {
|
|||
|
context.lineTo(vertices[vv].x, vertices[vv].y);
|
|||
|
}
|
|||
|
if (bodies[bb].color) {
|
|||
|
context.strokeStyle = convrgb(bodies[bb].color);
|
|||
|
context.closePath();
|
|||
|
context.stroke();
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
context.lineWidth = 1;
|
|||
|
|
|||
|
// draw all robots
|
|||
|
for (var rr = 0; rr < robots.length; ++rr) {
|
|||
|
robots[rr].plotRobot(context);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function repaintBay() {
|
|||
|
// update inset canvas showing information about selected robot
|
|||
|
const robotBay = document.getElementById('bayLemming'),
|
|||
|
context = robotBay.getContext('2d');
|
|||
|
context.clearRect(0, 0, robotBay.width, robotBay.height);
|
|||
|
simInfo.bayRobot.plotRobot(context, 10, 10);
|
|||
|
for (var ss = 0; ss < sensors.length; ss++) {
|
|||
|
sensors[ss].plotSensor(context);
|
|||
|
}
|
|||
|
|
|||
|
// print sensor values of selected robot next to canvas
|
|||
|
if (!(simInfo.curSteps % 5)) { // update slow enough to read
|
|||
|
var sensorString = '';
|
|||
|
const rsensors = simInfo.bayRobot.sensors;
|
|||
|
for (ss = 0; ss < rsensors.length; ss++) {
|
|||
|
sensorString += '<br> id \'' + rsensors[ss].id + '\': ' +
|
|||
|
padnumber(rsensors[ss].value, 2);
|
|||
|
}
|
|||
|
document.getElementById('SensorLabel').innerHTML = sensorString;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function setRobotNumber(newValue) {
|
|||
|
var n;
|
|||
|
while (robots.length > newValue) {
|
|||
|
n = robots.length - 1;
|
|||
|
Matter.World.remove(simInfo.world, robots[n].body);
|
|||
|
robots[n] = null;
|
|||
|
robots.length = n;
|
|||
|
}
|
|||
|
|
|||
|
while (robots.length < newValue) {
|
|||
|
if (newValue > RobotInfo.length) {
|
|||
|
console.warn('You request '+newValue+' robots, but only ' + RobotInfo.length +
|
|||
|
' are defined in RobotInfo!');
|
|||
|
toggleSimulation();
|
|||
|
return;
|
|||
|
}
|
|||
|
n = robots.length;
|
|||
|
robots[n] = makeInteractiveElement(new InstantiateRobot(RobotInfo[n]),
|
|||
|
document.getElementById("arenaLemming"));
|
|||
|
|
|||
|
robots[n].onDrop = function(robot, event) {
|
|||
|
robot.isDragged = false;
|
|||
|
};
|
|||
|
|
|||
|
robots[n].onDrag = function(robot, event) {
|
|||
|
robot.isDragged = true;
|
|||
|
loadBay(robot);
|
|||
|
return true;
|
|||
|
};
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
function padnumber(number, size) {
|
|||
|
if (number == Infinity) {
|
|||
|
return 'inf';
|
|||
|
}
|
|||
|
const s = "000000" + number;
|
|||
|
return s.substr(s.length - size);
|
|||
|
}
|
|||
|
|
|||
|
function format(number) {
|
|||
|
// prevent HTML elements to jump around at sign flips etc
|
|||
|
return (number >= 0 ? '+' : '−') + Math.abs(number).toFixed(1);
|
|||
|
}
|
|||
|
|
|||
|
function toggleSimulation() {
|
|||
|
simInfo.doContinue = !simInfo.doContinue;
|
|||
|
if (simInfo.doContinue) {
|
|||
|
Matter.Runner.start(simInfo.runner, simInfo.engine);
|
|||
|
}
|
|||
|
else {
|
|||
|
Matter.Runner.stop(simInfo.runner);
|
|||
|
}
|
|||
|
}
|
|||
|
|