diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js
index 5d3e924ccf..1d7f4fc05e 100644
--- a/scripts/system/create/edit.js
+++ b/scripts/system/create/edit.js
@@ -121,6 +121,16 @@
var copiedPosition;
var copiedRotation;
+ var importUiPersistedData = {
+ "elJsonUrl": "",
+ "elImportAtAvatar": true,
+ "elImportAtSpecificPosition": false,
+ "elPositionX": 0,
+ "elPositionY": 0,
+ "elPositionZ": 0,
+ "elEntityHostTypeDomain": true,
+ "elEntityHostTypeAvatar": false
+ };
var cameraManager = new CameraManager();
@@ -2009,7 +2019,8 @@
return position;
}
- function importSVO(importURL) {
+ function importSVO(importURL, importEntityHostType) {
+ importEntityHostType = importEntityHostType || "domain";
if (!Entities.canRez() && !Entities.canRezTmp()) {
Window.notifyEditError(INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG);
return;
@@ -2032,7 +2043,7 @@
position = createApp.getPositionToCreateEntity(Clipboard.getClipboardContentsLargestDimension() / 2);
}
if (position !== null && position !== undefined) {
- var pastedEntityIDs = Clipboard.pasteEntities(position);
+ var pastedEntityIDs = Clipboard.pasteEntities(position, importEntityHostType);
if (!isLargeImport) {
// The first entity in Clipboard gets the specified position with the rest being relative to it. Therefore, move
// entities after they're imported so that they're all the correct distance in front of and with geometric mean
@@ -2792,6 +2803,72 @@
type: 'zoneListRequest',
zones: getExistingZoneList()
});
+ } else if (data.type === "importUiBrowse") {
+ let fileToImport = Window.browse("Select .json to Import", "", "*.json");
+ if (fileToImport !== null) {
+ emitScriptEvent({
+ type: 'importUi_SELECTED_FILE',
+ file: fileToImport
+ });
+ } else {
+ audioFeedback.rejection();
+ }
+ } else if (data.type === "importUiImport") {
+ if ((data.entityHostType === "domain" && Entities.canAdjustLocks() && Entities.canRez()) ||
+ (data.entityHostType === "avatar" && Entities.canRezAvatarEntities())) {
+ if (data.positioningMode === "avatar") {
+ importSVO(data.jsonURL, data.entityHostType);
+ } else {
+ if (Clipboard.importEntities(data.jsonURL)) {
+ let importedPastedEntities = Clipboard.pasteEntities(data.position, data.entityHostType);
+ if (importedPastedEntities.length === 0) {
+ emitScriptEvent({
+ type: 'importUi_IMPORT_ERROR',
+ reason: "No Entity has been imported."
+ });
+ } else {
+ if (isActive) {
+ selectionManager.setSelections(importedPastedEntities, this);
+ }
+ emitScriptEvent({type: 'importUi_IMPORT_CONFIRMATION'});
+ }
+ } else {
+ emitScriptEvent({
+ type: 'importUi_IMPORT_ERROR',
+ reason: "Import Entities has failed."
+ });
+ }
+ }
+ } else {
+ emitScriptEvent({
+ type: 'importUi_IMPORT_ERROR',
+ reason: "You don't have permission to create in this domain."
+ });
+ }
+ } else if (data.type === "importUiGoBack") {
+ if (location.canGoBack()) {
+ location.goBack();
+ } else {
+ audioFeedback.rejection();
+ }
+ } else if (data.type === "importUiGoTutorial") {
+ Window.location = "file:///~/serverless/tutorial.json";
+ } else if (data.type === "importUiGetCopiedPosition") {
+ if (copiedPosition !== undefined) {
+ emitScriptEvent({
+ type: 'importUi_POSITION_TO_PASTE',
+ position: copiedPosition
+ });
+ } else {
+ audioFeedback.rejection();
+ }
+ } else if (data.type === "importUiPersistData") {
+ importUiPersistedData = data.importUiPersistedData;
+ } else if (data.type === "importUiGetPersistData") {
+ emitScriptEvent({
+ type: 'importUi_LOAD_DATA',
+ importUiPersistedData: importUiPersistedData
+ });
}
};
diff --git a/scripts/system/create/importEntities/html/css/importEntities.css b/scripts/system/create/importEntities/html/css/importEntities.css
new file mode 100644
index 0000000000..61c75dabb3
--- /dev/null
+++ b/scripts/system/create/importEntities/html/css/importEntities.css
@@ -0,0 +1,160 @@
+/*
+// importEntities.css
+//
+// Created by Alezia Kurdis on March 13th, 2024
+// Copyright 2024 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
+*/
+
+@font-face {
+ font-family: FiraSans-SemiBold;
+ src: url(../../../../../../resources/fonts/FiraSans-SemiBold.ttf), /* Windows production */
+ url(../../../../../../fonts/FiraSans-SemiBold.ttf); /* OSX production */
+}
+
+@font-face {
+ font-family: FiraSans-Regular;
+ src: url(../../../../../../resources/fonts/FiraSans-Regular.ttf), /* Windows production */
+ url(../../../../../../fonts/FiraSans-Regular.ttf); /* OSX production */
+}
+
+@font-face {
+ font-family: Raleway-Bold;
+ src: url(../../../../../../resources/fonts/Raleway-Bold.ttf), /* Windows production */
+ url(../../../../../../fonts/Raleway-Bold.ttf); /* OSX production */
+}
+
+html {
+ width: 100%;
+ height: 100%;
+}
+input[type="text"] {
+ font-family: FiraSans-SemiBold;
+ color: #BBBBBB;
+ background-color: #222222;
+ border: 0;
+ padding: 4px;
+ margin: 1px;
+}
+
+input[type="number"] {
+ font-family: FiraSans-SemiBold;
+ color: #BBBBBB;
+ background-color: #222222;
+ border: 0;
+ padding: 4px;
+ margin: 1px;
+ width: 90px;
+}
+
+h2 {
+ font-size: 18px;
+ color: #FFFFFF;
+}
+body {
+ background: #404040;
+ font-family: FiraSans-Regular;
+ font-size: 14px;
+ color: #BBBBBB;
+ text-decoration: none;
+ font-style: normal;
+ font-variant: normal;
+ text-transform: none;
+}
+
+#importAtSpecificPositionContainer {
+ display: none;
+ width: 100%;
+}
+
+#jsonUrl {
+ width:90%;
+}
+#browseBtn {
+ font-family: FiraSans-SemiBold;
+}
+#browseBtn:hover {
+
+}
+
+label {
+ font-family: FiraSans-SemiBold;
+ color: #DDDDDD;
+}
+font.red {
+ font-family: FiraSans-SemiBold;
+ color: #e83333;
+}
+font.green {
+ font-family: FiraSans-SemiBold;
+ color: #0db518;
+}
+font.blue {
+ font-family: FiraSans-SemiBold;
+ color: #447ef2;
+}
+#importBtn {
+ color: #ffffff;
+ background-color: #1080b8;
+ background: linear-gradient(#00b4ef 20%, #1080b8 100%);
+ font-family: Raleway-Bold;
+ font-size: 13px;
+ text-transform: uppercase;
+ vertical-align: top;
+ height: 28px;
+ min-width: 70px;
+ padding: 0 18px;
+ margin: 3px 3px 12px 3px;
+ border-radius: 5px;
+ border: 0;
+ cursor: pointer;
+}
+#importBtn:hover {
+ background: linear-gradient(#00b4ef, #00b4ef);
+ border: none;
+}
+input:focus {
+ outline: none;
+ color: #FFFFFF;
+}
+button:focus {
+ outline: none;
+}
+div.explicative {
+ width: 96%;
+ padding: 7px;
+ font-family: FiraSans-SemiBold;
+ font-size: 12px;
+ text-decoration: none;
+ color: #BBBBBB;
+}
+button.black {
+ font-family: Raleway-Bold;
+ font-size: 10px;
+ text-transform: uppercase;
+ vertical-align: top;
+ height: 18px;
+ min-width: 60px;
+ padding: 0 14px;
+ margin: 5px;
+ border-radius: 4px;
+ border: none;
+ color: #fff;
+ background-color: #000;
+ background: linear-gradient(#343434 20%, #000 100%);
+ cursor: pointer;
+}
+button.black:hover {
+ background: linear-gradient(#000, #000);
+ border: none;
+}
+#messageContainer {
+ font-family: FiraSans-SemiBold;
+ width: 100%;
+}
+#testContainer {
+ border: 1px solid #AAAAAA;
+ padding: 0px;
+}
diff --git a/scripts/system/create/importEntities/html/importEntities.html b/scripts/system/create/importEntities/html/importEntities.html
new file mode 100644
index 0000000000..a1550a642e
--- /dev/null
+++ b/scripts/system/create/importEntities/html/importEntities.html
@@ -0,0 +1,77 @@
+
+
+
+ Import Entities
+
+
+
+
+
+
+ Import Entities (.json)
+ * URL/File (.json):
+
+
+
+
+
+
+
+
+
+
+
+ For large import, it can be wise to test it in a serverless environment before doing it in your real domain.
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
diff --git a/scripts/system/create/importEntities/html/js/importEntitiesUi.js b/scripts/system/create/importEntities/html/js/importEntitiesUi.js
new file mode 100644
index 0000000000..6e80c7f173
--- /dev/null
+++ b/scripts/system/create/importEntities/html/js/importEntitiesUi.js
@@ -0,0 +1,217 @@
+// importEntitiesUi.js
+//
+// Created by Alezia Kurdis on March 13th, 2024
+// Copyright 2024 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
+
+let elJsonUrl;
+let elBrowseBtn;
+let elImportAtAvatar;
+let elImportAtSpecificPosition;
+let elImportAtSpecificPositionContainer;
+let elPositionX;
+let elPositionY;
+let elPositionZ;
+let elEntityHostTypeDomain;
+let elEntityHostTypeAvatar;
+let elMessageContainer;
+let elImportBtn;
+let elBackBtn;
+let elTpTutorialBtn;
+let elPastePositionBtn;
+
+let lockUntil;
+
+const LOCK_BTN_DELAY = 2000; //2 sec
+
+function loaded() {
+ lockUntil = 0;
+
+ elJsonUrl = document.getElementById("jsonUrl");
+ elBrowseBtn = document.getElementById("browseBtn");
+ elImportAtAvatar = document.getElementById("importAtAvatar");
+ elImportAtSpecificPosition = document.getElementById("importAtSpecificPosition");
+ elImportAtSpecificPositionContainer = document.getElementById("importAtSpecificPositionContainer");
+ elPositionX = document.getElementById("positionX");
+ elPositionY = document.getElementById("positionY");
+ elPositionZ = document.getElementById("positionZ");
+ elEntityHostTypeDomain = document.getElementById("entityHostTypeDomain");
+ elEntityHostTypeAvatar = document.getElementById("entityHostTypeAvatar");
+ elMessageContainer = document.getElementById("messageContainer");
+ elImportBtn = document.getElementById("importBtn");
+ elBackBtn = document.getElementById("backBtn");
+ elTpTutorialBtn = document.getElementById("tpTutorialBtn");
+ elPastePositionBtn = document.getElementById("pastePositionBtn");
+
+ elJsonUrl.oninput = function() {
+ persistData();
+ }
+
+ elPositionX.oninput = function() {
+ persistData();
+ }
+
+ elPositionY.oninput = function() {
+ persistData();
+ }
+
+ elPositionZ.oninput = function() {
+ persistData();
+ }
+
+ elEntityHostTypeDomain.onclick = function() {
+ persistData();
+ }
+
+ elEntityHostTypeAvatar.onclick = function() {
+ persistData();
+ }
+
+ elBrowseBtn.onclick = function() {
+ const d = new Date();
+ let time = d.getTime();
+ if ((d.getTime() - lockUntil) > LOCK_BTN_DELAY) {
+ EventBridge.emitWebEvent(JSON.stringify({ "type": "importUiBrowse" }));
+ lockUntil = d.getTime() + LOCK_BTN_DELAY;
+ }
+ };
+
+ elImportAtAvatar.onclick = function() {
+ elImportAtSpecificPositionContainer.style.display = "None";
+ persistData();
+ };
+
+ elImportAtSpecificPosition.onclick = function() {
+ elImportAtSpecificPositionContainer.style.display = "Block";
+ persistData();
+ };
+
+ elImportBtn.onclick = function() {
+ const d = new Date();
+ let time = d.getTime();
+ if ((d.getTime() - lockUntil) > LOCK_BTN_DELAY) {
+ importJsonToWorld();
+ lockUntil = d.getTime() + LOCK_BTN_DELAY;
+ }
+ };
+
+ elBackBtn.onclick = function() {
+ const d = new Date();
+ let time = d.getTime();
+ if ((d.getTime() - lockUntil) > LOCK_BTN_DELAY) {
+ EventBridge.emitWebEvent(JSON.stringify({ "type": "importUiGoBack" }));
+ lockUntil = d.getTime() + LOCK_BTN_DELAY;
+ }
+ };
+
+ elTpTutorialBtn.onclick = function() {
+ const d = new Date();
+ let time = d.getTime();
+ if ((d.getTime() - lockUntil) > LOCK_BTN_DELAY) {
+ EventBridge.emitWebEvent(JSON.stringify({ "type": "importUiGoTutorial" }));
+ lockUntil = d.getTime() + LOCK_BTN_DELAY;
+ }
+ };
+
+ elPastePositionBtn.onclick = function() {
+ const d = new Date();
+ let time = d.getTime();
+ if ((d.getTime() - lockUntil) > LOCK_BTN_DELAY) {
+ EventBridge.emitWebEvent(JSON.stringify({ "type": "importUiGetCopiedPosition" }));
+ lockUntil = d.getTime() + LOCK_BTN_DELAY;
+ }
+ };
+
+ EventBridge.emitWebEvent(JSON.stringify({ "type": "importUiGetPersistData" }));
+}
+
+function persistData() {
+ let message = {
+ "type": "importUiPersistData",
+ "importUiPersistedData": {
+ "elJsonUrl": elJsonUrl.value,
+ "elImportAtAvatar": elImportAtAvatar.checked,
+ "elImportAtSpecificPosition": elImportAtSpecificPosition.checked,
+ "elPositionX": elPositionX.value,
+ "elPositionY": elPositionY.value,
+ "elPositionZ": elPositionZ.value,
+ "elEntityHostTypeDomain": elEntityHostTypeDomain.checked,
+ "elEntityHostTypeAvatar": elEntityHostTypeAvatar.checked
+ }
+ };
+ EventBridge.emitWebEvent(JSON.stringify(message));
+}
+
+function loadDataInUi(importUiPersistedData) {
+ elJsonUrl.value = importUiPersistedData.elJsonUrl;
+ elImportAtAvatar.checked = importUiPersistedData.elImportAtAvatar;
+ elImportAtSpecificPosition.checked = importUiPersistedData.elImportAtSpecificPosition;
+ elPositionX.value = importUiPersistedData.elPositionX;
+ elPositionY.value = importUiPersistedData.elPositionY;
+ elPositionZ.value = importUiPersistedData.elPositionZ;
+ elEntityHostTypeDomain.checked = importUiPersistedData.elEntityHostTypeDomain;
+ elEntityHostTypeAvatar.checked = importUiPersistedData.elEntityHostTypeAvatar;
+ if (elImportAtSpecificPosition.checked) {
+ elImportAtSpecificPositionContainer.style.display = "Block";
+ }
+}
+
+function importJsonToWorld() {
+ elMessageContainer.innerHTML = "";
+
+ if (elJsonUrl.value === "") {
+ elMessageContainer.innerHTML = "ERROR: 'URL/File (.json)' is required.
";
+ return;
+ }
+
+ let positioningMode = getRadioValue("importAtPosition");
+ let entityHostType = getRadioValue("entityHostType");
+
+ if (positioningMode === "position" && (elPositionX.value === "" || elPositionY.value === "" || elPositionZ.value === "")) {
+ elMessageContainer.innerHTML = "ERROR: 'Position' is required.
";
+ return;
+ }
+ let position = {"x": parseFloat(elPositionX.value), "y": parseFloat(elPositionY.value), "z": parseFloat(elPositionZ.value)};
+ let message = {
+ "type": "importUiImport",
+ "jsonURL": elJsonUrl.value,
+ "positioningMode": positioningMode,
+ "position": position,
+ "entityHostType": entityHostType
+ };
+ EventBridge.emitWebEvent(JSON.stringify(message));
+}
+
+function getRadioValue(objectName) {
+ let radios = document.getElementsByName(objectName);
+ let i;
+ let selectedValue = "";
+ for (i = 0; i < radios.length; i++) {
+ if (radios[i].checked) {
+ selectedValue = radios[i].value;
+ break;
+ }
+ }
+ return selectedValue;
+}
+
+EventBridge.scriptEventReceived.connect(function(message){
+ let messageObj = JSON.parse(message);
+ if (messageObj.type === "importUi_IMPORT_CONFIRMATION") {
+ elMessageContainer.innerHTML = "IMPORT SUCCESSFUL.
";
+ } else if (messageObj.type === "importUi_IMPORT_ERROR") {
+ elMessageContainer.innerHTML = "IMPORT ERROR: " + messageObj.reason + "
";
+ } else if (messageObj.type === "importUi_SELECTED_FILE") {
+ elJsonUrl.value = messageObj.file;
+ persistData();
+ } else if (messageObj.type === "importUi_POSITION_TO_PASTE") {
+ elPositionX.value = messageObj.position.x;
+ elPositionY.value = messageObj.position.y;
+ elPositionZ.value = messageObj.position.z;
+ persistData();
+ } else if (messageObj.type === "importUi_LOAD_DATA") {
+ loadDataInUi(messageObj.importUiPersistedData);
+ }
+});
diff --git a/scripts/system/create/qml/EditTabView.qml b/scripts/system/create/qml/EditTabView.qml
index 96e66c109e..2db23ec659 100644
--- a/scripts/system/create/qml/EditTabView.qml
+++ b/scripts/system/create/qml/EditTabView.qml
@@ -301,6 +301,22 @@ TabBar {
}
}
+ EditTabButton {
+ title: "IMPORT"
+ active: true
+ enabled: true
+ property string originalUrl: ""
+
+ property Component visualItem: Component {
+ WebView {
+ id: advancedImportWebView
+ url: Qt.resolvedUrl("../importEntities/html/importEntities.html")
+ enabled: true
+ blurOnCtrlShift: false
+ }
+ }
+ }
+
function fromScript(message) {
switch (message.method) {
case 'selectTab':
@@ -333,6 +349,9 @@ TabBar {
case 'grid':
editTabView.currentIndex = 3;
break;
+ case 'import':
+ editTabView.currentIndex = 4;
+ break;
default:
console.warn('Attempt to switch to invalid tab:', id);
}
diff --git a/scripts/system/create/qml/EditToolsTabView.qml b/scripts/system/create/qml/EditToolsTabView.qml
index 998c3a3aac..1000724458 100644
--- a/scripts/system/create/qml/EditToolsTabView.qml
+++ b/scripts/system/create/qml/EditToolsTabView.qml
@@ -291,6 +291,22 @@ TabBar {
}
}
+ EditTabButton {
+ title: "IMPORT"
+ active: true
+ enabled: true
+ property string originalUrl: ""
+
+ property Component visualItem: Component {
+ WebView {
+ id: advancedImportWebView
+ url: Qt.resolvedUrl("../importEntities/html/importEntities.html")
+ enabled: true
+ blurOnCtrlShift: false
+ }
+ }
+ }
+
function fromScript(message) {
switch (message.method) {
case 'selectTab':
@@ -304,7 +320,7 @@ TabBar {
// Changes the current tab based on tab index or title as input
function selectTab(id) {
if (typeof id === 'number') {
- if (id >= tabIndex.create && id <= tabIndex.grid) {
+ if (id >= tabIndex.create && id <= tabIndex.import) {
editTabView.currentIndex = id;
} else {
console.warn('Attempt to switch to invalid tab:', id);
@@ -320,6 +336,9 @@ TabBar {
case 'grid':
editTabView.currentIndex = tabIndex.grid;
break;
+ case 'import':
+ editTabView.currentIndex = tabIndex.import;
+ break;
default:
console.warn('Attempt to switch to invalid tab:', id);
}