diff --git a/libraries/entities/src/EntityTypes.h b/libraries/entities/src/EntityTypes.h index ab3233e639c..2b14e417dfb 100644 --- a/libraries/entities/src/EntityTypes.h +++ b/libraries/entities/src/EntityTypes.h @@ -2,9 +2,9 @@ // EntityTypes.h // libraries/entities/src // -// Created by Brad Hefta-Gaub on 12/4/13. +// Created by Brad Hefta-Gaub on December 4th, 2013. // Copyright 2013 High Fidelity, Inc. -// Copyright 2023 Overte e.V. +// Copyright 2023-2025 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -91,7 +91,7 @@ class EntityTypes { * "Material"Modifies the existing materials on entities and avatars. * {@link Entities.EntityProperties-Material|EntityProperties-Material} * "Sound"Plays a sound. - * {@link Entities.EntityProperties-Material|EntityProperties-Sound} + * {@link Entities.EntityProperties-Sound|EntityProperties-Sound} * * * @typedef {string} Entities.EntityType diff --git a/libraries/shared/src/PickFilter.h b/libraries/shared/src/PickFilter.h index 1cc1a8b0b58..acf0c70eaba 100644 --- a/libraries/shared/src/PickFilter.h +++ b/libraries/shared/src/PickFilter.h @@ -1,6 +1,7 @@ // -// Created by Sam Gondelman on 12/7/18. +// Created by Sam Gondelman on December 7th, 2018. // Copyright 2018 High Fidelity, Inc. +// Copyright 2025 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -26,7 +27,7 @@ class PickFilter { * PICK_DOMAIN_ENTITIES1Include domain entities when intersecting. * PICK_AVATAR_ENTITIES2Include avatar entities when intersecting. * PICK_LOCAL_ENTITIES4Include local entities when intersecting. - * PICK_AVATATRS8Include avatars when intersecting. + * PICK_AVATARS8Include avatars when intersecting. * PICK_HUD16Include the HUD surface when intersecting in HMD mode. * PICK_INCLUDE_VISIBLE32Include visible objects when intersecting. * PICK_INCLUDE_INVISIBLE64Include invisible objects when intersecting. diff --git a/scripts/system/places/icons/portalFX.png b/scripts/system/places/icons/portalFX.png new file mode 100644 index 00000000000..6c781c824be Binary files /dev/null and b/scripts/system/places/icons/portalFX.png differ diff --git a/scripts/system/places/places.css b/scripts/system/places/places.css index 37eac2d0029..684139a5b84 100644 --- a/scripts/system/places/places.css +++ b/scripts/system/places/places.css @@ -3,7 +3,7 @@ // places.css // // Created by Alezia Kurdis, January 1st, 2022. -// Copyright 2022 Overte e.V. +// Copyright 2022-2025 Overte e.V. // // css for the ui of the Places application. // @@ -750,19 +750,20 @@ font.domain-nbrUser_small { color: #cccccc; padding: 10px; text-align: justify; - text-justify: inter-word; + text-justify: inter-word; } #placeDetail-visitBtn { background: #0000ff; background-image: linear-gradient(to bottom, #0000ff, #000020); border: 0px; - border-radius: 10px; - font-weight: 800; + border-radius: 6px; + font-weight: 700; color: #ffffff; - font-size: 20px; - padding: 3px 22px 3px 22px; + font-size: 14px; + padding: 2px 22px 2px 22px; text-decoration: none; + width: 90%; } #placeDetail-visitBtn:hover { @@ -774,7 +775,57 @@ font.domain-nbrUser_small { #placeDetail-visitBtn-container { width: 100%; text-align: left; - margin-bottom: 40px; + margin-bottom: 8px; +} + +#placeDetail-rezPortalBtn { + background: #0000ff; + background-image: linear-gradient(to bottom, #0000ff, #000020); + border: 0px; + border-radius: 6px; + font-weight: 700; + color: #ffffff; + font-size: 14px; + padding: 2px 22px 2px 22px; + text-decoration: none; + width: 90%; +} + +#placeDetail-rezPortalBtn:hover { + background: #057eff; + background-image: linear-gradient(to bottom, #057eff, #00090f); + text-decoration: none; +} + +#placeDetail-rezPortalBtn-container { + width: 100%; + text-align: left; + margin-bottom: 8px; +} + +#placeDetail-copyPlaceURLBtn { + background: #0000ff; + background-image: linear-gradient(to bottom, #0000ff, #000020); + border: 0px; + border-radius: 6px; + font-weight: 700; + color: #ffffff; + font-size: 14px; + padding: 2px 22px 2px 22px; + text-decoration: none; + width: 90%; +} + +#placeDetail-copyPlaceURLBtn:hover { + background: #057eff; + background-image: linear-gradient(to bottom, #057eff, #00090f); + text-decoration: none; +} + +#placeDetail-copyPlaceURLBtn-container { + width: 100%; + text-align: left; + margin-bottom: 8px; } #placeDetail-placedata { @@ -804,7 +855,7 @@ font.domain-nbrUser_small { #placeDetail-users { font-size: 30px; - font-weight: 600; + font-weight: 600; } #placeDetail-capacity { diff --git a/scripts/system/places/places.html b/scripts/system/places/places.html index fda67f4066a..2df28f40734 100644 --- a/scripts/system/places/places.html +++ b/scripts/system/places/places.html @@ -4,7 +4,7 @@ // places.html // // Created by Alezia Kurdis, January 1st, 2022. -// Copyright 2022 Overte e.V. +// Copyright 2022-2025 Overte e.V. // // html for the ui of the Places application. // @@ -118,6 +118,8 @@
+
+
DOMAIN: @@ -502,6 +504,28 @@ } + function rezPortal(name, address, placeID) { + var portalOrder = { + "channel": channel, + "action": "REQUEST_PORTAL", + "name": name, + "address": address, + "placeID": placeID + }; + EventBridge.emitWebEvent(JSON.stringify(portalOrder)); + + } + + function copyPlaceURL(address) { + var portalOrder = { + "channel": channel, + "action": "COPY_URL", + "address": address + }; + EventBridge.emitWebEvent(JSON.stringify(portalOrder)); + + } + function goHome() { var message = { "channel": channel, @@ -766,6 +790,8 @@ placeUrl = "hifi://" + placeDetail.address; } document.getElementById("placeDetail-visitBtn-container").innerHTML = ""; + document.getElementById("placeDetail-rezPortalBtn-container").innerHTML = ""; + document.getElementById("placeDetail-copyPlaceURLBtn-container").innerHTML = ""; document.getElementById("placeDetail-maturity").innerHTML = placeDetail.maturity.toUpperCase(); document.getElementById("placeDetail-maturity").className = placeDetail.maturity + "FilterOn placeMaturity"; document.getElementById("placeDetail-domain").innerHTML = placeDetail.domain.toUpperCase(); diff --git a/scripts/system/places/places.js b/scripts/system/places/places.js index fa22d536b7f..5aa8d282b17 100644 --- a/scripts/system/places/places.js +++ b/scripts/system/places/places.js @@ -3,7 +3,7 @@ // places.js // // Created by Alezia Kurdis, January 1st, 2022. -// Copyright 2022-2023 Overte e.V. +// Copyright 2022-2025 Overte e.V. // // Generate an explore app based on the differents source of placename data. // @@ -36,6 +36,12 @@ var APP_ICON_ACTIVE = ROOT + "icons/appicon_a.png"; var appStatus = false; var channel = "com.overte.places"; + + var portalChannelName = "com.overte.places.portalRezzer"; + var MAX_DISTANCE_TO_CONSIDER_PORTAL = 100.0; //in meters + var PORTAL_DURATION_MILLISEC = 45000; //45 sec + var rezzerPortalCount = 0; + var MAX_REZZED_PORTAL = 15; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); @@ -92,6 +98,24 @@ Window.location = messageObj.address; } + } else if (messageObj.action === "REQUEST_PORTAL" && (n - timestamp) > INTERCALL_DELAY) { + d = new Date(); + timestamp = d.getTime(); + var portalPosition = Vec3.sum(MyAvatar.feetPosition, Vec3.multiplyQbyV(MyAvatar.orientation, {"x": 0.0, "y": 0.0, "z": -2.0})); + var requestToSend = { + "action": "REZ_PORTAL", + "position": portalPosition, + "url": messageObj.address, + "name": messageObj.name, + "placeID": messageObj.placeID + }; + Messages.sendMessage(portalChannelName, JSON.stringify(requestToSend), false); + + } else if (messageObj.action === "COPY_URL" && (n - timestamp) > INTERCALL_DELAY) { + d = new Date(); + timestamp = d.getTime(); + Window.copyToClipboard(messageObj.address); + Window.displayAnnouncement("Place URL copied."); } else if (messageObj.action === "GO_HOME" && (n - timestamp) > INTERCALL_DELAY) { d = new Date(); timestamp = d.getTime(); @@ -284,8 +308,8 @@ region = "local"; order = "A"; fetch = true; - pinned = false; - currentFound = true; + pinned = false; + currentFound = true; } else { region = "federation"; order = "F"; @@ -555,6 +579,57 @@ } //####### END of seed random library ################ + function onMessageReceived(paramChannel, paramMessage, paramSender, paramLocalOnly) { + if (paramChannel === portalChannelName) { + var instruction = JSON.parse(paramMessage); + if (instruction.action === "REZ_PORTAL") { + generatePortal(instruction.position, instruction.url, instruction.name, instruction.placeID); + } + } + } + + function generatePortal(position, url, name, placeID) { + if (rezzerPortalCount <= MAX_REZZED_PORTAL) { + var TOLERANCE_FACTOR = 1.1; + if (Vec3.distance(MyAvatar.position, position) < MAX_DISTANCE_TO_CONSIDER_PORTAL) { + var height = MyAvatar.userHeight * MyAvatar.scale * TOLERANCE_FACTOR; + + var portalPosition = Vec3.sum(position, {"x": 0.0, "y": height/2, "z": 0.0}); + var dimensions = {"x": height * 0.618, "y": height, "z": height * 0.618}; + var userdata = { + "url": url, + "name": name, + "placeID": placeID + }; + + var portalID = Entities.addEntity({ + "position": portalPosition, + "dimensions": dimensions, + "type": "Shape", + "shape": "Sphere", + "name": "Portal to " + name, + "canCastShadow": false, + "collisionless": true, + "userData": JSON.stringify(userdata), + "script": ROOT + "portal.js", + "visible": "false", + "grab": { + "grabbable": false + } + }, "local"); + rezzerPortalCount = rezzerPortalCount + 1; + + Script.setTimeout(function () { + Entities.deleteEntity(portalID); + rezzerPortalCount = rezzerPortalCount - 1; + if (rezzerPortalCount < 0) { + rezzerPortalCount = 0; + } + }, PORTAL_DURATION_MILLISEC); + } + } + } + function cleanup() { if (appStatus) { @@ -562,9 +637,15 @@ tablet.webEventReceived.disconnect(onAppWebEventReceived); } + Messages.messageReceived.disconnect(onMessageReceived); + Messages.unsubscribe(portalChannelName); + tablet.screenChanged.disconnect(onScreenChanged); tablet.removeButton(button); } + Messages.subscribe(portalChannelName); + Messages.messageReceived.connect(onMessageReceived); + Script.scriptEnding.connect(cleanup); }()); diff --git a/scripts/system/places/portal.js b/scripts/system/places/portal.js new file mode 100644 index 00000000000..c77fbc648dd --- /dev/null +++ b/scripts/system/places/portal.js @@ -0,0 +1,201 @@ +// +// portal.js +// +// Created by Alezia Kurdis, January 14th, 2025. +// Copyright 2025, Overte e.V. +// +// 3D portal for Places app. portal spawner. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +(function(){ + + var ROOT = Script.resolvePath('').split("portal.js")[0]; + var portalURL = ""; + var portalName = ""; + var TP_SOUND = SoundCache.getSound(ROOT + "sounds/teleportSound.mp3"); + + this.preload = function(entityID) { + + var properties = Entities.getEntityProperties(entityID, ["userData", "dimensions"]); + var userDataObj = JSON.parse(properties.userData); + portalURL = userDataObj.url; + portalName = userDataObj.name; + var portalColor = getColorFromPlaceID(userDataObj.placeID); + + var textLocalPosition = {"x": 0.0, "y": (properties.dimensions.y / 2) * 1.2, "z": 0.0}; + var scale = textLocalPosition.y/1.2; + var textID = Entities.addEntity({ + "type": "Text", + "parentID": entityID, + "localPosition": textLocalPosition, + "dimensions": { + "x": 1 * scale, + "y": 0.15 * scale, + "z": 0.01 + }, + "name": portalName, + "text": portalName, + "textColor": portalColor.light, + "lineHeight": 0.10 * scale, + "backgroundAlpha": 0.0, + "unlit": true, + "alignment": "center", + "verticalAlignment": "center", + "canCastShadow": false, + "billboardMode": "yaw", + "grab": { + "grabbable": false + } + },"local"); + + var fxID = Entities.addEntity({ + "type": "ParticleEffect", + "parentID": entityID, + "localPosition": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "name": "PORTAL_FX", + "dimensions": { + "x": 5.2 * scale, + "y": 5.2 * scale, + "z": 5.2 * scale + }, + "grab": { + "grabbable": false + }, + "shapeType": "ellipsoid", + "color": portalColor.light, + "alpha": 0.1, + "textures": ROOT + "icons/portalFX.png", + "maxParticles": 600, + "lifespan": 0.6, + "emitRate": 1000, + "emitSpeed": -1 * scale, + "speedSpread": 0 * scale, + "emitOrientation": { + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "emitDimensions": { + "x": 1.28 * scale, + "y": 2 * scale, + "z": 1.28 * scale + }, + "polarFinish": 3.1415927410125732, + "emitAcceleration": { + "x": 0, + "y": 0, + "z": 0 + }, + "particleRadius": 0.4000000059604645 * scale, + "radiusSpread": 0.30000001192092896 * scale, + "radiusStart": 1 * scale, + "radiusFinish": 0 * scale, + "colorStart": portalColor.saturated, + "colorFinish": { + "red": 255, + "green": 255, + "blue": 255 + }, + "alphaSpread": 0.019999999552965164, + "alphaStart": 0, + "alphaFinish": 0.20000000298023224, + "emitterShouldTrail": true, + "particleSpin": 1.5700000524520874, + "spinSpread": 2.9700000286102295, + "spinStart": 0, + "spinFinish": 0 + },"local"); + + var loopSoundID = Entities.addEntity({ + "type": "Sound", + "parentID": entityID, + "localPosition": {"x": 0.0, "y": 0.0, "z": 0.0}, + "name": "PORTAL SOUND", + "soundURL": ROOT + "sounds/portalSound.mp3", + "volume": 0.15, + "loop": true, + "positional": true, + "localOnly": true + },"local"); + + } + + this.enterEntity = function(entityID) { + var injectorOptions = { + "position": MyAvatar.position, + "volume": 0.3, + "loop": false, + "localOnly": true + }; + var injector = Audio.playSound(TP_SOUND, injectorOptions); + + var timer = Script.setTimeout(function () { + Window.location = portalURL; + Entities.deleteEntity(entityID); + }, 1000); + + }; + + function getColorFromPlaceID(placeID) { + var idIntegerConstant = getStringScore(placeID); + var hue = (idIntegerConstant%360)/360; + var color = hslToRgb(hue, 1, 0.5); + var colorLight = hslToRgb(hue, 1, 0.75); + return { + "saturated": {"red": color[0], "green": color[1], "blue": color[2]}, + "light": {"red": colorLight[0], "green": colorLight[1], "blue": colorLight[2]}, + }; + } + + function getStringScore(str) { + var score = 0; + for (var j = 0; j < str.length; j++){ + score += str.charCodeAt(j); + } + return score; + } + + /* + * Converts an HSL color value to RGB. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSL_color_space. + * Assumes h, s, and l are contained in the set [0, 1] and + * returns r, g, and b in the set [0, 255]. + * + * @param {number} h The hue + * @param {number} s The saturation + * @param {number} l The lightness + * @return {Array} The RGB representation + */ + function hslToRgb(h, s, l){ + var r, g, b; + + if(s == 0){ + r = g = b = l; // achromatic + }else{ + var hue2rgb = function hue2rgb(p, q, t){ + if(t < 0) t += 1; + if(t > 1) t -= 1; + if(t < 1/6) return p + (q - p) * 6 * t; + if(t < 1/2) return q; + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + } + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; + } + +}) diff --git a/scripts/system/places/sounds/portalSound.mp3 b/scripts/system/places/sounds/portalSound.mp3 new file mode 100644 index 00000000000..5e7f5a9bd0f Binary files /dev/null and b/scripts/system/places/sounds/portalSound.mp3 differ diff --git a/scripts/system/places/sounds/teleportSound.mp3 b/scripts/system/places/sounds/teleportSound.mp3 new file mode 100644 index 00000000000..e5000e55b57 Binary files /dev/null and b/scripts/system/places/sounds/teleportSound.mp3 differ