diff --git a/.dev/.env b/.dev/.env index 16f0228f8..61976a2c6 100644 --- a/.dev/.env +++ b/.dev/.env @@ -1,3 +1,3 @@ SPAN_PORT=8083 USER_PORT=8084 -COLLABORATION_PORT=4444 \ No newline at end of file +COLLABORATION_PORT=4444 diff --git a/app/components/visualization/page-setup/sidebar/customizationbar/settings/settings.ts b/app/components/visualization/page-setup/sidebar/customizationbar/settings/settings.ts index 9105d75a6..cafb89042 100644 --- a/app/components/visualization/page-setup/sidebar/customizationbar/settings/settings.ts +++ b/app/components/visualization/page-setup/sidebar/customizationbar/settings/settings.ts @@ -15,6 +15,7 @@ import LocalUser from 'collaboration/services/local-user'; import MessageSender from 'collaboration/services/message-sender'; import RoomSerializer from 'collaboration/services/room-serializer'; import PopupData from '../../../../rendering/popups/popup-data'; +import MinimapService from 'explorviz-frontend/services/minimap-service'; import SceneRepository from 'explorviz-frontend/services/repos/scene-repository'; import { Mesh } from 'three'; @@ -53,6 +54,9 @@ export default class Settings extends Component { @service('user-settings') private userSettings!: UserSettings; + @service('minimap-service') + private minimapService!: MinimapService; + colorSchemes: { name: string; id: ColorSchemeId }[] = [ { name: 'Default', id: 'default' }, { name: 'Classic (Initial)', id: 'classic' }, @@ -68,6 +72,7 @@ export default class Settings extends Component { ApplicationSettingId[] > = { Camera: [], + Minimap: [], Colors: [], Controls: [], Communication: [], @@ -141,6 +146,9 @@ export default class Settings extends Component { this.userSettings.applicationSettings.cameraFov.value; this.localUser.defaultCamera.updateProjectionMatrix(); break; + case 'zoom': + this.minimapService.updateSphereRadius(); + break; default: break; } @@ -176,12 +184,40 @@ export default class Settings extends Component { @action updateFlagSetting(name: ApplicationSettingId, value: boolean) { - const settingId = name as ApplicationSettingId; + const settingId = name; + const settingString = settingId as string; try { this.userSettings.updateApplicationSetting(settingId, value); } catch (e) { this.toastHandlerService.showErrorToastMessage(e.message); } + if (settingString.startsWith('layer')) { + const layerNumber = parseInt(settingString.slice(5), 10); // Extract the layer number from settingId + if (!isNaN(layerNumber)) { + // Ensure it's a valid number + if (value || value === undefined) { + this.localUser.minimapCamera.layers.enable(layerNumber); + } else { + this.localUser.minimapCamera.layers.disable(layerNumber); + } + } + } else { + switch (settingId) { + case 'applyHighlightingOnHover': + if (this.args.updateHighlighting) { + this.args.updateHighlighting(); + } + break; + case 'enableGamepadControls': + this.args.setGamepadSupport(value); + break; + case 'minimap': + this.minimapService.minimapEnabled = value; + break; + default: + break; + } + } const scene = this.sceneRepo.getScene(); const directionalLight = scene.getObjectByName('DirectionalLight'); diff --git a/app/components/visualization/rendering/browser-rendering.ts b/app/components/visualization/rendering/browser-rendering.ts index 1571ed17a..172c91732 100644 --- a/app/components/visualization/rendering/browser-rendering.ts +++ b/app/components/visualization/rendering/browser-rendering.ts @@ -30,6 +30,9 @@ import { Class } from 'explorviz-frontend/utils/landscape-schemes/structure-data import ApplicationObject3D from 'explorviz-frontend/view-objects/3d/application/application-object-3d'; import ComponentMesh from 'explorviz-frontend/view-objects/3d/application/component-mesh'; import FoundationMesh from 'explorviz-frontend/view-objects/3d/application/foundation-mesh'; +import HeatmapConfiguration from 'heatmap/services/heatmap-configuration'; +import { Vector3 } from 'three'; +import * as THREE from 'three'; import { MapControls } from 'three/examples/jsm/controls/MapControls'; import SpectateUser from 'collaboration/services/spectate-user'; import { @@ -42,14 +45,12 @@ import { removeAllHighlightingFor } from 'explorviz-frontend/utils/application-r import LinkRenderer from 'explorviz-frontend/services/link-renderer'; import SceneRepository from 'explorviz-frontend/services/repos/scene-repository'; import RoomSerializer from 'collaboration/services/room-serializer'; -import HeatmapConfiguration from 'explorviz-frontend/services/heatmap-configuration'; -import ThreeForceGraph from 'three-forcegraph'; -import { Vector3 } from 'three/src/math/Vector3'; -import * as THREE from 'three'; import AnnotationHandlerService from 'explorviz-frontend/services/annotation-handler'; import { SnapshotToken } from 'explorviz-frontend/services/snapshot-token'; import Auth from 'explorviz-frontend/services/auth'; import GamepadControls from 'explorviz-frontend/utils/controls/gamepad/gamepad-controls'; +import MinimapService from 'explorviz-frontend/services/minimap-service'; +import Raycaster from 'explorviz-frontend/utils/raycaster'; import PopupData from './popups/popup-data'; interface BrowserRenderingArgs { @@ -104,6 +105,9 @@ export default class BrowserRendering extends Component { @service('repos/scene-repository') sceneRepo!: SceneRepository; + @service('minimap-service') + minimapService!: MinimapService; + @service('annotation-handler') annotationHandler!: AnnotationHandlerService; @@ -115,7 +119,7 @@ export default class BrowserRendering extends Component { private ideCrossCommunication: IdeCrossCommunication; @tracked - readonly graph: ThreeForceGraph; + readonly graph: ForceGraph; @tracked readonly scene: THREE.Scene; @@ -178,7 +182,7 @@ export default class BrowserRendering extends Component { // Force graph const forceGraph = new ForceGraph(getOwner(this), 0.02); - this.graph = forceGraph.graph; + this.graph = forceGraph; this.scene.add(forceGraph.graph); this.updatables.push(forceGraph); this.updatables.push(this); @@ -186,8 +190,11 @@ export default class BrowserRendering extends Component { // Spectate this.updatables.push(this.spectateUserService); + // Minimap + this.updatables.push(this.minimapService); + this.popupHandler = new PopupHandler(getOwner(this)); - this.applicationRenderer.forceGraph = this.graph; + this.applicationRenderer.forceGraph = this.graph.graph; // IDE Websocket this.ideWebsocket = new IdeWebsocket( @@ -208,7 +215,6 @@ export default class BrowserRendering extends Component { this.collaborationSession.idToRemoteUser.forEach((remoteUser) => { remoteUser.update(delta); }); - if (this.initDone && this.linkRenderer.flag) { this.linkRenderer.flag = false; } @@ -337,9 +343,21 @@ export default class BrowserRendering extends Component { this.canvas ); this.spectateUserService.cameraControls = this.cameraControls; - + this.localUser.cameraControls = this.cameraControls; this.updatables.push(this.localUser); this.updatables.push(this.cameraControls); + + // initialize minimap + this.minimapService.initializeMinimap( + this.scene, + this.graph, + this.cameraControls + ); + + this.minimapService.raycaster = new Raycaster( + this.localUser.minimapCamera, + this.minimapService + ); } /** @@ -357,6 +375,7 @@ export default class BrowserRendering extends Component { this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.setSize(width, height); + this.debug('Renderer set up'); this.renderingLoop = new RenderingLoop(getOwner(this), { camera: this.camera, @@ -368,8 +387,8 @@ export default class BrowserRendering extends Component { // if snapshot is loaded, set the camera position of the saved camera position of the snapshot if (this.args.snapshot || this.args.snapshotReload) { - this.graph.onFinishUpdate(() => { - if (!this.initDone && this.graph.graphData().nodes.length > 0) { + this.graph.graph.onFinishUpdate(() => { + if (!this.initDone && this.graph.graph.graphData().nodes.length > 0) { this.debug('initdone!'); setTimeout(() => { this.applicationRenderer.getOpenApplications(); @@ -378,8 +397,8 @@ export default class BrowserRendering extends Component { } }); } else { - this.graph.onFinishUpdate(() => { - if (!this.initDone && this.graph.graphData().nodes.length > 0) { + this.graph.graph.onFinishUpdate(() => { + if (!this.initDone && this.graph.graph.graphData().nodes.length > 0) { this.debug('initdone!'); setTimeout(() => { this.cameraControls.resetCameraFocusOn( diff --git a/app/modifiers/interaction-modifier.ts b/app/modifiers/interaction-modifier.ts index af8dc215d..ec902a607 100644 --- a/app/modifiers/interaction-modifier.ts +++ b/app/modifiers/interaction-modifier.ts @@ -4,8 +4,10 @@ import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import CollaborationSession from 'collaboration/services/collaboration-session'; import LocalUser from 'collaboration/services/local-user'; +import RemoteUser from 'collaboration/utils/remote-user'; import debugLogger from 'ember-debug-logger'; import Modifier, { ArgsFor } from 'ember-modifier'; +import MinimapService from 'explorviz-frontend/services/minimap-service'; import UserSettings from 'explorviz-frontend/services/user-settings'; import Raycaster from 'explorviz-frontend/utils/raycaster'; import { Object3D, Vector2 } from 'three'; @@ -86,6 +88,9 @@ export default class InteractionModifierModifier extends Modifier) { super(owner, args); - this.raycaster = new Raycaster(); + this.raycaster = new Raycaster(this.localUser.minimapCamera); } @action @@ -225,9 +230,15 @@ export default class InteractionModifierModifier extends Modifier { this.mouseClickCounter = 0; - this.namedArgs.singleClick?.(intersectedViewObj); + this.namedArgs.singleClick?.(intersectedViewObjectCopy); }, this.DOUBLE_CLICK_TIME_MS); } @@ -317,6 +351,42 @@ export default class InteractionModifierModifier extends Modifier { { landscapeData, graph }: any ) { this.landscapeData = landscapeData; - this.graph = graph; + this.graph = graph.graph; this.handleUpdatedLandscapeData.perform(); } diff --git a/app/rendering/application/force-graph.ts b/app/rendering/application/force-graph.ts index e96f35c86..f3ef9feda 100644 --- a/app/rendering/application/force-graph.ts +++ b/app/rendering/application/force-graph.ts @@ -12,7 +12,7 @@ import ApplicationObject3D from 'explorviz-frontend/view-objects/3d/application/ import ClazzCommunicationMesh from 'explorviz-frontend/view-objects/3d/application/clazz-communication-mesh'; import GrabbableForceGraph from 'explorviz-frontend/view-objects/3d/landscape/grabbable-force-graph'; import ThreeForceGraph from 'three-forcegraph'; - +import * as THREE from 'three'; export interface GraphNode { // data: ApplicationData, id: string; @@ -55,8 +55,13 @@ export default class ForceGraph { @service('link-renderer') linkRenderer!: LinkRenderer; + boundingBox: THREE.Box3 = new THREE.Box3(); + + scaleFactor!: number; + constructor(owner: any, scale: number = 1) { this.scale = scale; + this.scaleFactor = 30; // https://stackoverflow.com/questions/65010591/emberjs-injecting-owner-to-native-class-from-component setOwner(this, owner); this.graph = new GrabbableForceGraph() @@ -103,5 +108,54 @@ export default class ForceGraph { tick() { this.graph.tickFrame(); + this.calculateBoundingBox(); + } + /** + * Calculates the bounding box of the graph using the position and collision radius of the nodes + */ + calculateBoundingBox() { + let minX = Infinity, + minY = Infinity, + minZ = Infinity; + let maxX = -Infinity, + maxY = -Infinity, + maxZ = -Infinity; + + const isAlone = this.graph.graphData().nodes.length == 1; + + this.graph.graphData().nodes.forEach((node) => { + if ( + node.x !== undefined && + node.y !== undefined && + node.z !== undefined + ) { + let collisionRadius: number; + if (!isAlone) { + collisionRadius = node.collisionRadius / 4 || 0; + } else { + collisionRadius = node.collisionRadius / 2 || 0; + } + + if (node.x - collisionRadius < minX) minX = node.x - collisionRadius; + if (node.y - collisionRadius < minY) minY = node.y - collisionRadius; + if (node.z - collisionRadius < minZ) minZ = node.z - collisionRadius; + if (node.x + collisionRadius > maxX) maxX = node.x + collisionRadius; + if (node.y + collisionRadius > maxY) maxY = node.y + collisionRadius; + if (node.z + collisionRadius > maxZ) maxZ = node.z + collisionRadius; + } + }); + + this.boundingBox = new THREE.Box3( + new THREE.Vector3( + minX / this.scaleFactor, + minY / this.scaleFactor, + minZ / this.scaleFactor + ), + new THREE.Vector3( + maxX / this.scaleFactor, + maxY / this.scaleFactor, + maxZ / this.scaleFactor + ) + ); } } diff --git a/app/rendering/application/rendering-loop.ts b/app/rendering/application/rendering-loop.ts index 62c98ead7..cd71df5dc 100644 --- a/app/rendering/application/rendering-loop.ts +++ b/app/rendering/application/rendering-loop.ts @@ -6,6 +6,8 @@ import { inject as service } from '@ember/service'; import debugLogger from 'ember-debug-logger'; import ArZoomHandler from 'extended-reality/utils/ar-helpers/ar-zoom-handler'; import * as THREE from 'three'; +import LocalUser from 'collaboration/services/local-user'; +import MinimapService from 'explorviz-frontend/services/minimap-service'; const clock = new Clock(); @@ -29,8 +31,16 @@ export default class RenderingLoop { @service('user-settings') userSettings!: UserSettings; + @service('local-user') + private localUser!: LocalUser; + + @service('minimap-service') + minimapService!: MinimapService; + camera: THREE.Camera; + minimapCamera: THREE.OrthographicCamera; + scene: THREE.Scene; renderer: THREE.WebGLRenderer; @@ -39,6 +49,13 @@ export default class RenderingLoop { zoomHandler?: ArZoomHandler; + currentViewport = new THREE.Vector4( + 0, + 0, + window.innerWidth, + window.innerHeight + ); + constructor(owner: any, args: Args) { setOwner(this, owner); this.camera = args.camera; @@ -46,6 +63,7 @@ export default class RenderingLoop { this.renderer = args.renderer; this.updatables = args.updatables; this.zoomHandler = args.zoomHandler; + this.minimapCamera = this.localUser.minimapCamera; } start() { @@ -81,6 +99,10 @@ export default class RenderingLoop { if (this.threePerformance) { this.threePerformance.stats.end(); } + + if (this.minimapService.minimapEnabled) { + this.renderMinimap(); + } }); } @@ -134,4 +156,47 @@ export default class RenderingLoop { this.axesHelper = undefined; } } + /** + * Renders the minimap every tick, either on top right corner or as the minimap overlay. + */ + renderMinimap() { + const minimapNums = this.minimapService.minimap(); + const minimapHeight = minimapNums[0]; + const minimapWidth = minimapNums[1]; + const minimapX = minimapNums[2]; + const minimapY = minimapNums[3]; + const borderWidth = 1; + + this.currentViewport = this.renderer.getViewport(new THREE.Vector4()); + + // Enable scissor test and set the scissor area for the border + this.renderer.setScissorTest(true); + this.renderer.setScissor( + minimapX - borderWidth, + minimapY - borderWidth, + minimapWidth + 2 * borderWidth, + minimapHeight + 2 * borderWidth + ); + this.renderer.setViewport( + minimapX - borderWidth, + minimapY - borderWidth, + minimapWidth + 2 * borderWidth, + minimapHeight + 2 * borderWidth + ); + + // Background color for the border + this.renderer.setClearColor('#989898'); + this.renderer.clear(); + + // Set scissor and viewport for the minimap itself + this.renderer.setScissor(minimapX, minimapY, minimapWidth, minimapHeight); + this.renderer.setViewport(minimapX, minimapY, minimapWidth, minimapHeight); + + // Render the minimap scene + this.renderer.render(this.scene, this.minimapCamera); + + // Restore original viewport and disable scissor test + this.renderer.setViewport(...this.currentViewport); + this.renderer.setScissorTest(false); + } } diff --git a/app/services/minimap-service.ts b/app/services/minimap-service.ts new file mode 100644 index 000000000..9fd568229 --- /dev/null +++ b/app/services/minimap-service.ts @@ -0,0 +1,427 @@ +import Service, { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import LocalUser from 'collaboration/services/local-user'; +import ForceGraph from 'explorviz-frontend/rendering/application/force-graph'; +import UserSettings from 'explorviz-frontend/services/user-settings'; +import * as THREE from 'three'; +import Raycaster from 'explorviz-frontend/utils/raycaster'; +import RemoteUser from 'collaboration/utils/remote-user'; +import CameraControls from 'explorviz-frontend/utils/application-rendering/camera-controls'; + +export enum SceneLayers { + Default = 0, + Foundation = 1, + Component = 2, + Clazz = 3, + Communication = 4, + Ping = 5, + MinimapLabel = 6, + MinimapMarkers = 7, + LocalMinimapMarker = 8, +} + +const MARKER_HEIGHT = 101; + +const MINIMAP_HEIGHT = 102; + +export default class MinimapService extends Service { + @service('user-settings') + userSettings!: UserSettings; + + @service('local-user') + private localUser!: LocalUser; + + @service('user-settings') + settings!: UserSettings; + + @tracked + makeFullsizeMinimap!: boolean; + + @tracked + minimapSize!: number; + + @tracked + minimapEnabled!: boolean; + + cameraControls!: CameraControls; + + graph!: ForceGraph; + + minimapUserMarkers: Map = new Map(); + + userPosition!: THREE.Vector3; + + distance!: number; + + raycaster!: Raycaster; + + scene!: THREE.Scene; + + /** + * Initializes the minimap Service class + * @param scene Scene containing all elements + * @param graph Graph including the boundingbox used by the minimap + * @param cameraControls CameraControls of the main camera + */ + initializeMinimap( + scene: THREE.Scene, + graph: ForceGraph, + cameraControls: CameraControls + ) { + this.minimapEnabled = this.settings.applicationSettings.minimap.value; + this.userPosition = new THREE.Vector3(0, 0, 0); + this.makeFullsizeMinimap = false; + this.minimapSize = 4; + this.graph = graph; + this.scene = scene; + + this.setupCamera(cameraControls); + this.setupLocalUserMarker(); + } + /** + * Sets up the camera for the minimap + * @param cameraControls CameraControls of the main camera + */ + setupCamera(cameraControls: CameraControls) { + this.cameraControls = cameraControls; + this.localUser.minimapCamera = new THREE.OrthographicCamera(); + this.localUser.minimapCamera.position.set(0, MINIMAP_HEIGHT, 0); + this.localUser.minimapCamera.lookAt(new THREE.Vector3(0, -1, 0)); + + this.localUser.minimapCamera.layers.disable(SceneLayers.Default); + this.localUser.minimapCamera.layers.enable(SceneLayers.Foundation); + this.localUser.minimapCamera.layers.enable(SceneLayers.Component); + this.localUser.minimapCamera.layers.enable(SceneLayers.Clazz); + this.localUser.minimapCamera.layers.enable(SceneLayers.Communication); + this.localUser.minimapCamera.layers.enable(SceneLayers.Ping); + this.localUser.minimapCamera.layers.enable(SceneLayers.MinimapLabel); + this.localUser.minimapCamera.layers.enable(SceneLayers.MinimapMarkers); + this.localUser.minimapCamera.layers.enable(SceneLayers.LocalMinimapMarker); + } + + /** + * Sets up the local user marker for the minimap + */ + setupLocalUserMarker() { + this.initializeUserMinimapMarker( + new THREE.Color(0x808080), + this.userPosition, + 'localUser' + ); + + this.minimapUserMarkers + .get('localUser')! + .layers.enable(SceneLayers.LocalMinimapMarker); + this.minimapUserMarkers + .get('localUser')! + .layers.disable(SceneLayers.MinimapMarkers); + + this.scene.add(this.minimapUserMarkers.get('localUser')!); + } + + tick() { + this.getCurrentPosition(); + this.updateMinimapCamera(); + this.updateUserMinimapMarker(this.userPosition, 'localUser'); + } + + /** + * Gets the current position of the user, either by camera position or camera target + */ + private getCurrentPosition() { + const userPosition = new THREE.Vector3(); + if (!this.settings.applicationSettings.version2.value) { + userPosition.copy(this.cameraControls.perspectiveCameraControls.target); + } else { + userPosition.copy(this.localUser.camera.position); + } + this.userPosition = this.checkBoundingBox(userPosition); + } + + /** + * Checks if the user is inside the bounding box + * @param intersection Intersection of the user + */ + private checkBoundingBox(intersection: THREE.Vector3): THREE.Vector3 { + if (this.graph.boundingBox) { + if (intersection.x > this.graph.boundingBox.max.x) { + intersection.x = this.graph.boundingBox.max.x; + } else if (intersection.x < this.graph.boundingBox.min.x) { + intersection.x = this.graph.boundingBox.min.x; + } + if (intersection.z > this.graph.boundingBox.max.z) { + intersection.z = this.graph.boundingBox.max.z; + } else if (intersection.z < this.graph.boundingBox.min.z) { + intersection.z = this.graph.boundingBox.min.z; + } + } + return intersection; + } + + /** + * Function used for initializing a minimap marker for a user + * @param userColor The color of the user + * @param position The position of the user + * @param name The name of the user + */ + initializeUserMinimapMarker( + userColor: THREE.Color, + position: THREE.Vector3, + name: string + ) { + const geometry = new THREE.SphereGeometry( + this.calculateDistanceFactor(), + 32 + ); + const material = new THREE.MeshBasicMaterial({ + color: userColor, + }); + const minimapMarker = new THREE.Mesh(geometry, material); + minimapMarker.position.set(position.x, MARKER_HEIGHT, position.z); + minimapMarker.layers.enable(SceneLayers.MinimapMarkers); + minimapMarker.layers.disable(SceneLayers.Default); + minimapMarker.name = name; + this.minimapUserMarkers.set(name, minimapMarker); + this.scene.add(minimapMarker); + } + + /** + * Function used for updating the minimap marker of a user + * @param intersection The intersection of the user + * @param name The name of the user + * @param remoteUser The remote user object + */ + updateUserMinimapMarker( + intersection: THREE.Vector3, + name: string, + remoteUser?: RemoteUser + ) { + if (!intersection) { + return; + } + if (!this.minimapUserMarkers.has(name) && remoteUser) { + this.initializeUserMinimapMarker( + remoteUser.color, + intersection, + remoteUser.userId + ); + return; + } + const position = this.checkBoundingBox(intersection); + const minimapMarker = this.minimapUserMarkers.get(name)!; + minimapMarker.position.set(position.x, MARKER_HEIGHT, position.z); + } + + /** + * Function used for deleting the minimap marker of a user + * @param name The name of the user + */ + deleteUserMinimapMarker(name: string) { + const minimapMarker = this.minimapUserMarkers.get(name); + if (minimapMarker) { + this.scene.remove(minimapMarker); + this.minimapUserMarkers.delete(name); + } + } + + /** + * Checks wether the click event is inside the minimap + * @param event MouseEvent of the click + * @returns true if the click is inside the minimap + */ + isClickInsideMinimap(event: MouseEvent) { + const minimap = this.minimap(); + const minimapHeight = minimap[0]; + const minimapWidth = minimap[1]; + const minimapX = minimap[2]; + const minimapY = window.innerHeight - minimap[3] - minimapHeight; + + const xInBounds = + event.clientX >= minimapX && event.clientX <= minimapX + minimapWidth; + const yInBounds = + event.clientY >= minimapY && event.clientY <= minimapY + minimapHeight; + + return xInBounds && yInBounds; + } + + /** + * Function used for handling a hit on the minimap + * @param userHit The user that was hit + * @returns true if the user was hit + */ + handleHit(userHit: RemoteUser) { + if (!userHit || userHit.camera?.model instanceof THREE.OrthographicCamera) + return; + this.localUser.camera.position.copy(userHit.camera!.model.position); + this.localUser.camera.quaternion.copy(userHit.camera!.model.quaternion); + this.cameraControls.perspectiveCameraControls.target.copy( + this.raycaster.raycastToCameraTarget( + this.localUser.minimapCamera, + this.graph.boundingBox + ) + ); + } + + /** + * Function used to toggle the fullsize minimap + * @param value true if the minimap should be fullsize + */ + toggleFullsizeMinimap(value: boolean) { + this.makeFullsizeMinimap = value; + this.cameraControls.enabled = !value; + this.cameraControls.perspectiveCameraControls.enabled = !value; + if (this.cameraControls.orthographicCameraControls) { + this.cameraControls.orthographicCameraControls.enabled = !value; + } + } + + /** + * Function used for raycasting on the minimap + * @param event MouseEvent of the click + * @param camera The camera used for the raycasting + * @param raycastObjects The objects to raycast + * @returns The objects that were hit + */ + raycastForObjects( + event: MouseEvent, + camera: THREE.Camera, + raycastObjects: THREE.Object3D | THREE.Object3D[] + ) { + const minimap = this.minimap(); + const width = minimap[1]; + const height = minimap[0]; + const left = minimap[2]; + const top = window.innerHeight - minimap[3] - height; + + const x = ((event.x - left) / width) * 2 - 1; + const y = -((event.y - top) / height) * 2 + 1; + + const origin = new THREE.Vector2(x, y); + const possibleObjects = + raycastObjects instanceof THREE.Object3D + ? [raycastObjects] + : raycastObjects; + return this.raycaster.raycasting(origin, camera, possibleObjects); + } + + /** + * Function used for raycasting on the minimap markers + * @param event MouseEvent of the click + * @returns The objects that were hit + */ + raycastForMarkers(event: MouseEvent) { + // Get the bounding rectangle of the minimap + const minimap = this.minimap(); + const width = minimap[1]; + const height = minimap[0]; + const left = minimap[2]; + const top = window.innerHeight - minimap[3] - height; + + // Calculate normalized device coordinates (NDC) based on the minimap + const x = ((event.clientX - left) / width) * 2 - 1; + const y = -((event.clientY - top) / height) * 2 + 1; + + // Create a Vector2 for the raycasting origin + const origin = new THREE.Vector2(x, y); + + return this.raycaster.raycastMinimapMarkers( + this.localUser.minimapCamera, + origin, + Array.from(this.minimapUserMarkers.values()!) + ); + } + + /** + * Function used for updating the minimap size + * @param value The new minimap size + */ + updateMinimapCamera() { + // Call the new function to check and adjust minimap size + const boundingBox = this.graph.boundingBox; + + // Calculate the size of the bounding box + const size = boundingBox.getSize(new THREE.Vector3()); + const boundingBoxWidth = size.x; + const boundingBoxHeight = size.z; + + if (this.makeFullsizeMinimap) { + this.distance = 1; + } else { + this.distance = this.userSettings.applicationSettings.zoom.value; + } + + this.localUser.minimapCamera.left = -boundingBoxWidth / 2 / this.distance; + this.localUser.minimapCamera.right = boundingBoxWidth / 2 / this.distance; + this.localUser.minimapCamera.top = boundingBoxHeight / 2 / this.distance; + this.localUser.minimapCamera.bottom = + -boundingBoxHeight / 2 / this.distance; + + if ( + this.userSettings.applicationSettings.zoom.value != 1 && + !this.makeFullsizeMinimap + ) { + this.localUser.minimapCamera.position.set( + this.userPosition.x, + MINIMAP_HEIGHT, + this.userPosition.z + ); + } else { + const center = new THREE.Vector3(); + boundingBox.getCenter(center); + this.localUser.minimapCamera.position.set( + center.x, + MINIMAP_HEIGHT, + center.z + ); + } + this.localUser.minimapCamera.updateProjectionMatrix(); + } + + calculateDistanceFactor(): number { + return 0.2 / this.settings.applicationSettings.zoom.value; + } + + updateSphereRadius() { + this.minimapUserMarkers.forEach((minimapMarker) => { + const geometry = new THREE.SphereGeometry(this.calculateDistanceFactor()); + minimapMarker.geometry.dispose(); + minimapMarker.geometry = geometry; + }); + } + + /** + * Function used for getting the minimap size and position + * @returns The minimap size, position and border width + */ + minimap() { + const borderWidth = 2; + if (this.makeFullsizeMinimap) { + const minimapSize = 0.9; + + const minimapHeight = + Math.min(window.innerHeight, window.innerWidth) * minimapSize; + const minimapWidth = minimapHeight; + + const minimapX = window.innerWidth / 2 - minimapWidth / 2; + const minimapY = window.innerHeight / 2 - minimapHeight / 2 - 20; + + return [minimapHeight, minimapWidth, minimapX, minimapY, borderWidth]; + } + const minimapHeight = + Math.min(window.innerHeight, window.innerWidth) / this.minimapSize; + const minimapWidth = minimapHeight; + + const marginSettingsSymbol = 55; + const margin = 10; + const minimapX = + window.innerWidth - minimapWidth - margin - marginSettingsSymbol; + const minimapY = window.innerHeight - minimapHeight - margin; + + return [minimapHeight, minimapWidth, minimapX, minimapY, borderWidth]; + } +} +declare module '@ember/service' { + interface Registry { + minimapService: MinimapService; + } +} diff --git a/app/utils/application-rendering/camera-controls.ts b/app/utils/application-rendering/camera-controls.ts index 58dfda9f4..3ea451c10 100644 --- a/app/utils/application-rendering/camera-controls.ts +++ b/app/utils/application-rendering/camera-controls.ts @@ -42,6 +42,9 @@ export default class CameraControls { const center = new Vector3(); box.getSize(size); box.getCenter(center); + const labelerHeightAdjustment = 1.3; + center.y -= labelerHeightAdjustment; + const fitOffset = 1.2; const maxSize = Math.max(size.x, size.y, size.z); diff --git a/app/utils/application-rendering/entity-rendering.ts b/app/utils/application-rendering/entity-rendering.ts index c41194b8a..8ceb66863 100644 --- a/app/utils/application-rendering/entity-rendering.ts +++ b/app/utils/application-rendering/entity-rendering.ts @@ -93,7 +93,6 @@ export function addComponentAndChildrenToScene( color, highlightedEntityColor ); - addMeshToApplication(mesh, applicationObject3D); updateMeshVisiblity(mesh, applicationObject3D); @@ -156,7 +155,6 @@ export function addFoundationAndChildrenToApplication( foundationColor, highlightedEntityColor ); - addMeshToApplication(mesh, applicationObject3D); const children = application.packages; diff --git a/app/utils/application-rendering/labeler.ts b/app/utils/application-rendering/labeler.ts index d246dc53f..13dadea6a 100644 --- a/app/utils/application-rendering/labeler.ts +++ b/app/utils/application-rendering/labeler.ts @@ -8,7 +8,9 @@ import { Font } from 'three/examples/jsm/loaders/FontLoader'; import ApplicationObject3D from 'explorviz-frontend/view-objects/3d/application/application-object-3d'; import gsap from 'gsap'; import { ApplicationColors } from 'explorviz-frontend/services/user-settings'; +import MinimapLabelMesh from '../../view-objects/3d/application/minimap-label-mesh'; import { getStoredSettings } from '../settings/local-storage-settings'; +import { SceneLayers } from 'explorviz-frontend/services/minimap-service'; /** * Positions label of a given component mesh. This function is standalone and not part @@ -84,6 +86,7 @@ export function addApplicationLabels( addBoxTextLabel(mesh, font, componentTextColor); } else if (mesh instanceof FoundationMesh) { addBoxTextLabel(mesh, font, foundationTextColor); + addMinimapTextLabel(mesh, font, foundationTextColor); } } }); @@ -159,6 +162,39 @@ export function addClazzTextLabel( clazzMesh.add(labelMesh); } +/** + * Adds a label to a foundation mesh for the minimap + * @param foundationMesh The mesh which shall be labeled + * @param font The font of the text + * @param color The color of the text + * @param size The size of the text + * @param heigth The height of the text + */ +export function addMinimapTextLabel( + foundationMesh: FoundationMesh, + font: Font, + color: THREE.Color, + size = 0.1, + heigth = 100 +) { + const text = foundationMesh.dataModel.name; + + const minimapLabelMesh = new MinimapLabelMesh(font, text, color, size); + minimapLabelMesh.computeLabel(text, size); + foundationMesh.minimapLabelMesh = minimapLabelMesh; + + minimapLabelMesh.geometry.center(); + minimapLabelMesh.position.y = heigth; + + // Rotate text + minimapLabelMesh.rotation.x = -(Math.PI / 2); + minimapLabelMesh.rotation.z = -(Math.PI / 2); + + minimapLabelMesh.layers.set(SceneLayers.MinimapLabel); + + foundationMesh.add(minimapLabelMesh); +} + export function updateBoxTextLabel( boxMesh: ComponentMesh | FoundationMesh, font: Font, diff --git a/app/utils/controls/OrbitControls.js b/app/utils/controls/OrbitControls.js index 0ade6dcb6..d4e0e0cbe 100644 --- a/app/utils/controls/OrbitControls.js +++ b/app/utils/controls/OrbitControls.js @@ -1,5 +1,4 @@ // Copied for modification from: https://github.com/mrdoob/three.js/blob/dev/examples/jsm/controls/OrbitControls.js - import { EventDispatcher, MOUSE, @@ -234,7 +233,6 @@ class OrbitControls extends EventDispatcher { spherical.makeSafe(); // move target to panned location - if (scope.enableDamping === true) { scope.target.addScaledVector(panOffset, scope.dampingFactor); } else { @@ -746,7 +744,6 @@ class OrbitControls extends EventDispatcher { needsUpdate = true; break; } - if (needsUpdate) { // prevent the browser from scrolling on cursor keys event.preventDefault(); diff --git a/app/utils/raycaster.ts b/app/utils/raycaster.ts index 8e367aa36..9987d8e15 100644 --- a/app/utils/raycaster.ts +++ b/app/utils/raycaster.ts @@ -5,6 +5,9 @@ import LogoMesh from 'explorviz-frontend/view-objects/3d/logo-mesh'; import PingMesh from 'extended-reality/utils/view-objects/vr/ping-mesh'; import * as THREE from 'three'; import ThreeMeshUI from 'three-mesh-ui'; +import MinimapService, { + SceneLayers, +} from 'explorviz-frontend/services/minimap-service'; export function defaultRaycastFilter( intersection: THREE.Intersection @@ -27,6 +30,8 @@ function isChildOfText(intersection: THREE.Intersection) { return isChild; } +const TARGET_Y_VALUE = 0.165; + export default class Raycaster extends THREE.Raycaster { /** * Calculate objects which intersect the ray - given by coordinates and camera @@ -35,6 +40,27 @@ export default class Raycaster extends THREE.Raycaster { * @param camera Camera - contains view information * @param possibleObjects Objects to check for raycasting */ + + minimapService?: MinimapService; + + groundPlane = new THREE.Plane(); + + cam: THREE.Camera | null = null; + + minimapCam: THREE.OrthographicCamera | undefined; + + boundingBox!: THREE.Box3; + + constructor( + minimap?: THREE.OrthographicCamera, + minimapService?: MinimapService + ) { + super(); + this.minimapCam = minimap; + this.minimapService = minimapService; + this.groundPlane.set(new THREE.Vector3(0, TARGET_Y_VALUE, 0), 0); + } + raycasting( coords: { x: number; y: number }, camera: THREE.Camera, @@ -42,7 +68,6 @@ export default class Raycaster extends THREE.Raycaster { raycastFilter?: (object: THREE.Intersection) => boolean ) { this.setFromCamera(new THREE.Vector2(coords.x, coords.y), camera); - // Calculate objects intersecting the picking ray const intersections = this.intersectObjects(possibleObjects, true); @@ -73,4 +98,37 @@ export default class Raycaster extends THREE.Raycaster { // Return null to indicate that no object was found return null; } + + raycastMinimapMarkers( + camera: THREE.OrthographicCamera, + coords: { x: number; y: number }, + userList: THREE.Mesh[] + ) { + this.setFromCamera(new THREE.Vector2(coords.x, coords.y), camera); + userList.forEach((mesh: THREE.Mesh) => { + mesh.layers.enable(SceneLayers.Default); + }); + const intersections = this.intersectObjects(userList, false); + userList.forEach((mesh: THREE.Mesh) => { + mesh.layers.disable(SceneLayers.Default); + }); + if (intersections.length > 0) { + return intersections[0]; + } + return null; + } + + raycastToCameraTarget(camera: THREE.Camera, box: THREE.Box3) { + this.cam = camera; + this.boundingBox = box; + + // Set up the raycaster with the camera's position and the calculated direction + this.setFromCamera(new THREE.Vector2(0, 0), this.cam); + + // Calculate the intersection point with the ground plane + const intersectionPoint = new THREE.Vector3(); + this.ray.intersectPlane(this.groundPlane, intersectionPoint); + + return intersectionPoint; + } } diff --git a/app/utils/scene.ts b/app/utils/scene.ts index c7ff4c1c2..cdedf128d 100644 --- a/app/utils/scene.ts +++ b/app/utils/scene.ts @@ -42,9 +42,13 @@ export function createScene(visualizationMode: VisualizationMode) { export function defaultScene() { const scene = new THREE.Scene(); - scene.add(ambientLight()); - scene.add(spotlight()); - scene.add(directionalLight()); + const defLight = ambientLight(); + scene.add(defLight); + defLight.layers.enableAll(); + //scene.add(spotlight()); + const directLight = directionalLight(); + directLight.layers.enableAll(); + scene.add(directLight); return scene; } diff --git a/app/utils/settings/default-settings.ts b/app/utils/settings/default-settings.ts index 0895a3b24..c65f80822 100644 --- a/app/utils/settings/default-settings.ts +++ b/app/utils/settings/default-settings.ts @@ -348,4 +348,84 @@ export const defaultApplicationSettings: ApplicationSettings = { buttonText: 'Reset', isButtonSetting: true, }, + // Minimap Settings + minimap: { + value: false, + orderNumber: 1, + group: 'Minimap', + displayName: 'Enable Minimap', + description: 'Toggle visibility of the minimap', + isFlagSetting: true, + }, + zoom: { + value: 1, + range: { + min: 1.0, + max: 3.0, + step: 0.1, + }, + orderNumber: 2, + group: 'Minimap', + displayName: 'Zoom of Minimap', + description: 'Set zoom of the minimap', + isRangeSetting: true, + }, + version2: { + value: true, + orderNumber: 3, + group: 'Minimap', + displayName: 'Use Camera Position', + description: + 'If off, calculate minimap position via intersection of camera with ground plane.', + isFlagSetting: true, + }, + layer1: { + value: true, + orderNumber: 4, + group: 'Minimap', + displayName: 'Enable foundation visibility', + description: 'Toggle foundation visibility for the minimap', + isFlagSetting: true, + }, + layer2: { + value: true, + orderNumber: 5, + group: 'Minimap', + displayName: 'Enable component visibility', + description: 'Toggle component visibility for the minimap', + isFlagSetting: true, + }, + layer3: { + value: true, + orderNumber: 6, + group: 'Minimap', + displayName: 'Enable clazz visibility', + description: 'Toggle clazz visibility for the minimap', + isFlagSetting: true, + }, + layer4: { + value: true, + orderNumber: 7, + group: 'Minimap', + displayName: 'Enable communication visibility', + description: 'Toggle communication visibility for the minimap', + isFlagSetting: true, + }, + layer6: { + value: true, + orderNumber: 8, + group: 'Minimap', + displayName: 'Enable labels visibility', + description: 'Toggle labels visibility for the minimap', + isFlagSetting: true, + }, + layer7: { + value: true, + orderNumber: 9, + group: 'Minimap', + displayName: 'Enable visibility of different user-markers', + description: + 'Toggle the different users position markers visibility for the minimap', + isFlagSetting: true, + }, }; diff --git a/app/utils/settings/settings-schemas.ts b/app/utils/settings/settings-schemas.ts index 094574e55..3dd43a3bf 100644 --- a/app/utils/settings/settings-schemas.ts +++ b/app/utils/settings/settings-schemas.ts @@ -1,5 +1,6 @@ export type SettingGroup = | 'Camera' + | 'Minimap' | 'Colors' | 'Controls' | 'Communication' @@ -43,6 +44,17 @@ export type ApplicationCommunicationSettingId = | 'commArrowSize' | 'curvyCommHeight'; +export type ApplicationMinimapSettingId = + | 'minimap' + | 'zoom' + | 'version2' + | 'layer1' + | 'layer2' + | 'layer3' + | 'layer4' + | 'layer6' + | 'layer7'; + export type ApplicationCameraSettingId = | 'cameraNear' | 'cameraFar' @@ -72,6 +84,7 @@ export type ApplicationSettingId = | ApplicationCameraSettingId | ApplicationXRSettingId | ApplicationPopupSettingId + | ApplicationMinimapSettingId | ApplicationAnnotationSettingId; export type ApplicationColorSettings = Record< @@ -126,6 +139,18 @@ export type ApplicationCameraSettings = { cameraFov: RangeSetting; }; +export type ApplicationMinimapSettings = { + minimap: FlagSetting; + zoom: RangeSetting; + version2: FlagSetting; + layer1: FlagSetting; + layer2: FlagSetting; + layer3: FlagSetting; + layer4: FlagSetting; + layer6: FlagSetting; + layer7: FlagSetting; +}; + export type ApplicationXRSettings = Record; export type ApplicationSettings = ApplicationColorSettings & @@ -137,7 +162,8 @@ export type ApplicationSettings = ApplicationColorSettings & ApplicationAnnotationSettings & ApplicationCameraSettings & ApplicationXRSettings & - ApplicationCommunicationSettings; + ApplicationCommunicationSettings & + ApplicationMinimapSettings; export interface Setting { value: T; diff --git a/app/view-objects/3d/application/clazz-communication-mesh.ts b/app/view-objects/3d/application/clazz-communication-mesh.ts index 215122126..31f4c5bc3 100644 --- a/app/view-objects/3d/application/clazz-communication-mesh.ts +++ b/app/view-objects/3d/application/clazz-communication-mesh.ts @@ -4,6 +4,7 @@ import BaseMesh from '../base-mesh'; import CommunicationArrowMesh from './communication-arrow-mesh'; import ClazzCommuMeshDataModel from './utils/clazz-communication-mesh-data-model'; import { VisualizationMode } from 'collaboration/services/local-user'; +import { SceneLayers } from 'explorviz-frontend/services/minimap-service'; export default class ClazzCommunicationMesh extends BaseMesh { dataModel: ClazzCommuMeshDataModel; @@ -32,6 +33,7 @@ export default class ClazzCommunicationMesh extends BaseMesh { this.material.transparent = true; this.castShadow = true; + this.layers.enable(SceneLayers.Communication); } getModelId() { diff --git a/app/view-objects/3d/application/clazz-mesh.ts b/app/view-objects/3d/application/clazz-mesh.ts index 25ebed54c..59286fe3b 100644 --- a/app/view-objects/3d/application/clazz-mesh.ts +++ b/app/view-objects/3d/application/clazz-mesh.ts @@ -4,6 +4,7 @@ import * as THREE from 'three'; import BoxMesh from './box-mesh'; import ClazzLabelMesh from './clazz-label-mesh'; import { VisualizationMode } from 'collaboration/services/local-user'; +import { SceneLayers } from 'explorviz-frontend/services/minimap-service'; export default class ClazzMesh extends BoxMesh { geometry: THREE.BoxGeometry; @@ -31,6 +32,8 @@ export default class ClazzMesh extends BoxMesh { const geometry = new THREE.BoxGeometry(1, 1, 1); this.geometry = geometry; this.dataModel = clazz; + + this.layers.enable(SceneLayers.Clazz); } getModelId() { diff --git a/app/view-objects/3d/application/component-mesh.ts b/app/view-objects/3d/application/component-mesh.ts index 90ab35a89..5afc5998a 100644 --- a/app/view-objects/3d/application/component-mesh.ts +++ b/app/view-objects/3d/application/component-mesh.ts @@ -3,6 +3,7 @@ import BoxLayout from 'explorviz-frontend/view-objects/layout-models/box-layout' import * as THREE from 'three'; import BoxMesh from './box-mesh'; import ComponentLabelMesh from './component-label-mesh'; +import { SceneLayers } from 'explorviz-frontend/services/minimap-service'; export default class ComponentMesh extends BoxMesh { geometry: THREE.BoxGeometry; @@ -32,6 +33,8 @@ export default class ComponentMesh extends BoxMesh { const geometry = new THREE.BoxGeometry(1, 1, 1); this.geometry = geometry; this.dataModel = component; + + this.layers.enable(SceneLayers.Component); } getModelId() { diff --git a/app/view-objects/3d/application/foundation-mesh.ts b/app/view-objects/3d/application/foundation-mesh.ts index 2851f64c2..a96eb4ab4 100644 --- a/app/view-objects/3d/application/foundation-mesh.ts +++ b/app/view-objects/3d/application/foundation-mesh.ts @@ -3,6 +3,8 @@ import BoxLayout from 'explorviz-frontend/view-objects/layout-models/box-layout' import * as THREE from 'three'; import BoxMesh from './box-mesh'; import ComponentLabelMesh from './component-label-mesh'; +import MinimapLabelMesh from './minimap-label-mesh'; +import { SceneLayers } from 'explorviz-frontend/services/minimap-service'; export default class FoundationMesh< TGeometry extends THREE.BufferGeometry = THREE.BufferGeometry, @@ -14,6 +16,7 @@ export default class FoundationMesh< dataModel: Application; labelMesh: ComponentLabelMesh | null = null; + minimapLabelMesh: MinimapLabelMesh | null = null; constructor( layout: BoxLayout, @@ -29,6 +32,8 @@ export default class FoundationMesh< this.geometry = geometry; this.setDefaultMaterial(); this.dataModel = foundation; + + this.layers.enable(SceneLayers.Foundation); } setDefaultMaterial() { diff --git a/app/view-objects/3d/application/minimap-label-mesh.ts b/app/view-objects/3d/application/minimap-label-mesh.ts new file mode 100644 index 000000000..b2b999b9a --- /dev/null +++ b/app/view-objects/3d/application/minimap-label-mesh.ts @@ -0,0 +1,55 @@ +import * as THREE from 'three'; +import LabelMesh from '../label-mesh'; +import { Font } from 'three/examples/jsm/loaders/FontLoader'; +import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js'; + +export default class MinimapLabelMesh extends LabelMesh { + constructor( + font: Font, + labelText: string, + textColor = new THREE.Color('white'), + size: number + ) { + super(font, labelText, textColor); + this.computeLabel(labelText, size); + } + + /** + * Computes the label geometry + * @param labelText The text to display + * @param size The size of the text + */ + computeLabel(labelText: string, size: number) { + // Text should look like it is written on the parent's box (no height required) + const TEXT_HEIGHT = 0.0; + + let displayedLabel = ''; + let changeLabel = true; + + for (let i = 0; i < labelText.length; i++) { + const char = labelText[i]; + if (char == '-' || char == ' ') { + displayedLabel += char + '\n'; + changeLabel = false; + } else { + displayedLabel += char; + } + } + // Prevent overlapping clazz labels by truncating + if (labelText.length > 13 && changeLabel) { + displayedLabel = `${labelText.substring(0, 11)}...`; + } + + this.geometry = new TextGeometry(displayedLabel, { + font: this.font, + size, + height: TEXT_HEIGHT, + curveSegments: 1, + }); + + this.material = new THREE.MeshBasicMaterial({ + color: this.defaultColor, + // depthTest: false, + }); + } +} diff --git a/lib/collaboration/addon/services/collaboration-session.ts b/lib/collaboration/addon/services/collaboration-session.ts index 83b87d8b6..676453d40 100644 --- a/lib/collaboration/addon/services/collaboration-session.ts +++ b/lib/collaboration/addon/services/collaboration-session.ts @@ -35,6 +35,7 @@ import { } from 'collaboration/utils/web-socket-messages/types/controller-id'; import { ForwardedMessage } from 'collaboration/utils/web-socket-messages/receivable/forwarded'; import LandscapeTokenService from 'explorviz-frontend/services/landscape-token'; +import MinimapService from 'explorviz-frontend/services/minimap-service'; import { USER_KICK_EVENT, UserKickEvent, @@ -81,6 +82,9 @@ export default class CollaborationSession extends Service.extend({ @service('landscape-token') tokenService!: LandscapeTokenService; + @service('minimap-service') + minimapService!: MinimapService; + @service('chat') chatService!: ChatService; @@ -465,10 +469,16 @@ export default class CollaborationSession extends Service.extend({ }: ForwardedMessage): void { const remoteUser = this.lookupRemoteUserById(userId); if (!remoteUser) return; - if (controller1) remoteUser.updateController(CONTROLLER_1_ID, controller1); if (controller2) remoteUser.updateController(CONTROLLER_2_ID, controller2); - if (camera) remoteUser.updateCamera(camera); + if (camera) { + remoteUser.updateCamera(camera); + this.minimapService.updateUserMinimapMarker( + remoteUser.camera!.model.position, + userId, + remoteUser + ); + } } } diff --git a/lib/collaboration/addon/services/local-user.ts b/lib/collaboration/addon/services/local-user.ts index 3796b0832..0793b3ec8 100644 --- a/lib/collaboration/addon/services/local-user.ts +++ b/lib/collaboration/addon/services/local-user.ts @@ -55,6 +55,9 @@ export default class LocalUser extends Service.extend({ @tracked defaultCamera!: THREE.PerspectiveCamera; + @tracked + minimapCamera!: THREE.OrthographicCamera; + @tracked visualizationMode: VisualizationMode = 'browser'; @@ -87,6 +90,7 @@ export default class LocalUser extends Service.extend({ // Initialize camera. The default aspect ratio is not known at this point // and must be updated when the canvas is inserted. this.defaultCamera = new THREE.PerspectiveCamera(); + this.minimapCamera = new THREE.OrthographicCamera(); // this.defaultCamera.position.set(0, 1, 2); if (this.xr?.isPresenting) { return this.xr.getCamera(); @@ -108,10 +112,7 @@ export default class LocalUser extends Service.extend({ tick(delta: number) { this.animationMixer.update(delta); - - if (this.visualizationMode === 'vr') { - this.sendPositions(); - } + this.sendPositions(); } sendPositions() { @@ -403,7 +404,6 @@ export default class LocalUser extends Service.extend({ controller.removeTeleportArea(); } } - // DO NOT DELETE: this is how TypeScript knows how to look up your services. declare module '@ember/service' { interface Registry { diff --git a/lib/collaboration/addon/services/spectate-user.ts b/lib/collaboration/addon/services/spectate-user.ts index cdc5cffdd..1ecd04228 100644 --- a/lib/collaboration/addon/services/spectate-user.ts +++ b/lib/collaboration/addon/services/spectate-user.ts @@ -278,7 +278,6 @@ export default class SpectateUser extends Service { if (!configuration || !configuration.devices) { return; } - // Adapt projection matrix according to spectate update const deviceId = new URLSearchParams(window.location.search).get( 'deviceId' diff --git a/lib/collaboration/addon/services/user-factory.ts b/lib/collaboration/addon/services/user-factory.ts index beddff332..76d7fbf68 100644 --- a/lib/collaboration/addon/services/user-factory.ts +++ b/lib/collaboration/addon/services/user-factory.ts @@ -6,6 +6,7 @@ import LocalUser from './local-user'; import { Color } from 'collaboration/utils/web-socket-messages/types/color'; import { Position } from 'collaboration/utils/web-socket-messages/types/position'; import { Quaternion } from 'collaboration/utils/web-socket-messages/types/quaternion'; +import MinimapService from 'explorviz-frontend/services/minimap-service'; export default class UserFactory extends Service.extend({}) { @service('hmd-service') @@ -14,6 +15,9 @@ export default class UserFactory extends Service.extend({}) { @service('local-user') localUser!: LocalUser; + @service('minimap-service') + minimapService!: MinimapService; + createUser({ userName, userId, @@ -33,6 +37,7 @@ export default class UserFactory extends Service.extend({}) { color: new THREE.Color(color.red, color.green, color.blue), state: 'online', localUser: this.localUser, + minimapService: this.minimapService, }); this.hmdService.headsetModel.then((hmd) => remoteUser.initCamera(hmd.clone(true), { position, quaternion }) diff --git a/lib/collaboration/addon/utils/mouse-ping-helper.ts b/lib/collaboration/addon/utils/mouse-ping-helper.ts index fc5df04f4..5ebd7d988 100644 --- a/lib/collaboration/addon/utils/mouse-ping-helper.ts +++ b/lib/collaboration/addon/utils/mouse-ping-helper.ts @@ -2,6 +2,7 @@ import { timeout, task } from 'ember-concurrency'; import * as THREE from 'three'; import { AnimationMixer } from 'three'; import PingMesh from 'extended-reality/utils/view-objects/vr/ping-mesh'; +import { SceneLayers } from 'explorviz-frontend/services/minimap-service'; export default class MousePing { mesh: PingMesh; @@ -16,6 +17,7 @@ export default class MousePing { animationMixer, color, }); + this.mesh.layers.enable(SceneLayers.Ping); } ping = task( diff --git a/lib/collaboration/addon/utils/remote-user.ts b/lib/collaboration/addon/utils/remote-user.ts index 1ec2c95fd..5ceb362e3 100644 --- a/lib/collaboration/addon/utils/remote-user.ts +++ b/lib/collaboration/addon/utils/remote-user.ts @@ -15,6 +15,7 @@ import { CONTROLLER_2_ID, ControllerId, } from './web-socket-messages/types/controller-id'; +import MinimapService from 'explorviz-frontend/services/minimap-service'; type Camera = { model: THREE.Object3D; @@ -29,6 +30,8 @@ type Controller = { }; export default class RemoteUser extends THREE.Object3D { + minimapService: MinimapService; + userName: string; userId: string; @@ -47,7 +50,7 @@ export default class RemoteUser extends THREE.Object3D { nameTag: NameTagSprite | null; - private localUser: LocalUser; + localUser: LocalUser; constructor({ userName, @@ -55,12 +58,14 @@ export default class RemoteUser extends THREE.Object3D { color, state, localUser, + minimapService, }: { userName: string; userId: string; color: THREE.Color; state: string; localUser: LocalUser; + minimapService: MinimapService; }) { super(); this.userName = userName; @@ -76,12 +81,18 @@ export default class RemoteUser extends THREE.Object3D { this.nameTag = null; this.localUser = localUser; + this.minimapService = minimapService; + this.minimapService.initializeUserMinimapMarker( + this.color, + new THREE.Vector3(0, 0, 0), + this.userId + ); } /** * Updates the camera model's position and rotation. * - * @param Object containing the new camera position and quaterion. + * @param Object containing the new camera position and quaternion. */ updateCamera(pose: Pose) { if (this.camera) { @@ -170,6 +181,20 @@ export default class RemoteUser extends THREE.Object3D { this.removeController(CONTROLLER_2_ID); this.removeCamera(); this.removeNameTag(); + this.removeMinimapMarker(); // Remove minimap marker + } + + /** + * Remove the minimap marker from the scene. + */ + removeMinimapMarker() { + const minimapMarker = this.minimapService.minimapUserMarkers.get( + this.userId + ); + if (minimapMarker) { + minimapMarker.parent?.remove(minimapMarker); + this.minimapService.deleteUserMinimapMarker(this.userId); + } } togglePing(controllerId: ControllerId, isPinging: boolean) { @@ -184,12 +209,11 @@ export default class RemoteUser extends THREE.Object3D { } getVisualizationMode(): string { - // TODO: refactoring! this method won't return the remote user's localUser instance! It is always our own return this.localUser.visualizationMode; } /** - * Updates the the remote user once per frame. + * Updates the remote user once per frame. * * @param delta The time since the last update. */