Move BlueMapVue and BlueMapWeb into the main repo

This commit is contained in:
Lukas Rieger (Blue) 2022-12-18 17:07:19 +01:00
parent 6642c11742
commit bd52fb6eb7
No known key found for this signature in database
GPG Key ID: 2D09EC5ED2687FF2
70 changed files with 15852 additions and 1 deletions

@ -1 +0,0 @@
Subproject commit b5cd8989340198464e10af891afa8e4a13565dc5

View File

@ -0,0 +1,25 @@
name: Node.js CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: build/*

View File

@ -0,0 +1,9 @@
.classpath
.project
.idea
build
doc
node_modules
public/js

View File

@ -0,0 +1,13 @@
.classpath
.project
.idea
build
doc
node_modules
public/js
###### Below is additionally to .gitignore
.github
public

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) Blue <https://www.bluecolored.de>
Copyright (c) contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "bluemap",
"version": "1.1.0",
"description": "A library to load and display Minecraft maps generated by BlueMap.",
"repository": {
"type": "git",
"url": "https://github.com/BlueMap-Minecraft/BlueMapWeb.git"
},
"keywords": [
"minecraft",
"minecraft-mod",
"minecraft-plugin",
"threejs",
"webgl",
"bluemap"
],
"author": "Lukas Rieger <contact@bluecolored.de> (https://bluecolored.de/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/BlueMap-Minecraft/BlueMap/issues"
},
"homepage": "https://bluecolo.red/bluemap",
"dependencies": {
"hammerjs": "~2.0.8",
"three": "~0.123.0"
},
"devDependencies": {
"@babel/core": "~7.11.6",
"@babel/plugin-proposal-class-properties": "~7.10.4",
"@babel/polyfill": "~7.11.5",
"@babel/preset-env": "~7.11.5",
"@rollup/plugin-babel": "~5.2.1",
"@types/babel__core": "~7.1.9",
"concurrently": "~5.3.0",
"copyfiles": "~2.3.0",
"http-server": "~0.12.3",
"rollup": "~2.28.2",
"rollup-plugin-terser": "~7.0.2"
},
"scripts": {
"build": "rollup -c"
}
}

View File

@ -0,0 +1,44 @@
import babel from "@rollup/plugin-babel";
import { terser } from "rollup-plugin-terser";
const babelrc = {
babelHelpers: 'bundled',
presets: [
['@babel/preset-env', {
targets: "> 0.25%, not dead",
bugfixes: true,
loose: true
}]
],
plugins: [
['@babel/plugin-proposal-class-properties', {
loose: true
}]
]
};
export default [
{
input: 'src/BlueMap.js',
external: [ 'three', 'hammerjs' ],
plugins: [
babel( {
compact: false,
babelrc: false,
...babelrc
} )
],
output: [
{
format: 'umd',
name: 'BlueMap',
file: 'build/bluemap.js',
indent: '\t',
globals: {
three: 'THREE',
hammerjs: 'Hammer',
}
}
],
}
];

View File

@ -0,0 +1,77 @@
import babel from "@rollup/plugin-babel";
import { terser } from "rollup-plugin-terser";
const babelrc = {
babelHelpers: 'bundled',
presets: [
['@babel/preset-env', {
targets: "> 0.25%, not dead",
bugfixes: true,
loose: true
}]
],
plugins: [
['@babel/plugin-proposal-class-properties', {
loose: true
}]
]
};
export default [
{
input: 'src/BlueMap.js',
external: [ 'three', 'hammerjs' ],
plugins: [
babel( {
compact: false,
babelrc: false,
...babelrc
} )
],
output: [
{
format: 'umd',
name: 'BlueMap',
file: 'build/bluemap.js',
indent: '\t',
globals: {
three: 'THREE',
hammerjs: 'Hammer',
}
}
],
},
{
input: 'src/BlueMap.js',
external: [ 'three', 'hammerjs' ],
plugins: [
babel( {
babelrc: false,
...babelrc
} ),
terser(),
],
output: [
{
format: 'umd',
name: 'BlueMap',
file: 'build/bluemap.min.js',
globals: {
three: 'THREE',
hammerjs: 'Hammer',
}
}
]
},
{
input: 'src/BlueMap.js',
external: [ 'three', 'hammerjs' ],
plugins: [],
output: [
{
format: 'esm',
file: 'build/bluemap.module.js'
}
]
}
];

View File

@ -0,0 +1,67 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {Object3D} from "three";
export * from "./MapViewer";
export * from "./map/Map";
export * from "./map/Tile";
export * from "./map/TileLoader";
export * from "./map/TileManager";
export * from "./map/TileMap";
export * from "./markers/ExtrudeMarker";
export * from "./markers/HtmlMarker";
export * from "./markers/LineMarker";
export * from "./markers/Marker";
export * from "./markers/MarkerManager";
export * from "./markers/MarkerSet";
export * from "./markers/PlayerMarkerSet";
export * from "./markers/ObjectMarker";
export * from "./markers/PlayerMarker";
export * from "./markers/PoiMarker";
export * from "./markers/ShapeMarker";
export * from "./controls/map/MapControls";
export * from "./controls/freeflight/FreeFlightControls";
export * from "./util/CombinedCamera";
export * from "./util/Utils";
/**
* @param event {object}
* @return {boolean} - whether the event has been consumed (true) or not (false)
*/
Object3D.prototype.onClick = function(event) {
if (this.parent){
if (!Array.isArray(event.eventStack)) event.eventStack = [];
event.eventStack.push(this);
return this.parent.onClick(event);asd
}
return false;
};

View File

@ -0,0 +1,492 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {Color, PerspectiveCamera, Raycaster, Scene, Vector2, Vector3, WebGLRenderer} from "three";
import {Map} from "./map/Map";
import {SkyboxScene} from "./skybox/SkyboxScene";
import {ControlsManager} from "./controls/ControlsManager";
import Stats from "./util/Stats";
import {alert, dispatchEvent, elementOffset, generateCacheHash, htmlToElement, softClamp} from "./util/Utils";
import {TileManager} from "./map/TileManager";
import {HIRES_VERTEX_SHADER} from "./map/hires/HiresVertexShader";
import {HIRES_FRAGMENT_SHADER} from "./map/hires/HiresFragmentShader";
import {LOWRES_VERTEX_SHADER} from "./map/lowres/LowresVertexShader";
import {LOWRES_FRAGMENT_SHADER} from "./map/lowres/LowresFragmentShader";
import {CombinedCamera} from "./util/CombinedCamera";
import {CSS2DRenderer} from "./util/CSS2DRenderer";
import {MarkerSet} from "./markers/MarkerSet";
export class MapViewer {
/**
* @param element {Element}
* @param events {EventTarget}
*/
constructor(element, events = element) {
Object.defineProperty( this, 'isMapViewer', { value: true } );
this.rootElement = element;
this.events = events;
this.data = {
map: null,
camera: null,
controlsManager: null,
uniforms: {
sunlightStrength: { value: 1 },
ambientLight: { value: 0 },
skyColor: { value: new Color(0.5, 0.5, 1) },
hiresTileMap: {
value: {
map: null,
size: TileManager.tileMapSize,
scale: new Vector2(1, 1),
translate: new Vector2(),
pos: new Vector2(),
}
}
},
superSampling: 1,
loadedCenter: new Vector2(0, 0),
loadedHiresViewDistance: 200,
loadedLowresViewDistance: 2000,
}
this.tileCacheHash = generateCacheHash();
this.stats = new Stats();
this.stats.hide();
// renderer
this.renderer = new WebGLRenderer({
antialias: true,
sortObjects: true,
preserveDrawingBuffer: true,
logarithmicDepthBuffer: true,
});
this.renderer.autoClear = false;
this.renderer.uniforms = this.data.uniforms;
// CSS2D renderer
this.css2dRenderer = new CSS2DRenderer(this.events);
this.skyboxScene = new SkyboxScene(this.data.uniforms);
this.camera = new CombinedCamera(75, 1, 0.1, 10000, 0);
this.skyboxCamera = new PerspectiveCamera(75, 1, 0.1, 10000);
this.skyboxCamera.updateProjectionMatrix();
this.controlsManager = new ControlsManager(this, this.camera);
this.raycaster = new Raycaster();
this.raycaster.layers.enableAll();
this.raycaster.params.Line2 = {threshold: 20}
/** @type {Map} */
this.map = null;
this.markers = new MarkerSet("bm-root");
this.lastFrame = 0;
// initialize
this.initializeRootElement();
// handle window resizes
window.addEventListener("resize", this.handleContainerResize);
// start render-loop
requestAnimationFrame(this.renderLoop);
}
/**
* Initializes the root-element
*/
initializeRootElement() {
this.rootElement.innerHTML = "";
let outerDiv = htmlToElement(`<div style="position: relative; width: 100%; height: 100%; overflow: hidden;"></div>`);
this.rootElement.appendChild(outerDiv);
// 3d-canvas
outerDiv.appendChild(this.renderer.domElement);
// html-markers
this.css2dRenderer.domElement.style.position = 'absolute';
this.css2dRenderer.domElement.style.top = '0';
this.css2dRenderer.domElement.style.left = '0';
this.css2dRenderer.domElement.style.pointerEvents = 'none';
outerDiv.appendChild(this.css2dRenderer.domElement);
// performance monitor
outerDiv.appendChild(this.stats.dom);
this.handleContainerResize();
}
/**
* Updates the render-resolution and aspect ratio based on the size of the root-element
*/
handleContainerResize = () => {
this.renderer.setSize(this.rootElement.clientWidth, this.rootElement.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio * this.superSampling);
this.css2dRenderer.setSize(this.rootElement.clientWidth, this.rootElement.clientHeight);
this.camera.aspect = this.rootElement.clientWidth / this.rootElement.clientHeight;
this.camera.updateProjectionMatrix();
};
/**
* Triggers an interaction on the screen (map), e.g. a mouse-click.
*
* This will first attempt to invoke the onClick() method on the Object3D (e.g. Markers) that has been clicked.
* And if none of those consumed the event, it will fire a <code>bluemapMapInteraction</code> event.
*
* @param screenPosition {Vector2} - Clicked position on the screen (usually event.x, event.y)
* @param data {object} - Custom event data that will be added to the interaction-event
*/
handleMapInteraction(screenPosition, data = {}) {
let rootOffset = elementOffset(this.rootElement);
let normalizedScreenPos = new Vector2(
((screenPosition.x - rootOffset.top) / this.rootElement.clientWidth) * 2 - 1,
-((screenPosition.y - rootOffset.left) / this.rootElement.clientHeight) * 2 + 1
);
if (this.map && this.map.isLoaded){
this.raycaster.setFromCamera(normalizedScreenPos, this.camera);
// check Object3D interactions
const intersectScenes = [this.map.hiresTileManager.scene, this.markers];
for (let i = 0; i < this.map.lowresTileManager.length; i++) {
intersectScenes.push(this.map.lowresTileManager[i].scene);
}
let intersects = this.raycaster.intersectObjects(intersectScenes, true);
let hit = null;
let lowresHits = [];
let hiresHit = null;
let covered = false;
for (let i = 0; i < intersects.length; i++) {
if (intersects[i].object){
let object = intersects[i].object;
// check if deeply-visible
let parent = object;
let visible = parent.visible;
while (visible && parent.parent){
parent = parent.parent;
visible = parent.visible;
}
if (visible) {
if (!hit) hit = intersects[i];
// find root-scene
let parentRoot = object;
while(parentRoot.parent) parentRoot = parentRoot.parent;
for (let l = 0; l < this.map.lowresTileManager.length; l++) {
if (parentRoot === this.map.lowresTileManager[l].sceneParent) {
if (!lowresHits[l]) lowresHits[l] = intersects[i];
}
}
if (parentRoot === this.map.hiresTileManager.sceneParent) {
if (!hiresHit) hiresHit = intersects[i];
}
if (!covered || (object.material && !object.material.depthTest)) {
if (object.onClick && object.onClick({
data: data,
intersection: intersects[i]
})) return;
}
if (parentRoot !== this.map.lowresTileManager[0].sceneParent) {
covered = true;
}
}
}
}
// fire event
dispatchEvent(this.events, "bluemapMapInteraction", {
data: data,
hit: hit,
hiresHit: hiresHit,
lowresHits: lowresHits,
intersections: intersects,
ray: this.raycaster.ray
});
}
}
/**
* @private
* The render-loop to update and possibly render a new frame.
* @param now {number} the current time in milliseconds
*/
renderLoop = (now) => {
requestAnimationFrame(this.renderLoop);
// calculate delta time
if (this.lastFrame <= 0) this.lastFrame = now;
let delta = now - this.lastFrame;
this.lastFrame = now;
// update stats
this.stats.begin();
// update controls
if (this.map != null) {
this.controlsManager.update(delta, this.map);
}
// render
this.render(delta);
// update stats
this.stats.update();
};
/**
* @private
* Renders a frame
* @param delta {number}
*/
render(delta) {
dispatchEvent(this.events, "bluemapRenderFrame", {
delta: delta,
});
// render
this.renderer.clear();
// prepare skybox camera
this.skyboxCamera.rotation.copy(this.camera.rotation);
// render skybox
this.renderer.render(this.skyboxScene, this.skyboxCamera);
this.renderer.clearDepth();
if (this.map && this.map.isLoaded) {
// shift whole scene including camera towards 0,0 to tackle shader-precision issues
const s = 10000;
const sX = Math.round(this.camera.position.x / s) * s;
const sZ = Math.round(this.camera.position.z / s) * s;
this.camera.position.x -= sX;
this.camera.position.z -= sZ;
// update uniforms
this.data.uniforms.hiresTileMap.value.pos.copy(this.map.hiresTileManager.centerTile);
this.data.uniforms.hiresTileMap.value.translate.set(
this.map.data.hires.translate.x - sX,
this.map.data.hires.translate.z - sZ
);
// prepare camera for lowres
const cameraFar = this.camera.far;
if (this.controlsManager.distance < 1000) {
this.camera.far = 1000000; // disable far clipping for lowres
}
this.camera.updateProjectionMatrix();
// render lowres
const highestLod = this.map.lowresTileManager.length - 1;
for (let i = this.map.lowresTileManager.length - 1; i >= 0; i--) {
if (i === highestLod || this.controlsManager.distance < 1000 * Math.pow(this.map.data.lowres.lodFactor, i + 1)) {
let scenePos = this.map.lowresTileManager[i].scene.position;
scenePos.x = -sX;
scenePos.z = -sZ;
if (i === 0) {
this.camera.far = cameraFar; // reset far clipping for the highest lowres lod to make depth-tests possible
this.camera.updateProjectionMatrix();
}
this.renderer.render(this.map.lowresTileManager[i].sceneParent, this.camera);
if (i !== 0) this.renderer.clearDepth(); // clear depth-buffer for all lowres except the highest
}
}
this.camera.far = cameraFar; // reset far clipping
// render hires
if (this.controlsManager.distance < 1000) {
this.camera.updateProjectionMatrix();
let scenePos = this.map.hiresTileManager.scene.position;
scenePos.x = -sX;
scenePos.z = -sZ;
this.renderer.render(this.map.hiresTileManager.sceneParent, this.camera);
}
// shift back
this.camera.position.x += sX;
this.camera.position.z += sZ;
}
// render markers
this.renderer.render(this.markers, this.camera);
this.css2dRenderer.render(this.markers, this.camera);
}
/**
* Changes / Sets the map that will be loaded and displayed
* @param map {Map}
* @returns Promise<void>
*/
switchMap(map = null) {
if (this.map && this.map.isMap) this.map.unload();
this.map = map;
if (this.map && this.map.isMap) {
return map.load(HIRES_VERTEX_SHADER, HIRES_FRAGMENT_SHADER, LOWRES_VERTEX_SHADER, LOWRES_FRAGMENT_SHADER, this.data.uniforms, this.tileCacheHash)
.then(() => {
for (let texture of this.map.loadedTextures){
this.renderer.initTexture(texture);
}
this.data.uniforms.skyColor.value = map.data.skyColor;
this.data.uniforms.ambientLight.value = map.data.ambientLight;
this.data.uniforms.hiresTileMap.value.map = map.hiresTileManager.tileMap.texture;
this.data.uniforms.hiresTileMap.value.scale.set(map.data.hires.tileSize.x, map.data.hires.tileSize.z);
this.data.uniforms.hiresTileMap.value.translate.set(map.data.hires.translate.x, map.data.hires.translate.z);
setTimeout(this.updateLoadedMapArea);
dispatchEvent(this.events, "bluemapMapChanged", {
map: map
});
})
.catch(error => {
alert(this.events, error, "error");
});
} else {
return Promise.resolve();
}
}
/**
* Loads the given area on the map (and unloads everything outside that area)
* @param centerX {number}
* @param centerZ {number}
* @param hiresViewDistance {number?}
* @param lowresViewDistance {number?}
*/
loadMapArea(centerX, centerZ, hiresViewDistance = -1, lowresViewDistance = -1) {
this.data.loadedCenter.set(centerX, centerZ);
if (hiresViewDistance >= 0) this.data.loadedHiresViewDistance = hiresViewDistance;
if (lowresViewDistance >= 0) this.data.loadedLowresViewDistance = lowresViewDistance;
this.updateLoadedMapArea();
}
updateLoadedMapArea = () => {
if (!this.map) return;
if (this.controlsManager.distance < 1000) {
this.map.loadMapArea(this.data.loadedCenter.x, this.data.loadedCenter.y, this.data.loadedHiresViewDistance, this.data.loadedLowresViewDistance);
} else {
this.map.loadMapArea(this.data.loadedCenter.x, this.data.loadedCenter.y, 0, this.data.loadedLowresViewDistance);
}
}
clearTileCache(newTileCacheHash) {
if (!newTileCacheHash) newTileCacheHash = generateCacheHash();
this.tileCacheHash = newTileCacheHash;
if (this.map) {
for (let i = 0; i < this.map.lowresTileManager.length; i++) {
this.map.lowresTileManager[i].tileLoader.tileCacheHash = this.tileCacheHash;
}
this.map.hiresTileManager.tileLoader.tileCacheHash = this.tileCacheHash;
}
}
/**
* @returns {number}
*/
get superSampling() {
return this.data.superSampling;
}
/**
* @param value {number}
*/
set superSampling(value) {
this.data.superSampling = value;
this.handleContainerResize();
}
/**
* @returns {CombinedCamera}
*/
get camera() {
return this._camera;
}
/**
* @param value {CombinedCamera}
*/
set camera(value) {
this._camera = value;
this.data.camera = value.data;
}
/**
* @returns {ControlsManager}
*/
get controlsManager() {
return this._controlsManager;
}
/**
* @param value {ControlsManager}
*/
set controlsManager(value) {
this._controlsManager = value;
this.data.controlsManager = value.data;
}
/**
* @returns {Map}
*/
get map() {
return this._map;
}
/**
* @param value {Map}
*/
set map(value) {
this._map = value;
if (value) this.data.map = value.data;
}
}

View File

@ -0,0 +1,333 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Vector3} from "three";
import {dispatchEvent} from "../util/Utils";
import {Map} from "../map/Map";
export class ControlsManager {
/**
* @param mapViewer {MapViewer}
* @param camera {CombinedCamera}
*/
constructor(mapViewer, camera) {
Object.defineProperty( this, 'isControlsManager', { value: true } );
this.data = {
mapViewer: null,
camera: null,
controls: null,
position: new Vector3(0, 0, 0),
rotation: 0,
angle: 0,
tilt: 0,
};
this.mapViewer = mapViewer;
this.camera = camera;
/** @type {Vector3} */
this.lastPosition = this.position.clone();
this.lastRotation = this.rotation;
this.lastAngle = this.angle;
this.lastDistance = this.distance;
this.lastOrtho = this.ortho;
this.lastTilt = this.tilt;
this.lastMapUpdatePosition = this.position.clone();
this.lastMapUpdateDistance = this.distance;
this.averageDeltaTime = 16;
this._controls = null;
// start
this.distance = 300;
this.position.set(0, 0, 0);
this.rotation = 0;
this.angle = 0;
this.tilt = 0;
this.ortho = 0;
this.updateCamera();
}
/**
* @param deltaTime {number}
* @param map {Map}
*/
update(deltaTime, map) {
if (deltaTime > 50) deltaTime = 50; // assume min 20 UPS
this.averageDeltaTime = this.averageDeltaTime * 0.9 + deltaTime * 0.1; // average delta-time to avoid choppy controls on lag-spikes
if (this._controls) this._controls.update(this.averageDeltaTime, map);
this.updateCamera();
}
updateCamera() {
let valueChanged = this.isValueChanged();
if (valueChanged) {
this.resetValueChanged();
// wrap rotation
while (this.rotation >= Math.PI) this.rotation -= Math.PI * 2;
while (this.rotation <= -Math.PI) this.rotation += Math.PI * 2;
// prevent problems with the rotation when the angle is 0 (top-down) or distance is 0 (first-person)
let rotatableAngle = this.angle;
if (Math.abs(rotatableAngle) <= 0.0001) rotatableAngle = 0.0001;
else if (Math.abs(rotatableAngle) - Math.PI <= 0.0001) rotatableAngle = rotatableAngle - 0.0001;
let rotatableDistance = this.distance;
if (Math.abs(rotatableDistance) <= 0.0001) rotatableDistance = 0.0001;
// fix distance for orthogonal-camera
if (this.ortho > 0) {
rotatableDistance = MathUtils.lerp(rotatableDistance, Math.max(rotatableDistance, 300), Math.pow(this.ortho, 8));
}
// calculate rotationVector
let rotationVector = new Vector3(Math.sin(this.rotation), 0, -Math.cos(this.rotation)); // 0 is towards north
let angleRotationAxis = new Vector3(0, 1, 0).cross(rotationVector);
rotationVector.applyAxisAngle(angleRotationAxis, (Math.PI / 2) - rotatableAngle);
rotationVector.multiplyScalar(rotatableDistance);
// position camera
this.camera.rotation.set(0, 0, 0);
this.camera.position.copy(this.position).sub(rotationVector);
this.camera.lookAt(this.position);
this.camera.rotateZ(this.tilt + rotatableAngle < 0 ? Math.PI : 0);
// optimize far/near planes
if (this.ortho <= 0) {
let near = MathUtils.clamp(rotatableDistance / 1000, 0.01, 1);
let far = MathUtils.clamp(rotatableDistance * 2, Math.max(near + 1, 2000), rotatableDistance + 5000);
if (far - near > 10000) near = far - 10000;
this.camera.near = near;
this.camera.far = far;
} else if (this.angle === 0) {
this.camera.near = 1;
this.camera.far = rotatableDistance + 300;
} else {
this.camera.near = 1;
this.camera.far = 100000;
}
// event
dispatchEvent(this.mapViewer.events, "bluemapCameraMoved", {
controlsManager: this,
camera: this.camera
});
}
// if the position changed, update map to show new position
if (this.mapViewer.map) {
let triggerDistance = 1;
if (valueChanged) {
if (this.distance > 300) {
triggerDistance = this.mapViewer.data.loadedLowresViewDistance * 0.5;
} else {
triggerDistance = this.mapViewer.data.loadedHiresViewDistance * 0.5;
}
}
if (
Math.abs(this.lastMapUpdatePosition.x - this.position.x) >= triggerDistance ||
Math.abs(this.lastMapUpdatePosition.z - this.position.z) >= triggerDistance ||
(this.distance < 1000 && this.lastMapUpdateDistance > 1000)
) {
this.lastMapUpdatePosition = this.position.clone();
this.lastMapUpdateDistance = this.distance;
this.mapViewer.loadMapArea(this.position.x, this.position.z);
}
}
}
/**
* Triggers an interaction on the screen (map), e.g. a mouse-click
* @param screenPosition {THREE.Vector2} - Clicked position on the screen (usually event.x, event.y)
* @param data {object} - Custom event data that will be added to the interaction-event
*/
handleMapInteraction(screenPosition, data = {}) {
this.mapViewer.handleMapInteraction(screenPosition, data);
}
isValueChanged() {
return !(
this.data.position.equals(this.lastPosition) &&
this.data.rotation === this.lastRotation &&
this.data.angle === this.lastAngle &&
this.distance === this.lastDistance &&
this.ortho === this.lastOrtho &&
this.data.tilt === this.lastTilt
);
}
resetValueChanged() {
this.lastPosition.copy(this.data.position);
this.lastRotation = this.data.rotation;
this.lastAngle = this.data.angle;
this.lastDistance = this.distance;
this.lastOrtho = this.ortho;
this.lastTilt = this.data.tilt;
}
/**
* @returns {number}
*/
get ortho() {
return this.camera.ortho;
}
/**
* @param ortho {number}
*/
set ortho(ortho) {
this.camera.ortho = ortho;
}
get distance() {
return this.camera.distance;
}
set distance(distance) {
this.camera.distance = distance;
}
/** @typedef ControlsLike {{
* start: function(controls: ControlsManager),
* stop: function(),
* update: function(deltaTime: number, map: Map)
* }}
/**
* @param controls {ControlsLike}
*/
set controls(controls) {
if (this._controls && this._controls.stop)
this._controls.stop();
this._controls = controls;
if (controls) this.data.controls = controls.data || null
if (this._controls && this._controls.start)
this._controls.start(this);
}
/**
* @returns {ControlsLike}
*/
get controls() {
return this._controls;
}
/**
* @returns {MapViewer}
*/
get mapViewer() {
return this._mapViewer;
}
/**
* @param value {MapViewer}
*/
set mapViewer(value) {
this._mapViewer = value;
this.data.mapViewer = value.data;
}
/**
* @returns {CombinedCamera}
*/
get camera() {
return this._camera;
}
/**
* @param value {CombinedCamera}
*/
set camera(value) {
this._camera = value;
this.data.camera = value.data;
}
/**
* @returns {Vector3}
*/
get position() {
return this.data.position;
}
/**
* @param value {Vector3}
*/
set position(value) {
this.data.position = value;
}
/**
* @returns {number}
*/
get rotation() {
return this.data.rotation;
}
/**
* @param value {number}
*/
set rotation(value) {
this.data.rotation = value;
}
/**
* @returns {number}
*/
get angle() {
return this.data.angle;
}
/**
* @param value {number}
*/
set angle(value) {
this.data.angle = value;
}
/**
* @returns {number}
*/
get tilt() {
return this.data.tilt;
}
/**
* @param value {number}
*/
set tilt(value) {
this.data.tilt = value;
}
}

View File

@ -0,0 +1,78 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
export class KeyCombination {
static CTRL = 0;
static SHIFT = 1;
static ALT = 2;
/**
* @param code {string}
* @param modifiers {...number}
*/
constructor(code, ...modifiers) {
this.code = code;
this.ctrl = modifiers.includes(KeyCombination.CTRL) || this.code === "CtrlLeft" || this.code === "CtrlRight";
this.shift = modifiers.includes(KeyCombination.SHIFT) || this.code === "ShiftLeft" || this.code === "ShiftRight";
this.alt = modifiers.includes(KeyCombination.ALT) || this.code === "AltLeft" || this.code === "AltRight";
}
/**
* @param evt {KeyboardEvent}
* @returns {boolean}
*/
testDown(evt) {
return this.code === evt.code &&
this.ctrl === evt.ctrlKey &&
this.shift === evt.shiftKey &&
this.alt === evt.altKey;
}
/**
* @param evt {KeyboardEvent}
* @returns {boolean}
*/
testUp(evt) {
return this.code === evt.code;
}
static oneDown(evt, ...combinations) {
for (let combination of combinations){
if (combination.testDown(evt)) return true;
}
return false;
}
static oneUp(evt, ...combinations) {
for (let combination of combinations){
if (combination.testUp(evt)) return true;
}
return false;
}
}

View File

@ -0,0 +1,147 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Vector2} from "three";
import {Manager, Pan, DIRECTION_ALL} from "hammerjs";
import {animate, EasingFunctions} from "../../util/Utils";
import {KeyMoveControls} from "./keyboard/KeyMoveControls";
import {MouseRotateControls} from "./mouse/MouseRotateControls";
import {MouseAngleControls} from "./mouse/MouseAngleControls";
import {KeyHeightControls} from "./keyboard/KeyHeightControls";
import {TouchPanControls} from "./touch/TouchPanControls";
export class FreeFlightControls {
/**
* @param target {Element}
*/
constructor(target) {
this.target = target;
this.manager = null;
this.data = {
};
this.hammer = new Manager(this.target);
this.initializeHammer();
this.keyMove = new KeyMoveControls(this.target, 0.5, 0.1);
this.keyHeight = new KeyHeightControls(this.target, 0.5, 0.2);
this.mouseRotate = new MouseRotateControls(this.target, 1.5, -2, -1.5, 0.5);
this.mouseAngle = new MouseAngleControls(this.target, 1.5, -2, -1.5, 0.5);
this.touchPan = new TouchPanControls(this.target, this.hammer, 5, 0.15);
this.started = false;
this.clickStart = new Vector2();
this.moveSpeed = 0.5;
this.animationTargetHeight = 0;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.keyMove.start(manager);
this.keyHeight.start(manager);
this.mouseRotate.start(manager);
this.mouseAngle.start(manager);
this.touchPan.start(manager);
this.target.addEventListener("contextmenu", this.onContextMenu);
this.target.addEventListener("mousedown", this.onMouseDown);
this.target.addEventListener("mouseup", this.onMouseUp);
window.addEventListener("wheel", this.onWheel, {passive: true});
}
stop() {
this.keyMove.stop();
this.keyHeight.stop();
this.mouseRotate.stop();
this.mouseAngle.stop();
this.touchPan.stop();
this.target.removeEventListener("contextmenu", this.onContextMenu);
this.target.removeEventListener("mousedown", this.onMouseDown);
this.target.removeEventListener("mouseup", this.onMouseUp);
window.removeEventListener("wheel", this.onWheel);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
this.keyMove.update(delta, map);
this.keyHeight.update(delta, map);
this.mouseRotate.update(delta, map);
this.mouseAngle.update(delta, map);
this.touchPan.update(delta, map);
this.manager.angle = MathUtils.clamp(this.manager.angle, 0, Math.PI);
this.manager.distance = 0;
this.manager.ortho = 0;
}
initializeHammer() {
let touchMove = new Pan({ event: 'move', pointers: 1, direction: DIRECTION_ALL, threshold: 0 });
this.hammer.add(touchMove);
}
onContextMenu = evt => {
evt.preventDefault();
}
onMouseDown = evt => {
this.clickStart.set(evt.x, evt.y);
}
onMouseUp = evt => {
if (Math.abs(this.clickStart.x - evt.x) > 5) return;
if (Math.abs(this.clickStart.y - evt.y) > 5) return;
document.body.requestFullscreen()
.finally(() => {
this.target.requestPointerLock();
});
}
onWheel = evt => {
let delta = evt.deltaY;
if (evt.deltaMode === WheelEvent.DOM_DELTA_PIXEL) delta *= 0.01;
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) delta *= 0.33;
this.moveSpeed *= Math.pow(1.5, -delta * 0.25);
this.moveSpeed = MathUtils.clamp(this.moveSpeed, 0.05, 5);
this.keyMove.speed = this.moveSpeed;
this.keyHeight.speed = this.moveSpeed;
}
}

View File

@ -0,0 +1,123 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
import {KeyCombination} from "../../KeyCombination";
export class KeyHeightControls {
static KEYS = {
UP: [
new KeyCombination("Space"),
new KeyCombination("PageUp")
],
DOWN: [
new KeyCombination("ShiftLeft"),
new KeyCombination("ShiftRight"),
new KeyCombination("PageDown")
],
}
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaY = 0;
this.up = false;
this.down = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.up) this.deltaY += 1;
if (this.down) this.deltaY -= 1;
if (this.deltaY === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.position.y += this.deltaY * smoothing * this.speed * delta * 0.06;
this.deltaY *= 1 - smoothing;
if (Math.abs(this.deltaY) < 0.0001) {
this.deltaY = 0;
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneUp(evt, ...KeyHeightControls.KEYS.UP)){
this.up = true;
evt.preventDefault();
}
else if (KeyCombination.oneUp(evt, ...KeyHeightControls.KEYS.DOWN)){
this.down = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyHeightControls.KEYS.UP)){
this.up = false;
}
if (KeyCombination.oneUp(evt, ...KeyHeightControls.KEYS.DOWN)){
this.down = false;
}
}
}

View File

@ -0,0 +1,155 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Vector2} from "three";
import {VEC2_ZERO} from "../../../util/Utils";
import {KeyCombination} from "../../KeyCombination";
export class KeyMoveControls {
static KEYS = {
LEFT: [
new KeyCombination("ArrowLeft"),
new KeyCombination("KeyA")
],
UP: [
new KeyCombination("ArrowUp"),
new KeyCombination("KeyW")
],
RIGHT: [
new KeyCombination("ArrowRight"),
new KeyCombination("KeyD")
],
DOWN: [
new KeyCombination("ArrowDown"),
new KeyCombination("KeyS")
],
}
static temp_v2 = new Vector2();
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaPosition = new Vector2();
this.up = false;
this.down = false;
this.left = false;
this.right = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.up) this.deltaPosition.y -= 1;
if (this.down) this.deltaPosition.y += 1;
if (this.left) this.deltaPosition.x -= 1;
if (this.right) this.deltaPosition.x += 1;
if (this.deltaPosition.x === 0 && this.deltaPosition.y === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
let rotatedDelta = KeyMoveControls.temp_v2.copy(this.deltaPosition);
rotatedDelta.rotateAround(VEC2_ZERO, this.manager.rotation);
this.manager.position.x += rotatedDelta.x * smoothing * this.speed * delta * 0.06;
this.manager.position.z += rotatedDelta.y * smoothing * this.speed * delta * 0.06;
this.deltaPosition.multiplyScalar(1 - smoothing);
if (this.deltaPosition.lengthSq() < 0.0001) {
this.deltaPosition.set(0, 0);
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.UP)){
this.up = true;
evt.preventDefault();
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.DOWN)){
this.down = true;
evt.preventDefault();
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.LEFT)){
this.left = true;
evt.preventDefault();
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.RIGHT)){
this.right = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.UP)){
this.up = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.DOWN)){
this.down = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.LEFT)){
this.left = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.RIGHT)){
this.right = false;
}
}
}

View File

@ -0,0 +1,139 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
export class MouseAngleControls {
/**
* @param target {EventTarget}
* @param speedLeft {number}
* @param speedRight {number}
* @param speedCapture {number}
* @param stiffness {number}
*/
constructor(target, speedLeft, speedRight, speedCapture, stiffness) {
this.target = target;
this.manager = null;
this.moving = false;
this.lastY = 0;
this.deltaAngle = 0;
this.speedLeft = speedLeft;
this.speedRight = speedRight;
this.speedCapture = speedCapture;
this.stiffness = stiffness;
this.pixelToSpeedMultiplier = 0;
this.updatePixelToSpeedMultiplier();
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
window.addEventListener("resize", this.updatePixelToSpeedMultiplier);
}
stop() {
this.target.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
window.removeEventListener("resize", this.updatePixelToSpeedMultiplier);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaAngle === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.angle += this.deltaAngle * smoothing;
this.deltaAngle *= 1 - smoothing;
if (Math.abs(this.deltaAngle) < 0.0001) {
this.deltaAngle = 0;
}
}
reset() {
this.deltaAngle = 0;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseDown = evt => {
this.moving = true;
this.deltaAngle = 0;
this.lastY = evt.y;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseMove = evt => {
if (document.pointerLockElement) {
this.deltaAngle += evt.movementY * this.speedCapture * this.pixelToSpeedMultiplier;
}
else if(this.moving){
if (evt.buttons === 1) {
this.deltaAngle += (evt.y - this.lastY) * this.speedLeft * this.pixelToSpeedMultiplier;
} else {
this.deltaAngle += (evt.y - this.lastY) * this.speedRight * this.pixelToSpeedMultiplier;
}
}
this.lastY = evt.y;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseUp = evt => {
this.moving = false;
}
updatePixelToSpeedMultiplier = () => {
this.pixelToSpeedMultiplier = 1 / this.target.clientHeight;
}
}

View File

@ -0,0 +1,139 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
export class MouseRotateControls {
/**
* @param target {Element}
* @param speedLeft {number}
* @param speedRight {number}
* @param speedCapture {number}
* @param stiffness {number}
*/
constructor(target, speedLeft, speedRight, speedCapture, stiffness) {
this.target = target;
this.manager = null;
this.moving = false;
this.lastX = 0;
this.deltaRotation = 0;
this.speedLeft = speedLeft;
this.speedRight = speedRight;
this.speedCapture = speedCapture;
this.stiffness = stiffness;
this.pixelToSpeedMultiplier = 0;
this.updatePixelToSpeedMultiplier();
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
window.addEventListener("resize", this.updatePixelToSpeedMultiplier);
}
stop() {
this.target.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
window.removeEventListener("resize", this.updatePixelToSpeedMultiplier);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaRotation === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.rotation += this.deltaRotation * smoothing;
this.deltaRotation *= 1 - smoothing;
if (Math.abs(this.deltaRotation) < 0.0001) {
this.deltaRotation = 0;
}
}
reset() {
this.deltaRotation = 0;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseDown = evt => {
this.moving = true;
this.deltaRotation = 0;
this.lastX = evt.x;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseMove = evt => {
if (document.pointerLockElement) {
this.deltaRotation -= evt.movementX * this.speedCapture * this.pixelToSpeedMultiplier;
}
else if(this.moving){
if (evt.buttons === 1) {
this.deltaRotation -= (evt.x - this.lastX) * this.speedLeft * this.pixelToSpeedMultiplier;
} else {
this.deltaRotation -= (evt.x - this.lastX) * this.speedRight * this.pixelToSpeedMultiplier;
}
}
this.lastX = evt.x;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseUp = evt => {
this.moving = false;
}
updatePixelToSpeedMultiplier = () => {
this.pixelToSpeedMultiplier = (1 / this.target.clientWidth) * (this.target.clientWidth / this.target.clientHeight);
}
}

View File

@ -0,0 +1,144 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Vector2} from "three";
export class TouchPanControls {
static tempVec2_1 = new Vector2();
/**
* @param target {Element}
* @param hammer {Manager}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, hammer, speed, stiffness) {
this.target = target;
this.hammer = hammer;
this.manager = null;
this.moving = false;
this.lastPosition = new Vector2();
this.deltaPosition = new Vector2();
this.speed = speed;
this.stiffness = stiffness;
this.pixelToSpeedMultiplierX = 0;
this.pixelToSpeedMultiplierY = 0;
this.updatePixelToSpeedMultiplier();
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.hammer.on("movestart", this.onTouchDown);
this.hammer.on("movemove", this.onTouchMove);
this.hammer.on("moveend", this.onTouchUp);
this.hammer.on("movecancel", this.onTouchUp);
window.addEventListener("resize", this.updatePixelToSpeedMultiplier);
}
stop() {
this.hammer.off("movestart", this.onTouchDown);
this.hammer.off("movemove", this.onTouchMove);
this.hammer.off("moveend", this.onTouchUp);
this.hammer.off("movecancel", this.onTouchUp);
window.removeEventListener("resize", this.updatePixelToSpeedMultiplier);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaPosition.x === 0 && this.deltaPosition.y === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.rotation += this.deltaPosition.x * this.speed * this.pixelToSpeedMultiplierX * this.stiffness;
this.manager.angle -= this.deltaPosition.y * this.speed * this.pixelToSpeedMultiplierY * this.stiffness;
this.deltaPosition.multiplyScalar(1 - smoothing);
if (this.deltaPosition.lengthSq() < 0.0001) {
this.deltaPosition.set(0, 0);
}
}
reset() {
this.deltaPosition.set(0, 0);
}
/**
* @private
* @param evt {object}
*/
onTouchDown = evt => {
if (evt.pointerType === "mouse") return;
this.moving = true;
this.deltaPosition.set(0, 0);
this.lastPosition.set(evt.center.x, evt.center.y);
}
/**
* @private
* @param evt {object}
*/
onTouchMove = evt => {
if (evt.pointerType === "mouse") return;
let position = TouchPanControls.tempVec2_1.set(evt.center.x, evt.center.y);
if(this.moving){
this.deltaPosition.sub(position).add(this.lastPosition);
}
this.lastPosition.copy(position);
}
/**
* @private
* @param evt {object}
*/
onTouchUp = evt => {
if (evt.pointerType === "mouse") return;
this.moving = false;
}
updatePixelToSpeedMultiplier = () => {
this.pixelToSpeedMultiplierX = (1 / this.target.clientWidth) * (this.target.clientWidth / this.target.clientHeight);
this.pixelToSpeedMultiplierY = 1 / this.target.clientHeight;
}
}

View File

@ -0,0 +1,272 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MouseMoveControls} from "./mouse/MouseMoveControls";
import {MouseZoomControls} from "./mouse/MouseZoomControls";
import {MouseRotateControls} from "./mouse/MouseRotateControls";
import {MouseAngleControls} from "./mouse/MouseAngleControls";
import {MathUtils, Vector2, Vector3} from "three";
import {Manager, Pan, Pinch, Rotate, Tap, DIRECTION_ALL, DIRECTION_VERTICAL} from "hammerjs";
import {softClamp} from "../../util/Utils";
import {MapHeightControls} from "./MapHeightControls";
import {KeyMoveControls} from "./keyboard/KeyMoveControls";
import {KeyAngleControls} from "./keyboard/KeyAngleControls";
import {KeyRotateControls} from "./keyboard/KeyRotateControls";
import {KeyZoomControls} from "./keyboard/KeyZoomControls";
import {TouchMoveControls} from "./touch/TouchMoveControls";
import {TouchRotateControls} from "./touch/TouchRotateControls";
import {TouchAngleControls} from "./touch/TouchAngleControls";
import {TouchZoomControls} from "./touch/TouchZoomControls";
import {PlayerMarker} from "../../markers/PlayerMarker";
const HALF_PI = Math.PI * 0.5;
export class MapControls {
static _beforeMoveTemp = new Vector3();
/**
* @param rootElement {Element}
*/
constructor(rootElement) {
this.rootElement = rootElement;
this.data = {
followingPlayer: null
};
/** @type {ControlsManager} */
this.manager = null;
this.hammer = new Manager(this.rootElement);
this.initializeHammer();
//controls
this.mouseMove = new MouseMoveControls(this.rootElement, 1.5,0.3);
this.mouseRotate = new MouseRotateControls(this.rootElement, 6, 0.3);
this.mouseAngle = new MouseAngleControls(this.rootElement, 3, 0.3);
this.mouseZoom = new MouseZoomControls(this.rootElement, 1, 0.2);
this.keyMove = new KeyMoveControls(this.rootElement, 0.025, 0.2);
this.keyRotate = new KeyRotateControls(this.rootElement, 0.06, 0.15);
this.keyAngle = new KeyAngleControls(this.rootElement, 0.04, 0.15);
this.keyZoom = new KeyZoomControls(this.rootElement, 0.2, 0.15);
this.touchMove = new TouchMoveControls(this.rootElement, this.hammer, 1.5,0.3);
this.touchRotate = new TouchRotateControls(this.hammer, 0.0174533, 0.3);
this.touchAngle = new TouchAngleControls(this.rootElement, this.hammer, 3, 0.3);
this.touchZoom = new TouchZoomControls(this.hammer);
this.mapHeight = new MapHeightControls(0.2, 0.1);
this.lastTap = -1;
this.lastTapCenter = null;
this.minDistance = 5;
this.maxDistance = 100000;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.rootElement.addEventListener("contextmenu", this.onContextMenu);
this.hammer.on("tap", this.onTap);
this.mouseMove.start(manager);
this.mouseRotate.start(manager);
this.mouseAngle.start(manager);
this.mouseZoom.start(manager);
this.keyMove.start(manager);
this.keyRotate.start(manager);
this.keyAngle.start(manager);
this.keyZoom.start(manager);
this.touchMove.start(manager);
this.touchRotate.start(manager);
this.touchAngle.start(manager);
this.touchZoom.start(manager);
this.mapHeight.start(manager);
}
stop() {
this.stopFollowingPlayerMarker();
this.rootElement.removeEventListener("contextmenu", this.onContextMenu);
this.hammer.off("tap", this.onTap);
this.mouseMove.stop();
this.mouseRotate.stop();
this.mouseAngle.stop();
this.mouseZoom.stop();
this.keyMove.stop();
this.keyRotate.stop();
this.keyAngle.stop();
this.keyZoom.stop();
this.touchMove.stop();
this.touchRotate.stop();
this.touchAngle.stop();
this.touchZoom.stop();
this.mapHeight.stop();
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
this.manager.position.y = 0; // reset target y position
// move
MapControls._beforeMoveTemp.copy(this.manager.position);
this.mouseMove.update(delta, map);
this.keyMove.update(delta, map);
this.touchMove.update(delta, map);
// if moved, stop following the marker and give back control
if (this.data.followingPlayer && !MapControls._beforeMoveTemp.equals(this.manager.position)) {
this.stopFollowingPlayerMarker();
}
// follow player marker
if (this.data.followingPlayer) {
this.manager.position.copy(this.data.followingPlayer.position);
}
// zoom
this.mouseZoom.update(delta, map);
this.keyZoom.update(delta, map);
this.touchZoom.update(delta, map);
this.manager.distance = softClamp(this.manager.distance, this.minDistance, this.maxDistance, 0.8);
// max angle for current distance
let maxAngleForZoom = this.getMaxPerspectiveAngleForDistance(this.manager.distance);
// rotation
this.mouseRotate.update(delta, map);
this.keyRotate.update(delta, map);
this.touchRotate.update(delta, map);
const rotating = this.mouseRotate.moving || this.touchRotate.moving ||
this.keyRotate.left || this.keyRotate.right
// snap rotation to north on orthographic view
if (this.manager.ortho !== 0 && Math.abs(this.manager.rotation) < (rotating ? 0.05 : 0.3)) {
this.manager.rotation = softClamp(this.manager.rotation, 0, 0, 0.1);
}
// tilt
if (this.manager.ortho === 0) {
this.mouseAngle.update(delta, map);
this.keyAngle.update(delta, map);
this.touchAngle.update(delta, map);
this.manager.angle = softClamp(this.manager.angle, 0, maxAngleForZoom, 0.8);
}
// target height
if (this.manager.ortho === 0 || this.manager.angle === 0) {
this.mapHeight.maxAngle = maxAngleForZoom;
this.mapHeight.update(delta, map);
}
}
reset() {
this.mouseMove.reset();
this.mouseRotate.reset();
this.mouseAngle.reset();
this.mouseZoom.reset();
this.touchMove.reset();
this.touchRotate.reset();
this.touchAngle.reset();
this.touchZoom.reset();
}
getMaxPerspectiveAngleForDistance(distance) {
return MathUtils.clamp((1 - Math.pow(Math.max(distance - 5, 0.001) / 500, 0.5)) * HALF_PI,0, HALF_PI)
}
initializeHammer() {
let touchTap = new Tap({ event: 'tap', pointers: 1, taps: 1, threshold: 5 });
let touchMove = new Pan({ event: 'move', pointers: 1, direction: DIRECTION_ALL, threshold: 0 });
let touchTilt = new Pan({ event: 'tilt', pointers: 2, direction: DIRECTION_VERTICAL, threshold: 0 });
let touchRotate = new Rotate({ event: 'rotate', pointers: 2, threshold: 0 });
let touchZoom = new Pinch({ event: 'zoom', pointers: 2, threshold: 0 });
touchMove.recognizeWith(touchRotate);
touchMove.recognizeWith(touchTilt);
touchMove.recognizeWith(touchZoom);
touchTilt.recognizeWith(touchRotate);
touchTilt.recognizeWith(touchZoom);
touchRotate.recognizeWith(touchZoom);
this.hammer.add(touchTap);
this.hammer.add(touchTilt);
this.hammer.add(touchMove);
this.hammer.add(touchRotate);
this.hammer.add(touchZoom);
}
/**
* @param marker {object}
*/
followPlayerMarker(marker) {
if (marker.isPlayerMarker) marker = marker.data;
this.data.followingPlayer = marker;
}
stopFollowingPlayerMarker() {
this.data.followingPlayer = null;
}
onContextMenu = evt => {
evt.preventDefault();
}
onTap = evt => {
let doubleTap = false;
let center = new Vector2(evt.center.x, evt.center.y);
let now = Date.now();
if (this.lastTap > 0 && this.lastTapCenter && now - this.lastTap < 500 && this.lastTapCenter.distanceTo(center) < 5){
doubleTap = true;
this.lastTap = -1;
} else {
this.lastTap = now;
this.lastTapCenter = center;
}
this.manager.handleMapInteraction(new Vector2(evt.center.x, evt.center.y), {doubleTap: doubleTap});
}
}

View File

@ -0,0 +1,103 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Vector2} from "three";
export class MapHeightControls {
/**
* @param cameraHeightStiffness {number}
* @param targetHeightStiffness {number}
*/
constructor(cameraHeightStiffness, targetHeightStiffness) {
this.manager = null;
this.cameraHeightStiffness = cameraHeightStiffness;
this.targetHeightStiffness = targetHeightStiffness;
this.maxAngle = Math.PI / 2;
this.targetHeight = 0;
this.cameraHeight = 0;
this.minCameraHeight = 0;
this.distanceTagretHeight = 0;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
}
stop() {}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
// adjust target height
this.updateHeights(delta, map);
this.manager.position.y = Math.max(this.manager.position.y, this.getSuggestedHeight());
}
updateHeights(delta, map) {
//target height
let targetSmoothing = this.targetHeightStiffness / (16.666 / delta);
targetSmoothing = MathUtils.clamp(targetSmoothing, 0, 1);
let targetTerrainHeight = map.terrainHeightAt(this.manager.position.x, this.manager.position.z) + 3 || 0;
let targetDelta = targetTerrainHeight - this.targetHeight;
this.targetHeight += targetDelta * targetSmoothing;
if (Math.abs(targetDelta) < 0.001) this.targetHeight = targetTerrainHeight;
// camera height
this.minCameraHeight = 0;
if (this.maxAngle >= 0.1) {
let cameraSmoothing = this.cameraHeightStiffness / (16.666 / delta);
cameraSmoothing = MathUtils.clamp(cameraSmoothing, 0, 1);
let cameraTerrainHeight = map.terrainHeightAt(this.manager.camera.position.x, this.manager.camera.position.z) || 0;
let cameraDelta = cameraTerrainHeight - this.cameraHeight;
this.cameraHeight += cameraDelta * cameraSmoothing;
if (Math.abs(cameraDelta) < 0.001) this.cameraHeight = cameraTerrainHeight;
let maxAngleHeight = Math.cos(this.maxAngle) * this.manager.distance;
this.minCameraHeight = this.cameraHeight - maxAngleHeight + 1;
}
// adjust targetHeight by distance
this.distanceTagretHeight = Math.max(MathUtils.lerp(this.targetHeight, 0, this.manager.distance / 500), 0);
}
getSuggestedHeight() {
return Math.max(this.distanceTagretHeight, this.minCameraHeight);
}
}

View File

@ -0,0 +1,124 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
import {KeyCombination} from "../../KeyCombination";
export class KeyAngleControls {
static KEYS = {
UP: [
new KeyCombination("ArrowUp", KeyCombination.ALT),
new KeyCombination("KeyW", KeyCombination.ALT),
new KeyCombination("PageUp")
],
DOWN: [
new KeyCombination("ArrowDown", KeyCombination.ALT),
new KeyCombination("KeyS", KeyCombination.ALT),
new KeyCombination("PageDown")
],
}
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaAngle = 0;
this.up = false;
this.down = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.up) this.deltaAngle -= 1;
if (this.down) this.deltaAngle += 1;
if (this.deltaAngle === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.angle += this.deltaAngle * smoothing * this.speed * delta * 0.06;
this.deltaAngle *= 1 - smoothing;
if (Math.abs(this.deltaAngle) < 0.0001) {
this.deltaAngle = 0;
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneDown(evt, ...KeyAngleControls.KEYS.UP)){
this.up = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyAngleControls.KEYS.DOWN)){
this.down = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyAngleControls.KEYS.UP)){
this.up = false;
}
if (KeyCombination.oneUp(evt, ...KeyAngleControls.KEYS.DOWN)){
this.down = false;
}
}
}

View File

@ -0,0 +1,155 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Vector2} from "three";
import {VEC2_ZERO} from "../../../util/Utils";
import {KeyCombination} from "../../KeyCombination";
export class KeyMoveControls {
static KEYS = {
LEFT: [
new KeyCombination("ArrowLeft"),
new KeyCombination("KeyA")
],
UP: [
new KeyCombination("ArrowUp"),
new KeyCombination("KeyW")
],
RIGHT: [
new KeyCombination("ArrowRight"),
new KeyCombination("KeyD")
],
DOWN: [
new KeyCombination("ArrowDown"),
new KeyCombination("KeyS")
],
}
static temp_v2 = new Vector2();
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaPosition = new Vector2();
this.up = false;
this.down = false;
this.left = false;
this.right = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.up) this.deltaPosition.y -= 1;
if (this.down) this.deltaPosition.y += 1;
if (this.left) this.deltaPosition.x -= 1;
if (this.right) this.deltaPosition.x += 1;
if (this.deltaPosition.x === 0 && this.deltaPosition.y === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
let rotatedDelta = KeyMoveControls.temp_v2.copy(this.deltaPosition);
rotatedDelta.rotateAround(VEC2_ZERO, this.manager.rotation);
this.manager.position.x += rotatedDelta.x * smoothing * this.manager.distance * this.speed * delta * 0.06;
this.manager.position.z += rotatedDelta.y * smoothing * this.manager.distance * this.speed * delta * 0.06;
this.deltaPosition.multiplyScalar(1 - smoothing);
if (this.deltaPosition.lengthSq() < 0.0001) {
this.deltaPosition.set(0, 0);
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneDown(evt, ...KeyMoveControls.KEYS.UP)){
this.up = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyMoveControls.KEYS.DOWN)){
this.down = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyMoveControls.KEYS.LEFT)){
this.left = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyMoveControls.KEYS.RIGHT)){
this.right = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.UP)){
this.up = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.DOWN)){
this.down = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.LEFT)){
this.left = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.RIGHT)){
this.right = false;
}
}
}

View File

@ -0,0 +1,124 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
import {KeyCombination} from "../../KeyCombination";
export class KeyRotateControls {
static KEYS = {
LEFT: [
new KeyCombination("ArrowLeft", KeyCombination.ALT),
new KeyCombination("KeyA", KeyCombination.ALT),
new KeyCombination("Delete"),
],
RIGHT: [
new KeyCombination("ArrowRight", KeyCombination.ALT),
new KeyCombination("KeyD", KeyCombination.ALT),
new KeyCombination("End"),
],
}
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaRotation = 0;
this.left = false;
this.right = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.left) this.deltaRotation += 1;
if (this.right) this.deltaRotation -= 1;
if (this.deltaRotation === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.rotation += this.deltaRotation * smoothing * this.speed * delta * 0.06;
this.deltaRotation *= 1 - smoothing;
if (Math.abs(this.deltaRotation) < 0.0001) {
this.deltaRotation = 0;
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneDown(evt, ...KeyRotateControls.KEYS.LEFT)){
this.left = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyRotateControls.KEYS.RIGHT)){
this.right = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyRotateControls.KEYS.LEFT)){
this.left = false;
}
if (KeyCombination.oneUp(evt, ...KeyRotateControls.KEYS.RIGHT)){
this.right = false;
}
}
}

View File

@ -0,0 +1,122 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
import {KeyCombination} from "../../KeyCombination";
export class KeyZoomControls {
static KEYS = {
IN: [
new KeyCombination("NumpadAdd"),
new KeyCombination("Insert"),
],
OUT: [
new KeyCombination("NumpadSubtract"),
new KeyCombination("Home"),
],
}
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaZoom = 0;
this.in = false;
this.out = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.in) this.deltaZoom -= 1;
if (this.out) this.deltaZoom += 1;
if (this.deltaZoom === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.distance *= Math.pow(1.5, this.deltaZoom * smoothing * this.speed * delta * 0.06);
this.deltaZoom *= 1 - smoothing;
if (Math.abs(this.deltaZoom) < 0.0001) {
this.deltaZoom = 0;
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneDown(evt, ...KeyZoomControls.KEYS.IN)){
this.in = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyZoomControls.KEYS.OUT)){
this.out = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyZoomControls.KEYS.IN)){
this.in = false;
}
if (KeyCombination.oneUp(evt, ...KeyZoomControls.KEYS.OUT)){
this.out = false;
}
}
}

View File

@ -0,0 +1,130 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
export class MouseAngleControls {
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.moving = false;
this.lastY = 0;
this.deltaAngle = 0;
this.speed = speed;
this.stiffness = stiffness;
this.pixelToSpeedMultiplierY = 0;
this.updatePixelToSpeedMultiplier();
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
window.addEventListener("resize", this.updatePixelToSpeedMultiplier);
}
stop() {
this.target.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
window.removeEventListener("resize", this.updatePixelToSpeedMultiplier);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaAngle === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.angle += this.deltaAngle * smoothing * this.speed * this.pixelToSpeedMultiplierY;
this.deltaAngle *= 1 - smoothing;
if (Math.abs(this.deltaAngle) < 0.0001) {
this.deltaAngle = 0;
}
}
reset() {
this.deltaAngle = 0;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseDown = evt => {
if ((evt.buttons !== undefined ? evt.buttons === 2 : evt.button === 2) ||
((evt.altKey || evt.ctrlKey) && (evt.buttons !== undefined ? evt.buttons === 1 : evt.button === 0))) {
this.moving = true;
this.deltaAngle = 0;
this.lastY = evt.y;
}
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseMove = evt => {
if(this.moving){
this.deltaAngle -= evt.y - this.lastY;
}
this.lastY = evt.y;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseUp = evt => {
this.moving = false;
}
updatePixelToSpeedMultiplier = () => {
this.pixelToSpeedMultiplierY = 1 / this.target.clientHeight;
}
}

View File

@ -0,0 +1,140 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Vector2} from "three";
import {VEC2_ZERO} from "../../../util/Utils";
export class MouseMoveControls {
static tempVec2_1 = new Vector2();
/**
* @param target {Element}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.moving = false;
this.lastPosition = new Vector2();
this.deltaPosition = new Vector2();
this.speed = speed;
this.stiffness = stiffness;
this.pixelToSpeedMultiplierX = 0;
this.pixelToSpeedMultiplierY = 0;
this.updatePixelToSpeedMultiplier();
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
window.addEventListener("resize", this.updatePixelToSpeedMultiplier);
}
stop() {
this.target.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
window.removeEventListener("resize", this.updatePixelToSpeedMultiplier);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaPosition.x === 0 && this.deltaPosition.y === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
let directionDelta = MouseMoveControls.tempVec2_1.copy(this.deltaPosition);
directionDelta.rotateAround(VEC2_ZERO, this.manager.rotation);
this.manager.position.x += directionDelta.x * smoothing * this.manager.distance * this.speed * this.pixelToSpeedMultiplierX;
this.manager.position.z += directionDelta.y * smoothing * this.manager.distance * this.speed * this.pixelToSpeedMultiplierY;
this.deltaPosition.multiplyScalar(1 - smoothing);
if (this.deltaPosition.lengthSq() < 0.0001) {
this.deltaPosition.set(0, 0);
}
}
reset() {
this.deltaPosition.set(0, 0);
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseDown = evt => {
if ((evt.buttons !== undefined ? evt.buttons === 1 : evt.button === 0) && !evt.altKey) {
this.moving = true;
this.deltaPosition.set(0, 0);
this.lastPosition.set(evt.x, evt.y);
}
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseMove = evt => {
let position = MouseMoveControls.tempVec2_1.set(evt.x, evt.y);
if(this.moving){
this.deltaPosition.sub(position).add(this.lastPosition);
}
this.lastPosition.copy(position);
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseUp = evt => {
if (evt.button === 0) this.moving = false;
}
updatePixelToSpeedMultiplier = () => {
this.pixelToSpeedMultiplierX = (1 / this.target.clientWidth) * (this.target.clientWidth / this.target.clientHeight);
this.pixelToSpeedMultiplierY = 1 / this.target.clientHeight;
}
}

View File

@ -0,0 +1,130 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
export class MouseRotateControls {
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.moving = false;
this.lastX = 0;
this.deltaRotation = 0;
this.speed = speed;
this.stiffness = stiffness;
this.pixelToSpeedMultiplierX = 0;
this.updatePixelToSpeedMultiplier();
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
window.addEventListener("resize", this.updatePixelToSpeedMultiplier);
}
stop() {
this.target.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
window.removeEventListener("resize", this.updatePixelToSpeedMultiplier);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaRotation === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.rotation += this.deltaRotation * smoothing * this.speed * this.pixelToSpeedMultiplierX;
this.deltaRotation *= 1 - smoothing;
if (Math.abs(this.deltaRotation) < 0.0001) {
this.deltaRotation = 0;
}
}
reset() {
this.deltaRotation = 0;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseDown = evt => {
if ((evt.buttons !== undefined ? evt.buttons === 2 : evt.button === 2) ||
((evt.altKey || evt.ctrlKey) && (evt.buttons !== undefined ? evt.buttons === 1 : evt.button === 0))) {
this.moving = true;
this.deltaRotation = 0;
this.lastX = evt.x;
}
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseMove = evt => {
if(this.moving){
this.deltaRotation += evt.x - this.lastX;
}
this.lastX = evt.x;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseUp = evt => {
this.moving = false;
}
updatePixelToSpeedMultiplier = () => {
this.pixelToSpeedMultiplierX = (1 / this.target.clientWidth); //* (this.target.clientWidth / this.target.clientHeight);
}
}

View File

@ -0,0 +1,92 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
export class MouseZoomControls {
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.stiffness = stiffness;
this.speed = speed;
this.deltaZoom = 0;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("wheel", this.onMouseWheel, {passive: true});
}
stop() {
this.target.removeEventListener("wheel", this.onMouseWheel);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaZoom === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.distance *= Math.pow(1.5, this.deltaZoom * smoothing * this.speed);
this.deltaZoom *= 1 - smoothing;
if (Math.abs(this.deltaZoom) < 0.0001) {
this.deltaZoom = 0;
}
}
reset() {
this.deltaZoom = 0;
}
/**
* @private
* @param evt {WheelEvent}
*/
onMouseWheel = evt => {
let delta = evt.deltaY;
if (evt.deltaMode === WheelEvent.DOM_DELTA_PIXEL) delta *= 0.01;
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) delta *= 0.33;
this.deltaZoom += delta;
}
}

View File

@ -0,0 +1,131 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
export class TouchAngleControls {
/**
* @param target {Element}
* @param hammer {Manager}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, hammer, speed, stiffness) {
this.target = target;
this.hammer = hammer;
this.manager = null;
this.moving = false;
this.lastY = 0;
this.deltaAngle = 0;
this.speed = speed;
this.stiffness = stiffness;
this.pixelToSpeedMultiplierY = 0;
this.updatePixelToSpeedMultiplier();
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.hammer.on("tiltstart", this.onTouchDown);
this.hammer.on("tiltmove", this.onTouchMove);
this.hammer.on("tiltend", this.onTouchUp);
this.hammer.on("tiltcancel", this.onTouchUp);
window.addEventListener("resize", this.updatePixelToSpeedMultiplier);
}
stop() {
this.hammer.off("tiltstart", this.onTouchDown);
this.hammer.off("tiltmove", this.onTouchMove);
this.hammer.off("tiltend", this.onTouchUp);
this.hammer.off("tiltcancel", this.onTouchUp);
window.removeEventListener("resize", this.updatePixelToSpeedMultiplier);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaAngle === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.angle += this.deltaAngle * smoothing * this.speed * this.pixelToSpeedMultiplierY;
this.deltaAngle *= 1 - smoothing;
if (Math.abs(this.deltaAngle) < 0.0001) {
this.deltaAngle = 0;
}
}
reset() {
this.deltaAngle = 0;
}
/**
* @private
* @param evt {object}
*/
onTouchDown = evt => {
this.moving = true;
this.deltaAngle = 0;
this.lastY = evt.center.y;
}
/**
* @private
* @param evt {object}
*/
onTouchMove = evt => {
if(this.moving){
this.deltaAngle -= evt.center.y - this.lastY;
}
this.lastY = evt.center.y;
}
/**
* @private
* @param evt {object}
*/
onTouchUp = evt => {
this.moving = false;
}
updatePixelToSpeedMultiplier = () => {
this.pixelToSpeedMultiplierY = 1 / this.target.clientHeight;
}
}

View File

@ -0,0 +1,148 @@
import {MathUtils, Vector2} from "three";
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {VEC2_ZERO} from "../../../util/Utils";
export class TouchMoveControls {
static tempVec2_1 = new Vector2();
/**
* @param target {Element}
* @param hammer {Manager}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, hammer, speed, stiffness) {
this.target = target;
this.hammer = hammer;
this.manager = null;
this.moving = false;
this.lastPosition = new Vector2();
this.deltaPosition = new Vector2();
this.speed = speed;
this.stiffness = stiffness;
this.pixelToSpeedMultiplierX = 0;
this.pixelToSpeedMultiplierY = 0;
this.updatePixelToSpeedMultiplier();
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.hammer.on("movestart", this.onTouchDown);
this.hammer.on("movemove", this.onTouchMove);
this.hammer.on("moveend", this.onTouchUp);
this.hammer.on("movecancel", this.onTouchUp);
window.addEventListener("resize", this.updatePixelToSpeedMultiplier);
}
stop() {
this.hammer.off("movestart", this.onTouchDown);
this.hammer.off("movemove", this.onTouchMove);
this.hammer.off("moveend", this.onTouchUp);
this.hammer.off("movecancel", this.onTouchUp);
window.removeEventListener("resize", this.updatePixelToSpeedMultiplier);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaPosition.x === 0 && this.deltaPosition.y === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
let directionDelta = TouchMoveControls.tempVec2_1.copy(this.deltaPosition);
directionDelta.rotateAround(VEC2_ZERO, this.manager.rotation);
this.manager.position.x += directionDelta.x * smoothing * this.manager.distance * this.speed * this.pixelToSpeedMultiplierX;
this.manager.position.z += directionDelta.y * smoothing * this.manager.distance * this.speed * this.pixelToSpeedMultiplierY;
this.deltaPosition.multiplyScalar(1 - smoothing);
if (this.deltaPosition.lengthSq() < 0.0001) {
this.deltaPosition.set(0, 0);
}
}
reset() {
this.deltaPosition.set(0, 0);
}
/**
* @private
* @param evt {object}
*/
onTouchDown = evt => {
if (evt.pointerType === "mouse") return;
this.moving = true;
this.deltaPosition.set(0, 0);
this.lastPosition.set(evt.center.x, evt.center.y);
}
/**
* @private
* @param evt {object}
*/
onTouchMove = evt => {
if (evt.pointerType === "mouse") return;
let position = TouchMoveControls.tempVec2_1.set(evt.center.x, evt.center.y);
if(this.moving){
this.deltaPosition.sub(position).add(this.lastPosition);
}
this.lastPosition.copy(position);
}
/**
* @private
* @param evt {object}
*/
onTouchUp = evt => {
if (evt.pointerType === "mouse") return;
this.moving = false;
}
updatePixelToSpeedMultiplier = () => {
this.pixelToSpeedMultiplierX = (1 / this.target.clientWidth) * (this.target.clientWidth / this.target.clientHeight);
this.pixelToSpeedMultiplierY = 1 / this.target.clientHeight;
}
}

View File

@ -0,0 +1,122 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils} from "three";
export class TouchRotateControls {
/**
* @param hammer {Manager}
* @param speed {number}
* @param stiffness {number}
*/
constructor(hammer, speed, stiffness) {
this.hammer = hammer;
this.manager = null;
this.moving = false;
this.lastRotation = 0;
this.deltaRotation = 0;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.hammer.on("rotatestart", this.onTouchDown);
this.hammer.on("rotatemove", this.onTouchMove);
this.hammer.on("rotateend", this.onTouchUp);
this.hammer.on("rotatecancel", this.onTouchUp);
}
stop() {
this.hammer.off("rotatestart", this.onTouchDown);
this.hammer.off("rotatemove", this.onTouchMove);
this.hammer.off("rotateend", this.onTouchUp);
this.hammer.off("rotatecancel", this.onTouchUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaRotation === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.rotation += this.deltaRotation * smoothing * this.speed;
this.deltaRotation *= 1 - smoothing;
if (Math.abs(this.deltaRotation) < 0.0001) {
this.deltaRotation = 0;
}
}
reset() {
this.deltaRotation = 0;
}
/**
* @private
* @param evt {object}
*/
onTouchDown = evt => {
this.moving = true;
this.deltaRotation = 0;
this.lastRotation = evt.rotation;
}
/**
* @private
* @param evt {object}
*/
onTouchMove = evt => {
if(this.moving){
let delta = evt.rotation - this.lastRotation;
if (delta > 180) delta -= 360;
if (delta < -180) delta += 360;
this.deltaRotation -= delta;
}
this.lastRotation = evt.rotation;
}
/**
* @private
* @param evt {object}
*/
onTouchUp = evt => {
this.moving = false;
}
}

View File

@ -0,0 +1,103 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
export class TouchZoomControls {
/**
* @param hammer {Manager}
*/
constructor(hammer) {
this.hammer = hammer;
this.manager = null;
this.moving = false;
this.deltaZoom = 1;
this.lastZoom = 1;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.hammer.on("zoomstart", this.onTouchDown);
this.hammer.on("zoommove", this.onTouchMove);
this.hammer.on("zoomend", this.onTouchUp);
this.hammer.on("zoomcancel", this.onTouchUp);
}
stop() {
this.hammer.off("zoomstart", this.onTouchDown);
this.hammer.off("zoommove", this.onTouchMove);
this.hammer.off("zoomend", this.onTouchUp);
this.hammer.off("zoomcancel", this.onTouchUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaZoom === 1) return;
this.manager.distance /= this.deltaZoom;
this.deltaZoom = 1;
}
reset() {
this.deltaZoom = 1;
}
/**
* @private
* @param evt {object}
*/
onTouchDown = evt => {
this.moving = true;
this.lastZoom = 1;
}
/**
* @private
* @param evt {object}
*/
onTouchMove = evt => {
if(this.moving){
this.deltaZoom *= evt.scale / this.lastZoom;
}
this.lastZoom = evt.scale;
}
/**
* @private
* @param evt {object}
*/
onTouchUp = evt => {
this.moving = false;
}
}

View File

@ -0,0 +1,138 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {pathFromCoords} from "../util/Utils";
import {
TextureLoader,
Mesh,
PlaneBufferGeometry,
VertexColors,
FrontSide,
ShaderMaterial,
NearestFilter,
ClampToEdgeWrapping,
NearestMipMapLinearFilter,
Vector2
} from "three";
export class LowresTileLoader {
constructor(tilePath, tileSettings, lod, vertexShader, fragmentShader, uniforms, loadBlocker = () => Promise.resolve(), tileCacheHash = 0) {
Object.defineProperty( this, 'isLowresTileLoader', { value: true } );
this.tilePath = tilePath;
this.tileSettings = tileSettings;
this.lod = lod;
this.loadBlocker = loadBlocker;
this.tileCacheHash = tileCacheHash;
this.vertexShader = vertexShader;
this.fragmentShader = fragmentShader;
this.uniforms = uniforms;
this.textureLoader = new TextureLoader();
this.geometry = new PlaneBufferGeometry(
tileSettings.tileSize.x + 1, tileSettings.tileSize.z + 1,
Math.ceil(100 / (lod * 2)), Math.ceil(100 / (lod * 2))
);
this.geometry.deleteAttribute('normal');
this.geometry.deleteAttribute('uv');
this.geometry.rotateX(-Math.PI / 2);
this.geometry.translate(tileSettings.tileSize.x / 2 + 1, 0, tileSettings.tileSize.x / 2 + 1);
}
load = (tileX, tileZ, cancelCheck = () => false) => {
let tileUrl = this.tilePath + this.lod + "/" + pathFromCoords(tileX, tileZ) + '.png';
//await this.loadBlocker();
return new Promise((resolve, reject) => {
this.textureLoader.load(tileUrl + '?' + this.tileCacheHash,
async texture => {
texture.anisotropy = 1;
texture.generateMipmaps = false;
texture.magFilter = NearestFilter;
texture.minFilter = texture.generateMipmaps ? NearestMipMapLinearFilter : NearestFilter;
texture.wrapS = ClampToEdgeWrapping;
texture.wrapT = ClampToEdgeWrapping;
texture.flipY = false;
texture.flatShading = true;
await this.loadBlocker();
if (cancelCheck()){
texture.dispose();
reject({status: "cancelled"});
return;
}
const scale = Math.pow(this.tileSettings.lodFactor, this.lod - 1);
let material = new ShaderMaterial({
uniforms: {
...this.uniforms,
tileSize: {
value: new Vector2(this.tileSettings.tileSize.x, this.tileSettings.tileSize.z)
},
textureSize: {
value: new Vector2(texture.image.width, texture.image.height)
},
textureImage: {
type: 't',
value: texture
},
lod: {
value: this.lod
},
lodScale: {
value: scale
}
},
vertexShader: this.vertexShader,
fragmentShader: this.fragmentShader,
transparent: false,
depthWrite: true,
depthTest: true,
vertexColors: VertexColors,
side: FrontSide,
wireframe: false,
});
let object = new Mesh(this.geometry, material);
object.position.set(tileX * this.tileSettings.tileSize.x * scale, 0, tileZ * this.tileSettings.tileSize.z * scale);
object.scale.set(scale, 1, scale);
object.userData.tileUrl = tileUrl;
object.userData.tileType = "lowres";
object.updateMatrixWorld(true);
resolve(object);
},
undefined,
reject
);
});
}
}

View File

@ -0,0 +1,434 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {
ClampToEdgeWrapping,
Color,
FileLoader,
FrontSide,
NearestFilter,
NearestMipMapLinearFilter,
Raycaster,
Scene,
ShaderMaterial,
Texture,
Vector3,
VertexColors
} from "three";
import {alert, dispatchEvent, generateCacheHash, getPixel, hashTile, stringToImage, vecArrToObj} from "../util/Utils";
import {TileManager} from "./TileManager";
import {TileLoader} from "./TileLoader";
import {LowresTileLoader} from "./LowresTileLoader";
export class Map {
/**
* @param id {string}
* @param dataUrl {string}
* @param loadBlocker {function: Promise<void>}
* @param events {EventTarget}
*/
constructor(id, dataUrl, loadBlocker, events = null) {
Object.defineProperty( this, 'isMap', { value: true } );
this.loadBlocker = loadBlocker;
this.events = events;
this.data = {
id: id,
sorting: 0,
dataUrl: dataUrl,
settingsUrl: dataUrl + "settings.json",
texturesUrl: dataUrl + "textures.json",
name: id,
startPos: {x: 0, z: 0},
skyColor: new Color(),
ambientLight: 0,
hires: {
tileSize: {x: 32, z: 32},
scale: {x: 1, z: 1},
translate: {x: 2, z: 2}
},
lowres: {
tileSize: {x: 32, z: 32},
lodFactor: 5,
lodCount: 3
}
};
this.raycaster = new Raycaster();
/** @type {ShaderMaterial[]} */
this.hiresMaterial = null;
/** @type {ShaderMaterial} */
this.lowresMaterial = null;
/** @type {Texture[]} */
this.loadedTextures = [];
/** @type {TileManager} */
this.hiresTileManager = null;
/** @type {TileManager[]} */
this.lowresTileManager = null;
}
/**
* Loads textures and materials for this map so it is ready to load map-tiles
* @param hiresVertexShader {string}
* @param hiresFragmentShader {string}
* @param lowresVertexShader {string}
* @param lowresFragmentShader {string}
* @param uniforms {object}
* @param tileCacheHash {number}
* @returns {Promise<void>}
*/
load(hiresVertexShader, hiresFragmentShader, lowresVertexShader, lowresFragmentShader, uniforms, tileCacheHash = 0) {
this.unload()
let settingsPromise = this.loadSettings();
let textureFilePromise = this.loadTexturesFile();
this.lowresMaterial = this.createLowresMaterial(lowresVertexShader, lowresFragmentShader, uniforms);
return Promise.all([settingsPromise, textureFilePromise])
.then(values => {
let textures = values[1];
if (textures === null) throw new Error("Failed to parse textures.json!");
this.hiresMaterial = this.createHiresMaterial(hiresVertexShader, hiresFragmentShader, uniforms, textures);
this.hiresTileManager = new TileManager(new TileLoader(`${this.data.dataUrl}tiles/0/`, this.hiresMaterial, this.data.hires, this.loadBlocker, tileCacheHash), this.onTileLoad("hires"), this.onTileUnload("hires"), this.events);
this.hiresTileManager.scene.autoUpdate = false;
this.lowresTileManager = [];
for (let i = 0; i < this.data.lowres.lodCount; i++) {
this.lowresTileManager[i] = new TileManager(new LowresTileLoader(`${this.data.dataUrl}tiles/`, this.data.lowres, i + 1, lowresVertexShader, lowresFragmentShader, uniforms, async () => {}, tileCacheHash), this.onTileLoad("lowres"), this.onTileUnload("lowres"), this.events);
this.lowresTileManager[i].scene.autoUpdate = false;
}
alert(this.events, `Map '${this.data.id}' is loaded.`, "fine");
});
}
/**
* Loads the settings of this map
* @returns {Promise<void>}
*/
loadSettings() {
return this.loadSettingsFile()
.then(worldSettings => {
this.data.name = worldSettings.name ? worldSettings.name : this.data.name;
this.data.sorting = worldSettings.sorting ? worldSettings.sorting : this.data.sorting;
this.data.startPos = {...this.data.startPos, ...vecArrToObj(worldSettings.startPos, true)};
if (worldSettings.skyColor && worldSettings.skyColor.length >= 3) {
this.data.skyColor.setRGB(
worldSettings.skyColor[0],
worldSettings.skyColor[1],
worldSettings.skyColor[2]
);
}
this.data.ambientLight = worldSettings.ambientLight ? worldSettings.ambientLight : this.data.ambientLight;
if (worldSettings.hires === undefined) worldSettings.hires = {};
if (worldSettings.lowres === undefined) worldSettings.lowres = {};
this.data.hires = {
tileSize: {...this.data.hires.tileSize, ...vecArrToObj(worldSettings.hires.tileSize, true)},
scale: {...this.data.hires.scale, ...vecArrToObj(worldSettings.hires.scale, true)},
translate: {...this.data.hires.translate, ...vecArrToObj(worldSettings.hires.translate, true)}
};
this.data.lowres = {
tileSize: {...this.data.lowres.tileSize, ...vecArrToObj(worldSettings.lowres.tileSize, true)},
lodFactor: worldSettings.lowres.lodFactor !== undefined ? worldSettings.lowres.lodFactor : this.data.lowres.lodFactor,
lodCount: worldSettings.lowres.lodCount !== undefined ? worldSettings.lowres.lodCount : this.data.lowres.lodCount
};
alert(this.events, `Settings for map '${this.data.id}' loaded.`, "fine");
});
}
onTileLoad = layer => tile => {
dispatchEvent(this.events, "bluemapMapTileLoaded", {
tile: tile,
layer: layer
});
}
onTileUnload = layer => tile => {
dispatchEvent(this.events, "bluemapMapTileUnloaded", {
tile: tile,
layer: layer
});
}
/**
* @param x {number}
* @param z {number}
* @param hiresViewDistance {number}
* @param lowresViewDistance {number}
*/
loadMapArea(x, z, hiresViewDistance, lowresViewDistance) {
if (!this.isLoaded) return;
for (let i = this.lowresTileManager.length - 1; i >= 0; i--) {
const lod = i + 1;
const scale = Math.pow(this.data.lowres.lodFactor, lod - 1);
const lowresX = Math.floor(x / (this.data.lowres.tileSize.x * scale));
const lowresZ = Math.floor(z / (this.data.lowres.tileSize.z * scale));
const lowresViewX = Math.floor(lowresViewDistance / this.data.lowres.tileSize.x);
const lowresViewZ = Math.floor(lowresViewDistance / this.data.lowres.tileSize.z);
this.lowresTileManager[i].loadAroundTile(lowresX, lowresZ, lowresViewX, lowresViewZ);
}
const hiresX = Math.floor((x - this.data.hires.translate.x) / this.data.hires.tileSize.x);
const hiresZ = Math.floor((z - this.data.hires.translate.z) / this.data.hires.tileSize.z);
const hiresViewX = Math.floor(hiresViewDistance / this.data.hires.tileSize.x);
const hiresViewZ = Math.floor(hiresViewDistance / this.data.hires.tileSize.z);
this.hiresTileManager.loadAroundTile(hiresX, hiresZ, hiresViewX, hiresViewZ);
}
/**
* Loads the settings.json file for this map
* @returns {Promise<Object>}
*/
loadSettingsFile() {
return new Promise((resolve, reject) => {
alert(this.events, `Loading settings for map '${this.data.id}'...`, "fine");
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(this.data.settingsUrl + "?" + generateCacheHash(),
resolve,
() => {},
() => reject(`Failed to load the settings.json for map: ${this.data.id}`)
)
});
}
/**
* Loads the textures.json file for this map
* @returns {Promise<Object>}
*/
loadTexturesFile() {
return new Promise((resolve, reject) => {
alert(this.events, `Loading textures for map '${this.data.id}'...`, "fine");
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(this.data.texturesUrl + "?" + generateCacheHash(),
resolve,
() => {},
() => reject(`Failed to load the textures.json for map: ${this.data.id}`)
)
});
}
/**
* Creates a hires Material with the given textures
* @param vertexShader {string}
* @param fragmentShader {string}
* @param uniforms {object}
* @param textures {{
* resourcePath: string,
* color: number[],
* halfTransparent: boolean,
* texture: string
* }[]} the textures-data
* @returns {ShaderMaterial[]} the hires Material (array because its a multi-material)
*/
createHiresMaterial(vertexShader, fragmentShader, uniforms, textures) {
let materials = [];
if (!Array.isArray(textures)) throw new Error("Invalid texture.json: 'textures' is not an array!")
for (let i = 0; i < textures.length; i++) {
let textureSettings = textures[i];
let color = textureSettings.color;
if (!Array.isArray(color) || color.length < 4){
color = [0, 0, 0, 0];
}
let opaque = color[3] === 1;
let transparent = !!textureSettings.halfTransparent;
let texture = new Texture();
texture.image = stringToImage(textureSettings.texture);
texture.anisotropy = 1;
texture.generateMipmaps = opaque || transparent;
texture.magFilter = NearestFilter;
texture.minFilter = texture.generateMipmaps ? NearestMipMapLinearFilter : NearestFilter;
texture.wrapS = ClampToEdgeWrapping;
texture.wrapT = ClampToEdgeWrapping;
texture.flipY = false;
texture.flatShading = true;
texture.image.addEventListener("load", () => texture.needsUpdate = true);
this.loadedTextures.push(texture);
let material = new ShaderMaterial({
uniforms: {
...uniforms,
textureImage: {
type: 't',
value: texture
},
transparent: { value: transparent }
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: transparent,
depthWrite: true,
depthTest: true,
vertexColors: VertexColors,
side: FrontSide,
wireframe: false,
});
material.needsUpdate = true;
materials[i] = material;
}
return materials;
}
/**
* Creates a lowres Material
* @param vertexShader {string}
* @param fragmentShader {string}
* @param uniforms {object}
* @returns {ShaderMaterial} the hires Material
*/
createLowresMaterial(vertexShader, fragmentShader, uniforms) {
return new ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: false,
depthWrite: true,
depthTest: true,
vertexColors: VertexColors,
side: FrontSide,
wireframe: false
});
}
unload() {
if (this.hiresTileManager) this.hiresTileManager.unload();
this.hiresTileManager = null;
if (this.lowresTileManager) {
for (let i = 0; i < this.lowresTileManager.length; i++) {
this.lowresTileManager[i].unload();
}
this.lowresTileManager = null;
}
if (this.hiresMaterial) this.hiresMaterial.forEach(material => material.dispose());
this.hiresMaterial = null;
if (this.lowresMaterial) this.lowresMaterial.dispose();
this.lowresMaterial = null;
this.loadedTextures.forEach(texture => texture.dispose());
this.loadedTextures = [];
}
/**
* Ray-traces and returns the terrain-height at a specific location, returns <code>false</code> if there is no map-tile loaded at that location
* @param x {number}
* @param z {number}
* @returns {boolean|number}
*/
terrainHeightAt(x, z) {
if (!this.isLoaded) return false;
this.raycaster.set(
new Vector3(x, 300, z), // ray-start
new Vector3(0, -1, 0) // ray-direction
);
this.raycaster.near = 1;
this.raycaster.far = 300;
this.raycaster.layers.enableAll();
let hiresTileHash = hashTile(Math.floor((x - this.data.hires.translate.x) / this.data.hires.tileSize.x), Math.floor((z - this.data.hires.translate.z) / this.data.hires.tileSize.z));
let tile = this.hiresTileManager.tiles.get(hiresTileHash);
if (tile?.model) {
try {
let intersects = this.raycaster.intersectObjects([tile.model]);
if (intersects.length > 0) {
return intersects[0].point.y;
}
} catch (ignore) {
//empty
}
}
for (let i = 0; i < this.lowresTileManager.length; i++) {
const lod = i + 1;
const scale = Math.pow(this.data.lowres.lodFactor, lod - 1);
const scaledTileSize = {
x: this.data.lowres.tileSize.x * scale,
z: this.data.lowres.tileSize.z * scale
}
const tileX = Math.floor(x / scaledTileSize.x);
const tileZ = Math.floor(z / scaledTileSize.z);
let lowresTileHash = hashTile(tileX, tileZ);
tile = this.lowresTileManager[i].tiles.get(lowresTileHash);
if (!tile || !tile.model) continue;
const texture = tile.model.material.uniforms?.textureImage?.value?.image;
if (texture == null) continue;
const color = getPixel(texture, x - tileX * scaledTileSize.x, z - tileZ * scaledTileSize.z + this.data.lowres.tileSize.z + 1);
let heightUnsigned = color[1] * 256.0 + color[2];
if (heightUnsigned >= 32768.0) {
return -(65535.0 - heightUnsigned);
} else {
return heightUnsigned;
}
}
return false;
}
dispose() {
this.unload();
}
/**
* @returns {boolean}
*/
get isLoaded() {
return !!(this.hiresMaterial && this.lowresMaterial);
}
}

View File

@ -0,0 +1,103 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
export class Tile {
/**
* @param x {number}
* @param z {number}
* @param onLoad {function(Tile)}
* @param onUnload {function(Tile)}
*/
constructor(x, z, onLoad, onUnload) {
Object.defineProperty( this, 'isTile', { value: true } );
/** @type {THREE.Mesh} */
this.model = null;
this.onLoad = onLoad;
this.onUnload = onUnload;
this.x = x;
this.z = z;
this.unloaded = true;
this.loading = false;
}
/**
* @param tileLoader {TileLoader}
* @returns {Promise<void>}
*/
load(tileLoader) {
if (this.loading) return Promise.reject("tile is already loading!");
this.loading = true;
this.unload();
this.unloaded = false;
return tileLoader.load(this.x, this.z, () => this.unloaded)
.then(model => {
if (this.unloaded){
Tile.disposeModel(model);
return;
}
this.model = model;
this.onLoad(this);
})
.finally(() => {
this.loading = false;
});
}
unload() {
this.unloaded = true;
if (this.model) {
this.onUnload(this);
Tile.disposeModel(this.model);
this.model = null;
}
}
static disposeModel(model) {
if (model.userData?.tileType === "hires") {
model.geometry.dispose();
}
else if (model.userData?.tileType === "lowres") {
model.material.uniforms.textureImage.value.dispose();
model.material.dispose();
}
}
/**
* @returns {boolean}
*/
get loaded() {
return !!this.model;
}
}

View File

@ -0,0 +1,100 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {pathFromCoords} from "../util/Utils";
import {BufferGeometryLoader, FileLoader, Mesh} from "three";
export class TileLoader {
/**
* @param tilePath {string}
* @param material {THREE.Material | THREE.Material[]}
* @param tileSettings {{
* tileSize: {x: number, z: number},
* scale: {x: number, z: number},
* translate: {x: number, z: number}
* }}
* @param loadBlocker {function: Promise}
* @param tileCacheHash {number}
*/
constructor(tilePath, material, tileSettings, loadBlocker = () => Promise.resolve(), tileCacheHash = 0) {
Object.defineProperty( this, 'isTileLoader', { value: true } );
this.tilePath = tilePath;
this.material = material;
this.tileSettings = tileSettings;
this.tileCacheHash = tileCacheHash;
this.loadBlocker = loadBlocker;
this.fileLoader = new FileLoader();
this.fileLoader.setResponseType('json');
this.bufferGeometryLoader = new BufferGeometryLoader();
}
load = (tileX, tileZ, cancelCheck = () => false) => {
let tileUrl = this.tilePath + pathFromCoords(tileX, tileZ) + '.json';
//await this.loadBlocker();
return new Promise((resolve, reject) => {
this.fileLoader.load(tileUrl + '?' + this.tileCacheHash,
async json => {
let geometryJson = json.tileGeometry || {};
if (!geometryJson.type || geometryJson.type !== 'BufferGeometry'){
reject({status: "empty"});
return;
}
await this.loadBlocker();
if (cancelCheck()){
reject({status: "cancelled"});
return;
}
let geometry = this.bufferGeometryLoader.parse(geometryJson);
let object = new Mesh(geometry, this.material);
let tileSize = this.tileSettings.tileSize;
let translate = this.tileSettings.translate;
let scale = this.tileSettings.scale;
object.position.set(tileX * tileSize.x + translate.x, 0, tileZ * tileSize.z + translate.z);
object.scale.set(scale.x, 1, scale.z);
object.userData.tileUrl = tileUrl;
object.userData.tileType = "hires";
object.updateMatrixWorld(true);
resolve(object);
},
() => {},
reject
);
});
}
}

View File

@ -0,0 +1,221 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { Vector2, Scene, Group } from 'three';
import { Tile } from './Tile.js';
import {alert, hashTile} from '../util/Utils.js';
import {TileMap} from "./TileMap";
export class TileManager {
static tileMapSize = 100;
static tileMapHalfSize = TileManager.tileMapSize / 2;
/**
* @param tileLoader {TileLoader | LowresTileLoader}
* @param onTileLoad {function(Tile)}
* @param onTileUnload {function(Tile)}
* @param events {EventTarget}
*/
constructor(tileLoader, onTileLoad = null, onTileUnload = null, events = null) {
Object.defineProperty( this, 'isTileManager', { value: true } );
this.sceneParent = new Scene();
this.scene = new Group();
this.sceneParent.add(this.scene);
this.events = events;
this.tileLoader = tileLoader;
this.onTileLoad = onTileLoad || function(){};
this.onTileUnload = onTileUnload || function(){};
this.viewDistanceX = 1;
this.viewDistanceZ = 1;
this.centerTile = new Vector2(0, 0);
this.currentlyLoading = 0;
this.loadTimeout = null;
//map of loaded tiles
this.tiles = new Map();
// a canvas that keeps track of the loaded tiles, used for shaders
this.tileMap = new TileMap(TileManager.tileMapSize, TileManager.tileMapSize);
this.unloaded = true;
}
/**
* @param x {number}
* @param z {number}
* @param viewDistanceX {number}
* @param viewDistanceZ {number}
*/
loadAroundTile(x, z, viewDistanceX, viewDistanceZ) {
this.unloaded = false;
let unloadTiles = false;
if (this.viewDistanceX > viewDistanceX || this.viewDistanceZ > viewDistanceZ) {
unloadTiles = true;
}
this.viewDistanceX = viewDistanceX;
this.viewDistanceZ = viewDistanceZ;
if (viewDistanceX <= 0 || viewDistanceZ <= 0) {
this.removeAllTiles();
return;
}
if (unloadTiles || this.centerTile.x !== x || this.centerTile.y !== z) {
this.centerTile.set(x, z);
this.removeFarTiles();
this.tileMap.setAll(TileMap.EMPTY);
this.tiles.forEach(tile => {
if (!tile.loading && !tile.unloaded) {
this.tileMap.setTile(tile.x - this.centerTile.x + TileManager.tileMapHalfSize, tile.z - this.centerTile.y + TileManager.tileMapHalfSize, TileMap.LOADED);
}
});
}
this.loadCloseTiles();
}
unload() {
this.unloaded = true;
this.removeAllTiles();
}
removeFarTiles() {
this.tiles.forEach((tile, hash, map) => {
if (
tile.x + this.viewDistanceX < this.centerTile.x ||
tile.x - this.viewDistanceX > this.centerTile.x ||
tile.z + this.viewDistanceZ < this.centerTile.y ||
tile.z - this.viewDistanceZ > this.centerTile.y
) {
tile.unload();
map.delete(hash);
}
});
}
removeAllTiles() {
this.tileMap.setAll(TileMap.EMPTY);
this.tiles.forEach(tile => {
tile.unload();
});
this.tiles.clear();
}
loadCloseTiles = () => {
if (this.unloaded) return;
if (!this.loadNextTile()) return;
if (this.loadTimeout) clearTimeout(this.loadTimeout);
if (this.currentlyLoading < 8) {
this.loadTimeout = setTimeout(this.loadCloseTiles, 0);
} else {
this.loadTimeout = setTimeout(this.loadCloseTiles, 1000);
}
}
/**
* @returns {boolean}
*/
loadNextTile() {
if (this.unloaded) return false;
let x = 0;
let z = 0;
let d = 1;
let m = 1;
while (m < Math.max(this.viewDistanceX, this.viewDistanceZ) * 2 + 1) {
while (2 * x * d < m) {
if (this.tryLoadTile(this.centerTile.x + x, this.centerTile.y + z)) return true;
x = x + d;
}
while (2 * z * d < m) {
if (this.tryLoadTile(this.centerTile.x + x, this.centerTile.y + z)) return true;
z = z + d;
}
d = -1 * d;
m = m + 1;
}
return false;
}
/**
* @param x {number}
* @param z {number}
* @returns {boolean}
*/
tryLoadTile(x, z) {
if (this.unloaded) return false;
if (Math.abs(x - this.centerTile.x) > this.viewDistanceX) return false;
if (Math.abs(z - this.centerTile.y) > this.viewDistanceZ) return false;
let tileHash = hashTile(x, z);
let tile = this.tiles.get(tileHash);
if (tile !== undefined) return false;
this.currentlyLoading++;
tile = new Tile(x, z, this.handleLoadedTile, this.handleUnloadedTile);
this.tiles.set(tileHash, tile);
tile.load(this.tileLoader)
.then(() => {
if (this.loadTimeout) clearTimeout(this.loadTimeout);
this.loadTimeout = setTimeout(this.loadCloseTiles, 0);
})
.catch(error => {})
.finally(() => {
this.currentlyLoading--;
});
return true;
}
handleLoadedTile = tile => {
this.tileMap.setTile(tile.x - this.centerTile.x + TileManager.tileMapHalfSize, tile.z - this.centerTile.y + TileManager.tileMapHalfSize, TileMap.LOADED);
this.scene.add(tile.model);
this.onTileLoad(tile);
}
handleUnloadedTile = tile => {
this.tileMap.setTile(tile.x - this.centerTile.x + TileManager.tileMapHalfSize, tile.z - this.centerTile.y + TileManager.tileMapHalfSize, TileMap.EMPTY);
this.scene.remove(tile.model);
this.onTileUnload(tile);
}
}

View File

@ -0,0 +1,81 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {ClampToEdgeWrapping, LinearFilter, NearestFilter, Texture} from "three";
export class TileMap {
static EMPTY = "#000";
static LOADED = "#fff";
/**
* @param width {number}
* @param height {number}
*/
constructor(width, height) {
this.canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
this.canvas.width = width;
this.canvas.height = height;
/**
* @type CanvasRenderingContext2D
*/
this.tileMapContext = this.canvas.getContext('2d', {
alpha: false,
willReadFrequently: true,
});
this.texture = new Texture(this.canvas);
this.texture.generateMipmaps = false;
this.texture.magFilter = LinearFilter;
this.texture.minFilter = LinearFilter;
this.texture.wrapS = ClampToEdgeWrapping;
this.texture.wrapT = ClampToEdgeWrapping;
this.texture.flipY = false;
this.texture.needsUpdate = true;
}
/**
* @param state {string}
*/
setAll(state) {
this.tileMapContext.fillStyle = state;
this.tileMapContext.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.texture.needsUpdate = true;
}
/**
* @param x {number}
* @param z {number}
* @param state {string}
*/
setTile(x, z, state) {
this.tileMapContext.fillStyle = state;
this.tileMapContext.fillRect(x, z, 1, 1);
this.texture.needsUpdate = true;
}
}

View File

@ -0,0 +1,66 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the 'Software'), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { ShaderChunk } from 'three';
export const HIRES_FRAGMENT_SHADER = `
${ShaderChunk.logdepthbuf_pars_fragment}
#ifndef texture
#define texture texture2D
#endif
uniform sampler2D textureImage;
uniform float sunlightStrength;
uniform float ambientLight;
varying vec3 vPosition;
//varying vec3 vWorldPosition;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vColor;
varying float vAo;
varying float vSunlight;
varying float vBlocklight;
//varying float vDistance;
void main() {
vec4 color = texture(textureImage, vUv);
if (color.a <= 0.01) discard;
//apply vertex-color
color.rgb *= vColor.rgb;
//apply ao
color.rgb *= vAo;
//apply light
float light = mix(vBlocklight, max(vSunlight, vBlocklight), sunlightStrength);
color.rgb *= mix(ambientLight, 1.0, light / 15.0);
gl_FragColor = color;
${ShaderChunk.logdepthbuf_fragment}
}
`;

View File

@ -0,0 +1,56 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the 'Software'), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { ShaderChunk } from 'three';
export const HIRES_VERTEX_SHADER = `
#include <common>
${ShaderChunk.logdepthbuf_pars_vertex}
attribute float ao;
attribute float sunlight;
attribute float blocklight;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vColor;
varying float vAo;
varying float vSunlight;
varying float vBlocklight;
void main() {
vPosition = position;
vNormal = normal;
vUv = uv;
vColor = color;
vAo = ao;
vSunlight = sunlight;
vBlocklight = blocklight;
gl_Position = projectionMatrix * (viewMatrix * modelMatrix * vec4(position, 1));
${ShaderChunk.logdepthbuf_vertex}
}
`;

View File

@ -0,0 +1,125 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the 'Software'), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { ShaderChunk } from 'three';
export const LOWRES_FRAGMENT_SHADER = `
${ShaderChunk.logdepthbuf_pars_fragment}
#define PI 3.1415926535897932
#ifndef texture
#define texture texture2D
#endif
struct TileMap {
sampler2D map;
float size;
vec2 scale;
vec2 translate;
vec2 pos;
};
uniform float sunlightStrength;
uniform float ambientLight;
uniform TileMap hiresTileMap;
uniform sampler2D textureImage;
uniform vec2 tileSize;
uniform vec2 textureSize;
uniform float lod;
uniform float lodScale;
varying vec3 vPosition;
varying vec3 vWorldPosition;
varying float vDistance;
float metaToHeight(vec4 meta) {
float heightUnsigned = meta.g * 65280.0 + meta.b * 255.0;
if (heightUnsigned >= 32768.0) {
return -(65535.0 - heightUnsigned);
} else {
return heightUnsigned;
}
}
float metaToLight(vec4 meta) {
return meta.r * 255.0;
}
vec2 posToColorUV(vec2 pos) {
return vec2(pos.x / textureSize.x, min(pos.y, tileSize.y) / textureSize.y);
}
vec2 posToMetaUV(vec2 pos) {
return vec2(pos.x / textureSize.x, pos.y / textureSize.y + 0.5);
}
void main() {
//discard if hires tile is loaded at that position
if (vDistance < 900.0 && texture(hiresTileMap.map, ((vWorldPosition.xz - hiresTileMap.translate) / hiresTileMap.scale - hiresTileMap.pos) / hiresTileMap.size + 0.5).r > 0.75) discard;
vec4 color = texture(textureImage, posToColorUV(vPosition.xz));
vec4 meta = texture(textureImage, posToMetaUV(vPosition.xz));
float height = metaToHeight(meta);
float heightX = metaToHeight(texture(textureImage, posToMetaUV(vPosition.xz + vec2(1.0, 0.0))));
float heightZ = metaToHeight(texture(textureImage, posToMetaUV(vPosition.xz + vec2(0.0, 1.0))));
float heightDiff = ((height - heightX) + (height - heightZ)) / lodScale;
float shade = clamp(heightDiff * 0.06, -0.2, 0.04);
float ao = 0.0;
float aoStrength = 0.0;
if(lod == 1.0) {
aoStrength = smoothstep(PI - 0.8, PI - 0.2, acos(-clamp(viewMatrix[1][2], 0.0, 1.0)));
aoStrength *= 1.0 - smoothstep(300.0, 500.0, vDistance);
if (aoStrength > 0.0) {
const float r = 3.0;
const float step = 0.2;
const float o = step / r * 0.1;
for (float vx = -r; vx <= r; vx++) {
for (float vz = -r; vz <= r; vz++) {
heightDiff = height - metaToHeight(texture(textureImage, posToMetaUV(vPosition.xz + vec2(vx * step, vz * step))));
if (heightDiff < 0.0) {
ao -= o;
}
}
}
}
}
color.rgb += mix(shade, shade * 0.3 + ao, aoStrength);
float blockLight = metaToLight(meta);
float light = mix(blockLight, 15.0, sunlightStrength);
color.rgb *= mix(ambientLight, 1.0, light / 15.0);
gl_FragColor = color;
${ShaderChunk.logdepthbuf_fragment}
}
`;

View File

@ -0,0 +1,69 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the 'Software'), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { ShaderChunk } from 'three';
export const LOWRES_VERTEX_SHADER = `
#include <common>
${ShaderChunk.logdepthbuf_pars_vertex}
uniform sampler2D textureImage;
uniform vec2 tileSize;
uniform vec2 textureSize;
varying vec3 vPosition;
varying vec3 vWorldPosition;
varying float vDistance;
float metaToHeight(vec4 meta) {
float heightUnsigned = meta.g * 65280.0 + meta.b * 255.0;
if (heightUnsigned >= 32768.0) {
return -(65535.0 - heightUnsigned);
} else {
return heightUnsigned;
}
}
vec2 posToMetaUV(vec2 pos) {
return vec2(pos.x / textureSize.x, pos.y / textureSize.y + 0.5);
}
void main() {
vPosition = position;
vec4 meta = texture(textureImage, posToMetaUV(position.xz));
vPosition.y = metaToHeight(meta) + 1.0 - position.x * 0.0001 - position.z * 0.0002; //including small offset-tilt to prevent z-fighting
vec4 worldPos = modelMatrix * vec4(vPosition, 1);
vec4 viewPos = viewMatrix * worldPos;
vWorldPosition = worldPos.xyz;
vDistance = -viewPos.z;
gl_Position = projectionMatrix * viewPos;
${ShaderChunk.logdepthbuf_vertex}
}
`;

View File

@ -0,0 +1,467 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {Color, DoubleSide, ExtrudeBufferGeometry, Mesh, ShaderMaterial, Shape, Vector2} from "three";
import {LineMaterial} from "../util/lines/LineMaterial";
import {MARKER_FILL_VERTEX_SHADER} from "./MarkerFillVertexShader";
import {MARKER_FILL_FRAGMENT_SHADER} from "./MarkerFillFragmentShader";
import {Line2} from "../util/lines/Line2";
import {deepEquals} from "../util/Utils";
import {LineSegmentsGeometry} from "../util/lines/LineSegmentsGeometry";
import {ObjectMarker} from "./ObjectMarker";
export class ExtrudeMarker extends ObjectMarker {
/**
* @param markerId {string}
*/
constructor(markerId) {
super(markerId);
Object.defineProperty(this, 'isExtrudeMarker', {value: true});
this.data.type = "extrude";
let zero = new Vector2();
let shape = new Shape([zero, zero, zero]);
this.fill = new ExtrudeMarkerFill(shape);
this.border = new ExtrudeMarkerBorder(shape);
this.border.renderOrder = -1; // render border before fill
this.add(this.border, this.fill);
this._markerData = {};
}
/**
* @param minY {number}
* @param maxY {number}
*/
setShapeY(minY, maxY) {
let relativeY = maxY - this.position.y;
let height = maxY - minY;
this.fill.position.y = relativeY;
this.border.position.y = relativeY;
this.fill.scale.y = height;
this.border.scale.y = height;
}
/**
* @param shape {Shape}
*/
setShape(shape) {
this.fill.updateGeometry(shape);
this.border.updateGeometry(shape);
}
/**
* @typedef {{r: number, g: number, b: number, a: number}} ColorLike
*/
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* label: string,
* detail: string,
* shape: {x: number, z: number}[],
* shapeMinY: number,
* shapeMaxY: number,
* link: string,
* newTab: boolean,
* depthTest: boolean,
* lineWidth: number,
* lineColor: ColorLike,
* fillColor: ColorLike,
* minDistance: number,
* maxDistance: number
* }}
*/
updateFromData(markerData) {
super.updateFromData(markerData);
// update shape only if needed, based on last update-data
if (
!this._markerData.shape || !deepEquals(markerData.shape, this._markerData.shape) ||
!this._markerData.position || !deepEquals(markerData.position, this._markerData.position)
){
this.setShape(this.createShapeFromData(markerData.shape));
}
// update shapeY
this.setShapeY((markerData.shapeMinY || 0) - 0.01, (markerData.shapeMaxY || 0) + 0.01); // offset by 0.01 to avoid z-fighting
// update depthTest
this.border.depthTest = !!markerData.depthTest;
this.fill.depthTest = !!markerData.depthTest;
// update border-width
this.border.linewidth = markerData.lineWidth !== undefined ? markerData.lineWidth : 2;
// update border-color
let bc = markerData.lineColor || {};
this.border.color.setRGB((bc.r || 0) / 255, (bc.g || 0) / 255, (bc.b || 0) / 255);
this.border.opacity = bc.a || 0;
// update fill-color
let fc = markerData.fillColor || {};
this.fill.color.setRGB((fc.r || 0) / 255, (fc.g || 0) / 255, (fc.b || 0) / 255);
this.fill.opacity = fc.a || 0;
// update min/max distances
let minDist = markerData.minDistance || 0;
let maxDist = markerData.maxDistance !== undefined ? markerData.maxDistance : Number.MAX_VALUE;
this.border.fadeDistanceMin = minDist;
this.border.fadeDistanceMax = maxDist;
this.fill.fadeDistanceMin = minDist;
this.fill.fadeDistanceMax = maxDist;
// save used marker data for next update
this._markerData = markerData;
}
dispose() {
super.dispose();
this.fill.dispose();
this.border.dispose();
}
/**
* @private
* Creates a shape from a data object, usually parsed json from a markers.json
* @param shapeData {object}
* @returns {Shape}
*/
createShapeFromData(shapeData) {
/** @type {THREE.Vector2[]} **/
let points = [];
if (Array.isArray(shapeData)){
shapeData.forEach(point => {
let x = (point.x || 0) - this.position.x + 0.01; // offset by 0.01 to avoid z-fighting
let z = (point.z || 0) - this.position.z + 0.01;
points.push(new Vector2(x, z));
});
}
return new Shape(points);
}
}
class ExtrudeMarkerFill extends Mesh {
/**
* @param shape {Shape}
*/
constructor(shape) {
let geometry = ExtrudeMarkerFill.createGeometry(shape);
let material = new ShaderMaterial({
vertexShader: MARKER_FILL_VERTEX_SHADER,
fragmentShader: MARKER_FILL_FRAGMENT_SHADER,
side: DoubleSide,
depthTest: true,
transparent: true,
uniforms: {
markerColor: { value: new Color() },
markerOpacity: { value: 0 },
fadeDistanceMin: { value: 0 },
fadeDistanceMax: { value: Number.MAX_VALUE },
}
});
super(geometry, material);
}
/**
* @returns {Color}
*/
get color(){
return this.material.uniforms.markerColor.value;
}
/**
* @returns {number}
*/
get opacity() {
return this.material.uniforms.markerOpacity.value;
}
/**
* @param opacity {number}
*/
set opacity(opacity) {
this.material.uniforms.markerOpacity.value = opacity;
this.visible = opacity > 0;
}
/**
* @returns {boolean}
*/
get depthTest() {
return this.material.depthTest;
}
/**
* @param test {boolean}
*/
set depthTest(test) {
this.material.depthTest = test;
}
/**
* @returns {number}
*/
get fadeDistanceMin() {
return this.material.uniforms.fadeDistanceMin.value;
}
/**
* @param min {number}
*/
set fadeDistanceMin(min) {
this.material.uniforms.fadeDistanceMin.value = min;
}
/**
* @returns {number}
*/
get fadeDistanceMax() {
return this.material.uniforms.fadeDistanceMax.value;
}
/**
* @param max {number}
*/
set fadeDistanceMax(max) {
this.material.uniforms.fadeDistanceMax.value = max;
}
onClick(event) {
if (event.intersection) {
if (event.intersection.distance > this.fadeDistanceMax) return false;
if (event.intersection.distance < this.fadeDistanceMin) return false;
}
return super.onClick(event);
}
/**
* @param shape {Shape}
*/
updateGeometry(shape) {
this.geometry.dispose();
this.geometry = ExtrudeMarkerFill.createGeometry(shape);
}
dispose() {
this.geometry.dispose();
this.material.dispose();
}
/**
* @param shape {Shape}
* @returns {ExtrudeBufferGeometry}
*/
static createGeometry(shape) {
let geometry = new ExtrudeBufferGeometry(shape, {
depth: 1,
steps: 5,
bevelEnabled: false
});
geometry.rotateX(Math.PI / 2); //make y to z
return geometry;
}
}
class ExtrudeMarkerBorder extends Line2 {
/**
* @param shape {Shape}
*/
constructor(shape) {
let geometry = new LineSegmentsGeometry();
geometry.setPositions(ExtrudeMarkerBorder.createLinePoints(shape));
let material = new LineMaterial({
color: new Color(),
opacity: 0,
transparent: true,
linewidth: 1,
depthTest: true,
vertexColors: false,
dashed: false,
});
material.uniforms.fadeDistanceMin = { value: 0 };
material.uniforms.fadeDistanceMax = { value: Number.MAX_VALUE };
material.resolution.set(window.innerWidth, window.innerHeight);
super(geometry, material);
this.computeLineDistances();
}
/**
* @returns {Color}
*/
get color(){
return this.material.color;
}
/**
* @returns {number}
*/
get opacity() {
return this.material.opacity;
}
/**
* @param opacity {number}
*/
set opacity(opacity) {
this.material.opacity = opacity;
this.visible = opacity > 0;
}
/**
* @returns {number}
*/
get linewidth() {
return this.material.linewidth;
}
/**
* @param width {number}
*/
set linewidth(width) {
this.material.linewidth = width;
}
/**
* @returns {boolean}
*/
get depthTest() {
return this.material.depthTest;
}
/**
* @param test {boolean}
*/
set depthTest(test) {
this.material.depthTest = test;
}
/**
* @returns {number}
*/
get fadeDistanceMin() {
return this.material.uniforms.fadeDistanceMin.value;
}
/**
* @param min {number}
*/
set fadeDistanceMin(min) {
this.material.uniforms.fadeDistanceMin.value = min;
}
/**
* @returns {number}
*/
get fadeDistanceMax() {
return this.material.uniforms.fadeDistanceMax.value;
}
/**
* @param max {number}
*/
set fadeDistanceMax(max) {
this.material.uniforms.fadeDistanceMax.value = max;
}
onClick(event) {
if (event.intersection) {
if (event.intersection.distance > this.fadeDistanceMax) return false;
if (event.intersection.distance < this.fadeDistanceMin) return false;
}
return super.onClick(event);
}
/**
* @param shape {Shape}
*/
updateGeometry(shape) {
this.geometry = new LineSegmentsGeometry();
this.geometry.setPositions(ExtrudeMarkerBorder.createLinePoints(shape));
this.computeLineDistances();
}
/**
* @param renderer {THREE.WebGLRenderer}
*/
onBeforeRender(renderer) {
renderer.getSize(this.material.resolution);
}
dispose() {
this.geometry.dispose();
this.material.dispose();
}
/**
* @param shape {Shape}
* @returns {number[]}
*/
static createLinePoints(shape) {
let points3d = [];
let points = shape.getPoints(5);
points.push(points[0]);
let prevPoint = null;
points.forEach(point => {
// vertical line
points3d.push(point.x, 0, point.y);
points3d.push(point.x, -1, point.y);
if (prevPoint) {
// line to previous point top
points3d.push(prevPoint.x, 0, prevPoint.y);
points3d.push(point.x, 0, point.y);
// line to previous point bottom
points3d.push(prevPoint.x, -1, prevPoint.y);
points3d.push(point.x, -1, point.y);
}
prevPoint = point;
});
return points3d;
}
}

View File

@ -0,0 +1,143 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {Marker} from "./Marker";
import {CSS2DObject} from "../util/CSS2DRenderer";
import {htmlToElement} from "../util/Utils";
export class HtmlMarker extends Marker {
/**
* @param markerId {string}
*/
constructor(markerId) {
super(markerId);
Object.defineProperty(this, 'isHtmlMarker', {value: true});
this.data.type = "html";
this.data.label = null;
this.data.classes = [];
this.elementObject = new CSS2DObject(htmlToElement(`<div id="bm-marker-${this.data.id}" class="bm-marker-${this.data.type}"></div>`));
this.elementObject.onBeforeRender = (renderer, scene, camera) => this.onBeforeRender(renderer, scene, camera);
this.fadeDistanceMin = 0;
this.fadeDistanceMax = Number.MAX_VALUE;
this.addEventListener( 'removed', () => {
if (this.element?.parentNode) this.element.parentNode.removeChild(this.element);
});
this.add(this.elementObject);
}
onBeforeRender(renderer, scene, camera) {
if (this.fadeDistanceMax === Number.MAX_VALUE && this.fadeDistanceMin <= 0){
this.element.parentNode.style.opacity = undefined;
} else {
this.element.parentNode.style.opacity = Marker.calculateDistanceOpacity(this.position, camera, this.fadeDistanceMin, this.fadeDistanceMax).toString();
}
}
/**
* @returns {string}
*/
get html() {
return this.element.innerHTML;
}
/**
* @param html {string}
*/
set html(html) {
this.element.innerHTML = html;
}
/**
* @returns {THREE.Vector2}
*/
get anchor() {
return this.elementObject.anchor;
}
/**
* @returns {Element}
*/
get element() {
return this.elementObject.element.getElementsByTagName("div")[0];
}
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* label: string,
* anchor: {x: number, y: number},
* html: string,
* classes: string[],
* minDistance: number,
* maxDistance: number
* }}
*/
updateFromData(markerData) {
// update position
let pos = markerData.position || {};
this.position.setX(pos.x || 0);
this.position.setY(pos.y || 0);
this.position.setZ(pos.z || 0);
// update label
this.data.label = markerData.label || null;
// update anchor
let anch = markerData.anchor || {};
this.anchor.setX(anch.x || 0);
this.anchor.setY(anch.y || 0);
// update html
if (this.element.innerHTML !== markerData.html){
this.element.innerHTML = markerData.html;
}
// update style-classes
if (this.data.classes !== markerData.classes) {
this.data.classes = markerData.classes;
this.element.classList.value = `bm-marker-${this.data.type}`;
this.element.classList.add(...markerData.classes);
}
// update min/max distances
this.fadeDistanceMin = markerData.minDistance || 0;
this.fadeDistanceMax = markerData.maxDistance !== undefined ? markerData.maxDistance : Number.MAX_VALUE;
}
dispose() {
super.dispose();
if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
}
}

View File

@ -0,0 +1,296 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {Color} from "three";
import {LineMaterial} from "../util/lines/LineMaterial";
import {LineGeometry} from "../util/lines/LineGeometry";
import {Line2} from "../util/lines/Line2";
import {deepEquals} from "../util/Utils";
import {ObjectMarker} from "./ObjectMarker";
export class LineMarker extends ObjectMarker {
/**
* @param markerId {string}
*/
constructor(markerId) {
super(markerId);
Object.defineProperty(this, 'isLineMarker', {value: true});
this.data.type = "line";
this.line = new LineMarkerLine([0, 0, 0]);
this.add(this.line);
this._markerData = {};
}
/**
* @param line {number[] | THREE.Vector3[] | THREE.Curve}
*/
setLine(line) {
/** @type {number[]} */
let points;
if (line.type === 'Curve' || line.type === 'CurvePath') {
line = line.getPoints(5);
}
if (Array.isArray(line)) {
if (line.length === 0){
points = [];
} else if (line[0].isVector3) {
points = [];
line.forEach(point => {
points.push(point.x, point.y, point.z);
});
} else {
points = line;
}
} else {
throw new Error("Invalid argument type!");
}
this.line.updateGeometry(points);
}
/**
* @typedef {{r: number, g: number, b: number, a: number}} ColorLike
*/
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* label: string,
* detail: string,
* line: {x: number, y: number, z: number}[],
* link: string,
* newTab: boolean,
* depthTest: boolean,
* lineWidth: number,
* lineColor: ColorLike,
* minDistance: number,
* maxDistance: number
* }}
*/
updateFromData(markerData) {
super.updateFromData(markerData);
// update shape only if needed, based on last update-data
if (
!this._markerData.line || !deepEquals(markerData.line, this._markerData.line) ||
!this._markerData.position || !deepEquals(markerData.position, this._markerData.position)
){
this.setLine(this.createPointsFromData(markerData.line));
}
// update depthTest
this.line.depthTest = !!markerData.depthTest;
// update border-width
this.line.linewidth = markerData.lineWidth !== undefined ? markerData.lineWidth : 2;
// update line-color
let lc = markerData.lineColor || {};
this.line.color.setRGB((lc.r || 0) / 255, (lc.g || 0) / 255, (lc.b || 0) / 255);
this.line.opacity = lc.a || 0;
// update min/max distances
let minDist = markerData.minDistance || 0;
let maxDist = markerData.maxDistance !== undefined ? markerData.maxDistance : Number.MAX_VALUE;
this.line.fadeDistanceMin = minDist;
this.line.fadeDistanceMax = maxDist;
// save used marker data for next update
this._markerData = markerData;
}
dispose() {
super.dispose();
this.line.dispose();
}
/**
* @private
* Creates a shape from a data object, usually parsed json from a markers.json
* @param shapeData {object}
* @returns {number[]}
*/
createPointsFromData(shapeData) {
/** @type {number[]} **/
let points = [];
if (Array.isArray(shapeData)){
shapeData.forEach(point => {
let x = (point.x || 0) - this.position.x;
let y = (point.y || 0) - this.position.y;
let z = (point.z || 0) - this.position.z;
points.push(x, y, z);
});
}
return points;
}
}
class LineMarkerLine extends Line2 {
/**
* @param points {number[]}
*/
constructor(points) {
let geometry = new LineGeometry();
geometry.setPositions(points);
let material = new LineMaterial({
color: new Color(),
opacity: 0,
transparent: true,
linewidth: 1,
depthTest: true,
vertexColors: false,
dashed: false,
});
material.uniforms.fadeDistanceMin = { value: 0 };
material.uniforms.fadeDistanceMax = { value: Number.MAX_VALUE };
material.resolution.set(window.innerWidth, window.innerHeight);
super(geometry, material);
this.computeLineDistances();
}
/**
* @returns {Color}
*/
get color(){
return this.material.color;
}
/**
* @returns {number}
*/
get opacity() {
return this.material.opacity;
}
/**
* @param opacity {number}
*/
set opacity(opacity) {
this.material.opacity = opacity;
this.visible = opacity > 0;
}
/**
* @returns {number}
*/
get linewidth() {
return this.material.linewidth;
}
/**
* @param width {number}
*/
set linewidth(width) {
this.material.linewidth = width;
}
/**
* @returns {boolean}
*/
get depthTest() {
return this.material.depthTest;
}
/**
* @param test {boolean}
*/
set depthTest(test) {
this.material.depthTest = test;
}
/**
* @returns {number}
*/
get fadeDistanceMin() {
return this.material.uniforms.fadeDistanceMin.value;
}
/**
* @param min {number}
*/
set fadeDistanceMin(min) {
this.material.uniforms.fadeDistanceMin.value = min;
}
/**
* @returns {number}
*/
get fadeDistanceMax() {
return this.material.uniforms.fadeDistanceMax.value;
}
/**
* @param max {number}
*/
set fadeDistanceMax(max) {
this.material.uniforms.fadeDistanceMax.value = max;
}
onClick(event) {
if (event.intersection) {
if (event.intersection.distance > this.fadeDistanceMax) return false;
if (event.intersection.distance < this.fadeDistanceMin) return false;
}
return super.onClick(event);
}
/**
* @param points {number[]}
*/
updateGeometry(points) {
this.geometry = new LineGeometry();
this.geometry.setPositions(points);
this.computeLineDistances();
}
/**
* @param renderer {THREE.WebGLRenderer}
*/
onBeforeRender(renderer) {
renderer.getSize(this.material.resolution);
}
dispose() {
this.geometry.dispose();
this.material.dispose();
}
}

View File

@ -0,0 +1,95 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Object3D, Vector3} from "three";
export class Marker extends Object3D {
/**
* @param markerId {string}
*/
constructor(markerId) {
super();
Object.defineProperty(this, 'isMarker', {value: true});
this.data = {
id: markerId,
type: "marker",
position: this.position,
visible: this.visible
};
// redirect parent properties
Object.defineProperty(this, "position", {
get() { return this.data.position },
set(value) { this.data.position = value }
});
Object.defineProperty(this, "visible", {
get() { return this.data.visible },
set(value) { this.data.visible = value }
});
}
dispose() {}
/**
* Updates this marker from the provided data object, usually parsed form json from a markers.json
* @param markerData {object}
*/
updateFromData(markerData) {}
// -- helper methods --
static _posRelativeToCamera = new Vector3();
static _cameraDirection = new Vector3();
/**
* @param position {Vector3}
* @param camera {THREE.Camera}
* @param fadeDistanceMax {number}
* @param fadeDistanceMin {number}
* @returns {number} - opacity between 0 and 1
*/
static calculateDistanceOpacity(position, camera, fadeDistanceMin, fadeDistanceMax) {
let distance = Marker.calculateDistanceToCameraPlane(position, camera);
let minDelta = (distance - fadeDistanceMin) / fadeDistanceMin;
let maxDelta = (distance - fadeDistanceMax) / (fadeDistanceMax * 0.5);
return Math.min(
MathUtils.clamp(minDelta, 0, 1),
1 - MathUtils.clamp(maxDelta + 1, 0, 1)
);
}
/**
* @param position {Vector3}
* @param camera {THREE.Camera}
* @returns {number}
*/
static calculateDistanceToCameraPlane (position, camera) {
Marker._posRelativeToCamera.subVectors(position, camera.position);
camera.getWorldDirection(Marker._cameraDirection);
return Marker._posRelativeToCamera.dot(Marker._cameraDirection);
}
}

View File

@ -0,0 +1,68 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the 'Software'), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { ShaderChunk } from 'three';
export const MARKER_FILL_FRAGMENT_SHADER = `
${ShaderChunk.logdepthbuf_pars_fragment}
#define FLT_MAX 3.402823466e+38
varying vec3 vPosition;
//varying vec3 vWorldPosition;
//varying vec3 vNormal;
//varying vec2 vUv;
//varying vec3 vColor;
varying float vDistance;
uniform vec3 markerColor;
uniform float markerOpacity;
uniform float fadeDistanceMax;
uniform float fadeDistanceMin;
void main() {
vec4 color = vec4(markerColor, markerOpacity);
// distance fading
float fdMax = FLT_MAX;
if ( fadeDistanceMax > 0.0 ) fdMax = fadeDistanceMax;
float minDelta = (vDistance - fadeDistanceMin) / fadeDistanceMin;
float maxDelta = (vDistance - fadeDistanceMax) / (fadeDistanceMax * 0.5);
float distanceOpacity = min(
clamp(minDelta, 0.0, 1.0),
1.0 - clamp(maxDelta + 1.0, 0.0, 1.0)
);
color.a *= distanceOpacity;
// apply vertex-color
//color.rgb *= vColor.rgb;
gl_FragColor = color;
${ShaderChunk.logdepthbuf_fragment}
}
`;

View File

@ -0,0 +1,53 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the 'Software'), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { ShaderChunk } from 'three';
export const MARKER_FILL_VERTEX_SHADER = `
#include <common>
${ShaderChunk.logdepthbuf_pars_vertex}
varying vec3 vPosition;
//varying vec3 vWorldPosition;
//varying vec3 vNormal;
//varying vec2 vUv;
//varying vec3 vColor;
varying float vDistance;
void main() {
vec4 worldPos = modelMatrix * vec4(position, 1);
vec4 viewPos = viewMatrix * worldPos;
vPosition = position;
//vWorldPosition = worldPos.xyz;
//vNormal = normal;
//vUv = uv;
//vColor = vec3(1.0);
vDistance = -viewPos.z;
gl_Position = projectionMatrix * viewPos;
${ShaderChunk.logdepthbuf_vertex}
}
`;

View File

@ -0,0 +1,131 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {FileLoader} from "three";
import {MarkerSet} from "./MarkerSet";
import {alert, generateCacheHash} from "../util/Utils";
/**
* A manager for loading and updating markers from a file
*/
export class MarkerManager {
/**
* @constructor
* @param root {MarkerSet} - The scene to which all markers will be added
* @param fileUrl {string} - The marker file from which this manager updates its markers
* @param events {EventTarget}
*/
constructor(root, fileUrl, events = null) {
Object.defineProperty(this, 'isMarkerManager', {value: true});
this.root = root;
this.fileUrl = fileUrl;
this.events = events;
this.disposed = false;
/** @type {NodeJS.Timeout} */
this._updateInterval = null;
}
/**
* Sets the automatic-update frequency, setting this to 0 or negative disables automatic updates (default).
* This is better than using setInterval() on update() because this will wait for the update to finish before requesting the next update.
* @param ms - interval in milliseconds
*/
setAutoUpdateInterval(ms) {
if (this._updateInterval) clearTimeout(this._updateInterval);
if (ms > 0) {
let autoUpdate = () => {
if (this.disposed) return;
this.update()
.then(success => {
if (success) {
this._updateInterval = setTimeout(autoUpdate, ms);
} else {
this._updateInterval = setTimeout(autoUpdate, Math.max(ms, 1000 * 15));
}
})
.catch(e => {
alert(this.events, e, "warning");
this._updateInterval = setTimeout(autoUpdate, Math.max(ms, 1000 * 15));
});
};
this._updateInterval = setTimeout(autoUpdate, ms);
}
}
/**
* Loads the marker-file and updates all managed markers.
* @returns {Promise<object>} - A promise completing when the markers finished updating
*/
update() {
return this.loadMarkerFile()
.then(markerFileData => this.updateFromData(markerFileData));
}
/**
* @protected
* @param markerData
*/
updateFromData(markerData) {}
/**
* Stops automatic-updates and disposes all markersets and markers managed by this manager
*/
dispose() {
this.disposed = true;
this.setAutoUpdateInterval(0);
this.clear();
}
/**
* Removes all markers managed by this marker-manager
*/
clear() {
this.root.clear();
}
/**
* @private
* Loads the marker file
* @returns {Promise<Object>} - A promise completing with the parsed json object from the loaded file
*/
loadMarkerFile() {
return new Promise((resolve, reject) => {
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(this.fileUrl + "?" + generateCacheHash(),
markerFileData => {
if (!markerFileData) reject(`Failed to parse '${this.fileUrl}'!`);
else resolve(markerFileData);
},
() => {},
() => reject(`Failed to load '${this.fileUrl}'!`)
)
});
}
}

View File

@ -0,0 +1,215 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {Scene} from "three";
import {alert} from "../util/Utils";
import {ShapeMarker} from "./ShapeMarker";
import {ExtrudeMarker} from "./ExtrudeMarker";
import {LineMarker} from "./LineMarker";
import {HtmlMarker} from "./HtmlMarker";
import {PoiMarker} from "./PoiMarker";
export class MarkerSet extends Scene {
/**
* @param id {string}
*/
constructor(id) {
super();
Object.defineProperty(this, 'isMarkerSet', {value: true});
/** @type {Map<string, MarkerSet>} */
this.markerSets = new Map();
/** @type {Map<string, Marker>} */
this.markers = new Map();
this.data = {
id: id,
label: id,
toggleable: true,
defaultHide: false,
markerSets: [],
markers: [],
visible: this.visible,
};
Object.defineProperty(this, "visible", {
get() { return this.data.visible },
set(value) { this.data.visible = value }
});
}
updateFromData(data) {
// update set info
this.data.label = data.label || this.data.id;
this.data.toggleable = !!data.toggleable;
this.data.defaultHide = !!data.defaultHidden;
// update markerSets
this.updateMarkerSetsFromData(data.markerSets);
// update markers
this.updateMarkersFromData(data.markers);
}
updateMarkerSetsFromData(data = {}, ignore = []) {
let updatedMarkerSets = new Set(ignore);
// add & update MarkerSets
Object.keys(data).forEach(markerSetId => {
if (updatedMarkerSets.has(markerSetId)) return;
updatedMarkerSets.add(markerSetId);
let markerSetData = data[markerSetId];
try {
this.updateMarkerSetFromData(markerSetId, markerSetData);
} catch (err) {
alert(this.events, err, "fine");
}
});
// remove not updated MarkerSets
this.markerSets.forEach((markerSet, setId) => {
if (!updatedMarkerSets.has(setId)) {
this.remove(markerSet);
}
});
}
updateMarkerSetFromData(markerSetId, data) {
let markerSet = this.markerSets.get(markerSetId);
// create new if not existent
if (!markerSet) {
markerSet = new MarkerSet(markerSetId);
this.add(markerSet);
if (data.defaultHidden) {
markerSet.visible = false;
}
}
// update
markerSet.updateFromData(data);
}
updateMarkersFromData(data = {}, ignore = []) {
let updatedMarkers = new Set(ignore);
Object.keys(data).forEach(markerId => {
if (updatedMarkers.has(markerId)) return;
let markerData = data[markerId];
try {
this.updateMarkerFromData(markerId, markerData);
updatedMarkers.add(markerId);
} catch (err) {
alert(this.events, err, "fine");
console.debug(err);
}
});
// remove not updated Markers
this.markers.forEach((marker, markerId) => {
if (!updatedMarkers.has(markerId)) {
this.remove(marker);
}
});
}
updateMarkerFromData(markerId, data) {
if (!data.type) throw new Error("marker-data has no type!");
let marker = this.markers.get(markerId);
// create new if not existent of wrong type
if (!marker || marker.data.type !== data.type) {
if (marker) this.remove(marker);
switch (data.type) {
case "shape" : marker = new ShapeMarker(markerId); break;
case "extrude" : marker = new ExtrudeMarker(markerId); break;
case "line" : marker = new LineMarker(markerId); break;
case "html" : marker = new HtmlMarker(markerId); break;
case "poi" : marker = new PoiMarker(markerId); break;
default : throw new Error(`Unknown marker-type: '${data.type}'`);
}
this.add(marker);
}
// update marker
marker.updateFromData(data);
}
/**
* Removes all markers and marker-sets
*/
clear() {
[...this.data.markerSets].forEach(markerSet => this.remove(markerSet));
[...this.data.markers].forEach(marker => this.remove(marker));
}
add(...object) {
if (object.length === 1) { //super.add() will re-invoke this method for each array-entry if it's more than one
let o = object[0];
if (o.isMarkerSet && !this.markerSets.has(o.data.id)) {
this.markerSets.set(o.data.id, o);
this.data.markerSets.push(o.data);
}
if (o.isMarker && !this.markers.has(o.data.id)) {
this.markers.set(o.data.id, o);
this.data.markers.push(o.data);
}
}
return super.add(...object);
}
remove(...object) {
if (object.length === 1) { //super.remove() will re-invoke this method for each array-entry if it's more than one
let o = object[0];
if (o.isMarkerSet) {
let i = this.data.markerSets.indexOf(o.data);
if (i > -1) this.data.markerSets.splice(i, 1);
this.markerSets.delete(o.data.id);
o.dispose();
}
if (o.isMarker) {
let i = this.data.markers.indexOf(o.data);
if (i > -1) this.data.markers.splice(i, 1);
this.markers.delete(o.data.id);
o.dispose();
}
}
return super.remove(...object);
}
dispose() {
this.children.forEach(child => {
if (child.dispose) child.dispose();
});
}
}

View File

@ -0,0 +1,51 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { MarkerManager } from "./MarkerManager";
import { PLAYER_MARKER_SET_ID } from "./PlayerMarkerManager";
export class NormalMarkerManager extends MarkerManager {
/**
* @constructor
* @param root {MarkerSet} - The scene to which all markers will be added
* @param fileUrl {string} - The marker file from which this manager updates its markers
* @param events {EventTarget}
*/
constructor(root, fileUrl, events = null) {
super(root, fileUrl, events);
}
/**
* @protected
* @override
* @param markerData
* @returns {boolean}
*/
updateFromData(markerData) {
this.root.updateMarkerSetsFromData(markerData, [PLAYER_MARKER_SET_ID, "bm-popup-set"]);
return true;
}
}

View File

@ -0,0 +1,156 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {Marker} from "./Marker";
import {CSS2DObject} from "../util/CSS2DRenderer";
import {animate, htmlToElement} from "../util/Utils";
import {Vector3} from "three";
export class ObjectMarker extends Marker {
/**
* @param markerId {string}
*/
constructor(markerId) {
super(markerId);
Object.defineProperty(this, 'isObjectMarker', {value: true});
this.data.type = "object";
this.data.label = null;
this.data.detail = null;
this.data.link = null;
this.data.newTab = true;
this.lastClick = -1;
}
onClick(event) {
let pos = new Vector3();
if (event.intersection) {
pos.copy(event.intersection.pointOnLine || event.intersection.point);
pos.sub(this.position);
}
if (event.data.doubleTap) return false;
if (this.data.detail || this.data.label) {
let popup = new LabelPopup(this.data.detail || this.data.label);
popup.position.copy(pos);
this.add(popup);
popup.open();
}
if (this.data.link){
window.open(this.data.link, this.data.newTab ? '_blank' : '_self');
}
return true;
}
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* label: string,
* detail: string,
* link: string,
* newTab: boolean
* }}
*/
updateFromData(markerData) {
// update position
let pos = markerData.position || {};
this.position.setX(pos.x || 0);
this.position.setY(pos.y || 0);
this.position.setZ(pos.z || 0);
// update label
this.data.label = markerData.label || null;
//update detail
this.data.detail = markerData.detail || null;
// update link
this.data.link = markerData.link || null;
this.data.newTab = !!markerData.newTab;
}
}
export class LabelPopup extends CSS2DObject {
/**
* @param label {string}
*/
constructor(label) {
super(htmlToElement(`<div class="bm-marker-labelpopup">${label}</div>`));
}
/**
* @param autoClose {boolean} - whether this object should be automatically closed and removed again on any other interaction
*/
open(autoClose = true) {
let targetOpacity = this.element.style.opacity || 1;
this.element.style.opacity = 0;
let inAnimation = animate(progress => {
this.element.style.opacity = (progress * targetOpacity).toString();
}, 300);
if (autoClose) {
let removeHandler = evt => {
if (evt.composedPath().includes(this.element)) return;
inAnimation.cancel();
this.close();
window.removeEventListener("mousedown", removeHandler);
window.removeEventListener("touchstart", removeHandler);
window.removeEventListener("keydown", removeHandler);
window.removeEventListener("mousewheel", removeHandler);
};
window.addEventListener("mousedown", removeHandler);
window.addEventListener("touchstart", removeHandler);
window.addEventListener("keydown", removeHandler);
window.addEventListener("mousewheel", removeHandler);
}
}
/**
* @param remove {boolean} - whether this object should be removed from its parent when the close-animation finished
*/
close(remove = true) {
let startOpacity = parseFloat(this.element.style.opacity);
animate(progress => {
this.element.style.opacity = (startOpacity - progress * startOpacity).toString();
}, 300, completed => {
if (remove && completed && this.parent) {
this.parent.remove(this);
}
});
}
}

View File

@ -0,0 +1,151 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {Marker} from "./Marker";
import {CSS2DObject} from "../util/CSS2DRenderer";
import {animate, EasingFunctions, htmlToElement} from "../util/Utils";
export class PlayerMarker extends Marker {
/**
* @param markerId {string}
* @param playerUuid {string}
* @param playerHead {string}
*/
constructor(markerId, playerUuid, playerHead = "assets/steve.png") {
super(markerId);
Object.defineProperty(this, 'isPlayerMarker', {value: true});
this.data.type = "player";
this.data.playerUuid = playerUuid;
this.data.name = playerUuid;
this.data.playerHead = playerHead;
this.elementObject = new CSS2DObject(htmlToElement(`
<div id="bm-marker-${this.data.id}" class="bm-marker-${this.data.type}">
<img src="${this.data.playerHead}" alt="playerhead" draggable="false">
<div class="bm-player-name"></div>
</div>
`));
this.elementObject.onBeforeRender = (renderer, scene, camera) => this.onBeforeRender(renderer, scene, camera);
this.playerHeadElement = this.element.getElementsByTagName("img")[0];
this.playerNameElement = this.element.getElementsByTagName("div")[0];
this.addEventListener( 'removed', () => {
if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
});
this.playerHeadElement.addEventListener('error', () => {
this.playerHeadElement.src = "assets/steve.png";
}, {once: true});
this.add(this.elementObject);
}
/**
* @returns {Element}
*/
get element() {
return this.elementObject.element.getElementsByTagName("div")[0];
}
onBeforeRender(renderer, scene, camera) {
let distance = Marker.calculateDistanceToCameraPlane(this.position, camera);
let value = "near";
if (distance > 1000) {
value = "med";
}
if (distance > 5000) {
value = "far";
}
this.element.setAttribute("distance-data", value);
}
/**
* @typedef PlayerLike {{
* uuid: string,
* name: string,
* foreign: boolean,
* position: {x: number, y: number, z: number},
* rotation: {yaw: number, pitch: number, roll: number}
* }}
*/
/**
* @param markerData {PlayerLike}
*/
updateFromData(markerData) {
// animate position update
let pos = markerData.position || {};
if (!this.position.x && !this.position.y && !this.position.z) {
this.position.set(
pos.x || 0,
(pos.y || 0) + 1.8,
pos.z || 0
);
} else {
let startPos = {
x: this.position.x,
y: this.position.y,
z: this.position.z,
}
let deltaPos = {
x: (pos.x || 0) - startPos.x,
y: ((pos.y || 0) + 1.8) - startPos.y,
z: (pos.z || 0) - startPos.z,
}
if (deltaPos.x || deltaPos.y || deltaPos.z) {
animate(progress => {
let ease = EasingFunctions.easeInOutCubic(progress);
this.position.set(
startPos.x + deltaPos.x * ease || 0,
startPos.y + deltaPos.y * ease || 0,
startPos.z + deltaPos.z * ease || 0
);
}, 500);
}
}
// update name
let name = markerData.name || this.data.playerUuid;
this.data.name = name;
if (this.playerNameElement.innerHTML !== name)
this.playerNameElement.innerHTML = name;
// update world
this.data.foreign = markerData.foreign;
}
dispose() {
super.dispose();
let element = this.elementObject.element;
if (element.parentNode) element.parentNode.removeChild(element);
}
}

View File

@ -0,0 +1,80 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { PlayerMarkerSet } from "./PlayerMarkerSet";
import { MarkerManager } from "./MarkerManager";
export const PLAYER_MARKER_SET_ID = "bm-players";
export class PlayerMarkerManager extends MarkerManager {
/**
* @constructor
* @param root {MarkerSet} - The scene to which all markers will be added
* @param fileUrl {string} - The marker file from which this manager updates its markers
* @param playerheadsUrl {string} - The url from which playerhead images should be loaded
* @param events {EventTarget}
*/
constructor(root, fileUrl, playerheadsUrl, events = null) {
super(root, fileUrl, events);
this.playerheadsUrl = playerheadsUrl;
}
/**
* @protected
* @override
* @param markerFileData
* @returns {boolean}
*/
updateFromData(markerFileData) {
let playerMarkerSet = this.getPlayerMarkerSet();
return playerMarkerSet.updateFromPlayerData(markerFileData);
}
/**
* @private
* @returns {PlayerMarkerSet}
*/
getPlayerMarkerSet() {
/** @type {PlayerMarkerSet} */
let playerMarkerSet = /** @type {PlayerMarkerSet} */ this.root.markerSets.get(PLAYER_MARKER_SET_ID);
if (!playerMarkerSet) {
playerMarkerSet = new PlayerMarkerSet(PLAYER_MARKER_SET_ID, this.playerheadsUrl);
this.root.add(playerMarkerSet);
}
return playerMarkerSet;
}
/**
* @param playerUuid {string}
* @returns {PlayerMarker | undefined}
*/
getPlayerMarker(playerUuid) {
return this.getPlayerMarkerSet().getPlayerMarker(playerUuid)
}
}

View File

@ -0,0 +1,102 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MarkerSet} from "./MarkerSet";
import {alert} from "../util/Utils";
import {PlayerMarker} from "./PlayerMarker";
export class PlayerMarkerSet extends MarkerSet {
constructor(id, playerheadsUrl) {
super(id);
this.data.label = "Player";
this.data.toggleable = true;
this.data.defaultHide = false;
this.data.playerheadsUrl = playerheadsUrl;
}
updateFromPlayerData(data) {
if (!Array.isArray(data.players)) {
this.clear();
return false;
}
/** @type Set<Marker> */
let updatedPlayerMarkers = new Set();
// update
data.players.forEach(playerData => {
try {
let playerMarker = this.updatePlayerMarkerFromData(playerData);
updatedPlayerMarkers.add(playerMarker);
} catch (err) {
alert(this.events, err, "fine");
}
});
// remove
this.markers.forEach(playerMarker => {
if (!updatedPlayerMarkers.has(playerMarker)) {
this.remove(playerMarker);
}
});
return true;
}
updatePlayerMarkerFromData(markerData) {
let playerUuid = markerData.uuid;
if (!playerUuid) throw new Error("player-data has no uuid!");
let markerId = this.getPlayerMarkerId(playerUuid);
/** @type PlayerMarker */
let marker = this.markers.get(markerId);
// create new if not existent of wrong type
if (!marker || !marker.isPlayerMarker) {
if (marker) this.remove(marker);
marker = new PlayerMarker(markerId, playerUuid, `${this.data.playerheadsUrl}${playerUuid}.png`);
this.add(marker);
}
// update
marker.updateFromData(markerData);
// hide if from different world
marker.visible = !markerData.foreign;
return marker;
}
getPlayerMarker(playerUuid) {
return this.markers.get(this.getPlayerMarkerId(playerUuid));
}
getPlayerMarkerId(playerUuid) {
return "bm-player-" + playerUuid;
}
}

View File

@ -0,0 +1,147 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {HtmlMarker} from "./HtmlMarker";
export class PoiMarker extends HtmlMarker {
/**
* @param markerId {string}
*/
constructor(markerId) {
super(markerId);
Object.defineProperty(this, 'isPoiMarker', {value: true});
this.data.type = "poi";
this.data.detail = null;
this.html = `<img src="" alt="POI Icon (${this.data.id})" class="bm-marker-poi-icon" draggable="false" style="pointer-events: auto"><div class="bm-marker-poi-label"></div>`;
this.iconElement = this.element.getElementsByTagName("img").item(0);
this.labelElement = this.element.getElementsByTagName("div").item(0);
this._lastIcon = null;
}
onClick(event) {
if (event.data.doubleTap) return false;
if (this.highlight || !this.data.label) return true;
this.highlight = true;
let eventHandler = evt => {
if (evt.composedPath().includes(this.element)) return;
this.highlight = false;
window.removeEventListener("mousedown", eventHandler);
window.removeEventListener("touchstart", eventHandler);
window.removeEventListener("keydown", eventHandler);
window.removeEventListener("mousewheel", eventHandler);
};
setTimeout(function () {
window.addEventListener("mousedown", eventHandler);
window.addEventListener("touchstart", eventHandler);
window.addEventListener("keydown", eventHandler);
window.addEventListener("mousewheel", eventHandler);
}, 0);
return true;
}
set highlight(highlight) {
if (highlight) {
this.element.classList.add("bm-marker-highlight");
} else {
this.element.classList.remove("bm-marker-highlight");
}
}
get highlight() {
return this.element.classList.contains("bm-marker-highlight");
}
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* anchor: {x: number, y: number},
* iconAnchor: {x: number, y: number},
* label: string,
* detail: string,
* icon: string,
* classes: string[],
* minDistance: number,
* maxDistance: number
* }}
*/
updateFromData(markerData) {
// update position
let pos = markerData.position || {};
this.position.setX(pos.x || 0);
this.position.setY(pos.y || 0);
this.position.setZ(pos.z || 0);
// update anchor
let anch = markerData.anchor || markerData.iconAnchor || {}; //"iconAnchor" for backwards compatibility
this.iconElement.style.transform = `translate(${-anch.x}px, ${-anch.y}px)`;
//this.anchor.setX(anch.x || 0);
//this.anchor.setY(anch.y || 0);
// update label
if (this.data.label !== markerData.label){
this.data.label = markerData.label || "";
}
// update detail
if (this.data.detail !== markerData.detail){
this.data.detail = markerData.detail || this.data.label;
this.labelElement.innerHTML = this.data.detail || "";
}
// update icon
if (this._lastIcon !== markerData.icon){
this.iconElement.src = markerData.icon || "assets/poi.svg";
this._lastIcon = markerData.icon;
}
// update style-classes
if (this.data.classes !== markerData.classes) {
this.data.classes = markerData.classes;
let highlight = this.element.classList.contains("bm-marker-highlight");
this.element.classList.value = `bm-marker-html`;
if (highlight) this.element.classList.add("bm-marker-highlight");
this.element.classList.add(...markerData.classes);
}
// update min/max distances
this.fadeDistanceMin = markerData.minDistance || 0;
this.fadeDistanceMax = markerData.maxDistance !== undefined ? markerData.maxDistance : Number.MAX_VALUE;
}
}

View File

@ -0,0 +1,441 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {Color, DoubleSide, Mesh, ShaderMaterial, Shape, ShapeBufferGeometry, Vector2} from "three";
import {LineMaterial} from "../util/lines/LineMaterial";
import {MARKER_FILL_VERTEX_SHADER} from "./MarkerFillVertexShader";
import {MARKER_FILL_FRAGMENT_SHADER} from "./MarkerFillFragmentShader";
import {LineGeometry} from "../util/lines/LineGeometry";
import {Line2} from "../util/lines/Line2";
import {deepEquals} from "../util/Utils";
import {ObjectMarker} from "./ObjectMarker";
export class ShapeMarker extends ObjectMarker {
/**
* @param markerId {string}
*/
constructor(markerId) {
super(markerId);
Object.defineProperty(this, 'isShapeMarker', {value: true});
this.data.type = "shape";
let zero = new Vector2();
let shape = new Shape([zero, zero, zero]);
this.fill = new ShapeMarkerFill(shape);
this.border = new ShapeMarkerBorder(shape);
this.border.renderOrder = -1; // render border before fill
this.add(this.border, this.fill);
this._markerData = {};
}
/**
* @param y {number}
*/
setShapeY(y) {
let relativeY = y - this.position.y;
this.fill.position.y = relativeY;
this.border.position.y = relativeY;
}
/**
* @param shape {Shape}
*/
setShape(shape) {
this.fill.updateGeometry(shape);
this.border.updateGeometry(shape);
}
/**
* @typedef {{r: number, g: number, b: number, a: number}} ColorLike
*/
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* label: string,
* detail: string,
* shape: {x: number, z: number}[],
* shapeY: number,
* height: number,
* link: string,
* newTab: boolean,
* depthTest: boolean,
* lineWidth: number,
* borderColor: ColorLike,
* lineColor: ColorLike,
* fillColor: ColorLike,
* minDistance: number,
* maxDistance: number
* }}
*/
updateFromData(markerData) {
super.updateFromData(markerData);
// update shape only if needed, based on last update-data
if (
!this._markerData.shape || !deepEquals(markerData.shape, this._markerData.shape) ||
!this._markerData.position || !deepEquals(markerData.position, this._markerData.position)
){
this.setShape(this.createShapeFromData(markerData.shape));
}
// update shapeY
this.setShapeY((markerData.shapeY || markerData.height || 0) + 0.01); //"height" for backwards compatibility, adding 0.01 to avoid z-fighting
// update depthTest
this.border.depthTest = !!markerData.depthTest;
this.fill.depthTest = !!markerData.depthTest;
// update border-width
this.border.linewidth = markerData.lineWidth !== undefined ? markerData.lineWidth : 2;
// update border-color
let bc = markerData.lineColor || markerData.borderColor || {}; //"borderColor" for backwards compatibility
this.border.color.setRGB((bc.r || 0) / 255, (bc.g || 0) / 255, (bc.b || 0) / 255);
this.border.opacity = bc.a || 0;
// update fill-color
let fc = markerData.fillColor || {};
this.fill.color.setRGB((fc.r || 0) / 255, (fc.g || 0) / 255, (fc.b || 0) / 255);
this.fill.opacity = fc.a || 0;
// update min/max distances
let minDist = markerData.minDistance || 0;
let maxDist = markerData.maxDistance !== undefined ? markerData.maxDistance : Number.MAX_VALUE;
this.border.fadeDistanceMin = minDist;
this.border.fadeDistanceMax = maxDist;
this.fill.fadeDistanceMin = minDist;
this.fill.fadeDistanceMax = maxDist;
// save used marker data for next update
this._markerData = markerData;
}
dispose() {
super.dispose();
this.fill.dispose();
this.border.dispose();
}
/**
* @private
* Creates a shape from a data object, usually parsed json from a markers.json
* @param shapeData {object}
* @returns {Shape}
*/
createShapeFromData(shapeData) {
/** @type {THREE.Vector2[]} **/
let points = [];
if (Array.isArray(shapeData)){
shapeData.forEach(point => {
let x = (point.x || 0) - this.position.x;
let z = (point.z || 0) - this.position.z;
points.push(new Vector2(x, z));
});
}
return new Shape(points);
}
}
class ShapeMarkerFill extends Mesh {
/**
* @param shape {Shape}
*/
constructor(shape) {
let geometry = ShapeMarkerFill.createGeometry(shape);
let material = new ShaderMaterial({
vertexShader: MARKER_FILL_VERTEX_SHADER,
fragmentShader: MARKER_FILL_FRAGMENT_SHADER,
side: DoubleSide,
depthTest: true,
transparent: true,
uniforms: {
markerColor: { value: new Color() },
markerOpacity: { value: 0 },
fadeDistanceMin: { value: 0 },
fadeDistanceMax: { value: Number.MAX_VALUE },
}
});
super(geometry, material);
}
/**
* @returns {Color}
*/
get color(){
return this.material.uniforms.markerColor.value;
}
/**
* @returns {number}
*/
get opacity() {
return this.material.uniforms.markerOpacity.value;
}
/**
* @param opacity {number}
*/
set opacity(opacity) {
this.material.uniforms.markerOpacity.value = opacity;
this.visible = opacity > 0;
}
/**
* @returns {boolean}
*/
get depthTest() {
return this.material.depthTest;
}
/**
* @param test {boolean}
*/
set depthTest(test) {
this.material.depthTest = test;
}
/**
* @returns {number}
*/
get fadeDistanceMin() {
return this.material.uniforms.fadeDistanceMin.value;
}
/**
* @param min {number}
*/
set fadeDistanceMin(min) {
this.material.uniforms.fadeDistanceMin.value = min;
}
/**
* @returns {number}
*/
get fadeDistanceMax() {
return this.material.uniforms.fadeDistanceMax.value;
}
/**
* @param max {number}
*/
set fadeDistanceMax(max) {
this.material.uniforms.fadeDistanceMax.value = max;
}
onClick(event) {
if (event.intersection) {
if (event.intersection.distance > this.fadeDistanceMax) return false;
if (event.intersection.distance < this.fadeDistanceMin) return false;
}
return super.onClick(event);
}
/**
* @param shape {Shape}
*/
updateGeometry(shape) {
this.geometry.dispose();
this.geometry = ShapeMarkerFill.createGeometry(shape);
}
dispose() {
this.geometry.dispose();
this.material.dispose();
}
/**
* @param shape {Shape}
* @returns {ShapeBufferGeometry}
*/
static createGeometry(shape) {
let geometry = new ShapeBufferGeometry(shape, 5);
geometry.rotateX(Math.PI / 2); //make y to z
return geometry;
}
}
class ShapeMarkerBorder extends Line2 {
/**
* @param shape {Shape}
*/
constructor(shape) {
let geometry = new LineGeometry();
geometry.setPositions(ShapeMarkerBorder.createLinePoints(shape));
let material = new LineMaterial({
color: new Color(),
opacity: 0,
transparent: true,
linewidth: 1,
depthTest: true,
vertexColors: false,
dashed: false,
});
material.uniforms.fadeDistanceMin = { value: 0 };
material.uniforms.fadeDistanceMax = { value: Number.MAX_VALUE };
material.resolution.set(window.innerWidth, window.innerHeight);
super(geometry, material);
this.computeLineDistances();
}
/**
* @returns {Color}
*/
get color(){
return this.material.color;
}
/**
* @returns {number}
*/
get opacity() {
return this.material.opacity;
}
/**
* @param opacity {number}
*/
set opacity(opacity) {
this.material.opacity = opacity;
this.visible = opacity > 0;
}
/**
* @returns {number}
*/
get linewidth() {
return this.material.linewidth;
}
/**
* @param width {number}
*/
set linewidth(width) {
this.material.linewidth = width;
}
/**
* @returns {boolean}
*/
get depthTest() {
return this.material.depthTest;
}
/**
* @param test {boolean}
*/
set depthTest(test) {
this.material.depthTest = test;
}
/**
* @returns {number}
*/
get fadeDistanceMin() {
return this.material.uniforms.fadeDistanceMin.value;
}
/**
* @param min {number}
*/
set fadeDistanceMin(min) {
this.material.uniforms.fadeDistanceMin.value = min;
}
/**
* @returns {number}
*/
get fadeDistanceMax() {
return this.material.uniforms.fadeDistanceMax.value;
}
/**
* @param max {number}
*/
set fadeDistanceMax(max) {
this.material.uniforms.fadeDistanceMax.value = max;
}
onClick(event) {
if (event.intersection) {
if (event.intersection.distance > this.fadeDistanceMax) return false;
if (event.intersection.distance < this.fadeDistanceMin) return false;
}
return super.onClick(event);
}
/**
* @param shape {Shape}
*/
updateGeometry(shape) {
this.geometry = new LineGeometry();
this.geometry.setPositions(ShapeMarkerBorder.createLinePoints(shape));
this.computeLineDistances();
}
/**
* @param renderer {THREE.WebGLRenderer}
*/
onBeforeRender(renderer) {
renderer.getSize(this.material.resolution);
}
dispose() {
this.geometry.dispose();
this.material.dispose();
}
/**
* @param shape {Shape}
* @returns {number[]}
*/
static createLinePoints(shape) {
let points3d = [];
let points = shape.getPoints(5);
points.forEach(point => points3d.push(point.x, 0, point.y));
points3d.push(points[0].x, 0, points[0].y);
return points3d;
}
}

View File

@ -0,0 +1,42 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
export const SKY_FRAGMENT_SHADER = `
uniform float sunlightStrength;
uniform float ambientLight;
uniform vec3 skyColor;
varying vec3 vPosition;
void main() {
float horizonWidth = 0.005;
float horizonHeight = 0.0;
vec4 color = vec4(skyColor * max(sunlightStrength * sunlightStrength, ambientLight), 1.0);
float voidMultiplier = (clamp(vPosition.y - horizonHeight, -horizonWidth, horizonWidth) + horizonWidth) / (horizonWidth * 2.0);
color.rgb *= voidMultiplier;
gl_FragColor = color;
}
`;

View File

@ -0,0 +1,35 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
export const SKY_VERTEX_SHADER = `
varying vec3 vPosition;
void main() {
vPosition = position;
gl_Position =
projectionMatrix *
modelViewMatrix *
vec4(position, 1);
}
`;

View File

@ -0,0 +1,51 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {BackSide, Mesh, Scene, ShaderMaterial, SphereGeometry} from 'three';
import {SKY_FRAGMENT_SHADER} from './SkyFragmentShader';
import {SKY_VERTEX_SHADER} from './SkyVertexShader';
export class SkyboxScene extends Scene {
constructor(uniforms) {
super();
this.autoUpdate = false;
Object.defineProperty(this, 'isSkyboxScene', {value: true});
let geometry = new SphereGeometry(1, 40, 5);
let material = new ShaderMaterial({
uniforms: uniforms,
vertexShader: SKY_VERTEX_SHADER,
fragmentShader: SKY_FRAGMENT_SHADER,
side: BackSide
});
let skybox = new Mesh(geometry, material);
this.add(skybox);
}
}

View File

@ -0,0 +1,233 @@
/**
* @author mrdoob / http://mrdoob.com/
*
* adapted for bluemap's purposes
*/
import {
Matrix4,
Object3D, Vector2,
Vector3
} from "three";
import {dispatchEvent, htmlToElement} from "./Utils";
var CSS2DObject = function ( element ) {
Object3D.call( this );
this.element = document.createElement("div");
let parent = element.parentNode;
parent.replaceChild(this.element, element);
this.element.appendChild(element);
this.element.style.position = 'absolute';
this.anchor = new Vector2();
this.events = null;
this.addEventListener( 'removed', function () {
this.traverse( function ( object ) {
if ( object.element instanceof Element && object.element.parentNode !== null ) {
object.element.parentNode.removeChild( object.element );
}
} );
} );
let lastClick = -1;
let handleClick = event => {
let doubleTap = false;
let now = Date.now();
if (now - lastClick < 500){
doubleTap = true;
}
lastClick = now;
let data = {doubleTap: doubleTap};
if (this.onClick( {event: event, data: data} )) {
event.preventDefault();
event.stopPropagation();
} else {
// fire event
dispatchEvent(this.events, "bluemapMapInteraction", {
data: data,
object: this,
});
}
}
this.element.addEventListener("click", handleClick);
this.element.addEventListener("touch", handleClick);
};
CSS2DObject.prototype = Object.create( Object3D.prototype );
CSS2DObject.prototype.constructor = CSS2DObject;
//
var CSS2DRenderer = function (events = null) {
var _this = this;
var _width, _height;
var _widthHalf, _heightHalf;
var vector = new Vector3();
var viewMatrix = new Matrix4();
var viewProjectionMatrix = new Matrix4();
var cache = {
objects: new WeakMap()
};
var domElement = document.createElement( 'div' );
domElement.style.overflow = 'hidden';
this.domElement = domElement;
this.events = events;
this.getSize = function () {
return {
width: _width,
height: _height
};
};
this.setSize = function ( width, height ) {
_width = width;
_height = height;
_widthHalf = _width / 2;
_heightHalf = _height / 2;
domElement.style.width = width + 'px';
domElement.style.height = height + 'px';
};
var renderObject = function ( object, scene, camera, parentVisible ) {
if ( object instanceof CSS2DObject ) {
object.events = _this.events;
object.onBeforeRender( _this, scene, camera );
vector.setFromMatrixPosition( object.matrixWorld );
vector.applyMatrix4( viewProjectionMatrix );
var element = object.element;
var style = 'translate(' + ( vector.x * _widthHalf + _widthHalf - object.anchor.x) + 'px,' + ( - vector.y * _heightHalf + _heightHalf - object.anchor.y ) + 'px)';
element.style.WebkitTransform = style;
element.style.MozTransform = style;
element.style.oTransform = style;
element.style.transform = style;
element.style.display = ( parentVisible && object.visible && vector.z >= - 1 && vector.z <= 1 && element.style.opacity !== "0" ) ? '' : 'none';
var objectData = {
distanceToCameraSquared: getDistanceToSquared( camera, object )
};
cache.objects.set( object, objectData );
if ( element.parentNode !== domElement ) {
domElement.appendChild( element );
}
object.onAfterRender( _this, scene, camera );
}
for ( var i = 0, l = object.children.length; i < l; i ++ ) {
renderObject( object.children[ i ], scene, camera, parentVisible && object.visible );
}
};
var getDistanceToSquared = function () {
var a = new Vector3();
var b = new Vector3();
return function ( object1, object2 ) {
a.setFromMatrixPosition( object1.matrixWorld );
b.setFromMatrixPosition( object2.matrixWorld );
return a.distanceToSquared( b );
};
}();
var filterAndFlatten = function ( scene ) {
var result = [];
scene.traverse( function ( object ) {
if ( object instanceof CSS2DObject ) result.push( object );
} );
return result;
};
var zOrder = function ( scene ) {
var sorted = filterAndFlatten( scene ).sort( function ( a, b ) {
var distanceA = cache.objects.get( a ).distanceToCameraSquared;
var distanceB = cache.objects.get( b ).distanceToCameraSquared;
return distanceA - distanceB;
} );
var zMax = sorted.length;
for ( var i = 0, l = sorted.length; i < l; i ++ ) {
sorted[ i ].element.style.zIndex = zMax - i;
}
};
this.render = function ( scene, camera ) {
if ( scene.autoUpdate === true ) scene.updateMatrixWorld();
if ( camera.parent === null ) camera.updateMatrixWorld();
viewMatrix.copy( camera.matrixWorldInverse );
viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, viewMatrix );
renderObject( scene, scene, camera, true );
zOrder( scene );
};
};
export { CSS2DObject, CSS2DRenderer };

View File

@ -0,0 +1,193 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Matrix4, PerspectiveCamera} from "three";
export class CombinedCamera extends PerspectiveCamera {
/**
* @param fov {number}
* @param aspect {number}
* @param near {number}
* @param far {number}
* @param ortho {number}
*/
constructor(fov, aspect, near, far, ortho) {
super(fov, aspect, near, far);
this.needsUpdate = true;
this.data = {
fov: this.fov,
aspect: this.aspect,
near: this.near,
far: this.far,
zoom: this.zoom,
ortho: ortho,
distance: 1,
};
// redirect parent properties
Object.defineProperty(this, "fov", {
get() { return this.data.fov },
set(value) { if (value !== this.data.fov) { this.data.fov = value; this.needsUpdate = true }}
});
Object.defineProperty(this, "aspect", {
get() { return this.data.aspect },
set(value) { if (value !== this.data.aspect) { this.data.aspect = value; this.needsUpdate = true }}
});
Object.defineProperty(this, "near", {
get() { return this.data.near },
set(value) { if (value !== this.data.near) { this.data.near = value; this.needsUpdate = true }}
});
Object.defineProperty(this, "far", {
get() { return this.data.far },
set(value) { if (value !== this.data.far) { this.data.far = value; this.needsUpdate = true }}
});
Object.defineProperty(this, "zoom", {
get() { return this.data.zoom },
set(value) { if (value !== this.data.zoom) { this.data.zoom = value; this.needsUpdate = true }}
});
this.updateProjectionMatrix();
}
updateProjectionMatrix() {
if (!this.needsUpdate) return;
if (!this.ortographicProjection)
this.ortographicProjection = new Matrix4();
if (!this.perspectiveProjection)
this.perspectiveProjection = new Matrix4();
if (!this.data)
this.data = {};
//copied from PerspectiveCamera
const near = this.near;
let top = near * Math.tan( MathUtils.DEG2RAD * 0.5 * this.fov ) / this.zoom;
let height = 2 * top;
let width = this.aspect * height;
let left = - 0.5 * width;
const view = this.view;
if ( this.view !== null && this.view.enabled ) {
const fullWidth = view.fullWidth,
fullHeight = view.fullHeight;
left += view.offsetX * width / fullWidth;
top -= view.offsetY * height / fullHeight;
width *= view.width / fullWidth;
height *= view.height / fullHeight;
}
const skew = this.filmOffset;
if ( skew !== 0 ) left += near * skew / this.getFilmWidth();
// this part different to PerspectiveCamera
let normalizedOrtho = -Math.pow(this.ortho - 1, 6) + 1;
let orthoTop = Math.max(this.distance, 0.0001) * Math.tan( MathUtils.DEG2RAD * 0.5 * this.fov ) / this.zoom;
let orthoHeight = 2 * orthoTop;
let orthoWidth = this.aspect * orthoHeight;
let orthoLeft = - 0.5 * orthoWidth;
this.perspectiveProjection.makePerspective( left, left + width, top, top - height, near, this.far );
this.ortographicProjection.makeOrthographic( orthoLeft, orthoLeft + orthoWidth, orthoTop, orthoTop - orthoHeight, near, this.far );
for (let i = 0; i < 16; i++){
this.projectionMatrix.elements[i] = (this.perspectiveProjection.elements[i] * (1 - normalizedOrtho)) + (this.ortographicProjection.elements[i] * normalizedOrtho);
}
// to here
this.projectionMatrixInverse.copy( this.projectionMatrix ).invert();
this.needsUpdate = false;
}
/**
* @returns {boolean}
*/
get isPerspectiveCamera() {
return this.ortho < 1;
}
/**
* @returns {boolean}
*/
get isOrthographicCamera() {
return !this.isPerspectiveCamera;
}
/**
* @returns {string}
*/
get type() {
return this.isPerspectiveCamera ? 'PerspectiveCamera' : 'OrthographicCamera';
}
/**
* @param type {string}
*/
set type(type) {
//ignore
}
/**
* @returns {number}
*/
get ortho() {
return this.data.ortho;
}
/**
* @param value {number}
*/
set ortho(value) {
if (value !== this.data.ortho){
this.data.ortho = value;
this.needsUpdate = true;
}
}
/**
* @returns {number}
*/
get distance() {
return this.data.distance;
}
/**
* @param value {number}
*/
set distance(value) {
if (value !== this.data.distance) {
this.data.distance = value;
this.needsUpdate = true;
}
}
}

View File

@ -0,0 +1,180 @@
/**
* Taken from https://github.com/mrdoob/three.js/blob/master/examples/jsm/libs/stats.module.js
*/
let Stats = function () {
let mode = 0;
let container = document.createElement( 'div' );
container.style.cssText = 'position:absolute;bottom:5px;right:5px;cursor:pointer;opacity:0.9;z-index:10000';
container.addEventListener( 'click', function ( event ) {
event.preventDefault();
showPanel( ++ mode % container.children.length );
}, false );
//
function addPanel( panel ) {
container.appendChild( panel.dom );
return panel;
}
function showPanel( id ) {
for ( let i = 0; i < container.children.length; i ++ ) {
container.children[ i ].style.display = i === id ? 'block' : 'none';
}
mode = id;
}
function hide() {
showPanel(-1);
}
//
let beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0;
let prevFrameTime = beginTime;
let fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) );
let msPanel = addPanel( new Stats.Panel( 'MS (render)', '#0f0', '#020' ) );
let lastFrameMsPanel = addPanel( new Stats.Panel( 'MS (all)', '#f80', '#210' ) );
let memPanel = null;
if ( self.performance && self.performance.memory ) {
memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) );
}
showPanel( 0 );
return {
REVISION: 16,
dom: container,
addPanel: addPanel,
showPanel: showPanel,
hide: hide,
begin: function () {
beginTime = ( performance || Date ).now();
},
end: function () {
frames ++;
let time = ( performance || Date ).now();
msPanel.update( time - beginTime, 200 );
lastFrameMsPanel.update( time - prevFrameTime, 200 )
if ( time >= prevTime + 1000 ) {
fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 );
prevTime = time;
frames = 0;
if ( memPanel ) {
let memory = performance.memory;
memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 );
}
}
return time;
},
update: function () {
beginTime = this.end();
prevFrameTime = beginTime;
},
// Backwards Compatibility
domElement: container,
setMode: showPanel
};
};
Stats.Panel = function ( name, fg, bg ) {
let min = Infinity, max = 0, round = Math.round;
let PR = round( window.devicePixelRatio || 1 );
let WIDTH = 160 * PR, HEIGHT = 96 * PR,
TEXT_X = 3 * PR, TEXT_Y = 3 * PR,
GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR,
GRAPH_WIDTH = 154 * PR, GRAPH_HEIGHT = 77 * PR;
let canvas = document.createElement( 'canvas' );
canvas.width = WIDTH;
canvas.height = HEIGHT;
canvas.style.cssText = 'width:160px;height:96px';
let context = canvas.getContext( '2d' );
context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif';
context.textBaseline = 'top';
context.fillStyle = bg;
context.fillRect( 0, 0, WIDTH, HEIGHT );
context.fillStyle = fg;
context.fillText( name, TEXT_X, TEXT_Y );
context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );
context.fillStyle = bg;
context.globalAlpha = 0.9;
context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );
return {
dom: canvas,
update: function ( value, maxValue ) {
min = Math.min( min, value );
max = Math.max( max, value );
context.fillStyle = bg;
context.globalAlpha = 1;
context.fillRect( 0, 0, WIDTH, GRAPH_Y );
context.fillStyle = fg;
context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y );
context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT );
context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT );
context.fillStyle = bg;
context.globalAlpha = 0.9;
context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) );
}
};
};
export default Stats;

View File

@ -0,0 +1,394 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import {MathUtils, Vector2, Vector3} from "three";
export const VEC2_ZERO = new Vector2(0, 0);
export const VEC3_ZERO = new Vector3(0, 0, 0);
export const VEC3_X = new Vector3(1, 0, 0);
export const VEC3_Y = new Vector3(0, 1, 0);
export const VEC3_Z = new Vector3(0, 0, 1);
/**
* Converts a url-encoded image string to an actual image-element
* @param string {string}
* @returns {HTMLElement}
*/
export const stringToImage = string => {
let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');
image.src = string;
return image;
};
/**
* Creates an optimized path from x,z coordinates used by bluemap to save tiles
* @param x {number}
* @param z {number}
* @returns {string}
*/
export const pathFromCoords = (x, z) => {
let path = 'x';
path += splitNumberToPath(x);
path += 'z';
path += splitNumberToPath(z);
path = path.substring(0, path.length - 1);
return path;
};
/**
* Splits a number into an optimized folder-path used to save bluemap-tiles
* @param num {number}
* @returns {string}
*/
const splitNumberToPath = num => {
let path = '';
if (num < 0) {
num = -num;
path += '-';
}
let s = num.toString();
for (let i = 0; i < s.length; i++) {
path += s.charAt(i) + '/';
}
return path;
};
/**
* Hashes tile-coordinates to be saved in a map
* @param x {number}
* @param z {number}
* @returns {string}
*/
export const hashTile = (x, z) => `x${x}z${z}`;
export const generateCacheHash = () => {
return Math.round(Math.random() * 1000000);
}
/**
* Dispatches an event to the element of this map-viewer
* @param element {EventTarget} the element on that the event is dispatched
* @param event {string}
* @param detail {object}
* @returns {undefined|void|boolean}
*/
export const dispatchEvent = (element, event, detail = {}) => {
if (!element || !element.dispatchEvent) return;
return element.dispatchEvent(new CustomEvent(event, {
detail: detail
}));
}
/**
* Sends a "bluemapAlert" event with a message and a level.
* The level can be anything, but the app uses the levels
* - debug
* - fine
* - info
* - warning
* - error
* @param element {EventTarget} the element on that the event is dispatched
* @param message {object}
* @param level {string}
*/
export const alert = (element, message, level = "info") => {
// alert event
let printToConsole = dispatchEvent(element, "bluemapAlert", {
message: message,
level: level
});
// log alert to console
if (printToConsole !== false) {
if (level === "info") {
console.log(`[BlueMap/${level}]`, message);
} else if (level === "warning") {
console.warn(`[BlueMap/${level}]`, message);
} else if (level === "error") {
console.error(`[BlueMap/${level}]`, message);
} else {
console.debug(`[BlueMap/${level}]`, message);
}
}
}
/**
* Source: https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
*
* @param html {string} representing a single element
* @return {Element}
*/
export const htmlToElement = html => {
let template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.firstChild;
}
/**
* Source: https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
*
* @param html {string} representing any number of sibling elements
* @return {NodeList}
*/
export const htmlToElements = html => {
let template = document.createElement('template');
template.innerHTML = html;
return template.content.childNodes;
}
/**
* Schedules an animation
* @param durationMs {number} the duration of the animation in ms
* @param animationFrame {function(progress: number, deltaTime: number)} a function that is getting called each frame with the parameters (progress (0-1), deltaTime)
* @param postAnimation {function(finished: boolean)} a function that gets called once after the animation is finished or cancelled. The function accepts one bool-parameter whether the animation was finished (true) or canceled (false)
* @returns {{cancel: function()}} the animation object
*/
export const animate = function (animationFrame, durationMs = 1000, postAnimation = null) {
let animation = {
animationStart: -1,
lastFrame: -1,
cancelled: false,
frame: function (time) {
if (this.cancelled) return;
if (this.animationStart === -1) {
this.animationStart = time;
this.lastFrame = time;
}
let progress = durationMs === 0 ? 1 : MathUtils.clamp((time - this.animationStart) / durationMs, 0, 1);
let deltaTime = time - this.lastFrame;
animationFrame(progress, deltaTime);
if (progress < 1) window.requestAnimationFrame(time => this.frame(time));
else if (postAnimation) postAnimation(true);
this.lastFrame = time;
},
cancel: function () {
this.cancelled = true;
if (postAnimation) postAnimation(false);
}
};
window.requestAnimationFrame(time => animation.frame(time));
return animation;
}
/**
* Source: https://gist.github.com/gre/1650294
* @type {{
* easeOutCubic: (function(number): number),
* linear: (function(number): number),
* easeOutQuint: (function(number): number),
* easeInQuart: (function(number): number),
* easeInOutQuint: (function(number): number),
* easeInQuad: (function(number): number),
* easeOutQuart: (function(number): number),
* easeInCubic: (function(number): number),
* easeInQuint: (function(number): number),
* easeOutQuad: (function(number): number),
* easeInOutQuad: (function(number): number),
* easeInOutCubic: (function(number): number),
* easeInOutQuart: (function(number): number)
* }}
*/
export const EasingFunctions = {
// no easing, no acceleration
linear: t => t,
// accelerating from zero velocity
easeInQuad: t => t*t,
// decelerating to zero velocity
easeOutQuad: t => t*(2-t),
// acceleration until halfway, then deceleration
easeInOutQuad: t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
// accelerating from zero velocity
easeInCubic: t => t*t*t,
// decelerating to zero velocity
easeOutCubic: t => (--t)*t*t+1,
// acceleration until halfway, then deceleration
easeInOutCubic: t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
// accelerating from zero velocity
easeInQuart: t => t*t*t*t,
// decelerating to zero velocity
easeOutQuart: t => 1-(--t)*t*t*t,
// acceleration until halfway, then deceleration
easeInOutQuart: t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t,
// accelerating from zero velocity
easeInQuint: t => t*t*t*t*t,
// decelerating to zero velocity
easeOutQuint: t => 1+(--t)*t*t*t*t,
// acceleration until halfway, then deceleration
easeInOutQuint: t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t
}
/**
* Returns the offset position of an element
*
* Source: https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/
*
* @param element {Element}
* @returns {{top: number, left: number}}
*/
export const elementOffset = element => {
let rect = element.getBoundingClientRect(),
scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
scrollTop = window.pageYOffset || document.documentElement.scrollTop;
return { top: rect.top + scrollTop, left: rect.left + scrollLeft }
}
/**
* Very simple deep equals, should not be used for complex objects. Is designed for comparing parsed json-objects.
* @param object1 {object}
* @param object2 {object}
* @returns {boolean}
*/
export const deepEquals = (object1, object2) => {
if (Object.is(object1, object2)) return true;
let type = typeof object1;
if (type !== typeof object2) return false;
if (type === 'number' || type === 'boolean' || type === 'string') return false;
if (Array.isArray(object1)){
let len = object1.length;
if (len !== object2.length) return false;
for (let i = 0; i < len; i++) {
if (!deepEquals(object1[i], object2[i])) return false;
}
return true;
}
for (let property in object1) {
if (!object1.hasOwnProperty(property)) continue;
if (!deepEquals(object1[property], object2[property])) return false;
}
return true;
}
/**
* Adds one listener to multiple events
* @param target {EventTarget}
* @param types {string|string[]}
* @param listener {EventListenerOrEventListenerObject | null}
* @param options {boolean | AddEventListenerOptions?}
*/
export const addEventListeners = (target, types, listener, options) => {
if (!Array.isArray(types)){
types = types.trim().split(" ");
}
types.forEach(type => target.addEventListener(type, listener, options));
}
/**
* Removes one listener to multiple events
* @param target {EventTarget}
* @param types {string|string[]}
* @param listener {EventListenerOrEventListenerObject | null}
* @param options {boolean | EventListenerOptions?}
*/
export const removeEventListeners = (target, types, listener, options) => {
if (!Array.isArray(types)){
types = types.trim().split(" ");
}
types.forEach(type => target.removeEventListener(type, listener, options));
}
/**
* Softly clamps towards a minimum value
* @param value {number}
* @param min {number}
* @param stiffness {number}
* @returns {number}
*/
export const softMin = (value, min, stiffness) => {
if (value >= min) return value;
let delta = min - value;
if (delta < 0.0001) return min;
return value + delta * stiffness;
}
/**
* Softly clamps towards a maximum value
* @param value {number}
* @param max {number}
* @param stiffness {number}
* @returns {number}
*/
export const softMax = (value, max, stiffness) => {
if (value <= max) return value;
let delta = value - max;
if (delta < 0.0001) return max;
return value - delta * stiffness;
}
/**
* Softly clamps towards a minimum and maximum value
* @param value {number}
* @param min {number}
* @param max {number}
* @param stiffness {number}
* @returns {number}
*/
export const softClamp = (value, min, max, stiffness) => {
return softMax(softMin(value, min, stiffness), max, stiffness);
}
export const vecArrToObj = (val, useZ = false) => {
if (val && val.length >= 2) {
if (useZ) return {x: val[0], z: val[1]};
return {x: val[0], y: val[1]};
}
return {};
}
const pixel = document.createElement('canvas');
pixel.width = 1;
pixel.height = 1;
const pixelContext = pixel.getContext('2d', {
willReadFrequently: true
});
export const getPixel = (img, x, y) => {
pixelContext.drawImage(img, Math.floor(x), Math.floor(y), 1, 1, 0, 0, 1, 1);
return pixelContext.getImageData(0, 0, 1, 1).data;
}

View File

@ -0,0 +1,25 @@
import { LineSegments2 } from "./LineSegments2";
import { LineGeometry } from "./LineGeometry";
import { LineMaterial } from "./LineMaterial";
var Line2 = function ( geometry, material ) {
if ( geometry === undefined ) geometry = new LineGeometry();
if ( material === undefined ) material = new LineMaterial( { color: Math.random() * 0xffffff } );
LineSegments2.call( this, geometry, material );
this.type = 'Line2';
};
Line2.prototype = Object.assign( Object.create( LineSegments2.prototype ), {
constructor: Line2,
isLine2: true
} );
export { Line2 };

View File

@ -0,0 +1,96 @@
import { LineSegmentsGeometry } from "./LineSegmentsGeometry";
var LineGeometry = function () {
LineSegmentsGeometry.call( this );
this.type = 'LineGeometry';
};
LineGeometry.prototype = Object.assign( Object.create( LineSegmentsGeometry.prototype ), {
constructor: LineGeometry,
isLineGeometry: true,
setPositions: function ( array ) {
// converts [ x1, y1, z1, x2, y2, z2, ... ] to pairs format
var length = array.length - 3;
var points = new Float32Array( 2 * length );
for ( var i = 0; i < length; i += 3 ) {
points[ 2 * i ] = array[ i ];
points[ 2 * i + 1 ] = array[ i + 1 ];
points[ 2 * i + 2 ] = array[ i + 2 ];
points[ 2 * i + 3 ] = array[ i + 3 ];
points[ 2 * i + 4 ] = array[ i + 4 ];
points[ 2 * i + 5 ] = array[ i + 5 ];
}
LineSegmentsGeometry.prototype.setPositions.call( this, points );
return this;
},
setColors: function ( array ) {
// converts [ r1, g1, b1, r2, g2, b2, ... ] to pairs format
var length = array.length - 3;
var colors = new Float32Array( 2 * length );
for ( var i = 0; i < length; i += 3 ) {
colors[ 2 * i ] = array[ i ];
colors[ 2 * i + 1 ] = array[ i + 1 ];
colors[ 2 * i + 2 ] = array[ i + 2 ];
colors[ 2 * i + 3 ] = array[ i + 3 ];
colors[ 2 * i + 4 ] = array[ i + 4 ];
colors[ 2 * i + 5 ] = array[ i + 5 ];
}
LineSegmentsGeometry.prototype.setColors.call( this, colors );
return this;
},
fromLine: function ( line ) {
var geometry = line.geometry;
if ( geometry.isGeometry ) {
this.setPositions( geometry.vertices );
} else if ( geometry.isBufferGeometry ) {
this.setPositions( geometry.attributes.position.array ); // assumes non-indexed
}
// set colors, maybe
return this;
},
copy: function ( /* source */ ) {
return this;
}
} );
export { LineGeometry };

View File

@ -0,0 +1,424 @@
import {
ShaderLib,
ShaderMaterial,
UniformsLib,
UniformsUtils,
Vector2
} from "three";
/**
* parameters = {
* color: <hex>,
* linewidth: <float>,
* dashed: <boolean>,
* dashScale: <float>,
* dashSize: <float>,
* gapSize: <float>,
* resolution: <Vector2>, // to be set by renderer
* }
*/
UniformsLib.line = {
linewidth: { value: 1 },
resolution: { value: new Vector2( 1, 1 ) },
dashScale: { value: 1 },
dashSize: { value: 1 },
gapSize: { value: 1 },
opacity: { value: 1 }
};
ShaderLib[ 'line' ] = {
uniforms: UniformsUtils.merge( [
UniformsLib.common,
UniformsLib.fog,
UniformsLib.line
] ),
vertexShader:
`
#include <common>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
uniform float linewidth;
uniform vec2 resolution;
attribute vec3 instanceStart;
attribute vec3 instanceEnd;
attribute vec3 instanceColorStart;
attribute vec3 instanceColorEnd;
varying vec2 vUv;
varying float vDistance;
#ifdef USE_DASH
uniform float dashScale;
attribute float instanceDistanceStart;
attribute float instanceDistanceEnd;
varying float vLineDistance;
#endif
void trimSegment( const in vec4 start, inout vec4 end ) {
// trim end segment so it terminates between the camera plane and the near plane
// conservative estimate of the near plane
float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column
float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column
float nearEstimate = - 0.5 * b / a;
float alpha = ( nearEstimate - start.z ) / ( end.z - start.z );
end.xyz = mix( start.xyz, end.xyz, alpha );
}
void main() {
#ifdef USE_COLOR
vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd;
#endif
#ifdef USE_DASH
vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd;
#endif
float aspect = resolution.x / resolution.y;
vUv = uv;
// camera space
vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );
vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );
// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
// but we need to perform ndc-space calculations in the shader, so we must address this issue directly
// perhaps there is a more elegant solution -- WestLangley
bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column
if ( perspective ) {
if ( start.z < 0.0 && end.z >= 0.0 ) {
trimSegment( start, end );
} else if ( end.z < 0.0 && start.z >= 0.0 ) {
trimSegment( end, start );
}
}
// clip space
vec4 clipStart = projectionMatrix * start;
vec4 clipEnd = projectionMatrix * end;
// ndc space
vec2 ndcStart = clipStart.xy / clipStart.w;
vec2 ndcEnd = clipEnd.xy / clipEnd.w;
// direction
vec2 dir = ndcEnd - ndcStart;
// account for clip-space aspect ratio
dir.x *= aspect;
dir = normalize( dir );
// perpendicular to dir
vec2 offset = vec2( dir.y, - dir.x );
// undo aspect ratio adjustment
dir.x /= aspect;
offset.x /= aspect;
// sign flip
if ( position.x < 0.0 ) offset *= - 1.0;
// endcaps
if ( position.y < 0.0 ) {
offset += - dir;
} else if ( position.y > 1.0 ) {
offset += dir;
}
// adjust for linewidth
offset *= linewidth;
// adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
offset /= resolution.y;
// select end
vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd;
// back to clip space
offset *= clip.w;
clip.xy += offset;
gl_Position = clip;
vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation
vDistance = -mvPosition.z;
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
#include <fog_vertex>
}
`,
fragmentShader:
`
#define FLT_MAX 3.402823466e+38
uniform vec3 diffuse;
uniform float opacity;
uniform float fadeDistanceMax;
uniform float fadeDistanceMin;
#ifdef USE_DASH
uniform float dashSize;
uniform float gapSize;
#endif
varying float vLineDistance;
#include <common>
#include <color_pars_fragment>
#include <fog_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
varying vec2 vUv;
varying float vDistance;
void main() {
#include <clipping_planes_fragment>
#ifdef USE_DASH
if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps
if ( mod( vLineDistance, dashSize + gapSize ) > dashSize ) discard; // todo - FIX
#endif
if ( abs( vUv.y ) > 1.0 ) {
float a = vUv.x;
float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;
float len2 = a * a + b * b;
if ( len2 > 1.0 ) discard;
}
// distance fading
float fdMax = FLT_MAX;
if ( fadeDistanceMax > 0.0 ) fdMax = fadeDistanceMax;
float minDelta = (vDistance - fadeDistanceMin) / fadeDistanceMin;
float maxDelta = (vDistance - fadeDistanceMax) / (fadeDistanceMax * 0.5);
float distanceOpacity = min(
clamp(minDelta, 0.0, 1.0),
1.0 - clamp(maxDelta + 1.0, 0.0, 1.0)
);
vec4 diffuseColor = vec4( diffuse, opacity * distanceOpacity );
#include <logdepthbuf_fragment>
#include <color_fragment>
gl_FragColor = vec4( diffuseColor.rgb, diffuseColor.a );
#include <tonemapping_fragment>
#include <encodings_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
}
`
};
var LineMaterial = function ( parameters ) {
ShaderMaterial.call( this, {
type: 'LineMaterial',
uniforms: UniformsUtils.clone( ShaderLib[ 'line' ].uniforms ),
vertexShader: ShaderLib[ 'line' ].vertexShader,
fragmentShader: ShaderLib[ 'line' ].fragmentShader,
clipping: true // required for clipping support
} );
this.dashed = false;
Object.defineProperties( this, {
color: {
enumerable: true,
get: function () {
return this.uniforms.diffuse.value;
},
set: function ( value ) {
this.uniforms.diffuse.value = value;
}
},
linewidth: {
enumerable: true,
get: function () {
return this.uniforms.linewidth.value;
},
set: function ( value ) {
this.uniforms.linewidth.value = value;
}
},
dashScale: {
enumerable: true,
get: function () {
return this.uniforms.dashScale.value;
},
set: function ( value ) {
this.uniforms.dashScale.value = value;
}
},
dashSize: {
enumerable: true,
get: function () {
return this.uniforms.dashSize.value;
},
set: function ( value ) {
this.uniforms.dashSize.value = value;
}
},
gapSize: {
enumerable: true,
get: function () {
return this.uniforms.gapSize.value;
},
set: function ( value ) {
this.uniforms.gapSize.value = value;
}
},
opacity: {
enumerable: true,
get: function () {
return this.uniforms.opacity.value;
},
set: function ( value ) {
this.uniforms.opacity.value = value;
}
},
resolution: {
enumerable: true,
get: function () {
return this.uniforms.resolution.value;
},
set: function ( value ) {
this.uniforms.resolution.value.copy( value );
}
}
} );
this.setValues( parameters );
};
LineMaterial.prototype = Object.create( ShaderMaterial.prototype );
LineMaterial.prototype.constructor = LineMaterial;
LineMaterial.prototype.isLineMaterial = true;
export { LineMaterial };

View File

@ -0,0 +1,209 @@
import {
InstancedInterleavedBuffer,
InterleavedBufferAttribute,
Line3,
MathUtils,
Matrix4,
Mesh,
Vector3,
Vector4
} from "three";
import { LineSegmentsGeometry } from "./LineSegmentsGeometry";
import { LineMaterial } from "./LineMaterial";
var LineSegments2 = function ( geometry, material ) {
if ( geometry === undefined ) geometry = new LineSegmentsGeometry();
if ( material === undefined ) material = new LineMaterial( { color: Math.random() * 0xffffff } );
Mesh.call( this, geometry, material );
this.type = 'LineSegments2';
};
LineSegments2.prototype = Object.assign( Object.create( Mesh.prototype ), {
constructor: LineSegments2,
isLineSegments2: true,
computeLineDistances: ( function () { // for backwards-compatability, but could be a method of LineSegmentsGeometry...
var start = new Vector3();
var end = new Vector3();
return function computeLineDistances() {
var geometry = this.geometry;
var instanceStart = geometry.attributes.instanceStart;
var instanceEnd = geometry.attributes.instanceEnd;
var lineDistances = new Float32Array( 2 * instanceStart.data.count );
for ( var i = 0, j = 0, l = instanceStart.data.count; i < l; i ++, j += 2 ) {
start.fromBufferAttribute( instanceStart, i );
end.fromBufferAttribute( instanceEnd, i );
lineDistances[ j ] = ( j === 0 ) ? 0 : lineDistances[ j - 1 ];
lineDistances[ j + 1 ] = lineDistances[ j ] + start.distanceTo( end );
}
var instanceDistanceBuffer = new InstancedInterleavedBuffer( lineDistances, 2, 1 ); // d0, d1
geometry.setAttribute( 'instanceDistanceStart', new InterleavedBufferAttribute( instanceDistanceBuffer, 1, 0 ) ); // d0
geometry.setAttribute( 'instanceDistanceEnd', new InterleavedBufferAttribute( instanceDistanceBuffer, 1, 1 ) ); // d1
return this;
};
}() ),
raycast: ( function () {
var start = new Vector4();
var end = new Vector4();
var ssOrigin = new Vector4();
var ssOrigin3 = new Vector3();
var mvMatrix = new Matrix4();
var line = new Line3();
var closestPoint = new Vector3();
return function raycast( raycaster, intersects ) {
if ( raycaster.camera === null ) {
console.error( 'LineSegments2: "Raycaster.camera" needs to be set in order to raycast against LineSegments2.' );
}
var threshold = ( raycaster.params.Line2 !== undefined ) ? raycaster.params.Line2.threshold || 0 : 0;
var ray = raycaster.ray;
var camera = raycaster.camera;
var projectionMatrix = camera.projectionMatrix;
var geometry = this.geometry;
var material = this.material;
var resolution = material.resolution;
var lineWidth = material.linewidth + threshold;
var instanceStart = geometry.attributes.instanceStart;
var instanceEnd = geometry.attributes.instanceEnd;
// pick a point 1 unit out along the ray to avoid the ray origin
// sitting at the camera origin which will cause "w" to be 0 when
// applying the projection matrix.
ray.at( 1, ssOrigin );
// ndc space [ - 1.0, 1.0 ]
ssOrigin.w = 1;
ssOrigin.applyMatrix4( camera.matrixWorldInverse );
ssOrigin.applyMatrix4( projectionMatrix );
ssOrigin.multiplyScalar( 1 / ssOrigin.w );
// screen space
ssOrigin.x *= resolution.x / 2;
ssOrigin.y *= resolution.y / 2;
ssOrigin.z = 0;
ssOrigin3.copy( ssOrigin );
var matrixWorld = this.matrixWorld;
mvMatrix.multiplyMatrices( camera.matrixWorldInverse, matrixWorld );
for ( var i = 0, l = instanceStart.count; i < l; i ++ ) {
start.fromBufferAttribute( instanceStart, i );
end.fromBufferAttribute( instanceEnd, i );
start.w = 1;
end.w = 1;
// camera space
start.applyMatrix4( mvMatrix );
end.applyMatrix4( mvMatrix );
// clip space
start.applyMatrix4( projectionMatrix );
end.applyMatrix4( projectionMatrix );
// ndc space [ - 1.0, 1.0 ]
start.multiplyScalar( 1 / start.w );
end.multiplyScalar( 1 / end.w );
// skip the segment if it's outside the camera near and far planes
var isBehindCameraNear = start.z < - 1 && end.z < - 1;
var isPastCameraFar = start.z > 1 && end.z > 1;
if ( isBehindCameraNear || isPastCameraFar ) {
continue;
}
// screen space
start.x *= resolution.x / 2;
start.y *= resolution.y / 2;
end.x *= resolution.x / 2;
end.y *= resolution.y / 2;
// create 2d segment
line.start.copy( start );
line.start.z = 0;
line.end.copy( end );
line.end.z = 0;
// get closest point on ray to segment
var param = line.closestPointToPointParameter( ssOrigin3, true );
line.at( param, closestPoint );
// check if the intersection point is within clip space
var zPos = MathUtils.lerp( start.z, end.z, param );
var isInClipSpace = zPos >= - 1 && zPos <= 1;
var isInside = ssOrigin3.distanceTo( closestPoint ) < lineWidth * 0.5;
if ( isInClipSpace && isInside ) {
line.start.fromBufferAttribute( instanceStart, i );
line.end.fromBufferAttribute( instanceEnd, i );
line.start.applyMatrix4( matrixWorld );
line.end.applyMatrix4( matrixWorld );
var pointOnLine = new Vector3();
var point = new Vector3();
ray.distanceSqToSegment( line.start, line.end, point, pointOnLine );
intersects.push( {
point: point,
pointOnLine: pointOnLine,
distance: ray.origin.distanceTo( point ),
object: this,
face: null,
faceIndex: i,
uv: null,
uv2: null,
} );
}
}
};
}() )
} );
export { LineSegments2 };

View File

@ -0,0 +1,258 @@
import {
Box3,
Float32BufferAttribute,
InstancedBufferGeometry,
InstancedInterleavedBuffer,
InterleavedBufferAttribute,
Sphere,
Vector3,
WireframeGeometry
} from "three";
var LineSegmentsGeometry = function () {
InstancedBufferGeometry.call( this );
this.type = 'LineSegmentsGeometry';
var positions = [ - 1, 2, 0, 1, 2, 0, - 1, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, - 1, - 1, 0, 1, - 1, 0 ];
var uvs = [ - 1, 2, 1, 2, - 1, 1, 1, 1, - 1, - 1, 1, - 1, - 1, - 2, 1, - 2 ];
var index = [ 0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5 ];
this.setIndex( index );
this.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
};
LineSegmentsGeometry.prototype = Object.assign( Object.create( InstancedBufferGeometry.prototype ), {
constructor: LineSegmentsGeometry,
isLineSegmentsGeometry: true,
applyMatrix4: function ( matrix ) {
var start = this.attributes.instanceStart;
var end = this.attributes.instanceEnd;
if ( start !== undefined ) {
start.applyMatrix4( matrix );
end.applyMatrix4( matrix );
start.needsUpdate = true;
}
if ( this.boundingBox !== null ) {
this.computeBoundingBox();
}
if ( this.boundingSphere !== null ) {
this.computeBoundingSphere();
}
return this;
},
setPositions: function ( array ) {
var lineSegments;
if ( array instanceof Float32Array ) {
lineSegments = array;
} else if ( Array.isArray( array ) ) {
lineSegments = new Float32Array( array );
}
var instanceBuffer = new InstancedInterleavedBuffer( lineSegments, 6, 1 ); // xyz, xyz
this.setAttribute( 'instanceStart', new InterleavedBufferAttribute( instanceBuffer, 3, 0 ) ); // xyz
this.setAttribute( 'instanceEnd', new InterleavedBufferAttribute( instanceBuffer, 3, 3 ) ); // xyz
//
this.computeBoundingBox();
this.computeBoundingSphere();
return this;
},
setColors: function ( array ) {
var colors;
if ( array instanceof Float32Array ) {
colors = array;
} else if ( Array.isArray( array ) ) {
colors = new Float32Array( array );
}
var instanceColorBuffer = new InstancedInterleavedBuffer( colors, 6, 1 ); // rgb, rgb
this.setAttribute( 'instanceColorStart', new InterleavedBufferAttribute( instanceColorBuffer, 3, 0 ) ); // rgb
this.setAttribute( 'instanceColorEnd', new InterleavedBufferAttribute( instanceColorBuffer, 3, 3 ) ); // rgb
return this;
},
fromWireframeGeometry: function ( geometry ) {
this.setPositions( geometry.attributes.position.array );
return this;
},
fromEdgesGeometry: function ( geometry ) {
this.setPositions( geometry.attributes.position.array );
return this;
},
fromMesh: function ( mesh ) {
this.fromWireframeGeometry( new WireframeGeometry( mesh.geometry ) );
// set colors, maybe
return this;
},
fromLineSegments: function ( lineSegments ) {
var geometry = lineSegments.geometry;
if ( geometry.isGeometry ) {
this.setPositions( geometry.vertices );
} else if ( geometry.isBufferGeometry ) {
this.setPositions( geometry.attributes.position.array ); // assumes non-indexed
}
// set colors, maybe
return this;
},
computeBoundingBox: function () {
var box = new Box3();
return function computeBoundingBox() {
if ( this.boundingBox === null ) {
this.boundingBox = new Box3();
}
var start = this.attributes.instanceStart;
var end = this.attributes.instanceEnd;
if ( start !== undefined && end !== undefined ) {
this.boundingBox.setFromBufferAttribute( start );
box.setFromBufferAttribute( end );
this.boundingBox.union( box );
}
};
}(),
computeBoundingSphere: function () {
var vector = new Vector3();
return function computeBoundingSphere() {
if ( this.boundingSphere === null ) {
this.boundingSphere = new Sphere();
}
if ( this.boundingBox === null ) {
this.computeBoundingBox();
}
var start = this.attributes.instanceStart;
var end = this.attributes.instanceEnd;
if ( start !== undefined && end !== undefined ) {
var center = this.boundingSphere.center;
this.boundingBox.getCenter( center );
var maxRadiusSq = 0;
for ( var i = 0, il = start.count; i < il; i ++ ) {
vector.fromBufferAttribute( start, i );
maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) );
vector.fromBufferAttribute( end, i );
maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) );
}
this.boundingSphere.radius = Math.sqrt( maxRadiusSq );
if ( isNaN( this.boundingSphere.radius ) ) {
console.error( 'THREE.LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this );
}
}
};
}(),
toJSON: function () {
},
applyMatrix: function ( matrix ) {
console.warn( 'THREE.LineSegmentsGeometry: applyMatrix() has been renamed to applyMatrix4().' );
return this.applyMatrix4( matrix );
}
} );
export { LineSegmentsGeometry };