Cleaned up map rendering code into modules.
This commit is contained in:
parent
9c0d588543
commit
9b6eb7b667
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-8" id="railSystemMapCanvasContainer">
|
||||
<canvas id="railSystemMapCanvas" height="600">
|
||||
<canvas id="railSystemMapCanvas" height="700">
|
||||
Your browser doesn't support canvas.
|
||||
</canvas>
|
||||
</div>
|
||||
|
@ -21,17 +21,20 @@
|
|||
|
||||
<script>
|
||||
import {RailSystem} from "src/api/railSystems";
|
||||
import {draw, initMap} from "src/render/mapRenderer";
|
||||
import {draw, initMap} from "src/map/mapRenderer";
|
||||
import SelectedComponentView from "components/rs/SelectedComponentView.vue";
|
||||
import {useQuasar} from "quasar";
|
||||
import AddComponentForm from "components/rs/add_component/AddComponentForm.vue";
|
||||
import { useRailSystemsStore } from "stores/railSystemsStore";
|
||||
import { registerComponentSelectionListener } from "src/map/mapEventListener";
|
||||
|
||||
export default {
|
||||
name: "MapView",
|
||||
components: { AddComponentForm, SelectedComponentView },
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
const quasar = useQuasar();
|
||||
return {quasar};
|
||||
return {quasar, rsStore};
|
||||
},
|
||||
props: {
|
||||
railSystem: {
|
||||
|
@ -48,9 +51,11 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
initMap(this.railSystem);
|
||||
registerComponentSelectionListener("addComponentFormHide", () => this.addComponent.visible = false);
|
||||
},
|
||||
updated() {
|
||||
initMap(this.railSystem);
|
||||
registerComponentSelectionListener("addComponentFormHide", () => this.addComponent.visible = false);
|
||||
},
|
||||
watch: {
|
||||
railSystem: {
|
||||
|
@ -58,6 +63,11 @@ export default {
|
|||
draw();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
'addComponent.visible'(newValue) { // Deselect all components when the user opens the "Add Component" form.
|
||||
if (newValue === true) {
|
||||
this.rsStore.selectedRailSystem.selectedComponents.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -15,10 +15,8 @@
|
|||
dense
|
||||
size="sm"
|
||||
:label="node.name"
|
||||
clickable
|
||||
/>
|
||||
</q-item-label>
|
||||
<q-item-label caption>Configuration #{{config.id}}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section v-if="!isConfigActive(config)" side top>
|
||||
<q-btn dense size="sm" color="positive" @click="setActiveSwitchConfig(config.id)">Set Active</q-btn>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { MAP_CANVAS } from "src/map/mapRenderer";
|
||||
|
||||
export function roundedRect(ctx, x, y, w, h, r) {
|
||||
if (w < 2 * r) r = w / 2;
|
||||
if (h < 2 * r) r = h / 2;
|
||||
|
@ -48,3 +50,15 @@ function distCompare(p0, a, b) {
|
|||
const distB = (p0.x-b.x)*(p0.x-b.x) + (p0.y-b.y)*(p0.y-b.y);
|
||||
return distA - distB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the point at which the user clicked on the map.
|
||||
* @param {MouseEvent} event
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
export function getMousePoint(event) {
|
||||
const rect = MAP_CANVAS.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
return new DOMPoint(x, y, 0, 1);
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
/*
|
||||
Helper functions to actually perform rendering of different components.
|
||||
*/
|
||||
|
||||
import { isComponentHovered, isComponentSelected, MAP_CTX, MAP_RAIL_SYSTEM } from "./mapRenderer";
|
||||
import { circle, roundedRect, sortPoints } from "./canvasUtils";
|
||||
import randomColor from "randomcolor";
|
||||
import { camGetTransform, camScale } from "src/map/mapCamera";
|
||||
|
||||
export function drawMap() {
|
||||
const worldTx = camGetTransform();
|
||||
MAP_CTX.setTransform(worldTx);
|
||||
drawSegments();
|
||||
drawNodeConnections(worldTx);
|
||||
drawComponents(worldTx);
|
||||
}
|
||||
|
||||
function drawSegments() {
|
||||
const segmentPoints = new Map();
|
||||
// Gather for each segment a set of points representing its bounds.
|
||||
MAP_RAIL_SYSTEM.segments.forEach(segment => segmentPoints.set(segment.id, []));
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.components.length; i++) {
|
||||
const c = MAP_RAIL_SYSTEM.components[i];
|
||||
if (c.type === "SEGMENT_BOUNDARY") {
|
||||
for (let j = 0; j < c.segments.length; j++) {
|
||||
segmentPoints.get(c.segments[j].id).push({ x: c.position.x, y: c.position.z });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort the points to make regular convex polygons.
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.segments.length; i++) {
|
||||
const unsortedPoints = segmentPoints.get(MAP_RAIL_SYSTEM.segments[i].id);
|
||||
segmentPoints.set(MAP_RAIL_SYSTEM.segments[i].id, sortPoints(unsortedPoints));
|
||||
}
|
||||
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.segments.length; i++) {
|
||||
const segment = MAP_RAIL_SYSTEM.segments[i];
|
||||
const color = randomColor({ luminosity: "light", format: "rgb", seed: segment.id });
|
||||
MAP_CTX.fillStyle = color;
|
||||
MAP_CTX.strokeStyle = color;
|
||||
MAP_CTX.lineWidth = 5;
|
||||
MAP_CTX.lineCap = "round";
|
||||
MAP_CTX.lineJoin = "round";
|
||||
MAP_CTX.font = "3px Sans-Serif";
|
||||
|
||||
const points = segmentPoints.get(segment.id);
|
||||
if (points.length === 0) continue;
|
||||
const avgPoint = { x: points[0].x, y: points[0].y };
|
||||
if (points.length === 1) {
|
||||
circle(MAP_CTX, points[0].x, points[0].y, 5);
|
||||
MAP_CTX.fill();
|
||||
} else {
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.moveTo(points[0].x, points[0].y);
|
||||
for (let j = 1; j < points.length; j++) {
|
||||
MAP_CTX.lineTo(points[j].x, points[j].y);
|
||||
avgPoint.x += points[j].x;
|
||||
avgPoint.y += points[j].y;
|
||||
}
|
||||
avgPoint.x /= points.length;
|
||||
avgPoint.y /= points.length;
|
||||
MAP_CTX.lineTo(points[0].x, points[0].y);
|
||||
MAP_CTX.fill();
|
||||
MAP_CTX.stroke();
|
||||
}
|
||||
|
||||
// Draw the segment name.
|
||||
MAP_CTX.fillStyle = randomColor({ luminosity: "dark", format: "rgb", seed: segment.id });
|
||||
MAP_CTX.fillText(segment.name, avgPoint.x, avgPoint.y);
|
||||
}
|
||||
}
|
||||
|
||||
function drawNodeConnections(worldTx) {
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.components.length; i++) {
|
||||
const c = MAP_RAIL_SYSTEM.components[i];
|
||||
if (c.connectedNodes !== undefined && c.connectedNodes !== null) {
|
||||
drawConnectedNodes(worldTx, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawComponents(worldTx) {
|
||||
// Draw switch configurations first
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.components.length; i++) {
|
||||
const c = MAP_RAIL_SYSTEM.components[i];
|
||||
if (c.type === "SWITCH") {
|
||||
drawSwitchConfigurations(worldTx, c);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.components.length; i++) {
|
||||
drawComponent(worldTx, MAP_RAIL_SYSTEM.components[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function drawComponent(worldTx, component) {
|
||||
const componentTransform = DOMMatrix.fromMatrix(worldTx);
|
||||
componentTransform.translateSelf(component.position.x, component.position.z, 0);
|
||||
const s = camScale();
|
||||
componentTransform.scaleSelf(1 / s, 1 / s, 1 / s);
|
||||
componentTransform.scaleSelf(20, 20, 20);
|
||||
MAP_CTX.setTransform(componentTransform);
|
||||
|
||||
if (component.type === "SIGNAL") {
|
||||
drawSignal(component);
|
||||
} else if (component.type === "SEGMENT_BOUNDARY") {
|
||||
drawSegmentBoundary();
|
||||
} else if (component.type === "SWITCH") {
|
||||
drawSwitch();
|
||||
} else if (component.type === "LABEL") {
|
||||
drawLabel(component);
|
||||
}
|
||||
|
||||
MAP_CTX.setTransform(componentTransform.translate(0.75, -0.75));
|
||||
if (component.online !== undefined && component.online !== null) {
|
||||
drawOnlineIndicator(component);
|
||||
}
|
||||
|
||||
MAP_CTX.setTransform(componentTransform);
|
||||
// Draw hovered status.
|
||||
if (isComponentHovered(component) || isComponentSelected(component)) {
|
||||
MAP_CTX.fillStyle = `rgba(255, 255, 0, 0.5)`;
|
||||
circle(MAP_CTX, 0, 0, 0.75);
|
||||
MAP_CTX.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawSignal(signal) {
|
||||
roundedRect(MAP_CTX, -0.3, -0.5, 0.6, 1, 0.25);
|
||||
MAP_CTX.fillStyle = "black";
|
||||
MAP_CTX.fill();
|
||||
if (signal.segment) {
|
||||
if (signal.segment.occupied === true) {
|
||||
MAP_CTX.fillStyle = `rgb(255, 0, 0)`;
|
||||
} else if (signal.segment.occupied === false) {
|
||||
MAP_CTX.fillStyle = `rgb(0, 255, 0)`;
|
||||
} else {
|
||||
MAP_CTX.fillStyle = `rgb(255, 255, 0)`;
|
||||
}
|
||||
} else {
|
||||
MAP_CTX.fillStyle = `rgb(0, 0, 255)`;
|
||||
}
|
||||
circle(MAP_CTX, 0, -0.2, 0.15);
|
||||
MAP_CTX.fill();
|
||||
}
|
||||
|
||||
function drawSegmentBoundary() {
|
||||
MAP_CTX.fillStyle = `rgb(150, 58, 224)`;
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.moveTo(0, -0.5);
|
||||
MAP_CTX.lineTo(-0.5, 0);
|
||||
MAP_CTX.lineTo(0, 0.5);
|
||||
MAP_CTX.lineTo(0.5, 0);
|
||||
MAP_CTX.lineTo(0, -0.5);
|
||||
MAP_CTX.fill();
|
||||
}
|
||||
|
||||
function drawSwitchConfigurations(worldTx, sw) {
|
||||
const tx = DOMMatrix.fromMatrix(worldTx);
|
||||
tx.translateSelf(sw.position.x, sw.position.z, 0);
|
||||
const s = camScale();
|
||||
tx.scaleSelf(1 / s, 1 / s, 1 / s);
|
||||
tx.scaleSelf(20, 20, 20);
|
||||
MAP_CTX.setTransform(tx);
|
||||
|
||||
for (let i = 0; i < sw.possibleConfigurations.length; i++) {
|
||||
const config = sw.possibleConfigurations[i];
|
||||
MAP_CTX.strokeStyle = randomColor({
|
||||
seed: config.id,
|
||||
format: "rgb",
|
||||
luminosity: "bright"
|
||||
});
|
||||
if (sw.activeConfiguration !== null && sw.activeConfiguration.id === config.id) {
|
||||
MAP_CTX.lineWidth = 0.6;
|
||||
} else {
|
||||
MAP_CTX.lineWidth = 0.3;
|
||||
}
|
||||
for (let j = 0; j < config.nodes.length; j++) {
|
||||
const node = config.nodes[j];
|
||||
const diff = {
|
||||
x: sw.position.x - node.position.x,
|
||||
y: sw.position.z - node.position.z
|
||||
};
|
||||
const mag = Math.sqrt(Math.pow(diff.x, 2) + Math.pow(diff.y, 2));
|
||||
diff.x = 3 * -diff.x / mag;
|
||||
diff.y = 3 * -diff.y / mag;
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.moveTo(0, 0);
|
||||
MAP_CTX.lineTo(diff.x, diff.y);
|
||||
MAP_CTX.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawSwitch() {
|
||||
MAP_CTX.fillStyle = `rgb(245, 188, 66)`;
|
||||
MAP_CTX.strokeStyle = `rgb(245, 188, 66)`;
|
||||
MAP_CTX.lineWidth = 0.2;
|
||||
circle(MAP_CTX, 0, 0.3, 0.2);
|
||||
MAP_CTX.fill();
|
||||
circle(MAP_CTX, -0.3, -0.3, 0.2);
|
||||
MAP_CTX.fill();
|
||||
circle(MAP_CTX, 0.3, -0.3, 0.2);
|
||||
MAP_CTX.fill();
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.moveTo(0, 0.3);
|
||||
MAP_CTX.lineTo(0, 0);
|
||||
MAP_CTX.lineTo(0.3, -0.3);
|
||||
MAP_CTX.moveTo(0, 0);
|
||||
MAP_CTX.lineTo(-0.3, -0.3);
|
||||
MAP_CTX.stroke();
|
||||
}
|
||||
|
||||
function drawLabel(lbl) {
|
||||
MAP_CTX.fillStyle = "black";
|
||||
circle(MAP_CTX, 0, 0, 0.1);
|
||||
MAP_CTX.fill();
|
||||
MAP_CTX.strokeStyle = "black";
|
||||
MAP_CTX.font = "0.5px Sans-Serif";
|
||||
MAP_CTX.fillText(lbl.text, 0.1, -0.2);
|
||||
}
|
||||
|
||||
function drawOnlineIndicator(component) {
|
||||
MAP_CTX.lineWidth = 0.1;
|
||||
if (component.online) {
|
||||
MAP_CTX.fillStyle = `rgba(52, 174, 235, 128)`;
|
||||
MAP_CTX.strokeStyle = `rgba(52, 174, 235, 128)`;
|
||||
} else {
|
||||
MAP_CTX.fillStyle = `rgba(153, 153, 153, 128)`;
|
||||
MAP_CTX.strokeStyle = `rgba(153, 153, 153, 128)`;
|
||||
}
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.arc(0, 0.2, 0.125, 0, Math.PI * 2);
|
||||
MAP_CTX.fill();
|
||||
for (let r = 0; r < 3; r++) {
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.arc(0, 0, 0.1 + 0.2 * r, 7 * Math.PI / 6, 11 * Math.PI / 6);
|
||||
MAP_CTX.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function drawConnectedNodes(worldTx, component) {
|
||||
const s = camScale();
|
||||
MAP_CTX.lineWidth = 5 / s;
|
||||
MAP_CTX.strokeStyle = "black";
|
||||
for (let i = 0; i < component.connectedNodes.length; i++) {
|
||||
const node = component.connectedNodes[i];
|
||||
MAP_CTX.beginPath();
|
||||
MAP_CTX.moveTo(component.position.x, component.position.z);
|
||||
MAP_CTX.lineTo(node.position.x, node.position.z);
|
||||
MAP_CTX.stroke();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
import { MAP_CANVAS } from "src/map/mapRenderer";
|
||||
|
||||
const SCALE_VALUES = [0.01, 0.1, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 16.0, 20.0, 30.0, 45.0, 60.0, 80.0, 100.0];
|
||||
const SCALE_INDEX_NORMAL = 7;
|
||||
|
||||
let scaleIndex = SCALE_INDEX_NORMAL;
|
||||
let translation = {x: 0, y: 0};
|
||||
let panOrigin = null;
|
||||
let panTranslation = null;
|
||||
let lastPanPoint = null;
|
||||
|
||||
/**
|
||||
* Resets the camera view to the default values.
|
||||
*/
|
||||
export function camResetView() {
|
||||
scaleIndex = SCALE_INDEX_NORMAL;
|
||||
translation.x = 0;
|
||||
translation.y = 0;
|
||||
panOrigin = null;
|
||||
panTranslation = null;
|
||||
lastPanPoint = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms in the camera, if possible.
|
||||
*/
|
||||
export function camZoomIn() {
|
||||
if (scaleIndex < SCALE_VALUES.length - 1) {
|
||||
scaleIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms out the camera, if possible.
|
||||
*/
|
||||
export function camZoomOut() {
|
||||
if (scaleIndex > 0) {
|
||||
scaleIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current zoom scale of the camera.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function camScale() {
|
||||
return SCALE_VALUES[scaleIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the camera transform that's used to map world coordinates to the canvas.
|
||||
* @return {DOMMatrix}
|
||||
*/
|
||||
export function camGetTransform() {
|
||||
const tx = new DOMMatrix();
|
||||
const canvasRect = MAP_CANVAS.getBoundingClientRect();
|
||||
tx.translateSelf(canvasRect.width / 2, canvasRect.height / 2, 0);
|
||||
const scale = SCALE_VALUES[scaleIndex];
|
||||
tx.scaleSelf(scale, scale, scale);
|
||||
tx.translateSelf(translation.x, translation.y, 0);
|
||||
if (panOrigin && panTranslation) {
|
||||
tx.translateSelf(panTranslation.x, panTranslation.y, 0);
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
export function camPanStart(point) {
|
||||
panOrigin = point;
|
||||
panTranslation = {x: 0, y: 0};
|
||||
}
|
||||
|
||||
export function camPanActive() {
|
||||
return panOrigin !== null;
|
||||
}
|
||||
|
||||
export function camPanNonzero() {
|
||||
return panTranslation !== null && (panTranslation.x !== 0 || panTranslation.y !== 0);
|
||||
}
|
||||
|
||||
export function camPanMove(point) {
|
||||
if (panOrigin) {
|
||||
lastPanPoint = point;
|
||||
const scale = SCALE_VALUES[scaleIndex];
|
||||
const dx = point.x - panOrigin.x;
|
||||
const dy = point.y - panOrigin.y;
|
||||
panTranslation = {x: dx / scale, y: dy / scale};
|
||||
}
|
||||
}
|
||||
|
||||
export function camPanFinish() {
|
||||
if (panTranslation) {
|
||||
translation.x += panTranslation.x;
|
||||
translation.y += panTranslation.y;
|
||||
}
|
||||
panOrigin = null;
|
||||
panTranslation = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a point on the map coordinates to world coordinates.
|
||||
* @param {DOMPoint} point
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
export function camTransformMapToWorld(point) {
|
||||
return camGetTransform().invertSelf().transformPoint(point);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a point in the world to map coordinates.
|
||||
* @param {DOMPoint} point
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
export function camTransformWorldToMap(point) {
|
||||
return camGetTransform().transformPoint(point);
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
import { draw, MAP_COMPONENTS_HOVERED, MAP_CANVAS, MAP_RAIL_SYSTEM } from "src/map/mapRenderer";
|
||||
import {
|
||||
camPanActive,
|
||||
camPanFinish,
|
||||
camPanMove,
|
||||
camPanNonzero,
|
||||
camPanStart, camResetView, camTransformWorldToMap,
|
||||
camZoomIn,
|
||||
camZoomOut
|
||||
} from "src/map/mapCamera";
|
||||
import { getMousePoint } from "src/map/canvasUtils";
|
||||
|
||||
const HOVER_RADIUS = 10;
|
||||
|
||||
export let LAST_MOUSE_POINT = null;
|
||||
|
||||
const componentSelectionListeners = new Map();
|
||||
|
||||
/**
|
||||
* Initializes all event listeners for the map controls.
|
||||
*/
|
||||
export function initListeners() {
|
||||
registerListener(MAP_CANVAS, "wheel", onMouseWheel);
|
||||
registerListener(MAP_CANVAS, "mousedown", onMouseDown);
|
||||
registerListener(MAP_CANVAS, "mouseup", onMouseUp);
|
||||
registerListener(MAP_CANVAS, "mousemove", onMouseMove);
|
||||
registerListener(window, "keydown", onKeyDown);
|
||||
}
|
||||
|
||||
function registerListener(element, type, callback) {
|
||||
element.removeEventListener(type, callback);
|
||||
element.addEventListener(type, callback);
|
||||
}
|
||||
|
||||
export function registerComponentSelectionListener(name, callback) {
|
||||
componentSelectionListeners.set(name, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse scroll wheel. This is used to control camera zoom.
|
||||
* @param {WheelEvent} event
|
||||
*/
|
||||
function onMouseWheel(event) {
|
||||
if (!event.shiftKey) return;
|
||||
const s = event.deltaY;
|
||||
if (s < 0) {
|
||||
camZoomIn();
|
||||
} else if (s > 0) {
|
||||
camZoomOut();
|
||||
}
|
||||
draw();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse down clicks. We only use this to start tracking panning.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function onMouseDown(event) {
|
||||
const p = getMousePoint(event);
|
||||
if (event.shiftKey) {
|
||||
camPanStart({ x: p.x, y: p.y });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse up events. This means we do the following:
|
||||
* - Finish any camera panning, if it was active.
|
||||
* - Select any components that the mouse is close enough to.
|
||||
* - If no components were selected, clear the list of selected components.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function onMouseUp(event) {
|
||||
const finishingDrag = camPanNonzero();
|
||||
camPanFinish();
|
||||
if (MAP_COMPONENTS_HOVERED.length > 0) {
|
||||
if (!event.shiftKey) {// If the user isn't holding SHIFT, clear the set of selected components first.
|
||||
MAP_RAIL_SYSTEM.selectedComponents.length = 0;
|
||||
}
|
||||
// If the user is clicking on a component that's already selected, deselect it.
|
||||
for (let i = 0; i < MAP_COMPONENTS_HOVERED.length; i++) {
|
||||
const hoveredComponent = MAP_COMPONENTS_HOVERED[i];
|
||||
const idx = MAP_RAIL_SYSTEM.selectedComponents.findIndex(c => c.id === hoveredComponent.id);
|
||||
if (idx > -1) {
|
||||
MAP_RAIL_SYSTEM.selectedComponents.splice(idx, 1);
|
||||
} else {
|
||||
MAP_RAIL_SYSTEM.selectedComponents.push(hoveredComponent);
|
||||
}
|
||||
}
|
||||
componentSelectionListeners.forEach(callback => callback(MAP_RAIL_SYSTEM.selectedComponents));
|
||||
} else if (!finishingDrag) {
|
||||
MAP_RAIL_SYSTEM.selectedComponents.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle when the mouse moves.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function onMouseMove(event) {
|
||||
const p = getMousePoint(event);
|
||||
LAST_MOUSE_POINT = p;
|
||||
if (camPanActive()) {
|
||||
if (event.shiftKey) {
|
||||
camPanMove({ x: p.x, y: p.y });
|
||||
} else {
|
||||
camPanFinish();
|
||||
}
|
||||
} else {
|
||||
MAP_COMPONENTS_HOVERED.length = 0;
|
||||
// Populate with list of hovered elements.
|
||||
for (let i = 0; i < MAP_RAIL_SYSTEM.components.length; i++) {
|
||||
const c = MAP_RAIL_SYSTEM.components[i];
|
||||
const componentPoint = new DOMPoint(c.position.x, c.position.z, 0, 1);
|
||||
const mapComponentPoint = camTransformWorldToMap(componentPoint);
|
||||
const dist2 = (p.x - mapComponentPoint.x) * (p.x - mapComponentPoint.x) + (p.y - mapComponentPoint.y) * (p.y - mapComponentPoint.y);
|
||||
if (dist2 < HOVER_RADIUS * HOVER_RADIUS) {
|
||||
MAP_COMPONENTS_HOVERED.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
function onKeyDown(event) {
|
||||
if (event.ctrlKey && event.code === "Space") {
|
||||
camResetView();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
This is the main script which organizes the drawing of the rail system map.
|
||||
*/
|
||||
|
||||
import { drawMap } from "./drawing";
|
||||
import { camResetView, camScale, camTransformMapToWorld } from "./mapCamera";
|
||||
import { initListeners, LAST_MOUSE_POINT } from "src/map/mapEventListener";
|
||||
|
||||
/**
|
||||
* The div containing the canvas element.
|
||||
* @type {Element | null}
|
||||
*/
|
||||
let mapContainerDiv = null;
|
||||
|
||||
/**
|
||||
* The canvas element.
|
||||
* @type {HTMLCanvasElement | null}
|
||||
*/
|
||||
export let MAP_CANVAS = null;
|
||||
|
||||
/**
|
||||
* The map's 2D rendering context.
|
||||
* @type {CanvasRenderingContext2D | null}
|
||||
*/
|
||||
export let MAP_CTX = null;
|
||||
|
||||
/**
|
||||
* The rail system that we're currently rendering.
|
||||
* @type {RailSystem | null}
|
||||
*/
|
||||
export let MAP_RAIL_SYSTEM = null;
|
||||
|
||||
/**
|
||||
* The set of components that are currently hovered over.
|
||||
* @type {Object[]}
|
||||
*/
|
||||
export const MAP_COMPONENTS_HOVERED = [];
|
||||
|
||||
export function initMap(rs) {
|
||||
MAP_RAIL_SYSTEM = rs;
|
||||
console.log("Initializing map for rail system: " + rs.name);
|
||||
camResetView();
|
||||
MAP_CANVAS = document.getElementById("railSystemMapCanvas");
|
||||
mapContainerDiv = document.getElementById("railSystemMapCanvasContainer");
|
||||
MAP_CTX = MAP_CANVAS?.getContext("2d");
|
||||
|
||||
initListeners();
|
||||
|
||||
// Do an initial draw.
|
||||
draw();
|
||||
}
|
||||
|
||||
export function draw() {
|
||||
if (!(MAP_CANVAS && MAP_RAIL_SYSTEM && MAP_RAIL_SYSTEM.components)) {
|
||||
console.warn("Attempted to draw map without canvas or railSystem.");
|
||||
return;
|
||||
}
|
||||
if (MAP_CANVAS.width !== mapContainerDiv.clientWidth) {
|
||||
MAP_CANVAS.width = mapContainerDiv.clientWidth;
|
||||
}
|
||||
if (MAP_CANVAS.height !== mapContainerDiv.clientHeight) {
|
||||
MAP_CANVAS.height = mapContainerDiv.clientHeight - 6;
|
||||
}
|
||||
const width = MAP_CANVAS.width;
|
||||
const height = MAP_CANVAS.height;
|
||||
MAP_CTX.resetTransform();
|
||||
MAP_CTX.fillStyle = `rgb(240, 240, 240)`;
|
||||
MAP_CTX.fillRect(0, 0, width, height);
|
||||
|
||||
drawMap();
|
||||
drawDebugInfo();
|
||||
}
|
||||
|
||||
function drawDebugInfo() {
|
||||
MAP_CTX.resetTransform();
|
||||
MAP_CTX.fillStyle = "black";
|
||||
MAP_CTX.strokeStyle = "black";
|
||||
MAP_CTX.font = "10px Sans-Serif";
|
||||
const lastWorldPoint = camTransformMapToWorld(LAST_MOUSE_POINT);
|
||||
const lines = [
|
||||
"Scale factor: " + camScale(),
|
||||
`(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`,
|
||||
`Components: ${MAP_RAIL_SYSTEM.components.length}`,
|
||||
`Hovered components: ${MAP_COMPONENTS_HOVERED.length}`
|
||||
];
|
||||
for (let i = 0; i < MAP_COMPONENTS_HOVERED.length; i++) {
|
||||
lines.push(" " + MAP_COMPONENTS_HOVERED[i].name);
|
||||
}
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
MAP_CTX.fillText(lines[i], 10, 20 + (i * 15));
|
||||
}
|
||||
}
|
||||
|
||||
export function isComponentHovered(component) {
|
||||
return MAP_COMPONENTS_HOVERED.some(c => c.id === component.id);
|
||||
}
|
||||
|
||||
export function isComponentSelected(component) {
|
||||
return MAP_RAIL_SYSTEM.selectedComponents.some(c => c.id === component.id);
|
||||
}
|
|
@ -1,250 +0,0 @@
|
|||
/*
|
||||
Helper functions to actually perform rendering of different components.
|
||||
*/
|
||||
|
||||
import { getScaleFactor, getWorldTransform, isComponentHovered, isComponentSelected } from "./mapRenderer";
|
||||
import { circle, roundedRect, sortPoints } from "./canvasUtils";
|
||||
import randomColor from "randomcolor";
|
||||
|
||||
export function drawMap(ctx, rs) {
|
||||
const worldTx = getWorldTransform();
|
||||
ctx.setTransform(worldTx);
|
||||
drawSegments(ctx, rs);
|
||||
drawNodeConnections(ctx, rs, worldTx);
|
||||
drawComponents(ctx, rs, worldTx);
|
||||
}
|
||||
|
||||
function drawSegments(ctx, rs) {
|
||||
const segmentPoints = new Map();
|
||||
// Gather for each segment a set of points representing its bounds.
|
||||
rs.segments.forEach(segment => segmentPoints.set(segment.id, []));
|
||||
for (let i = 0; i < rs.components.length; i++) {
|
||||
const c = rs.components[i];
|
||||
if (c.type === "SEGMENT_BOUNDARY") {
|
||||
for (let j = 0; j < c.segments.length; j++) {
|
||||
segmentPoints.get(c.segments[j].id).push({x: c.position.x, y: c.position.z});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort the points to make regular convex polygons.
|
||||
for (let i = 0; i < rs.segments.length; i++) {
|
||||
const unsortedPoints = segmentPoints.get(rs.segments[i].id);
|
||||
segmentPoints.set(rs.segments[i].id, sortPoints(unsortedPoints));
|
||||
}
|
||||
|
||||
for (let i = 0; i < rs.segments.length; i++) {
|
||||
const color = randomColor({ luminosity: 'light', format: 'rgb', seed: rs.segments[i].id });
|
||||
ctx.fillStyle = color;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 5;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.font = "3px Sans-Serif";
|
||||
|
||||
const points = segmentPoints.get(rs.segments[i].id);
|
||||
if (points.length === 0) continue;
|
||||
const avgPoint = {x: points[0].x, y: points[0].y};
|
||||
if (points.length === 1) {
|
||||
circle(ctx, points[0].x, points[0].y, 5);
|
||||
ctx.fill();
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0].x, points[0].y);
|
||||
for (let j = 1; j < points.length; j++) {
|
||||
ctx.lineTo(points[j].x, points[j].y);
|
||||
avgPoint.x += points[j].x;
|
||||
avgPoint.y += points[j].y;
|
||||
}
|
||||
avgPoint.x /= points.length;
|
||||
avgPoint.y /= points.length;
|
||||
ctx.lineTo(points[0].x, points[0].y);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw the segment name.
|
||||
ctx.fillStyle = randomColor({luminosity: 'dark', format: 'rgb', seed: rs.segments[i].id});
|
||||
ctx.fillText(rs.segments[i].name, avgPoint.x, avgPoint.y);
|
||||
}
|
||||
}
|
||||
|
||||
function drawNodeConnections(ctx, rs, worldTx) {
|
||||
for (let i = 0; i < rs.components.length; i++) {
|
||||
const c = rs.components[i];
|
||||
if (c.connectedNodes !== undefined && c.connectedNodes !== null) {
|
||||
drawConnectedNodes(ctx, worldTx, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawComponents(ctx, rs, worldTx) {
|
||||
// Draw switch configurations first
|
||||
for (let i = 0; i < rs.components.length; i++) {
|
||||
if (rs.components[i].type === "SWITCH") {
|
||||
drawSwitchConfigurations(ctx, worldTx, rs.components[i]);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < rs.components.length; i++) {
|
||||
drawComponent(ctx, worldTx, rs.components[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function drawComponent(ctx, worldTx, component) {
|
||||
const tx = DOMMatrix.fromMatrix(worldTx);
|
||||
tx.translateSelf(component.position.x, component.position.z, 0);
|
||||
const s = getScaleFactor();
|
||||
tx.scaleSelf(1/s, 1/s, 1/s);
|
||||
tx.scaleSelf(20, 20, 20);
|
||||
ctx.setTransform(tx);
|
||||
|
||||
if (component.type === "SIGNAL") {
|
||||
drawSignal(ctx, component);
|
||||
} else if (component.type === "SEGMENT_BOUNDARY") {
|
||||
drawSegmentBoundary(ctx, component);
|
||||
} else if (component.type === "SWITCH") {
|
||||
drawSwitch(ctx, component);
|
||||
} else if (component.type === "LABEL") {
|
||||
drawLabel(ctx, component);
|
||||
}
|
||||
|
||||
ctx.setTransform(tx.translate(0.75, -0.75));
|
||||
if (component.online !== undefined && component.online !== null) {
|
||||
drawOnlineIndicator(ctx, component);
|
||||
}
|
||||
|
||||
ctx.setTransform(tx);
|
||||
// Draw hovered status.
|
||||
if (isComponentHovered(component) || isComponentSelected(component)) {
|
||||
ctx.fillStyle = `rgba(255, 255, 0, 0.5)`;
|
||||
circle(ctx, 0, 0, 0.75);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawSignal(ctx, signal) {
|
||||
roundedRect(ctx, -0.3, -0.5, 0.6, 1, 0.25);
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fill();
|
||||
if (signal.segment) {
|
||||
if (signal.segment.occupied === true) {
|
||||
ctx.fillStyle = `rgb(255, 0, 0)`;
|
||||
} else if (signal.segment.occupied === false) {
|
||||
ctx.fillStyle = `rgb(0, 255, 0)`;
|
||||
} else {
|
||||
ctx.fillStyle = `rgb(255, 255, 0)`;
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = `rgb(0, 0, 255)`;
|
||||
}
|
||||
circle(ctx, 0, -0.2, 0.15);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawSegmentBoundary(ctx) {
|
||||
ctx.fillStyle = `rgb(150, 58, 224)`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -0.5);
|
||||
ctx.lineTo(-0.5, 0);
|
||||
ctx.lineTo(0, 0.5);
|
||||
ctx.lineTo(0.5, 0);
|
||||
ctx.lineTo(0, -0.5);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawSwitchConfigurations(ctx, worldTx, sw) {
|
||||
const tx = DOMMatrix.fromMatrix(worldTx);
|
||||
tx.translateSelf(sw.position.x, sw.position.z, 0);
|
||||
const s = getScaleFactor();
|
||||
tx.scaleSelf(1/s, 1/s, 1/s);
|
||||
tx.scaleSelf(20, 20, 20);
|
||||
ctx.setTransform(tx);
|
||||
|
||||
for (let i = 0; i < sw.possibleConfigurations.length; i++) {
|
||||
const config = sw.possibleConfigurations[i];
|
||||
ctx.strokeStyle = randomColor({
|
||||
seed: config.id,
|
||||
format: 'rgb',
|
||||
luminosity: 'bright'
|
||||
});
|
||||
if (sw.activeConfiguration !== null && sw.activeConfiguration.id === config.id) {
|
||||
ctx.lineWidth = 0.6;
|
||||
} else {
|
||||
ctx.lineWidth = 0.3;
|
||||
}
|
||||
for (let j = 0; j < config.nodes.length; j++) {
|
||||
const node = config.nodes[j];
|
||||
const diff = {
|
||||
x: sw.position.x - node.position.x,
|
||||
y: sw.position.z - node.position.z,
|
||||
};
|
||||
const mag = Math.sqrt(Math.pow(diff.x, 2) + Math.pow(diff.y, 2));
|
||||
diff.x = 3 * -diff.x / mag;
|
||||
diff.y = 3 * -diff.y / mag;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(diff.x, diff.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawSwitch(ctx, sw) {
|
||||
ctx.fillStyle = `rgb(245, 188, 66)`;
|
||||
ctx.strokeStyle = `rgb(245, 188, 66)`;
|
||||
ctx.lineWidth = 0.2;
|
||||
circle(ctx, 0, 0.3, 0.2);
|
||||
ctx.fill();
|
||||
circle(ctx, -0.3, -0.3, 0.2);
|
||||
ctx.fill();
|
||||
circle(ctx, 0.3, -0.3, 0.2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0.3);
|
||||
ctx.lineTo(0, 0);
|
||||
ctx.lineTo(0.3, -0.3);
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(-0.3, -0.3);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawLabel(ctx, lbl) {
|
||||
ctx.fillStyle = "black";
|
||||
circle(ctx, 0, 0, 0.1);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.font = "0.5px Sans-Serif";
|
||||
ctx.fillText(lbl.text, 0.1, -0.2);
|
||||
}
|
||||
|
||||
function drawOnlineIndicator(ctx, component) {
|
||||
ctx.lineWidth = 0.1;
|
||||
if (component.online) {
|
||||
ctx.fillStyle = `rgba(52, 174, 235, 128)`;
|
||||
ctx.strokeStyle = `rgba(52, 174, 235, 128)`;
|
||||
} else {
|
||||
ctx.fillStyle = `rgba(153, 153, 153, 128)`;
|
||||
ctx.strokeStyle = `rgba(153, 153, 153, 128)`;
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0.2, 0.125, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
for (let r = 0; r < 3; r++) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 0.1 + 0.2 * r, 7 * Math.PI / 6, 11 * Math.PI / 6);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
export function drawConnectedNodes(ctx, worldTx, component) {
|
||||
const s = getScaleFactor();
|
||||
ctx.lineWidth = 5 / s;
|
||||
ctx.strokeStyle = "black";
|
||||
for (let i = 0; i < component.connectedNodes.length; i++) {
|
||||
const node = component.connectedNodes[i];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(component.position.x, component.position.z);
|
||||
ctx.lineTo(node.position.x, node.position.z);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
/*
|
||||
This component is responsible for the rendering of a RailSystem in a 2d map
|
||||
view.
|
||||
*/
|
||||
|
||||
import { drawMap } from "./drawing";
|
||||
|
||||
const SCALE_VALUES = [0.01, 0.1, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 16.0, 20.0, 30.0, 45.0, 60.0, 80.0, 100.0];
|
||||
const SCALE_INDEX_NORMAL = 7;
|
||||
const HOVER_RADIUS = 10;
|
||||
|
||||
let mapContainerDiv = null;
|
||||
let mapCanvas = null;
|
||||
let railSystem = null;
|
||||
|
||||
let mapScaleIndex = SCALE_INDEX_NORMAL;
|
||||
let mapTranslation = {x: 0, y: 0};
|
||||
let mapDragOrigin = null;
|
||||
let mapDragTranslation = null;
|
||||
let lastMousePoint = new DOMPoint(0, 0, 0, 0);
|
||||
const hoveredElements = [];
|
||||
|
||||
export function initMap(rs) {
|
||||
railSystem = rs;
|
||||
console.log("Initializing map for rail system: " + rs.name);
|
||||
resetView();
|
||||
mapCanvas = document.getElementById("railSystemMapCanvas");
|
||||
mapContainerDiv = document.getElementById("railSystemMapCanvasContainer");
|
||||
mapCanvas.removeEventListener("wheel", onMouseWheel);
|
||||
mapCanvas.addEventListener("wheel", onMouseWheel);
|
||||
mapCanvas.removeEventListener("mousedown", onMouseDown);
|
||||
mapCanvas.addEventListener("mousedown", onMouseDown);
|
||||
mapCanvas.removeEventListener("mouseup", onMouseUp);
|
||||
mapCanvas.addEventListener("mouseup", onMouseUp);
|
||||
mapCanvas.removeEventListener("mousemove", onMouseMove);
|
||||
mapCanvas.addEventListener("mousemove", onMouseMove);
|
||||
|
||||
// Do an initial draw.
|
||||
draw();
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
mapTranslation.x = 0;
|
||||
mapTranslation.y = 0;
|
||||
mapDragOrigin = null;
|
||||
mapDragTranslation = null;
|
||||
lastMousePoint = new DOMPoint(0, 0, 0, 0);
|
||||
hoveredElements.length = 0;
|
||||
mapScaleIndex = SCALE_INDEX_NORMAL;
|
||||
}
|
||||
|
||||
export function draw() {
|
||||
if (!(mapCanvas && railSystem && railSystem.components)) {
|
||||
console.warn("Attempted to draw map without canvas or railSystem.");
|
||||
return;
|
||||
}
|
||||
const ctx = mapCanvas.getContext("2d");
|
||||
if (mapCanvas.width !== mapContainerDiv.clientWidth) {
|
||||
mapCanvas.width = mapContainerDiv.clientWidth;
|
||||
}
|
||||
if (mapCanvas.height !== mapContainerDiv.clientHeight) {
|
||||
mapCanvas.height = mapContainerDiv.clientHeight - 6;
|
||||
}
|
||||
const width = mapCanvas.width;
|
||||
const height = mapCanvas.height;
|
||||
ctx.resetTransform();
|
||||
ctx.fillStyle = `rgb(240, 240, 240)`;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
drawMap(ctx, railSystem);
|
||||
drawDebugInfo(ctx);
|
||||
}
|
||||
|
||||
function drawDebugInfo(ctx) {
|
||||
ctx.resetTransform();
|
||||
ctx.fillStyle = "black";
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.font = "10px Sans-Serif";
|
||||
const lastWorldPoint = mapPointToWorld(lastMousePoint);
|
||||
const lines = [
|
||||
"Scale factor: " + getScaleFactor(),
|
||||
`(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`,
|
||||
`Components: ${railSystem.components.length}`,
|
||||
`Hovered components: ${hoveredElements.length}`
|
||||
]
|
||||
for (let i = 0; i < hoveredElements.length; i++) {
|
||||
lines.push(" " + hoveredElements[i].name);
|
||||
}
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
ctx.fillText(lines[i], 10, 20 + (i * 15));
|
||||
}
|
||||
}
|
||||
|
||||
export function getScaleFactor() {
|
||||
return SCALE_VALUES[mapScaleIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a matrix that transforms world coordinates to canvas.
|
||||
* @returns {DOMMatrix}
|
||||
*/
|
||||
export function getWorldTransform() {
|
||||
const canvasRect = mapCanvas.getBoundingClientRect();
|
||||
const scale = getScaleFactor();
|
||||
const tx = new DOMMatrix();
|
||||
tx.translateSelf(canvasRect.width / 2, canvasRect.height / 2, 0);
|
||||
tx.scaleSelf(scale, scale, scale);
|
||||
tx.translateSelf(mapTranslation.x, mapTranslation.y, 0);
|
||||
if (mapDragOrigin !== null && mapDragTranslation !== null) {
|
||||
tx.translateSelf(mapDragTranslation.x, mapDragTranslation.y, 0);
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
export function isComponentHovered(component) {
|
||||
return hoveredElements.some(c => c.id === component.id);
|
||||
}
|
||||
|
||||
export function isComponentSelected(component) {
|
||||
return railSystem.selectedComponents.some(c => c.id === component.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a point on the map coordinates to world coordinates.
|
||||
* @param {DOMPoint} p
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
export function mapPointToWorld(p) {
|
||||
return getWorldTransform().invertSelf().transformPoint(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a point in the world to map coordinates.
|
||||
* @param {DOMPoint} p
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
export function worldPointToMap(p) {
|
||||
return getWorldTransform().transformPoint(p);
|
||||
}
|
||||
|
||||
/*
|
||||
EVENT HANDLING
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {WheelEvent} event
|
||||
*/
|
||||
function onMouseWheel(event) {
|
||||
const s = event.deltaY;
|
||||
if (s > 0) {
|
||||
mapScaleIndex = Math.max(0, mapScaleIndex - 1);
|
||||
} else if (s < 0) {
|
||||
mapScaleIndex = Math.min(SCALE_VALUES.length - 1, mapScaleIndex + 1);
|
||||
}
|
||||
draw();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function onMouseDown(event) {
|
||||
const p = getMousePoint(event);
|
||||
mapDragOrigin = {x: p.x, y: p.y};
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
let finishedDrag = false;
|
||||
if (mapDragTranslation !== null) {
|
||||
mapTranslation.x += mapDragTranslation.x;
|
||||
mapTranslation.y += mapDragTranslation.y;
|
||||
finishedDrag = true;
|
||||
}
|
||||
if (hoveredElements.length > 0) {
|
||||
railSystem.selectedComponents.length = 0;
|
||||
railSystem.selectedComponents.push(...hoveredElements);
|
||||
} else if (!finishedDrag) {
|
||||
railSystem.selectedComponents.length = 0;
|
||||
}
|
||||
mapDragOrigin = null;
|
||||
mapDragTranslation = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function onMouseMove(event) {
|
||||
const p = getMousePoint(event);
|
||||
lastMousePoint = p;
|
||||
if (mapDragOrigin !== null) {
|
||||
const scale = getScaleFactor();
|
||||
const dx = p.x - mapDragOrigin.x;
|
||||
const dy = p.y - mapDragOrigin.y;
|
||||
mapDragTranslation = {x: dx / scale, y: dy / scale};
|
||||
} else {
|
||||
hoveredElements.length = 0;
|
||||
// Populate with list of hovered elements.
|
||||
for (let i = 0; i < railSystem.components.length; i++) {
|
||||
const c = railSystem.components[i];
|
||||
const componentPoint = new DOMPoint(c.position.x, c.position.z, 0, 1);
|
||||
const mapComponentPoint = worldPointToMap(componentPoint);
|
||||
const dist2 = (p.x - mapComponentPoint.x) * (p.x - mapComponentPoint.x) + (p.y - mapComponentPoint.y) * (p.y - mapComponentPoint.y);
|
||||
if (dist2 < HOVER_RADIUS * HOVER_RADIUS) {
|
||||
hoveredElements.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the point at which the user clicked on the map.
|
||||
* @param {MouseEvent} event
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
function getMousePoint(event) {
|
||||
const rect = mapCanvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
return new DOMPoint(x, y, 0, 1);
|
||||
}
|
Loading…
Reference in New Issue