mirror of
https://github.com/overte-org/overte.git
synced 2025-07-17 21:56:50 +02:00
* mirrors wip * fix view + projection, texture flipping, billboarding * wip portals * wip * fix cpu frustum culling (hacky?) * fix mirrors in deferred * mirrors on models + text * portals use exit as ignoreItem * cleanup * entity tags * wild guess to handle view correction, hide portalExitID in create when mirrorMode != portal * let's try this?? * plz * promising * fix paramsOffset and view flipping * portals shouldn't flip * break when tag found * fix portal view calculation * Revert "Mirrors + Portals" * Revert "Revert "Mirrors + Portals"" * web entity wantsKeyboardFocus property * fix typo * move audio zones to zone entity properties * fix audio zones in create * set dynamic factory * new procecural particle entity type * fix particle intersection * shorten create labels * fix 0 update props case * Ability to smooth model animations * sound entities * fix layered simulate items * fix stereo sound speed * support non-localOnly sound avatar entities * add sound url prompt * support registration point, improve locking * remove keyboardRasied * locking attempt #2 * fix keyboardRasied typo * add default particle props * add unlit property for shapes * Merge branch master into protocol_changes * add ambient light color * fix create issue * fix create issue * add tonemapping props to zones, wip ambient occlusion * wip ambient occlusion * it's working! * remove attachments * fix non-localOnly positional sounds not updating * change AO default to HBAO, remove from create * more graphics options * fix AO setting + effects in mirrors * fix AA in mirrors * alezia's fixes * fix haze in mirrors * add comment for SKYBOX_DISTANCE * new line * model loading priority updates over time, takes into account out of bounds, avatar entities have higher priority, and fsts can specify to wait for wearables to load before rendering * add loadPriority to model entities, working on other avatars waitForWearables * fix build error * try to fix isServer assert * fix stats + waitForWearables * Listen for click instead of release. * Reverted initial commit. Implemented hack to listen for menu click events. * Missed some reverts. * Missed another one. * Prevent duplicate actions. * Added extra needed checks. * Fix without formatting? (#91) * Hopefully fixed formatting. * Things can't be too easy. * Remove google poly * automated entity property serialization * cleanup + automate EntityPropertyFlags * text vertical alignment, use uint8_t for entity property enums, fix text recalculating too often * fix text size * Update interface/resources/controllers/keyboardMouse.json Co-authored-by: HifiExperiments <53453710+HifiExperiments@users.noreply.github.com> * fix component mode serialization * Fixed mouse look in selfie mode. * fix text debug assert on invalid or unloaded font * missed some enums * fix ADD_GROUP_PROPERTY_TO_MAP * fix PROP_GRAB_EQUIPPABLE_INDICATOR_URL missing urlPermission * fix KeyLightPropertyGroup legacy properties * fix PolyLineEntityItem::getEntityProperties * comment cmake script * fix copyright * Replaced key value with key text. Added additional comment about the specific delete key used. * weekly promoted place Highlight the first place in the list as the weekly promoted place * Fixed lingering references to `avatarIcon`. Signed-off-by: armored-dragon <publicmail@armoreddragon.com> * Adding icon for "Grab And Equip" section Adding icon for "Grab And Equip" section * Add "Grab And Equip" section Add "Grab And Equip" section for the grabbale and Equipable groups of properties. * Add files via upload * Add tooltips for the "Grab and Equip" properties Add the tooltips for the "Grab and Equip" properties * Text adjustments for grab.equippable Text adjustments for grab.equippable * Make Maturity Filter persisted Make Maturity Filter persisted and with a default value (Teen & Everyone) * Adjust the default value for maturity Adjust the default value for maturity * move "triggerable" under GRAB & EQUIP move "triggerable" under GRAB & EQUIP * Remove hifi-screenshare Cherry picked and updated from Tivoli dd5b6ea6ee5597a06603e16509640e7ed18106bb Co-authored-by: Julian Groß <julian.g@posteo.de> * Insert placeholder to not break protocol yet. * Fix incorrectly resolved merge conflict, left too much code. * Fixes based on review comments on previous PR * Remove code accidentally re-added during a conflict fix * bump protocol * rebuild fonts with full charset (NOT -allglyphs) * Attempt at fixing Windows master branch builds * Change minimum angular velocity to a lower one * Fix Uuid.NULL behavior --------- Signed-off-by: armored-dragon <publicmail@armoreddragon.com> Co-authored-by: HifiExperiments <thingsandstuffblog@gmail.com> Co-authored-by: ksuprynowicz <ksuprynowicz@post.pl> Co-authored-by: Dale Glass <51060919+daleglass@users.noreply.github.com> Co-authored-by: HifiExperiments <53453710+HifiExperiments@users.noreply.github.com> Co-authored-by: Julian Groß <julian.g@posteo.de> Co-authored-by: armored-dragon <publicmail@armoreddragon.com> Co-authored-by: Armored-Dragon <github56254@armoreddragon.com> Co-authored-by: Alezia Kurdis <60075796+AleziaKurdis@users.noreply.github.com> Co-authored-by: Maki <mxmcube@gmail.com> Co-authored-by: Dale Glass <dale@daleglass.net>
445 lines
18 KiB
JavaScript
445 lines
18 KiB
JavaScript
"use strict";
|
|
|
|
// entityList.js
|
|
//
|
|
// Copyright 2014 High Fidelity, Inc.
|
|
// Copyright 2020 Vircadia contributors.
|
|
// Copyright 2023-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
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
/* global EntityListTool, Tablet, Entities, Camera, MyAvatar, Vec3, Menu, Messages,
|
|
MENU_EASE_ON_FOCUS,
|
|
Script, Clipboard */
|
|
|
|
var PROFILING_ENABLED = false;
|
|
var profileIndent = '';
|
|
|
|
const PROFILE_NOOP = function(_name, fn, args) {
|
|
fn.apply(this, args);
|
|
};
|
|
const PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) {
|
|
console.log("PROFILE-Script " + profileIndent + "(" + name + ") Begin");
|
|
var previousIndent = profileIndent;
|
|
profileIndent += ' ';
|
|
var before = Date.now();
|
|
fn.apply(this, args);
|
|
var delta = Date.now() - before;
|
|
profileIndent = previousIndent;
|
|
console.log("PROFILE-Script " + profileIndent + "(" + name + ") End " + delta + "ms");
|
|
};
|
|
|
|
var EntityListTool = function(shouldUseEditTabletApp, selectionManager) {
|
|
var that = {};
|
|
that.selectionManager = selectionManager;
|
|
|
|
var CreateWindow = Script.require('../modules/createWindow.js');
|
|
|
|
var TITLE_OFFSET = 60;
|
|
var ENTITY_LIST_WIDTH = 495;
|
|
var MAX_DEFAULT_CREATE_TOOLS_HEIGHT = 778;
|
|
var entityListWindow = new CreateWindow(
|
|
Script.resolvePath("./qml/EditEntityList.qml"),
|
|
'Entity List',
|
|
'com.highfidelity.create.entityListWindow',
|
|
function () {
|
|
var windowHeight = Window.innerHeight - TITLE_OFFSET;
|
|
if (windowHeight > MAX_DEFAULT_CREATE_TOOLS_HEIGHT) {
|
|
windowHeight = MAX_DEFAULT_CREATE_TOOLS_HEIGHT;
|
|
}
|
|
return {
|
|
size: {
|
|
x: ENTITY_LIST_WIDTH,
|
|
y: windowHeight
|
|
},
|
|
position: {
|
|
x: Window.x,
|
|
y: Window.y + TITLE_OFFSET
|
|
}
|
|
};
|
|
},
|
|
false
|
|
);
|
|
|
|
var webView = null;
|
|
webView = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
|
webView.setVisible = function(value){ };
|
|
|
|
var filterInView = false;
|
|
var searchRadius = 100;
|
|
|
|
var visible = false;
|
|
|
|
that.webView = webView;
|
|
|
|
that.setVisible = function(newVisible) {
|
|
visible = newVisible;
|
|
webView.setVisible(shouldUseEditTabletApp() && visible);
|
|
entityListWindow.setVisible(!shouldUseEditTabletApp() && visible);
|
|
};
|
|
|
|
that.isVisible = function() {
|
|
return entityListWindow.isVisible();
|
|
};
|
|
|
|
that.setVisible(false);
|
|
|
|
function emitJSONScriptEvent(data) {
|
|
var dataString;
|
|
PROFILE("Script-JSON.stringify", function() {
|
|
dataString = JSON.stringify(data);
|
|
});
|
|
PROFILE("Script-emitScriptEvent", function() {
|
|
webView.emitScriptEvent(dataString);
|
|
if (entityListWindow.window) {
|
|
entityListWindow.window.emitScriptEvent(dataString);
|
|
}
|
|
});
|
|
}
|
|
|
|
that.toggleVisible = function() {
|
|
that.setVisible(!visible);
|
|
};
|
|
|
|
selectionManager.addEventListener(function(isSelectionUpdate, caller) {
|
|
if (caller === that) {
|
|
// ignore events that we emitted from the entity list itself
|
|
return;
|
|
}
|
|
// Otherwise this will emit tens of events every second when objects are moved.
|
|
if (!isSelectionUpdate) {
|
|
return;
|
|
}
|
|
var selectedIDs = [];
|
|
|
|
for (var i = 0; i < that.selectionManager.selections.length; i++) {
|
|
selectedIDs.push(that.selectionManager.selections[i]);
|
|
}
|
|
|
|
emitJSONScriptEvent({
|
|
type: 'selectionUpdate',
|
|
selectedIDs: selectedIDs
|
|
});
|
|
});
|
|
|
|
that.setSpaceMode = function(spaceMode) {
|
|
emitJSONScriptEvent({
|
|
type: 'setSpaceMode',
|
|
spaceMode: spaceMode
|
|
});
|
|
};
|
|
|
|
that.clearEntityList = function() {
|
|
emitJSONScriptEvent({
|
|
type: 'clearEntityList'
|
|
});
|
|
};
|
|
|
|
that.removeEntities = function (deletedIDs, selectedIDs) {
|
|
emitJSONScriptEvent({
|
|
type: 'removeEntities',
|
|
deletedIDs: deletedIDs,
|
|
selectedIDs: selectedIDs
|
|
});
|
|
};
|
|
|
|
that.deleteEntities = function (deletedIDs) {
|
|
emitJSONScriptEvent({
|
|
type: "deleted",
|
|
ids: deletedIDs
|
|
});
|
|
};
|
|
|
|
that.setListMenuSnapToGrid = function (isSnapToGrid) {
|
|
emitJSONScriptEvent({ "type": "setSnapToGrid", "snap": isSnapToGrid });
|
|
};
|
|
|
|
that.toggleSnapToGrid = function () {
|
|
if (!grid.getSnapToGrid()) {
|
|
grid.setSnapToGrid(true);
|
|
emitJSONScriptEvent({ "type": "setSnapToGrid", "snap": true });
|
|
} else {
|
|
grid.setSnapToGrid(false);
|
|
emitJSONScriptEvent({ "type": "setSnapToGrid", "snap": false });
|
|
}
|
|
};
|
|
|
|
function valueIfDefined(value) {
|
|
return value !== undefined ? value : "";
|
|
}
|
|
|
|
function entityIsBaked(properties) {
|
|
if (properties.type === "Model") {
|
|
var lowerModelURL = properties.modelURL.toLowerCase();
|
|
return lowerModelURL.endsWith(".baked.fbx") || lowerModelURL.endsWith(".baked.fst");
|
|
} else if (properties.type === "Zone") {
|
|
var lowerSkyboxURL = properties.skybox ? properties.skybox.url.toLowerCase() : "";
|
|
var lowerAmbientURL = properties.ambientLight ? properties.ambientLight.ambientURL.toLowerCase() : "";
|
|
return (lowerSkyboxURL === "" || lowerSkyboxURL.endsWith(".texmeta.json")) &&
|
|
(lowerAmbientURL === "" || lowerAmbientURL.endsWith(".texmeta.json"));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
that.sendUpdate = function() {
|
|
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
|
if (HMD.active) {
|
|
tablet.setLandscape(true);
|
|
}
|
|
emitJSONScriptEvent({
|
|
"type": "confirmHMDstate",
|
|
"isHmd": HMD.active
|
|
});
|
|
|
|
PROFILE('Script-sendUpdate', function() {
|
|
var entities = [];
|
|
|
|
var ids;
|
|
PROFILE("findEntities", function() {
|
|
if (filterInView) {
|
|
ids = Entities.findEntitiesInFrustum(Camera.frustum);
|
|
} else {
|
|
ids = Entities.findEntities(MyAvatar.position, searchRadius);
|
|
}
|
|
});
|
|
|
|
var cameraPosition = Camera.position;
|
|
PROFILE("getMultipleProperties", function () {
|
|
var multipleProperties = Entities.getMultipleEntityProperties(ids, ['position', 'name', 'type', 'locked',
|
|
'visible', 'renderInfo', 'modelURL', 'materialURL', 'imageURL', 'script', 'serverScripts',
|
|
'skybox.url', 'ambientLight.url', 'soundURL', 'created', 'lastEdited', 'entityHostType']);
|
|
for (var i = 0; i < multipleProperties.length; i++) {
|
|
var properties = multipleProperties[i];
|
|
|
|
if (!filterInView || Vec3.distance(properties.position, cameraPosition) <= searchRadius) {
|
|
var url = "";
|
|
if (properties.type === "Model") {
|
|
url = properties.modelURL;
|
|
} else if (properties.type === "Material") {
|
|
url = properties.materialURL;
|
|
} else if (properties.type === "Image") {
|
|
url = properties.imageURL;
|
|
} else if (properties.type === "Sound") {
|
|
url = properties.soundURL;
|
|
}
|
|
//print("Global object before getParentState call: " + JSON.stringify(globalThis));
|
|
var parentStatus = that.createApp.getParentState(ids[i]);
|
|
var parentState = "";
|
|
if (parentStatus === "PARENT") {
|
|
parentState = "A";
|
|
} else if (parentStatus === "CHILDREN") {
|
|
parentState = "C";
|
|
} else if (parentStatus === "PARENT_CHILDREN") {
|
|
parentState = "B";
|
|
}
|
|
|
|
entities.push({
|
|
id: ids[i],
|
|
name: properties.name,
|
|
type: properties.type,
|
|
url: url,
|
|
locked: properties.locked,
|
|
visible: properties.visible,
|
|
verticesCount: (properties.renderInfo !== undefined ?
|
|
valueIfDefined(properties.renderInfo.verticesCount) : ""),
|
|
texturesCount: (properties.renderInfo !== undefined ?
|
|
valueIfDefined(properties.renderInfo.texturesCount) : ""),
|
|
texturesSize: (properties.renderInfo !== undefined ?
|
|
valueIfDefined(properties.renderInfo.texturesSize) : ""),
|
|
hasTransparent: (properties.renderInfo !== undefined ?
|
|
valueIfDefined(properties.renderInfo.hasTransparent) : ""),
|
|
isBaked: entityIsBaked(properties),
|
|
drawCalls: (properties.renderInfo !== undefined ?
|
|
valueIfDefined(properties.renderInfo.drawCalls) : ""),
|
|
hasScript: (properties.script !== "" || properties.serverScripts !== ""),
|
|
parentState: parentState,
|
|
created: formatToStringDateTime(properties.created),
|
|
lastEdited: formatToStringDateTime(properties.lastEdited),
|
|
entityHostType: properties.entityHostType
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
var selectedIDs = [];
|
|
for (var j = 0; j < that.selectionManager.selections.length; j++) {
|
|
selectedIDs.push(that.selectionManager.selections[j]);
|
|
}
|
|
|
|
emitJSONScriptEvent({
|
|
type: "update",
|
|
entities: entities,
|
|
selectedIDs: selectedIDs,
|
|
spaceMode: SelectionDisplay.getSpaceMode(),
|
|
});
|
|
});
|
|
};
|
|
|
|
function formatToStringDateTime(timestamp) {
|
|
var d = new Date(Math.floor(timestamp/1000));
|
|
var dateTime = d.getUTCFullYear() + "-" + zeroPad((d.getUTCMonth() + 1), 2) + "-" + zeroPad(d.getUTCDate(), 2);
|
|
dateTime = dateTime + " " + zeroPad(d.getUTCHours(), 2) + ":" + zeroPad(d.getUTCMinutes(), 2) + ":" + zeroPad(d.getUTCSeconds(), 2);
|
|
dateTime = dateTime + "." + zeroPad(d.getUTCMilliseconds(), 3);
|
|
return dateTime;
|
|
}
|
|
|
|
function zeroPad(num, size) {
|
|
num = num.toString();
|
|
while (num.length < size) {
|
|
num = "0" + num;
|
|
}
|
|
return num;
|
|
}
|
|
|
|
function onFileSaveChanged(filename) {
|
|
Window.saveFileChanged.disconnect(onFileSaveChanged);
|
|
if (filename !== "") {
|
|
var success = Clipboard.exportEntities(filename, that.selectionManager.selections);
|
|
if (!success) {
|
|
Window.notifyEditError("Export failed.");
|
|
}
|
|
}
|
|
}
|
|
|
|
var onWebEventReceived = function(data) {
|
|
//print("entityList.js onWebEventReceived: " + data);
|
|
try {
|
|
data = JSON.parse(data);
|
|
} catch(e) {
|
|
print("entityList.js: Error parsing JSON");
|
|
return;
|
|
}
|
|
|
|
if (data.type === "selectionUpdate") {
|
|
var ids = data.entityIds;
|
|
var entityIDs = [];
|
|
for (var i = 0; i < ids.length; i++) {
|
|
entityIDs.push(ids[i]);
|
|
}
|
|
that.selectionManager.setSelections(entityIDs, that);
|
|
if (data.focus) {
|
|
that.cameraManager.enable();
|
|
that.cameraManager.focus(that.selectionManager.worldPosition,
|
|
that.selectionManager.worldDimensions,
|
|
Menu.isOptionChecked(MENU_EASE_ON_FOCUS));
|
|
}
|
|
} else if (data.type === "refresh") {
|
|
that.sendUpdate();
|
|
} else if (data.type === "teleport") {
|
|
if (that.selectionManager.hasSelection()) {
|
|
MyAvatar.position = that.selectionManager.worldPosition;
|
|
}
|
|
} else if (data.type === "export") {
|
|
if (!that.selectionManager.hasSelection()) {
|
|
Window.notifyEditError("No entities have been selected.");
|
|
} else {
|
|
Window.saveFileChanged.connect(onFileSaveChanged);
|
|
Window.saveAsync("Select Where to Save", "", "*.json");
|
|
}
|
|
} else if (data.type === "delete") {
|
|
that.createApp.deleteSelectedEntities();
|
|
} else if (data.type === "toggleLocked") {
|
|
that.createApp.toggleSelectedEntitiesLocked();
|
|
} else if (data.type === "toggleVisible") {
|
|
that.createApp.toggleSelectedEntitiesVisible();
|
|
} else if (data.type === "filterInView") {
|
|
filterInView = data.filterInView === true;
|
|
} else if (data.type === "radius") {
|
|
searchRadius = data.radius;
|
|
} else if (data.type === "cut") {
|
|
that.selectionManager.cutSelectedEntities();
|
|
} else if (data.type === "copy") {
|
|
that.selectionManager.copySelectedEntities();
|
|
} else if (data.type === "copyID") {
|
|
that.selectionManager.copyIdsFromSelectedEntities();
|
|
} else if (data.type === "paste") {
|
|
that.selectionManager.pasteEntities();
|
|
} else if (data.type === "duplicate") {
|
|
that.selectionManager.duplicateSelection();
|
|
that.sendUpdate();
|
|
} else if (data.type === "rename") {
|
|
Entities.editEntity(data.entityID, {name: data.name});
|
|
// make sure that the name also gets updated in the properties window
|
|
that.selectionManager._update();
|
|
} else if (data.type === "toggleSpaceMode") {
|
|
SelectionDisplay.toggleSpaceMode();
|
|
} else if (data.type === 'keyUpEvent') {
|
|
that.createApp.keyUpEventFromUIWindow(data.keyUpEvent);
|
|
} else if (data.type === 'undo') {
|
|
that.createApp.undoHistory.undo();
|
|
} else if (data.type === 'redo') {
|
|
that.createApp.undoHistory.redo();
|
|
} else if (data.type === 'parent') {
|
|
that.createApp.parentSelectedEntities();
|
|
} else if (data.type === 'unparent') {
|
|
that.createApp.unparentSelectedEntities();
|
|
} else if (data.type === 'hmdMultiSelectMode') {
|
|
that.createApp.hmdMultiSelectMode = data.value;
|
|
} else if (data.type === 'selectAllInBox') {
|
|
that.createApp.selectAllEntitiesInCurrentSelectionBox(false);
|
|
} else if (data.type === 'selectAllTouchingBox') {
|
|
that.createApp.selectAllEntitiesInCurrentSelectionBox(true);
|
|
} else if (data.type === 'selectParent') {
|
|
that.selectionManager.selectParent();
|
|
} else if (data.type === 'selectTopParent') {
|
|
that.selectionManager.selectTopParent();
|
|
} else if (data.type === 'addChildrenToSelection') {
|
|
that.selectionManager.addChildrenToSelection();
|
|
} else if (data.type === 'selectFamily') {
|
|
that.selectionManager.selectFamily();
|
|
} else if (data.type === 'selectTopFamily') {
|
|
that.selectionManager.selectTopFamily();
|
|
} else if (data.type === 'teleportToEntity') {
|
|
that.selectionManager.teleportToEntity();
|
|
} else if (data.type === 'rotateAsTheNextClickedSurface') {
|
|
that.createApp.rotateAsNextClickedSurface();
|
|
} else if (data.type === 'quickRotate90x') {
|
|
that.selectionDisplay.rotate90degreeSelection("X");
|
|
} else if (data.type === 'quickRotate90y') {
|
|
that.selectionDisplay.rotate90degreeSelection("Y");
|
|
} else if (data.type === 'quickRotate90z') {
|
|
that.selectionDisplay.rotate90degreeSelection("Z");
|
|
} else if (data.type === 'moveEntitySelectionToAvatar') {
|
|
that.selectionManager.moveEntitiesSelectionToAvatar();
|
|
} else if (data.type === 'loadConfigSetting') {
|
|
var columnsData = Settings.getValue(that.createApp.SETTING_EDITOR_COLUMNS_SETUP, "NO_DATA");
|
|
var defaultRadius = Settings.getValue(that.createApp.SETTING_ENTITY_LIST_DEFAULT_RADIUS, 100);
|
|
emitJSONScriptEvent({
|
|
"type": "loadedConfigSetting",
|
|
"columnsData": columnsData,
|
|
"defaultRadius": defaultRadius
|
|
});
|
|
} else if (data.type === 'saveColumnsConfigSetting') {
|
|
Settings.setValue(that.createApp.SETTING_EDITOR_COLUMNS_SETUP, data.columnsData);
|
|
} else if (data.type === 'importFromFile') {
|
|
that.createApp.importEntitiesFromFile();
|
|
} else if (data.type === 'importFromUrl') {
|
|
that.createApp.importEntitiesFromUrl();
|
|
} else if (data.type === 'setCameraFocusToSelection') {
|
|
that.createApp.setCameraFocusToSelection();
|
|
} else if (data.type === 'alignGridToSelection') {
|
|
that.createApp.alignGridToSelection();
|
|
} else if (data.type === 'alignGridToAvatar') {
|
|
that.createApp.alignGridToAvatar();
|
|
} else if (data.type === 'brokenURLReport') {
|
|
brokenURLReport(that.selectionManager.selections);
|
|
} else if (data.type === 'renderWithZonesManager') {
|
|
renderWithZonesManager(that.selectionManager.selections);
|
|
} else if (data.type === 'toggleGridVisibility') {
|
|
that.createApp.toggleGridVisibility();
|
|
} else if (data.type === 'toggleSnapToGrid') {
|
|
that.toggleSnapToGrid();
|
|
}
|
|
|
|
};
|
|
|
|
webView.webEventReceived.connect(onWebEventReceived);
|
|
entityListWindow.webEventReceived.addListener(onWebEventReceived);
|
|
that.interactiveWindowHidden = entityListWindow.interactiveWindowHidden;
|
|
|
|
return that;
|
|
};
|