Rework markers again and add lots of documentation

This commit is contained in:
Blue (Lukas Rieger) 2021-01-28 02:56:26 +01:00
parent 6296ba341a
commit 31c159d9ae
No known key found for this signature in database
GPG Key ID: 904C4995F9E1F800
35 changed files with 2094 additions and 1212 deletions

View File

@ -70,7 +70,7 @@
"z": 20.0
},
"minDistance": 100.0,
"maxDistance": 1000.0,
"maxDistance": 800.0,
"label": "<div style='background-color: white; padding: 5px;'><b>Styling this labels will be the job of the main-plugin, not this Library :)</b></div>",
"newTab": true,
"lineWidth": 5,
@ -109,15 +109,15 @@
"type": "poi",
"map": "world",
"position": {
"x": 0.0,
"y": 81.0,
"z": 144.0
"x": 50.0,
"y": 65.0,
"z": -44.0
},
"iconAnchor": {
"x": 25,
"y": 45
},
"minDistance": 100.0,
"minDistance": 0.0,
"maxDistance": 800.0,
"label": "Spawn"
},
@ -131,64 +131,64 @@
"z": 144.0
},
"minDistance": 0.0,
"maxDistance": 200.0,
"maxDistance": 400.0,
"label": "WOAH! i am <b>3D</b>!!",
"newTab": true,
"lineWidth": 2,
"shape": [
{
"x": 48.0,
"z": 104.0
"z": 4.0
},
{
"x": 48.0,
"z": 200.0
"z": 100.0
},
{
"x": 112.0,
"z": 200.0
"z": 100.0
},
{
"x": 112.0,
"z": 136.0
"z": 36.0
},
{
"x": 96.0,
"z": 136.0
"z": 36.0
},
{
"x": 96.0,
"z": 174.0
"z": 74.0
},
{
"x": 80.0,
"z": 184.0
"z": 84.0
},
{
"x": 80.0,
"z": 120.0
"z": 20.0
},
{
"x": 112.0,
"z": 120.0
"z": 20.0
},
{
"x": 112.0,
"z": 98.0
"z": -2.0
},
{
"x": 80.0,
"z": 88.0
"z": -12.0
},
{
"x": 80.0,
"z": 104.0
"z": 4.0
}
],
"depthTest": true,
"minHeight": 0,
"maxHeight": 150,
"borderColor": {
"shapeMinY": 0,
"shapeMaxY": 150,
"lineColor": {
"r": 103,
"g": 252,
"b": 100,
@ -210,8 +210,8 @@
"y": 80,
"z": 2352.0
},
"minDistance": 0.0,
"maxDistance": 500.0,
"minDistance": 200.0,
"maxDistance": 100000.0,
"label": "Styling this labels will be the job of the main-plugin, not this Library :)",
"newTab": true,
"shape": [

View File

@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<title>BlueMap Lib Test</title>
<link rel="shortcut icon" href="favicon.png">
</head>
@ -20,9 +21,14 @@
// load map
let maps = [];
let markerManager = null;
BlueMap.loadMaps("data/", bluemap.events).then(loadedMaps => {
maps = loadedMaps;
bluemap.setMap(maps[0]);
markerManager = maps[0].createMarkerFileManager(bluemap.markerScene);
markerManager.update();
markerManager.setAutoUpdateInterval(1000 * 10);
});
</script>
</body>

View File

@ -1,4 +1,4 @@
import {FileLoader} from "three";
import {FileLoader, Object3D} from "three";
import {Map} from "./map/Map";
export { MapViewer } from "./MapViewer";
@ -7,8 +7,8 @@ export * from "./util/Utils";
/**
* Loads and returns a promise with an array of Maps loaded from that root-path.<br>
* <b>DONT FORGET TO dispose() ALL MAPS RETURNED BY THIS METHOD IF YOU DONT NEED THEM ANYMORE!</b>
* @param dataUrl
* @param events
* @param dataUrl {string}
* @param events {EventTarget}
* @returns {Promise<Map[]>}
*/
export const loadMaps = (dataUrl, events = null) => {
@ -50,3 +50,19 @@ export const loadMaps = (dataUrl, events = null) => {
});
}
/**
* @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);
}
return false;
};

View File

@ -1,7 +1,7 @@
import {
PerspectiveCamera,
WebGLRenderer,
Vector2, Raycaster, Layers
Vector2, Raycaster, Layers, Scene
} from "three";
import {Map} from "./map/Map";
import {SkyboxScene} from "./skybox/SkyboxScene";
@ -24,6 +24,12 @@ export class MapViewer {
RIGHTCLICK: 1
};
/**
* @param element {Element}
* @param dataUrl {string}
* @param liveApiUrl {string}
* @param events {EventTarget}
*/
constructor(element, dataUrl = "data/", liveApiUrl = "live/", events = element) {
Object.defineProperty( this, 'isMapViewer', { value: true } );
@ -84,8 +90,11 @@ export class MapViewer {
this.raycaster.layers.enableAll();
this.raycaster.params.Line2 = {threshold: 20}
/** @type {Map} */
this.map = null;
this.markerScene = new Scene();
this.lastFrame = 0;
// initialize
@ -124,13 +133,6 @@ export class MapViewer {
let outerDiv = htmlToElement(`<div style="position: relative; width: 100%; height: 100%; overflow: hidden;"></div>`);
this.rootElement.appendChild(outerDiv)
/*this.rootElement.addEventListener('click', event => {
let rootOffset = elementOffset(this.rootElement);
this.handleMapInteraction(new Vector2(
((event.pageX - rootOffset.top) / this.rootElement.clientWidth) * 2 - 1,
-((event.pageY - rootOffset.left) / this.rootElement.clientHeight) * 2 + 1
));
});*/
this.hammer.on('tap', event => {
let rootOffset = elementOffset(this.rootElement);
this.handleMapInteraction(new Vector2(
@ -168,6 +170,10 @@ export class MapViewer {
this.camera.updateProjectionMatrix();
};
/**
* @param screenPos {{x: number, y:number}}
* @param interactionType {number}
*/
handleMapInteraction(screenPos, interactionType = MapViewer.InteractionType.LEFTCLICK) {
if (this.map && this.map.isLoaded){
this.raycaster.setFromCamera(screenPos, this.camera);
@ -176,16 +182,21 @@ export class MapViewer {
lowresLayer.set(2);
// check marker interactions
let intersects = this.raycaster.intersectObjects([this.map.scene, this.map.markerManager.objectMarkerScene], true);
let intersects = this.raycaster.intersectObjects([this.map.scene, this.markerScene], true);
let covered = false;
for (let i = 0; i < intersects.length; i++) {
if (intersects[0].object){
let marker = intersects[i].object.marker;
if (marker && marker._opacity > 0 && (!covered || !marker.depthTest)) {
marker.onClick(intersects[i].pointOnLine || intersects[i].point);
return;
} else if (!intersects[i].object.layers.test(lowresLayer)) {
covered = true;
if (intersects[i].object){
let object = intersects[i].object;
if (object.visible) {
if (!covered || (object.material && !object.material.depthTest)) {
if (object.onClick({
interactionType: interactionType,
intersection: intersects[i]
})) return;
covered = true;
} else if (!intersects[i].object.layers.test(lowresLayer)) {
covered = true;
}
}
}
}
@ -201,7 +212,7 @@ export class MapViewer {
/**
* The render-loop to update and possibly render a new frame.
* @param now the current time in milliseconds
* @param now {number} the current time in milliseconds
*/
renderLoop = (now) => {
requestAnimationFrame(this.renderLoop);
@ -229,6 +240,7 @@ export class MapViewer {
/**
* Renders a frame
* @param delta {number}
*/
render(delta) {
dispatchEvent(this.events, "bluemapRenderFrame", {
@ -263,15 +275,18 @@ export class MapViewer {
this.camera.layers.set(0);
if (this.controlsManager.distance < 2000) this.camera.layers.enable(1);
this.renderer.render(this.map.scene, this.camera);
this.renderer.render(this.map.markerManager.objectMarkerScene, this.camera);
this.css2dRenderer.render(this.map.markerManager.elementMarkerScene, this.camera);
//this.renderer.render(this.map.markerManager.objectMarkerScene, this.camera);
//this.css2dRenderer.render(this.map.markerManager.elementMarkerScene, this.camera);
}
// render markers
this.renderer.render(this.markerScene, this.camera);
this.css2dRenderer.render(this.markerScene, this.camera);
}
/**
* Changes / Sets the map that will be loaded and displayed
* @param map
* @param map {Map}
*/
setMap(map = null) {
if (this.map && this.map.isMap) this.map.unload();
@ -303,6 +318,12 @@ export class MapViewer {
}
}
/**
* @param centerX {number}
* @param centerZ {number}
* @param hiresViewDistance {number}
* @param lowresViewDistance {number}
*/
loadMapArea(centerX, centerZ, hiresViewDistance = -1, lowresViewDistance = -1) {
this.loadedCenter.set(centerX, centerZ);
if (hiresViewDistance >= 0) this.loadedHiresViewDistance = hiresViewDistance;
@ -311,10 +332,16 @@ export class MapViewer {
this.updateLoadedMapArea();
}
/**
* @returns {number}
*/
get superSampling() {
return this.superSamplingValue;
}
/**
* @param value {number}
*/
set superSampling(value) {
this.superSamplingValue = value;
this.handleContainerResize();
@ -324,7 +351,7 @@ export class MapViewer {
/**
* Applies a loaded settings-object (settings.json)
* @param settings
* @param settings {{maps: {}}}
*/
applySettings(settings) {

View File

@ -1,8 +1,13 @@
import {MathUtils, Vector3} from "three";
import {dispatchEvent} from "../util/Utils";
import {Map} from "../map/Map";
export class ControlsManager {
/**
* @param mapViewer {MapViewer}
* @param camera {THREE.Camera}
*/
constructor(mapViewer, camera) {
Object.defineProperty( this, 'isControlsManager', { value: true } );
@ -26,6 +31,10 @@ export class ControlsManager {
this.updateCamera();
}
/**
* @param deltaTime {number}
* @param map {Map}
*/
update(deltaTime, map) {
if (deltaTime > 50) deltaTime = 50; // assume min 20 UPS
@ -101,78 +110,133 @@ export class ControlsManager {
this.valueChanged = true;
}
/**
* @returns {number}
*/
get x() {
return this.positionValue.x;
}
/**
* @param x {number}
*/
set x(x) {
this.positionValue.x = x;
this.handleValueChange();
}
/**
* @returns {number}
*/
get y() {
return this.positionValue.y;
}
/**
* @param y {number}
*/
set y(y) {
this.positionValue.y = y;
this.handleValueChange();
}
/**
* @returns {number}
*/
get z() {
return this.positionValue.z;
}
/**
* @param z {number}
*/
set z(z) {
this.positionValue.z = z;
this.handleValueChange();
}
/**
* @returns {Vector3}
*/
get position() {
return this.positionValue;
}
/**
* @param position {Vector3}
*/
set position(position) {
this.position.copy(position);
this.handleValueChange();
}
/**
* @returns {number}
*/
get rotation() {
return this.rotationValue;
}
/**
* @param rotation {number}
*/
set rotation(rotation) {
this.rotationValue = rotation;
this.handleValueChange();
}
/**
* @returns {number}
*/
get angle() {
return this.angleValue;
}
/**
* @param angle {number}
*/
set angle(angle) {
this.angleValue = angle;
this.handleValueChange();
}
/**
* @returns {number}
*/
get distance() {
return this.distanceValue;
}
/**
* @param distance {number}
*/
set distance(distance) {
this.distanceValue = distance;
this.handleValueChange();
}
/**
* @returns {number}
*/
get ortho() {
return this.orthoValue;
}
/**
* @param ortho {number}
*/
set ortho(ortho) {
this.orthoValue = ortho;
this.handleValueChange();
}
/**
* @param controls {{
* start: function(controls: ControlsManager),
* stop: function(),
* update: function(deltaTime: number, map: Map)
* }}
*/
set controls(controls) {
if (this.controlsValue && typeof this.controlsValue.stop === "function")
this.controlsValue.stop();
@ -183,6 +247,13 @@ export class ControlsManager {
this.controlsValue.start(this);
}
/**
* @returns {{
* start: function(controls: ControlsManager),
* stop: function(),
* update: function(deltaTime: number, map: Map)
* }}
*/
get controls() {
return this.controlsValue;
}

View File

@ -25,6 +25,11 @@ export class MapControls {
static VECTOR2_ZERO = new Vector2(0, 0);
/**
* @param rootElement {Element}
* @param hammerLib {Hammer.Manager}
* @param events {EventTarget}
*/
constructor(rootElement, hammerLib, events = null) {
Object.defineProperty( this, 'isMapControls', { value: true } );
@ -32,6 +37,7 @@ export class MapControls {
this.hammer = hammerLib;
this.events = events;
/** @type {ControlsManager} */
this.controls = null;
this.targetPosition = new Vector3();
@ -59,6 +65,9 @@ export class MapControls {
}
/**
* @param controls {ControlsManager}
*/
start(controls) {
this.controls = controls;
@ -123,6 +132,10 @@ export class MapControls {
window.removeEventListener('contextmenu', this.onContextMenu);
}
/**
* @param deltaTime {number}
* @param map {Map}
*/
update(deltaTime, map) {
// == process mouse movements ==
let deltaMouse = this.lastMouse.clone().sub(this.mouse);
@ -155,11 +168,10 @@ export class MapControls {
this.targetPosition.y,
this.targetPosition.z + (moveDelta.y * this.targetDistance / this.rootElement.clientHeight * 1.5)
);
this.updatePositionTerrainHeight(map);
} else if (!this.positionTerrainHeight) {
this.updatePositionTerrainHeight(map);
}
this.updatePositionTerrainHeight(map);
// tilt/pan
if (this.state === MapControls.STATES.ORBIT) {
if (deltaMouse.x !== 0) {

View File

@ -1,15 +1,20 @@
import {
ClampToEdgeWrapping,
ClampToEdgeWrapping, Color,
FileLoader, FrontSide, NearestFilter, NearestMipMapLinearFilter, Raycaster,
Scene, ShaderMaterial, Texture, Vector2, Vector3, VertexColors
} from "three";
import {alert, dispatchEvent, hashTile, stringToImage} from "../util/Utils";
import {TileManager} from "./TileManager";
import {TileLoader} from "./TileLoader";
import {MarkerManager} from "../markers/MarkerManager";
import {MarkerFileManager} from "../markers/MarkerFileManager";
export class Map {
/**
* @param id {string}
* @param dataUrl {string}
* @param events {EventTarget}
*/
constructor(id, dataUrl, events = null) {
Object.defineProperty( this, 'isMap', { value: true } );
@ -21,7 +26,7 @@ export class Map {
this.world = "-";
this.startPos = {x: 0, z: 0};
this.skyColor = {r: 0, g: 0, b: 0};
this.skyColor = new Color();
this.ambientLight = 0;
this.hires = {
@ -40,18 +45,26 @@ export class Map {
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;
this.markerManager = new MarkerManager(this.dataUrl + "../markers.json", this.id, this.events);
}
/**
* 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}
* @returns {Promise<void>}
*/
load(hiresVertexShader, hiresFragmentShader, lowresVertexShader, lowresFragmentShader, uniforms) {
@ -59,7 +72,6 @@ export class Map {
let settingsFilePromise = this.loadSettingsFile();
let textureFilePromise = this.loadTexturesFile();
let markerUpdatePromise = this.markerManager.update();
this.lowresMaterial = this.createLowresMaterial(lowresVertexShader, lowresFragmentShader, uniforms);
@ -69,7 +81,11 @@ export class Map {
this.world = worldSettings.world ? worldSettings.world : this.world;
this.startPos = {...this.startPos, ...worldSettings.startPos};
this.skyColor = {...this.skyColor, ...worldSettings.skyColor};
this.skyColor.setRGB(
worldSettings.skyColor.r || this.skyColor.r,
worldSettings.skyColor.g || this.skyColor.g,
worldSettings.skyColor.b || this.skyColor.b,
);
this.ambientLight = worldSettings.ambientLight ? worldSettings.ambientLight : 0;
if (worldSettings.hires === undefined) worldSettings.hires = {};
@ -87,7 +103,7 @@ export class Map {
};
});
let mapPromise = Promise.all([settingsPromise, textureFilePromise])
return Promise.all([settingsPromise, textureFilePromise])
.then(values => {
let textures = values[1];
if (textures === null) throw new Error("Failed to parse textures.json!");
@ -99,8 +115,6 @@ export class Map {
alert(this.events, `Map '${this.id}' is loaded.`, "fine");
});
return Promise.all([mapPromise, markerUpdatePromise]);
}
onTileLoad = layer => tile => {
@ -117,6 +131,12 @@ export class Map {
});
}
/**
* @param x {number}
* @param z {number}
* @param hiresViewDistance {number}
* @param lowresViewDistance {number}
*/
loadMapArea(x, z, hiresViewDistance, lowresViewDistance) {
if (!this.isLoaded) return;
@ -178,10 +198,10 @@ export class Map {
/**
* Creates a hires Material with the given textures
* @param vertexShader
* @param fragmentShader
* @param uniforms
* @param textures the textures
* @param vertexShader {string}
* @param fragmentShader {string}
* @param uniforms {object}
* @param textures {object} the textures-data
* @returns {ShaderMaterial[]} the hires Material (array because its a multi-material)
*/
createHiresMaterial(vertexShader, fragmentShader, uniforms, textures) {
@ -240,6 +260,9 @@ export class Map {
/**
* Creates a lowres Material
* @param vertexShader {string}
* @param fragmentShader {string}
* @param uniforms {object}
* @returns {ShaderMaterial} the hires Material
*/
createLowresMaterial(vertexShader, fragmentShader, uniforms) {
@ -271,14 +294,12 @@ export class Map {
this.loadedTextures.forEach(texture => texture.dispose());
this.loadedTextures = [];
this.markerManager.dispose();
}
/**
* 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
* @param z
* @param x {number}
* @param z {number}
* @returns {boolean|number}
*/
terrainHeightAt(x, z) {
@ -313,10 +334,22 @@ export class Map {
}
}
/**
* Creates a MarkerFileManager that is loading and updating the markers for this map.
* @param markerScene {Scene} - The scene to which all markers will be added
* @returns {MarkerFileManager}
*/
createMarkerFileManager(markerScene) {
return new MarkerFileManager(markerScene, this.dataUrl + "../markers.json", this.id, this.events);
}
dispose() {
this.unload();
}
/**
* @returns {boolean}
*/
get isLoaded() {
return !!(this.hiresMaterial && this.lowresMaterial);
}

View File

@ -25,9 +25,16 @@
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;
@ -40,8 +47,12 @@ export class Tile {
this.loading = false;
}
/**
* @param tileLoader {TileLoader}
* @returns {Promise<void>}
*/
load(tileLoader) {
if (this.loading) return;
if (this.loading) return Promise.reject("tile is already loading!");
this.loading = true;
this.unload();
@ -72,6 +83,9 @@ export class Tile {
}
}
/**
* @returns {boolean}
*/
get loaded() {
return !!this.model;
}

View File

@ -3,6 +3,16 @@ 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 layer {number}
*/
constructor(tilePath, material, tileSettings, layer = 0) {
Object.defineProperty( this, 'isTileLoader', { value: true } );

View File

@ -33,6 +33,13 @@ export class TileManager {
static tileMapSize = 100;
static tileMapHalfSize = TileManager.tileMapSize / 2;
/**
* @param scene {THREE.Scene}
* @param tileLoader {TileLoader}
* @param onTileLoad {function(Tile)}
* @param onTileUnload {function(Tile)}
* @param events {EventTarget}
*/
constructor(scene, tileLoader, onTileLoad = null, onTileUnload = null, events = null) {
Object.defineProperty( this, 'isTileManager', { value: true } );
@ -59,6 +66,12 @@ export class TileManager {
this.unloaded = true;
}
/**
* @param x {number}
* @param z {number}
* @param viewDistanceX {number}
* @param viewDistanceZ {number}
*/
loadAroundTile(x, z, viewDistanceX, viewDistanceZ) {
this.unloaded = false;
@ -114,16 +127,18 @@ export class TileManager {
if (this.loadTimeout) clearTimeout(this.loadTimeout);
if (this.currentlyLoading < 4) {
if (this.currentlyLoading < 8) {
this.loadTimeout = setTimeout(this.loadCloseTiles, 0);
} else {
this.loadTimeout = setTimeout(this.loadCloseTiles, 1000);
}
}
/**
* @returns {boolean}
*/
loadNextTile() {
if (this.unloaded) return;
if (this.unloaded) return false;
let x = 0;
let z = 0;
@ -146,8 +161,13 @@ export class TileManager {
return false;
}
/**
* @param x {number}
* @param z {number}
* @returns {boolean}
*/
tryLoadTile(x, z) {
if (this.unloaded) return;
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;

View File

@ -5,11 +5,18 @@ 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,
@ -25,6 +32,9 @@ export class TileMap {
this.texture.needsUpdate = true;
}
/**
* @param state {string}
*/
setAll(state) {
this.tileMapContext.fillStyle = state;
this.tileMapContext.fillRect(0, 0, this.canvas.width, this.canvas.height);
@ -32,6 +42,11 @@ export class TileMap {
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);

View File

@ -32,13 +32,14 @@ uniform float sunlightStrength;
uniform float ambientLight;
varying vec3 vPosition;
varying vec3 vWorldPosition;
//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);

View File

@ -33,29 +33,30 @@ attribute float sunlight;
attribute float blocklight;
varying vec3 vPosition;
varying vec3 vWorldPosition;
//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 worldPos = modelMatrix * vec4(position, 1);
vec4 viewPos = viewMatrix * worldPos;
vPosition = position;
vWorldPosition = (modelMatrix * vec4(position, 1)).xyz;
//vWorldPosition = worldPos.xyz;
vNormal = normal;
vUv = uv;
vColor = color;
vAo = ao;
vSunlight = sunlight;
vBlocklight = blocklight;
//vDistance = -viewPos.z
gl_Position =
projectionMatrix *
viewMatrix *
modelMatrix *
vec4(position, 1);
gl_Position = projectionMatrix * viewPos;
${ShaderChunk.logdepthbuf_vertex}
}

View File

@ -44,12 +44,11 @@ varying vec3 vWorldPosition;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vColor;
varying float vDistance;
void main() {
float depth = gl_FragCoord.z / gl_FragCoord.w;
//discard if hires tile is loaded at that position
if (!isOrthographic && depth < 1900.0 && texture(hiresTileMap.map, ((vWorldPosition.xz - hiresTileMap.translate) / hiresTileMap.scale - hiresTileMap.pos) / hiresTileMap.size + 0.5).r >= 1.0) discard;
if (vDistance < 1900.0 && texture(hiresTileMap.map, ((vWorldPosition.xz - hiresTileMap.translate) / hiresTileMap.scale - hiresTileMap.pos) / hiresTileMap.size + 0.5).r >= 1.0) discard;
vec4 color = vec4(vColor, 1.0);

View File

@ -33,19 +33,20 @@ 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 = (modelMatrix * vec4(position, 1)).xyz;
vWorldPosition = worldPos.xyz;
vNormal = normal;
vUv = uv;
vColor = color;
vDistance = -viewPos.z;
gl_Position =
projectionMatrix *
viewMatrix *
modelMatrix *
vec4(position, 1);
gl_Position = projectionMatrix * viewPos;
${ShaderChunk.logdepthbuf_vertex}
}

View File

@ -1,272 +1,442 @@
import {Marker} from "./Marker";
import {
Color,
DoubleSide, ExtrudeBufferGeometry,
Mesh,
Object3D, ShaderMaterial,
Shape,
Vector2
} from "three";
import {Color, DoubleSide, ExtrudeBufferGeometry, Mesh, ShaderMaterial, Shape, Vector2} from "three";
import {LineMaterial} from "../util/lines/LineMaterial";
import {LineGeometry} from "../util/lines/LineGeometry";
import {MARKER_FILL_VERTEX_SHADER} from "./MarkerFillVertexShader";
import {MARKER_FILL_FRAGMENT_SHADER} from "./MarkerFillFragmentShader";
import {Line2} from "../util/lines/Line2";
import {MARKER_FILL_FRAGMENT_SHADER} from "./shader/MarkerFillFragmentShader";
import {MARKER_FILL_VERTEX_SHADER} from "./shader/MarkerFillVertexShader";
import {deepEquals} from "../util/Utils";
import {LineSegmentsGeometry} from "../util/lines/LineSegmentsGeometry";
import {ObjectMarker} from "./ObjectMarker";
export class ExtrudeMarker extends Marker {
export class ExtrudeMarker extends ObjectMarker {
constructor(markerSet, id, parentObject) {
super(markerSet, id);
/**
* @param markerId {string}
*/
constructor(markerId) {
super(markerId);
Object.defineProperty(this, 'isExtrudeMarker', {value: true});
Object.defineProperty(this, 'type', {value: "extrude"});
this.markerType = "extrude";
let fillColor = Marker.normalizeColor({});
let borderColor = Marker.normalizeColor({});
let lineWidth = 2;
let depthTest = false;
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._lineOpacity = 1;
this._fillOpacity = 1;
this.add(this.border, this.fill);
this._markerObject = new Object3D();
this._markerObject.position.copy(this.position);
parentObject.add(this._markerObject);
this._markerFillMaterial = new ShaderMaterial({
vertexShader: MARKER_FILL_VERTEX_SHADER,
fragmentShader: MARKER_FILL_FRAGMENT_SHADER,
side: DoubleSide,
depthTest: depthTest,
transparent: true,
uniforms: {
markerColor: { value: fillColor.vec4 }
}
});
this._markerLineMaterial = new LineMaterial({
color: new Color(borderColor.rgb),
opacity: borderColor.a,
transparent: true,
linewidth: lineWidth,
depthTest: depthTest,
vertexColors: false,
dashed: false
});
this._markerLineMaterial.resolution.set(window.innerWidth, window.innerHeight);
this._markerData = {};
}
update(markerData) {
super.update(markerData);
this.minHeight = markerData.minHeight ? parseFloat(markerData.minHeight) : 0.0;
this.maxHeight = markerData.maxHeight ? parseFloat(markerData.maxHeight) : 255.0;
this.depthTest = !!markerData.depthTest;
/**
* @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;
}
if (markerData.fillColor) this.fillColor = markerData.fillColor;
if (markerData.borderColor) this.borderColor = markerData.borderColor;
/**
* @param shape {Shape}
*/
setShape(shape) {
this.fill.updateGeometry(shape);
this.border.updateGeometry(shape);
}
this.lineWidth = markerData.lineWidth ? parseFloat(markerData.lineWidth) : 2;
/**
* @typedef {{r: number, g: number, b: number, a: number}} ColorLike
*/
let points = [];
if (Array.isArray(markerData.shape)) {
markerData.shape.forEach(point => {
points.push(new Vector2(parseFloat(point.x), parseFloat(point.z)));
});
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* label: 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));
}
this.shape = points;
}
_onBeforeRender(renderer, scene, camera) {
super._onBeforeRender(renderer, scene, camera);
// update shapeY
this.setShapeY((markerData.shapeMinY || 0) - 0.01, (markerData.shapeMaxY || 0) + 0.01); // offset by 0.01 to avoid z-fighting
this._markerFillMaterial.uniforms.markerColor.value.w = this._fillOpacity * this._opacity;
this._markerLineMaterial.opacity = this._lineOpacity * this._opacity;
// 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() {
this._markerObject.parent.remove(this._markerObject);
this._markerObject.children.forEach(child => {
if (child.geometry && child.geometry.isGeometry) child.geometry.dispose();
});
this._markerObject.clear();
this._markerFillMaterial.dispose();
this._markerLineMaterial.dispose();
super.dispose();
this.fill.dispose();
this.border.dispose();
}
/**
* Sets the fill-color
*
* color-object format:
* <code><pre>
* {
* r: 0, // int 0-255 red
* g: 0, // int 0-255 green
* b: 0, // int 0-255 blue
* a: 0 // float 0-1 alpha
* }
* </pre></code>
*
* @param color {Object}
* @private
* Creates a shape from a data object, usually parsed json from a markers.json
* @param shapeData {object}
* @returns {Shape}
*/
set fillColor(color) {
color = Marker.normalizeColor(color);
createShapeFromData(shapeData) {
/** @type {THREE.Vector2[]} **/
let points = [];
this._markerFillMaterial.uniforms.markerColor.value.copy(color.vec4);
this._fillOpacity = color.a;
this._markerFillMaterial.needsUpdate = true;
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 {
/**
* Sets the border-color
*
* color-object format:
* <code><pre>
* {
* r: 0, // int 0-255 red
* g: 0, // int 0-255 green
* b: 0, // int 0-255 blue
* a: 0 // float 0-1 alpha
* }
* </pre></code>
*
* @param color {Object}
* @param shape {Shape}
*/
set borderColor(color) {
color = Marker.normalizeColor(color);
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 },
}
});
this._markerLineMaterial.color.setHex(color.rgb);
this._lineOpacity = color.a;
this._markerLineMaterial.needsUpdate = true;
super(geometry, material);
}
/**
* Sets the width of the marker-line
* @param width {number}
* @returns {Color}
*/
set lineWidth(width) {
this._markerLineMaterial.linewidth = width;
this._markerLineMaterial.needsUpdate = true;
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;
}
/**
* Sets if this marker can be seen through terrain
* @param test {boolean}
*/
set depthTest(test) {
this._markerFillMaterial.depthTest = test;
this._markerFillMaterial.needsUpdate = true;
this._markerLineMaterial.depthTest = test;
this._markerLineMaterial.needsUpdate = true;
}
get depthTest() {
return this._markerFillMaterial.depthTest;
this.material.depthTest = test;
}
/**
* Sets the min-height of this marker
* @param height {number}
* @returns {number}
*/
set minHeight(height) {
this._minHeight = height;
get fadeDistanceMin() {
return this.material.uniforms.fadeDistanceMin.value;
}
/**
* Sets the max-height of this marker
* @param height {number}
* @param min {number}
*/
set maxHeight(height) {
this._markerObject.position.y = height + 0.01;
set fadeDistanceMin(min) {
this.material.uniforms.fadeDistanceMin.value = min;
}
/**
* Sets the points for the shape of this marker.
* @param points {Vector2[]}
* @returns {number}
*/
set shape(points) {
get fadeDistanceMax() {
return this.material.uniforms.fadeDistanceMax.value;
}
// remove old marker
this._markerObject.children.forEach(child => {
if (child.geometry && child.geometry.isGeometry) child.geometry.dispose();
/**
* @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
});
this._markerObject.clear();
geometry.rotateX(Math.PI / 2); //make y to z
if (points.length < 3) return;
return geometry;
}
this._markerObject.position.x = this.position.x + 0.01;
this._markerObject.position.z = this.position.z + 0.01;
}
let maxY = this._markerObject.position.y;
let minY = this._minHeight;
let depth = maxY - minY;
class ExtrudeMarkerBorder extends Line2 {
let shape = new Shape(points);
/**
* @param shape {Shape}
*/
constructor(shape) {
let geometry = new LineSegmentsGeometry();
geometry.setPositions(ExtrudeMarkerBorder.createLinePoints(shape));
// border-line
if (this._markerLineMaterial.opacity > 0) {
let points3d = [];
points.forEach(point => points3d.push(point.x, 0, point.y));
points3d.push(points[0].x, 0, points[0].y)
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 };
const preRenderHook = line => renderer => {
renderer.getSize(line.material.resolution);
};
material.resolution.set(window.innerWidth, window.innerHeight);
let topLineGeo = new LineGeometry()
topLineGeo.setPositions(points3d);
topLineGeo.translate(-this.position.x, 0, -this.position.z);
let topLine = new Line2(topLineGeo, this._markerLineMaterial);
topLine.computeLineDistances();
topLine.onBeforeRender = preRenderHook(topLine);
this._markerObject.add(topLine);
super(geometry, material);
let bottomLine = topLine.clone();
bottomLine.position.y = -depth;
bottomLine.computeLineDistances();
bottomLine.onBeforeRender = preRenderHook(bottomLine);
this._markerObject.add(bottomLine);
this.computeLineDistances();
}
points.forEach(point => {
let pointLineGeo = new LineGeometry();
pointLineGeo.setPositions([
point.x, 0, point.y,
point.x, -depth, point.y
])
pointLineGeo.translate(-this.position.x, 0, -this.position.z);
let pointLine = new Line2(pointLineGeo, this._markerLineMaterial);
pointLine.computeLineDistances();
pointLine.onBeforeRender = preRenderHook(pointLine);
pointLine.marker = this;
this._markerObject.add(pointLine);
});
/**
* @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;
}
// fill
if (this._markerFillMaterial.uniforms.markerColor.value.w > 0) {
let fillGeo = new ExtrudeBufferGeometry(shape, {
steps: 1,
depth: depth,
bevelEnabled: false
});
fillGeo.rotateX(Math.PI / 2); //make y to z
fillGeo.translate(-this.position.x, 0, -this.position.z);
let fill = new Mesh(fillGeo, this._markerFillMaterial);
fill.onBeforeRender = (renderer, scene, camera) => this._onBeforeRender(renderer, scene, camera);
fill.marker = this;
this._markerObject.add(fill);
}
return super.onClick(event);
}
// put render-hook on line (only) if there is no fill
else if (this._markerObject.children.length > 0) {
let oldHook = this._markerObject.children[0].onBeforeRender;
this._markerObject.children[0].onBeforeRender = (renderer, scene, camera, geometry, material, group) => {
this._onBeforeRender(renderer, scene, camera);
oldHook(renderer, scene, camera, geometry, material, group);
/**
* @param shape {Shape}
*/
updateGeometry(shape) {
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

@ -1,66 +0,0 @@
import {Marker} from "./Marker";
import {CSS2DObject} from "../util/CSS2DRenderer";
import {htmlToElement} from "../util/Utils";
export class HTMLMarker extends Marker {
constructor(markerSet, id, parentObject) {
super(markerSet, id);
Object.defineProperty(this, 'isHTMLMarker', {value: true});
Object.defineProperty(this, 'type', {value: "html"});
this._markerElement = htmlToElement(`<div id="bm-marker-${this.id}" class="bm-marker-${this.type}"></div>`);
this._markerElement.addEventListener('click', event => this.onClick(this.position));
this._markerObject = new CSS2DObject(this._markerElement);
this._markerObject.position.copy(this.position);
this._markerObject.onBeforeRender = (renderer, scene, camera) => this._onBeforeRender(renderer, scene, camera);
parentObject.add(this._markerObject);
}
update(markerData) {
super.update(markerData);
if (markerData.html) {
this.html = markerData.html;
}
if (markerData.anchor) {
this.setAnchor(parseInt(markerData.anchor.x), parseInt(markerData.anchor.y));
}
}
_onBeforeRender(renderer, scene, camera) {
super._onBeforeRender(renderer, scene, camera);
this._markerElement.style.opacity = this._opacity;
this._markerElement.setAttribute("data-distance", Math.round(this._distance));
if (this._opacity <= 0){
this._markerElement.style.pointerEvents = "none";
} else {
this._markerElement.style.pointerEvents = "auto";
}
}
dispose() {
this._markerObject.parent.remove(this._markerObject);
super.dispose();
}
set html(html) {
this._markerElement.innerHTML = html;
}
setAnchor(x, y) {
this._markerObject.anchor.set(x, y);
}
setPosition(x, y, z) {
super.setPosition(x, y, z);
this._markerObject.position.set(x, y, z);
}
}

103
src/markers/HtmlMarker.js Normal file
View File

@ -0,0 +1,103 @@
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.markerType = "html";
this.elementObject = new CSS2DObject(htmlToElement(`<div id="bm-marker-${this.markerId}" class="bm-marker-${this.markerType}"></div>`));
this.elementObject.onBeforeRender = (renderer, scene, camera) => this.onBeforeRender(renderer, scene, camera);
this.fadeDistanceMin = 0;
this.fadeDistanceMax = Number.MAX_VALUE;
this.addEventListener( 'removed', () => {
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.style.opacity = undefined;
} else {
this.element.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;
}
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* anchor: {x: number, y: number},
* html: 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 || {};
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 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

@ -1,153 +1,271 @@
import {Marker} from "./Marker";
import {
Color,
Object3D,
Vector3,
} from "three";
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 Marker {
export class LineMarker extends ObjectMarker {
constructor(markerSet, id, parentObject) {
super(markerSet, id);
/**
* @param markerId {string}
*/
constructor(markerId) {
super(markerId);
Object.defineProperty(this, 'isLineMarker', {value: true});
Object.defineProperty(this, 'type', {value: "line"});
this.markerType = "line";
let lineColor = Marker.normalizeColor({});
let lineWidth = 2;
let depthTest = false;
this.line = new LineMarkerLine([0, 0, 0]);
this._lineOpacity = 1;
this.add(this.line);
this._markerObject = new Object3D();
this._markerObject.position.copy(this.position);
parentObject.add(this._markerObject);
this._markerLineMaterial = new LineMaterial({
color: new Color(lineColor.rgb),
opacity: lineColor.a,
transparent: true,
linewidth: lineWidth,
depthTest: depthTest,
vertexColors: false,
dashed: false
});
this._markerLineMaterial.resolution.set(window.innerWidth, window.innerHeight);
this._markerData = {};
}
update(markerData) {
super.update(markerData);
/**
* @param line {number[] | THREE.Vector3[] | THREE.Curve}
*/
setLine(line) {
/** @type {number[]} */
let points;
if (markerData.lineColor) this.lineColor = markerData.lineColor;
this.lineWidth = markerData.lineWidth ? parseFloat(markerData.lineWidth) : 2;
this.depthTest = !!markerData.depthTest;
let points = [];
if (Array.isArray(markerData.line)) {
markerData.line.forEach(point => {
points.push(new Vector3(parseFloat(point.x), parseFloat(point.y), parseFloat(point.z)));
});
if (line.type === 'Curve' || line.type === 'CurvePath') {
line = line.getPoints(5);
}
this.line = points;
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);
}
_onBeforeRender(renderer, scene, camera) {
super._onBeforeRender(renderer, scene, camera);
/**
* @typedef {{r: number, g: number, b: number, a: number}} ColorLike
*/
this._markerLineMaterial.opacity = this._lineOpacity * this._opacity;
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* label: 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() {
this._markerObject.parent.remove(this._markerObject);
this._markerObject.children.forEach(child => {
if (child.geometry && child.geometry.isGeometry) child.geometry.dispose();
});
this._markerObject.clear();
this._markerLineMaterial.dispose();
super.dispose();
this.line.dispose();
}
/**
* Sets the line-color
*
* color-object format:
* <code><pre>
* {
* r: 0, // int 0-255 red
* g: 0, // int 0-255 green
* b: 0, // int 0-255 blue
* a: 0 // float 0-1 alpha
* }
* </pre></code>
*
* @param color {Object}
* @private
* Creates a shape from a data object, usually parsed json from a markers.json
* @param shapeData {object}
* @returns {number[]}
*/
set lineColor(color) {
color = Marker.normalizeColor(color);
createPointsFromData(shapeData) {
/** @type {number[]} **/
let points = [];
this._markerLineMaterial.color.setHex(color.rgb);
this._lineOpacity = color.a;
this._markerLineMaterial.needsUpdate = true;
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;
}
/**
* Sets the width of the marker-line
* @param width {number}
*/
set lineWidth(width) {
this._markerLineMaterial.linewidth = width;
this._markerLineMaterial.needsUpdate = true;
set linewidth(width) {
this.material.linewidth = width;
}
/**
* @returns {boolean}
*/
get depthTest() {
return this.material.depthTest;
}
/**
* Sets if this marker can be seen through terrain
* @param test {boolean}
*/
set depthTest(test) {
this._markerLineMaterial.depthTest = test;
this._markerLineMaterial.needsUpdate = true;
}
get depthTest() {
return this._markerLineMaterial.depthTest;
this.material.depthTest = test;
}
/**
* Sets the points for the shape of this marker.
* @param points {Vector3[]}
* @returns {number}
*/
set line(points) {
// remove old marker
this._markerObject.children.forEach(child => {
if (child.geometry && child.geometry.isGeometry) child.geometry.dispose();
});
this._markerObject.clear();
get fadeDistanceMin() {
return this.material.uniforms.fadeDistanceMin.value;
}
if (points.length < 3) return;
/**
* @param min {number}
*/
set fadeDistanceMin(min) {
this.material.uniforms.fadeDistanceMin.value = min;
}
this._markerObject.position.copy(this.position);
/**
* @returns {number}
*/
get fadeDistanceMax() {
return this.material.uniforms.fadeDistanceMax.value;
}
// line
let points3d = [];
points.forEach(point => points3d.push(point.x, point.y, point.z));
let lineGeo = new LineGeometry();
lineGeo.setPositions(points3d);
lineGeo.translate(-this.position.x, -this.position.y, -this.position.z);
let line = new Line2(lineGeo, this._markerLineMaterial);
line.computeLineDistances();
/**
* @param max {number}
*/
set fadeDistanceMax(max) {
this.material.uniforms.fadeDistanceMax.value = max;
}
line.onBeforeRender = (renderer, camera, scene) => {
this._onBeforeRender(renderer, camera, scene);
renderer.getSize(line.material.resolution);
onClick(event) {
if (event.intersection) {
if (event.intersection.distance > this.fadeDistanceMax) return false;
if (event.intersection.distance < this.fadeDistanceMin) return false;
}
line.marker = this;
this._markerObject.add(line);
return super.onClick(event);
}
/**
* @param points {number[]}
*/
updateGeometry(points) {
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

@ -1,156 +1,52 @@
import {MathUtils, Vector3, Vector4} from "three";
import {animate, dispatchEvent} from "../util/Utils";
import {MathUtils, Object3D, Vector3} from "three";
export class Marker {
export class Marker extends Object3D {
static Source = {
CUSTOM: 0,
MARKER_FILE: 1
}
constructor(markerSet, id) {
/**
* @param markerId {string}
*/
constructor(markerId) {
super();
Object.defineProperty(this, 'isMarker', {value: true});
this.manager = markerSet.manager;
this.markerSet = markerSet;
this.id = id;
this.markerId = markerId;
this.markerType = "marker";
this._position = new Vector3();
this._label = null;
this.link = null;
this.newTab = true;
this.minDistance = 0.0;
this.maxDistance = 100000.0;
this.opacity = 1;
this._source = Marker.Source.CUSTOM;
this._onDisposal = [];
this._distance = 0;
this._opacity = 1;
this._posRelativeToCamera = new Vector3();
this._cameraDirection = new Vector3();
}
update(markerData) {
this._source = Marker.Source.MARKER_FILE;
dispose() {};
if (markerData.position) {
this.setPosition(parseFloat(markerData.position.x), parseFloat(markerData.position.y), parseFloat(markerData.position.z));
} else {
this.setPosition(0, 0, 0);
}
/**
* Updates this marker from the provided data object, usually parsed form json from a markers.json
* @param markerData {object}
*/
updateFromData(markerData) {}
this.label = markerData.label ? markerData.label : null;
this.link = markerData.link ? markerData.link : null;
this.newTab = !!markerData.newTab;
// -- helper methods --
this.minDistance = parseFloat(markerData.minDistance ? markerData.minDistance : 0.0);
this.maxDistance = parseFloat(markerData.maxDistance ? markerData.maxDistance : 100000.0);
}
setPosition(x, y, z) {
this.position.set(x, y, z);
}
get position() {
return this._position;
}
onClick(clickPosition){
if (!dispatchEvent(this.manager.events, 'bluemapMarkerClick', {marker: this})) return;
this.followLink();
if (this.label){
this.manager.showPopup(`<div class="bm-marker-label">${this.label}</div>`, clickPosition.x, clickPosition.y, clickPosition.z, true);
}
}
followLink(){
if (this.link){
if (this.newTab){
window.open(this.link, '_blank');
} else {
location.href = this.link;
}
}
}
_onBeforeRender(renderer, scene, camera) {
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) {
//calculate "orthographic distance" to marker
this._posRelativeToCamera.subVectors(this.position, camera.position);
camera.getWorldDirection(this._cameraDirection);
this._distance = this._posRelativeToCamera.dot(this._cameraDirection);
Marker._posRelativeToCamera.subVectors(position, camera.position);
camera.getWorldDirection(Marker._cameraDirection);
let distance = Marker._posRelativeToCamera.dot(Marker._cameraDirection);
//calculate opacity based on (min/max)distance
this._opacity = Math.min(
1 - MathUtils.clamp((this._distance - this.maxDistance) / (this.maxDistance * 2), 0, 1),
MathUtils.clamp((this._distance - this.minDistance) / (this.minDistance * 2 + 1), 0, 1)
) * this.opacity;
}
blendIn(durationMs = 500, postAnimation = null){
this.opacity = 0;
animate(progress => {
this.opacity = progress;
}, durationMs, postAnimation);
}
blendOut(durationMs = 500, postAnimation = null){
let startOpacity = this.opacity;
animate(progress => {
this.opacity = startOpacity * (1 - progress);
}, durationMs, postAnimation);
}
set label(label){
this._label = label;
}
get label(){
return this._label;
}
set onDisposal(callback) {
this._onDisposal.push(callback);
}
dispose() {
this._onDisposal.forEach(callback => callback(this));
delete this.markerSet._marker[this.id];
}
static normalizeColor(color){
if (!color) color = {};
color.r = Marker.normaliseNumber(color.r, 255, true);
color.g = Marker.normaliseNumber(color.g, 0, true);
color.b = Marker.normaliseNumber(color.b, 0, true);
color.a = Marker.normaliseNumber(color.a, 1, false);
color.rgb = (color.r << 16) + (color.g << 8) + (color.b);
color.vec4 = new Vector4(color.r / 255, color.g / 255, color.b / 255, color.a);
return color;
}
static normaliseNumber(nr, def, integer = false) {
if (isNaN(nr)){
if (integer) nr = parseInt(nr);
else nr = parseFloat(nr);
if (isNaN(nr)) return def;
return nr;
}
if (integer) return Math.floor(nr);
return nr;
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)
);
}
}

View File

@ -0,0 +1,264 @@
import {FileLoader, Scene} from "three";
import {MarkerSet} from "./MarkerSet";
import {ShapeMarker} from "./ShapeMarker";
import {alert} from "../util/Utils";
import {ExtrudeMarker} from "./ExtrudeMarker";
import {LineMarker} from "./LineMarker";
import {HtmlMarker} from "./HtmlMarker";
import {PoiMarker} from "./PoiMarker";
/**
* A manager for loading and updating markers from a markers.json file
*/
export class MarkerFileManager {
/**
* @constructor
* @param markerScene {Scene} - The scene to which all markers will be added
* @param fileUrl {string} - The marker file from which this manager updates its markers
* @param mapId {string} - The mapId of the map for which the markers should be loaded
* @param events {EventTarget}
*/
constructor(markerScene, fileUrl, mapId, events = null) {
Object.defineProperty(this, 'isMarkerFileManager', {value: true});
this.markerScene = markerScene;
this.fileUrl = fileUrl;
this.mapId = mapId;
this.events = events;
/** @type {Map<string, MarkerSet>} */
this.markerSets = new Map();
/** @type {Map<string, Marker>} */
this.markers = new Map();
/** @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) clearInterval(this._updateInterval);
if (ms > 0) {
let autoUpdate = () => {
this.update().finally(() => {
this._updateInterval = setTimeout(autoUpdate, ms);
});
};
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))
.catch(error => {
alert(this.events, error, "error");
});
}
/**
* Stops automatic-updates and disposes all markersets and markers managed by this manager
*/
dispose() {
this.setAutoUpdateInterval(0);
this.markerSets.forEach(markerSet => markerSet.dispose());
}
/**
* @private
* Adds a MarkerSet to this Manager, removing any existing markerSet with this id first.
* @param markerSet {MarkerSet}
*/
addMarkerSet(markerSet) {
this.removeMarkerSet(markerSet.markerSetId);
this.markerSets.set(markerSet.markerSetId, markerSet);
this.markerScene.add(markerSet)
}
/**
* @private
* Removes a MarkerSet from this Manager
* @param setId {string} - The id of the MarkerSet
*/
removeMarkerSet(setId) {
let markerSet = this.markerSets.get(setId);
if (markerSet) {
this.markerScene.remove(markerSet);
this.markerSets.delete(setId);
markerSet.dispose();
}
}
/**
* @private
* Adds a marker to this manager
* @param markerSet {MarkerSet}
* @param marker {Marker}
*/
addMarker(markerSet, marker) {
this.removeMarker(marker.markerId);
this.markers.set(marker.markerId, marker);
markerSet.add(marker);
}
/**
* @private
* Removes a marker from this manager
* @param markerId {string}
*/
removeMarker(markerId) {
let marker = this.markers.get(markerId);
if (marker) {
if (marker.parent) marker.parent.remove(marker);
this.markers.delete(markerId);
marker.dispose();
}
}
/**
* @private
* Updates all managed markers using the provided data.
* @param markerData {object} - The data object, usually parsed json from a markers.json
*/
updateFromData(markerData) {
if (!Array.isArray(markerData.markerSets)) return;
let updatedMarkerSets = new Set();
// add & update
markerData.markerSets.forEach(markerSetData => {
try {
let markerSet = this.updateMarkerSetFromData(markerSetData);
updatedMarkerSets.add(markerSet);
} catch (err) {
alert(this.events, "Failed to parse markerset-data: " + err, "fine");
}
});
// remove not updated MarkerSets
this.markerSets.forEach((markerSet, setId) => {
if (!updatedMarkerSets.has(markerSet)) {
this.removeMarkerSet(setId);
}
});
}
/**
* @private
* Updates a managed MarkerSet using the provided data
* @param markerSetData {object} - The data object for a MarkerSet, usually parsed json from a markers.json
* @returns {MarkerSet} - The updated MarkerSet
* @throws {Error} - On invalid / missing data
*/
updateMarkerSetFromData(markerSetData) {
if (!markerSetData.id) throw new Error("markerset-data has no id!");
let markerSet = this.markerSets.get(markerSetData.id);
// create new if not existent
if (!markerSet) {
markerSet = new MarkerSet(markerSetData.id);
this.addMarkerSet(markerSet);
}
// update set info
markerSet.label = markerSetData.label || markerSetData.id;
markerSet.toggleable = !!markerSetData.toggleable;
markerSet.defaultHide = !!markerSetData.defaultHide;
// update markers
let updatedMarkers = new Set();
if (Array.isArray(markerSetData.marker)) {
markerSetData.marker.forEach(markerData => {
if (markerData.map && markerData.map !== this.mapId) return;
try {
let marker = this.updateMarkerFromData(markerSet, markerData);
updatedMarkers.add(marker);
} catch (err) {
alert(this.events, "Failed to parse marker-data: " + err, "fine");
console.debug(err);
}
});
}
// remove not updated Markers
markerSet.children.forEach((marker) => {
if (marker.isMarker && !updatedMarkers.has(marker) && !this.markers.has(marker.markerId)) {
this.removeMarker(marker.markerId);
}
});
return markerSet;
}
/**
* @private
* Updates a managed Marker using the provided data
* @param markerSet {MarkerSet} - The MarkerSet this marker should be in
* @param markerData {object}
* @returns {Marker} - The updated Marker
* @throws {Error} - On invalid / missing data
*/
updateMarkerFromData(markerSet, markerData) {
if (!markerData.id) throw new Error("marker-data has no id!");
if (!markerData.type) throw new Error("marker-data has no type!");
let marker = this.markers.get(markerData.id);
// create new if not existent of wrong type
if (!marker || marker.markerType !== markerData.type) {
switch (markerData.type) {
case "shape" : marker = new ShapeMarker(markerData.id); break;
case "extrude" : marker = new ExtrudeMarker(markerData.id); break;
case "line" : marker = new LineMarker(markerData.id); break;
case "html" : marker = new HtmlMarker(markerData.id); break;
case "poi" : marker = new PoiMarker(markerData.id); break;
default : throw new Error(`Unknown marker-type: '${markerData.type}'`);
}
this.addMarker(markerSet, marker);
}
// make sure marker is in the correct MarkerSet
if (marker.parent !== markerSet) markerSet.add(marker);
// update marker
marker.updateFromData(markerData);
return marker;
}
/**
* @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,
markerFileData => {
if (!markerFileData) reject(`Failed to parse '${this.fileUrl}'!`);
else resolve(markerFileData);
},
() => {},
() => reject(`Failed to load '${this.fileUrl}'!`)
)
});
}
}

View File

@ -27,19 +27,39 @@ import { ShaderChunk } from 'three';
export const MARKER_FILL_FRAGMENT_SHADER = `
${ShaderChunk.logdepthbuf_pars_fragment}
varying vec3 vPosition;
varying vec3 vWorldPosition;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vColor;
#define FLT_MAX 3.402823466e+38
uniform vec4 markerColor;
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 = markerColor;
vec4 color = vec4(markerColor, markerOpacity);
//apply vertex-color
color.rgb *= vColor.rgb;
// 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;

View File

@ -29,23 +29,24 @@ export const MARKER_FILL_VERTEX_SHADER = `
${ShaderChunk.logdepthbuf_pars_vertex}
varying vec3 vPosition;
varying vec3 vWorldPosition;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vColor;
//varying vec3 vWorldPosition;
//varying vec3 vNormal;
//varying vec2 vUv;
//varying vec3 vColor;
varying float vDistance;
void main() {
vPosition = position;
vWorldPosition = (modelMatrix * vec4(position, 1)).xyz;
vNormal = normal;
vUv = uv;
vColor = vec3(1.0);
vec4 worldPos = modelMatrix * vec4(position, 1);
vec4 viewPos = viewMatrix * worldPos;
gl_Position =
projectionMatrix *
viewMatrix *
modelMatrix *
vec4(position, 1);
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

@ -1,135 +0,0 @@
import {alert, dispatchEvent} from "../util/Utils";
import {FileLoader, Scene} from "three";
import {MarkerSet} from "./MarkerSet";
import {HTMLMarker} from "./HTMLMarker";
export class MarkerManager {
constructor(markerFileUrl, mapId, events = null) {
Object.defineProperty(this, 'isMarkerManager', {value: true});
this.markerFileUrl = markerFileUrl;
this.mapId = mapId;
this.events = events;
this.markerSets = {};
this.objectMarkerScene = new Scene(); //3d markers
this.elementMarkerScene = new Scene(); //html markers
this._popupId = 0;
}
update() {
return this.loadMarkersFile()
.then(markersFile => {
let prevMarkerSets = this.markerSets;
this.markerSets = {};
if (Array.isArray(markersFile.markerSets)){
for (let markerSetData of markersFile.markerSets){
let markerSetId = markerSetData.id;
if (!markerSetId) continue;
if (this.markerSets[markerSetId]) continue; // skip duplicate id's
this.markerSets[markerSetId] = prevMarkerSets[markerSetId];
delete prevMarkerSets[markerSetId];
this.updateMarkerSet(markerSetId, markerSetData);
}
}
//remaining (removed) markerSets
for (let markerSetId in prevMarkerSets) {
if (!prevMarkerSets.hasOwnProperty(markerSetId)) continue;
if (!prevMarkerSets[markerSetId] || !prevMarkerSets[markerSetId].isMarkerSet) continue;
// keep marker-sets that were not loaded from the marker-file
if (prevMarkerSets[markerSetId]._source !== MarkerSet.Source.MARKER_FILE){
this.markerSets[markerSetId] = prevMarkerSets[markerSetId];
continue;
}
prevMarkerSets[markerSetId].dispose();
}
})
.catch(reason => {
alert(this.events, reason, "warning");
});
}
updateMarkerSet(markerSetId, markerSetData) {
if (!this.markerSets[markerSetId] || !this.markerSets[markerSetId].isMarkerSet){
this.createMarkerSet(markerSetId);
this.objectMarkerScene.add(this.markerSets[markerSetId]._objectMarkerObject);
this.elementMarkerScene.add(this.markerSets[markerSetId]._elementMarkerObject);
}
this.markerSets[markerSetId].update(markerSetData);
}
createMarkerSet(id) {
this.markerSets[id] = new MarkerSet(this, id, this.mapId, this.events);
return this.markerSets[id];
}
dispose() {
let sets = {...this.markerSets};
for (let markerSetId in sets){
if (!sets.hasOwnProperty(markerSetId)) continue;
if (!sets[markerSetId] || !sets[markerSetId].isMarkerSet) continue;
sets[markerSetId].dispose();
}
this.markerSets = {};
}
showPopup(html, x, y, z, autoRemove = true, onRemoval = null){
let marker = new HTMLMarker(this, `popup-${this._popupId++}`, this.elementMarkerScene);
marker.setPosition(x, y, z);
marker.html = html;
marker.onDisposal = onRemoval;
dispatchEvent(this.events, 'bluemapPopupMarker', {marker: marker});
if (autoRemove){
let onRemove = () => {
marker.blendOut(200, finished => {
if (finished) marker.dispose();
});
};
this.events.addEventListener('bluemapPopupMarker', onRemove, {once: true});
setTimeout(() => {
this.events.addEventListener('bluemapCameraMoved', onRemove, {once: true});
}, 1000);
}
marker.blendIn(200);
return marker;
}
/**
* Loads the markers.json file for this map
* @returns {Promise<Object>}
*/
loadMarkersFile() {
return new Promise((resolve, reject) => {
alert(this.events, `Loading markers from '${this.markerFileUrl}'...`, "fine");
let loader = new FileLoader();
loader.setResponseType("json");
loader.load(this.markerFileUrl,
markerFile => {
if (!markerFile) reject(`Failed to parse '${this.markerFileUrl}'!`);
else resolve(markerFile);
},
() => {},
() => reject(`Failed to load '${this.markerFileUrl}'!`)
)
});
}
}

View File

@ -1,132 +1,27 @@
import {ShapeMarker} from "./ShapeMarker";
import {Object3D} from "three";
import {LineMarker} from "./LineMarker";
import {ExtrudeMarker} from "./ExtrudeMarker";
import {HTMLMarker} from "./HTMLMarker";
import {POIMarker} from "./POIMarker";
import {Marker} from "./Marker";
import {PlayerMarker} from "./PlayerMarker";
export class MarkerSet {
export class MarkerSet extends Object3D {
static Source = {
CUSTOM: 0,
MARKER_FILE: 1
}
constructor(manager, id, mapId, events = null) {
/**
* @param markerSetId {string}
*/
constructor(markerSetId) {
super();
Object.defineProperty(this, 'isMarkerSet', {value: true});
this.manager = manager;
this.id = id;
this.markerSetId = markerSetId;
this.label = markerSetId;
this._mapId = mapId;
this._objectMarkerObject = new Object3D();
this._elementMarkerObject = new Object3D();
this.events = events;
this.label = this.id;
this.toggleable = true;
this.defaultHide = false;
this.visible = undefined;
this._source = MarkerSet.Source.CUSTOM;
this._marker = {};
}
update(markerSetData) {
this._source = MarkerSet.Source.MARKER_FILE;
this.label = markerSetData.label ? markerSetData.label : this.id;
this.toggleable = markerSetData.toggleable !== undefined ? !!markerSetData.toggleable : true;
this.defaultHide = !!markerSetData.defaultHide;
if (this.visible === undefined) this.visible = this.defaultHide;
let prevMarkers = this._marker;
this._marker = {};
if (Array.isArray(markerSetData.marker)){
for (let markerData of markerSetData.marker) {
let markerId = markerData.id;
if (!markerId) continue;
if (this._marker[markerId]) continue; // skip duplicate id's
let mapId = markerData.map;
if (mapId !== this._mapId) continue;
this._marker[markerId] = prevMarkers[markerId];
delete prevMarkers[markerId];
this.updateMarker(markerId, markerData);
}
}
//remaining (removed) markers
for (let markerId in prevMarkers) {
if (!prevMarkers.hasOwnProperty(markerId)) continue;
if (!prevMarkers[markerId] || !prevMarkers[markerId].isMarker) continue;
// keep markers that were not loaded from the marker-file
if (prevMarkers[markerId]._source !== Marker.Source.MARKER_FILE){
this._marker[markerId] = prevMarkers[markerId];
continue;
}
prevMarkers[markerId].dispose();
}
}
updateMarker(markerId, markerData){
let markerType = markerData.type;
if (!markerType) return;
if (!this._marker[markerId] || !this._marker[markerId].isMarker) {
this.createMarker(markerId, markerType);
} else if (this._marker[markerId].type !== markerType){
this._marker[markerId].dispose();
this.createMarker(markerId, markerType);
}
if (!this._marker[markerId]) return;
this._marker[markerId].update(markerData);
}
createMarker(id, type) {
switch (type) {
case "html" : this._marker[id] = new HTMLMarker(this, id, this._elementMarkerObject); break;
case "poi" : this._marker[id] = new POIMarker(this, id, this._elementMarkerObject); break;
case "shape" : this._marker[id] = new ShapeMarker(this, id, this._objectMarkerObject); break;
case "line" : this._marker[id] = new LineMarker(this, id, this._objectMarkerObject); break;
case "extrude" : this._marker[id] = new ExtrudeMarker(this, id, this._objectMarkerObject); break;
default : return null;
}
return this._marker[id];
}
createPlayerMarker(playerUuid) {
let id = playerUuid;
this._marker[id] = new PlayerMarker(this, id, this._elementMarkerObject, playerUuid);
return this._marker[id];
}
get marker() {
return this._marker.values();
}
dispose() {
let marker = {...this._marker};
for (let markerId in marker){
if (!marker.hasOwnProperty(markerId)) continue;
if (!marker[markerId] || !marker[markerId].isMarker) continue;
super.dispose();
marker[markerId].dispose();
}
this._marker = {};
delete this.manager.markerSets[this.id];
this.children.forEach(child => {
if (child.dispose) child.dispose();
});
}
}

120
src/markers/ObjectMarker.js Normal file
View File

@ -0,0 +1,120 @@
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.markerType = "object";
this.label = null;
this.link = null;
this.newTab = true;
}
onClick(event) {
let pos = new Vector3();
if (event.intersection) {
pos.copy(event.intersection.pointOnLine || event.intersection.point);
pos.sub(this.position);
}
if (this.label) {
let popup = new LabelPopup(this.label);
popup.position.copy(pos);
this.add(popup);
popup.open();
}
return true;
}
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* label: 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.label = markerData.label || null;
// update link
this.link = markerData.link || null;
this.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.path.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

@ -1,62 +0,0 @@
import {HTMLMarker} from "./HTMLMarker";
import {dispatchEvent} from "../util/Utils";
export class POIMarker extends HTMLMarker {
constructor(markerSet, id, parentObject) {
super(markerSet, id, parentObject);
this._markerElement.classList.add("bm-marker-poi");
Object.defineProperty(this, 'isPOIMarker', {value: true});
}
update(markerData) {
super.update(markerData);
this.icon = markerData.icon ? markerData.icon : "assets/poi.svg";
//backwards compatibility for "iconAnchor"
if (!markerData.anchor) {
if (markerData.iconAnchor) {
this.setAnchor(parseInt(markerData.iconAnchor.x), parseInt(markerData.iconAnchor.y));
}
}
}
onClick(clickPosition) {
if (!dispatchEvent(this.manager.events, 'bluemapMarkerClick', {marker: this})) return;
this.followLink();
this._markerElement.classList.add("bm-marker-poi-show-label");
let onRemoveLabel = () => {
this._markerElement.classList.remove("bm-marker-poi-show-label");
};
this.manager.events.addEventListener('bluemapPopupMarker', onRemoveLabel, {once: true});
setTimeout(() => {
this.manager.events.addEventListener('bluemapCameraMoved', onRemoveLabel, {once: true});
}, 1000);
}
set label(label){
this._label = label;
this.updateHtml();
}
set icon(icon) {
this._icon = icon;
this.updateHtml();
}
updateHtml() {
let labelHtml = '';
if (this._label) labelHtml = `<div class="bm-marker-poi-label">${this._label}</div>`;
this.html = `<img src="${this._icon}" alt="POI-${this.id}" draggable="false">${labelHtml}`;
}
}

View File

@ -1,52 +0,0 @@
import {HTMLMarker} from "./HTMLMarker";
export class PlayerMarker extends HTMLMarker {
constructor(markerSet, id, parentObject, playerUuid) {
super(markerSet, id, parentObject);
this._markerElement.classList.add("bm-marker-player");
Object.defineProperty(this, 'isPlayerMarker', {value: true});
this._name = id;
this._head = "assets/playerheads/steve.png";
this.playerUuid = playerUuid;
this.updateHtml();
}
onClick(clickPosition) {
this.followLink();
this._markerElement.classList.add("bm-marker-poi-show-label");
let onRemoveLabel = () => {
this._markerElement.classList.remove("bm-marker-poi-show-label");
};
this.manager.events.addEventListener('bluemapPopupMarker', onRemoveLabel, {once: true});
setTimeout(() => {
this.manager.events.addEventListener('bluemapCameraMoved', onRemoveLabel, {once: true});
}, 1000);
}
set name(name){
this._name = name;
this.updateHtml();
}
set head(headImage) {
this._head = headImage;
this.updateHtml();
}
updateHtml() {
let labelHtml = '';
if (this._name) labelHtml = `<div class="bm-marker-poi-label">${this._name}</div>`;
this.html = `<img src="${this._head}" alt="PlayerHead-${this.id}" draggable="false">${labelHtml}`;
}
}

102
src/markers/PoiMarker.js Normal file
View File

@ -0,0 +1,102 @@
import {HtmlMarker} from "./HtmlMarker";
export class PoiMarker extends HtmlMarker {
/**
* @param markerId {string}
*/
constructor(markerId) {
super(markerId);
Object.defineProperty(this, 'isPoiMarker', {value: true});
this.markerType = "poi";
this.html = `<img src="" alt="POI Icon (${this.markerId})" 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 (this.hightlight) return;
this.hightlight = true;
let eventHandler = evt => {
if (evt.path.includes(this.element)) return;
this.hightlight = 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 hightlight(highlight) {
if (highlight) {
this.element.classList.add("bm-marker-highlight");
} else {
this.element.classList.remove("bm-marker-highlight");
}
}
get hightlight() {
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,
* icon: string,
* link: string,
* newTab: boolean,
* 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.anchor.setX(anch.x || 0);
this.anchor.setY(anch.y || 0);
// update label
if (this.labelElement.innerHTML !== markerData.label){
this.labelElement.innerHTML = markerData.label || "";
}
// update icon
if (this._lastIcon !== markerData.icon){
this.iconElement.src = markerData.icon || "assets/poi.svg";
this._lastIcon = markerData.icon;
}
// update min/max distances
this.fadeDistanceMin = markerData.minDistance || 0;
this.fadeDistanceMax = markerData.maxDistance !== undefined ? markerData.maxDistance : Number.MAX_VALUE;
}
}

View File

@ -1,227 +1,416 @@
import {Marker} from "./Marker";
import {
Color,
DoubleSide,
Mesh,
Object3D, ShaderMaterial,
Shape,
ShapeBufferGeometry,
Vector2
} from "three";
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 {MARKER_FILL_FRAGMENT_SHADER} from "./shader/MarkerFillFragmentShader";
import {MARKER_FILL_VERTEX_SHADER} from "./shader/MarkerFillVertexShader";
import {deepEquals} from "../util/Utils";
import {ObjectMarker} from "./ObjectMarker";
export class ShapeMarker extends Marker {
export class ShapeMarker extends ObjectMarker {
constructor(markerSet, id, parentObject) {
super(markerSet, id);
/**
* @param markerId {string}
*/
constructor(markerId) {
super(markerId);
Object.defineProperty(this, 'isShapeMarker', {value: true});
Object.defineProperty(this, 'type', {value: "shape"});
this.markerType = "shape";
let fillColor = Marker.normalizeColor({});
let borderColor = Marker.normalizeColor({});
let lineWidth = 2;
let depthTest = false;
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._lineOpacity = 1;
this._fillOpacity = 1;
this.add(this.border, this.fill);
this._markerObject = new Object3D();
this._markerObject.position.copy(this.position);
parentObject.add(this._markerObject);
this._markerFillMaterial = new ShaderMaterial({
vertexShader: MARKER_FILL_VERTEX_SHADER,
fragmentShader: MARKER_FILL_FRAGMENT_SHADER,
side: DoubleSide,
depthTest: depthTest,
transparent: true,
uniforms: {
markerColor: { value: fillColor.vec4 }
}
});
this._markerLineMaterial = new LineMaterial({
color: new Color(borderColor.rgb),
opacity: borderColor.a,
transparent: true,
linewidth: lineWidth,
depthTest: depthTest,
vertexColors: false,
dashed: false
});
this._markerLineMaterial.resolution.set(window.innerWidth, window.innerHeight);
this._markerData = {};
}
update(markerData) {
super.update(markerData);
this.height = markerData.height ? parseFloat(markerData.height) : 0.0;
this.depthTest = !!markerData.depthTest;
/**
* @param y {number}
*/
setShapeY(y) {
let relativeY = y - this.position.y;
this.fill.position.y = relativeY;
this.border.position.y = relativeY;
}
if (markerData.fillColor) this.fillColor = markerData.fillColor;
if (markerData.borderColor) this.borderColor = markerData.borderColor;
/**
* @param shape {Shape}
*/
setShape(shape) {
this.fill.updateGeometry(shape);
this.border.updateGeometry(shape);
}
this.lineWidth = markerData.lineWidth ? parseFloat(markerData.lineWidth) : 2;
/**
* @typedef {{r: number, g: number, b: number, a: number}} ColorLike
*/
let points = [];
if (Array.isArray(markerData.shape)) {
markerData.shape.forEach(point => {
points.push(new Vector2(parseFloat(point.x), parseFloat(point.z)));
});
/**
* @param markerData {{
* position: {x: number, y: number, z: number},
* label: 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));
}
this.shape = points;
}
_onBeforeRender(renderer, scene, camera) {
super._onBeforeRender(renderer, scene, camera);
// update shapeY
this.setShapeY((markerData.shapeY || markerData.height || 0) + 0.01); //"height" for backwards compatibility, adding 0.01 to avoid z-fighting
this._markerFillMaterial.uniforms.markerColor.value.w = this._fillOpacity * this._opacity;
this._markerLineMaterial.opacity = this._lineOpacity * this._opacity;
// 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() {
this._markerObject.parent.remove(this._markerObject);
this._markerObject.children.forEach(child => {
if (child.geometry && child.geometry.isGeometry) child.geometry.dispose();
});
this._markerObject.clear();
this._markerFillMaterial.dispose();
this._markerLineMaterial.dispose();
super.dispose();
this.fill.dispose();
this.border.dispose();
}
/**
* Sets the fill-color
*
* color-object format:
* <code><pre>
* {
* r: 0, // int 0-255 red
* g: 0, // int 0-255 green
* b: 0, // int 0-255 blue
* a: 0 // float 0-1 alpha
* }
* </pre></code>
*
* @param color {Object}
* @private
* Creates a shape from a data object, usually parsed json from a markers.json
* @param shapeData {object}
* @returns {Shape}
*/
set fillColor(color) {
color = Marker.normalizeColor(color);
createShapeFromData(shapeData) {
/** @type {THREE.Vector2[]} **/
let points = [];
this._markerFillMaterial.uniforms.markerColor.value = color.vec4;
this._fillOpacity = color.a;
this._markerFillMaterial.needsUpdate = true;
}
if (Array.isArray(shapeData)){
shapeData.forEach(point => {
let x = (point.x || 0) - this.position.x;
let z = (point.z || 0) - this.position.z;
/**
* Sets the border-color
*
* color-object format:
* <code><pre>
* {
* r: 0, // int 0-255 red
* g: 0, // int 0-255 green
* b: 0, // int 0-255 blue
* a: 0 // float 0-1 alpha
* }
* </pre></code>
*
* @param color {Object}
*/
set borderColor(color) {
color = Marker.normalizeColor(color);
this._markerLineMaterial.color.setHex(color.rgb);
this._lineOpacity = color.a;
this._markerLineMaterial.needsUpdate = true;
}
/**
* Sets the width of the marker-line
* @param width {number}
*/
set lineWidth(width) {
this._markerLineMaterial.linewidth = width;
this._markerLineMaterial.needsUpdate = true;
}
/**
* Sets if this marker can be seen through terrain
* @param test {boolean}
*/
set depthTest(test) {
this._markerFillMaterial.depthTest = test;
this._markerFillMaterial.needsUpdate = true;
this._markerLineMaterial.depthTest = test;
this._markerLineMaterial.needsUpdate = true;
}
get depthTest() {
return this._markerFillMaterial.depthTest;
}
/**
* Sets the height of this marker
* @param height {number}
*/
set height(height) {
this._markerObject.position.y = height;
}
/**
* Sets the points for the shape of this marker.
* @param points {Vector2[]}
*/
set shape(points) {
// remove old marker
this._markerObject.children.forEach(child => {
if (child.geometry && child.geometry.isGeometry) child.geometry.dispose();
});
this._markerObject.clear();
if (points.length < 3) return;
this._markerObject.position.x = this.position.x;
this._markerObject.position.z = this.position.z;
// border-line
let points3d = [];
points.forEach(point => points3d.push(point.x, 0, point.y));
points3d.push(points[0].x, 0, points[0].y)
let lineGeo = new LineGeometry()
lineGeo.setPositions(points3d);
lineGeo.translate(-this.position.x, 0.01456, -this.position.z);
let line = new Line2(lineGeo, this._markerLineMaterial);
line.onBeforeRender = renderer => renderer.getSize(line.material.resolution);
line.computeLineDistances();
line.marker = this;
this._markerObject.add(line);
// fill
if (this._markerFillMaterial.uniforms.markerColor.value.w > 0) {
let shape = new Shape(points);
let fillGeo = new ShapeBufferGeometry(shape, 1);
fillGeo.rotateX(Math.PI / 2); //make y to z
fillGeo.translate(-this.position.x, 0.01456, -this.position.z);
let fill = new Mesh(fillGeo, this._markerFillMaterial);
fill.marker = this;
this._markerObject.add(fill);
points.push(new Vector2(x, z));
});
}
// put render-hook on first object
if (this._markerObject.children.length > 0) {
let oldHook = this._markerObject.children[0].onBeforeRender;
this._markerObject.children[0].onBeforeRender = (renderer, scene, camera, geometry, material, group) => {
this._onBeforeRender(renderer, scene, camera);
oldHook(renderer, scene, camera, geometry, material, group);
}
}
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.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

@ -4,7 +4,7 @@ import {
Mesh,
SphereGeometry,
ShaderMaterial,
BackSide
BackSide, Color
} from 'three';
import { SKY_FRAGMENT_SHADER } from './SkyFragmentShader';
@ -24,7 +24,7 @@ export class SkyboxScene extends Scene {
};
this.UNIFORM_skyColor = {
value: new Vector3(0.5, 0.5, 1)
value: new Color(0.5, 0.5, 1)
};
this.UNIFORM_ambientLight = {
@ -47,26 +47,44 @@ export class SkyboxScene extends Scene {
this.add(skybox);
}
/**
* @returns {number}
*/
get sunlight() {
return this.UNIFORM_sunlight.value;
}
/**
* @param strength {number}
*/
set sunlight(strength) {
this.UNIFORM_sunlight.value = strength;
}
/**
* @returns {Color}
*/
get skyColor() {
return this.UNIFORM_skyColor.value;
}
/**
* @param color {Color}
*/
set skyColor(color) {
this.UNIFORM_skyColor.value = color;
this.UNIFORM_skyColor.value.set(color);
}
/**
* @returns {number}
*/
get ambientLight() {
return this.UNIFORM_ambientLight.value;
}
/**
* @param strength {number}
*/
set ambientLight(strength) {
this.UNIFORM_ambientLight.value = strength;
}

View File

@ -33,6 +33,9 @@ var CSS2DObject = function ( element ) {
} );
this.element.addEventListener("click", event => this.onClick(event));
this.element.addEventListener("touch", event => this.onClick(event));
};
CSS2DObject.prototype = Object.create( Object3D.prototype );
@ -82,7 +85,7 @@ var CSS2DRenderer = function () {
};
var renderObject = function ( object, scene, camera ) {
var renderObject = function ( object, scene, camera, parentVisible ) {
if ( object instanceof CSS2DObject ) {
@ -99,7 +102,7 @@ var CSS2DRenderer = function () {
element.style.oTransform = style;
element.style.transform = style;
element.style.display = ( object.visible && vector.z >= - 1 && vector.z <= 1 ) ? '' : 'none';
element.style.display = ( parentVisible && object.visible && vector.z >= - 1 && vector.z <= 1 && element.style.opacity > 0 ) ? '' : 'none';
var objectData = {
distanceToCameraSquared: getDistanceToSquared( camera, object )
@ -119,7 +122,7 @@ var CSS2DRenderer = function () {
for ( var i = 0, l = object.children.length; i < l; i ++ ) {
renderObject( object.children[ i ], scene, camera );
renderObject( object.children[ i ], scene, camera, parentVisible && object.visible );
}
@ -184,7 +187,7 @@ var CSS2DRenderer = function () {
viewMatrix.copy( camera.matrixWorldInverse );
viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, viewMatrix );
renderObject( scene, scene, camera );
renderObject( scene, scene, camera, true );
zOrder( scene );
};

View File

@ -2,6 +2,13 @@ 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);
@ -58,18 +65,30 @@ export class CombinedCamera extends PerspectiveCamera {
}
/**
* @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
}

View File

@ -1,6 +1,6 @@
/**
* Takes a base46 string and converts it into an image element
* @param string
* @param string {string}
* @returns {HTMLElement}
*/
import {MathUtils} from "three";
@ -13,8 +13,8 @@ export const stringToImage = string => {
/**
* Creates an optimized path from x,z coordinates used by bluemap to save tiles
* @param x
* @param z
* @param x {number}
* @param z {number}
* @returns {string}
*/
export const pathFromCoords = (x, z) => {
@ -31,7 +31,7 @@ export const pathFromCoords = (x, z) => {
/**
* Splits a number into an optimized folder-path used to save bluemap-tiles
* @param num
* @param num {number}
* @returns {string}
*/
const splitNumberToPath = num => {
@ -42,7 +42,7 @@ const splitNumberToPath = num => {
path += '-';
}
let s = parseInt(num).toString();
let s = num.toString();
for (let i = 0; i < s.length; i++) {
path += s.charAt(i) + '/';
@ -53,8 +53,8 @@ const splitNumberToPath = num => {
/**
* Hashes tile-coordinates to be saved in a map
* @param x
* @param z
* @param x {number}
* @param z {number}
* @returns {string}
*/
export const hashTile = (x, z) => `x${x}z${z}`;
@ -62,9 +62,9 @@ export const hashTile = (x, z) => `x${x}z${z}`;
/**
* Dispatches an event to the element of this map-viewer
* @param element the element on that the event is dispatched
* @param event
* @param detail
* @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 = {}) => {
@ -83,9 +83,9 @@ export const dispatchEvent = (element, event, detail = {}) => {
* - info
* - warning
* - error
* @param element the element on that the event is dispatched
* @param message
* @param level
* @param element {EventTarget} the element on that the event is dispatched
* @param message {string}
* @param level {string}
*/
export const alert = (element, message, level = "info") => {
@ -112,7 +112,7 @@ export const alert = (element, message, level = "info") => {
/**
* 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 {String} html representing a single element
* @param html {string} representing a single element
* @return {Element}
*/
export const htmlToElement = html => {
@ -124,7 +124,7 @@ export const htmlToElement = html => {
/**
* 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 {String} html representing any number of sibling elements
* @param html {string} representing any number of sibling elements
* @return {NodeList}
*/
export const htmlToElements = html => {
@ -135,10 +135,10 @@ export const htmlToElements = html => {
/**
* Schedules an animation
* @param durationMs the duration of the animation in ms
* @param animationFrame a function that is getting called each frame with the parameters (progress (0-1), deltaTime)
* @param postAnimation 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 the animation object
* @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 = {
@ -181,13 +181,44 @@ export const animate = function (animationFrame, durationMs = 1000, postAnimatio
*
* Source: https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/
*
* @param element
* @param element {Element}
* @returns {{top: number, left: number}}
*/
export const elementOffset = (element) => {
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;
}

View File

@ -54,6 +54,8 @@ ShaderLib[ 'line' ] = {
attribute vec3 instanceColorEnd;
varying vec2 vUv;
varying float vDistance;
#ifdef USE_DASH
@ -175,6 +177,8 @@ ShaderLib[ 'line' ] = {
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>
@ -185,8 +189,13 @@ ShaderLib[ 'line' ] = {
fragmentShader:
`
#define FLT_MAX 3.402823466e+38
uniform vec3 diffuse;
uniform float opacity;
uniform float fadeDistanceMax;
uniform float fadeDistanceMin;
#ifdef USE_DASH
@ -204,6 +213,8 @@ ShaderLib[ 'line' ] = {
#include <clipping_planes_pars_fragment>
varying vec2 vUv;
varying float vDistance;
void main() {
@ -227,7 +238,18 @@ ShaderLib[ 'line' ] = {
}
vec4 diffuseColor = vec4( diffuse, opacity );
// 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>