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_ENTITIES 1
Include domain entities when intersecting.
* PICK_AVATAR_ENTITIES 2
Include avatar entities when intersecting.
* PICK_LOCAL_ENTITIES 4
Include local entities when intersecting.
- * PICK_AVATATRS 8
Include avatars when intersecting.
+ * PICK_AVATARS 8
Include avatars when intersecting.
* PICK_HUD 16
Include the HUD surface when intersecting in HMD mode.
* PICK_INCLUDE_VISIBLE 32
Include visible objects when intersecting.
* PICK_INCLUDE_INVISIBLE 64
Include 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 = "Visit ";
+ document.getElementById("placeDetail-rezPortalBtn-container").innerHTML = "Portal ";
+ document.getElementById("placeDetail-copyPlaceURLBtn-container").innerHTML = "Copy URL ";
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