overte/scripts/communityScripts/contextMenu.js
2025-08-25 04:50:32 +10:00

843 lines
24 KiB
JavaScript

// SPDX-License-Identifier: Apache-2.0
"use strict";
const CONTEXT_MENU_SETTINGS = Settings.getValue("Context Menu", {
actionsPerPage: 6,
font: "Roboto",
noSfx: false,
parented: true,
public: false,
});
const ACTIONS_PER_PAGE = CONTEXT_MENU_SETTINGS["actionsPerPage"] ?? 6;
const SOUND_OPEN = SoundCache.getSound(Script.resourcesPath() + "sounds/Expand.wav");
const SOUND_CLOSE = SoundCache.getSound(Script.resourcesPath() + "sounds/Collapse.wav");
const SOUND_CLICK = SoundCache.getSound(Script.resourcesPath() + "sounds/Button06.wav");
const SOUND_HOVER = SoundCache.getSound(Script.resourcesPath() + "sounds/Button04.wav");
// Roboto
// Inconsolata
// Courier
// Timeless
// https://*
const CONTEXT_MENU_FONT = CONTEXT_MENU_SETTINGS["font"] ?? "Roboto";
const MENU_TOGGLE_ACTION = [Controller.Standard.LeftPrimaryThumb, Controller.Standard.RightPrimaryThumb];
const MENU_TOGGLE_KEY = "b";
const TARGET_HOVER_ACTION = [Controller.Standard.LT, Controller.Standard.RT];
const CLICK_FUNC_CHANNEL = "org.overte.context-menu.click";
const ACTIONS_CHANNEL = "org.overte.context-menu.actions";
const MAIN_CHANNEL = "org.overte.context-menu";
const SENSOR_TO_WORLD_MATRIX_INDEX = 65534;
const CAMERA_MATRIX_INDEX = 65529;
const EMPTY_FUNCTION = () => {};
const SELF_ACTIONS = [
_target => {
if (!HMD.active) { return; }
return {
text: "Recenter",
textColor: [255, 192, 64],
clickFunc: _target => {
HMD.centerUI();
MyAvatar.centerBody();
},
};
},
_target => {
return {
text: MyAvatar.getCollisionsEnabled() ? "[ ] Noclip" : "[X] Noclip",
textColor: [127, 255, 255],
clickFunc: _target => MyAvatar.setCollisionsEnabled(!MyAvatar.getCollisionsEnabled()),
};
},
_target => {
return {
text: MyAvatar.getOtherAvatarsCollisionsEnabled() ? "[X] Avatar collisions" : "[ ] Avatar collisions",
textColor: [127, 255, 255],
clickFunc: _target => MyAvatar.setOtherAvatarsCollisionsEnabled(!MyAvatar.getOtherAvatarsCollisionsEnabled()),
};
},
];
const OBJECT_ACTIONS = [
ent => {
if (Entities.getNestableType(ent) !== "entity") { return; }
const { cloneable } = Entities.getEntityProperties(ent, "cloneable");
if (!cloneable) { return; }
return {
text: "Clone",
textColor: [0, 255, 0],
clickFunc: ent => {
const newEnt = Entities.cloneEntity(ent);
Entities.editEntity(newEnt, {
position: Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation)),
grab: {grabbable: true},
});
},
};
},
ent => {
if (Entities.getNestableType(ent) !== "entity") { return; }
const { cloneOriginID } = Entities.getEntityProperties(ent, "cloneOriginID");
if (Uuid.isNull(cloneOriginID)) { return; }
return {
text: "Delete",
textColor: [255, 0, 0],
clickFunc: ent => Entities.deleteEntity(ent),
};
},
];
const ROOT_ACTIONS = [
target => {
if (Entities.getNestableType(target) !== "entity") { return; }
let { userData } = Entities.getEntityProperties(target, "userData");
let submenu = "_OBJECT";
if (userData) {
try {
userData = JSON.parse(userData);
if (!(userData?.contextMenu?.root ?? true)) {
submenu = "_TARGET";
}
} catch (e) {
console.error(`ROOT_ACTIONS._OBJECT: ${e}`);
}
}
return {
text: "Object",
textColor: [0, 255, 0],
keepMenuOpen: true,
submenu: submenu,
priority: -101,
};
},
target => {
if (Entities.getNestableType(target) !== "avatar") { return; }
const avatar = AvatarList.getAvatar(target);
if (!avatar || Object.keys(avatar).length === 0) { return; }
return {
text: avatar.displayName || "unnamed",
textColor: [255, 255, 0],
keepMenuOpen: true,
submenu: "_AVATAR",
priority: -101,
};
},
_target => ({
text: "My Avatar",
textColor: [126, 255, 255],
keepMenuOpen: true,
submenu: "_SELF",
priority: -100,
}),
];
let registeredActionSets = {
"_ROOT": [...ROOT_ACTIONS],
"_SELF": [...SELF_ACTIONS],
"_OBJECT": [...OBJECT_ACTIONS],
"_AVATAR": [],
"_TARGET": [],
};
let registeredActionSetParents = {};
let registeredActionSetTitles = {};
const targetingPick = [
Picks.createPick(PickType.Ray, {
enabled: true,
filter: Picks.PICK_DOMAIN_ENTITIES | Picks.PICK_AVATAR_ENTITIES | Picks.PICK_LOCAL_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE | Picks.PICK_AVATARS,
maxDistance: 20,
joint: HMD.active ? "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND" : "Mouse",
}),
Picks.createPick(PickType.Ray, {
enabled: true,
filter: Picks.PICK_DOMAIN_ENTITIES | Picks.PICK_AVATAR_ENTITIES | Picks.PICK_LOCAL_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE | Picks.PICK_AVATARS,
maxDistance: 20,
joint: HMD.active ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "Mouse",
}),
];
let currentMenuOpen = false;
let currentMenuEntities = new Set();
let currentMenuActionFuncs = [];
let currentMenuTarget = Uuid.NULL;
let currentMenuTargetIsAvatar = false;
let currentMenuInSubmenu = false;
let currentMenuTargetLine = Uuid.NULL;
let mouseWasCaptured = false;
let disableCounter = 0;
function ContextMenu_DeleteMenu() {
currentMenuEntities.forEach((_, e) => Entities.deleteEntity(e));
currentMenuEntities.clear();
currentMenuActionFuncs = [];
currentMenuOpen = false;
currentMenuInSubmenu = false;
currentMenuTargetIsAvatar = false;
currentMenuTarget = Uuid.NULL;
currentMenuTargetLine = Uuid.NULL;
Camera.captureMouse = mouseWasCaptured;
}
function ContextMenu_EntityClick(eid, event) {
if (!event.isPrimaryButton) { return; }
if (!currentMenuEntities.has(eid)) { return; }
currentMenuInSubmenu = false;
try {
const data = JSON.parse(Entities.getEntityProperties(eid, "userData").userData);
if (data.nextPage !== undefined && data.actionSetName !== undefined) {
ContextMenu_OpenActions(data.actionSetName, data.nextPage);
} else {
const func = data.actionFunc;
currentMenuActionFuncs[func][0](currentMenuTarget, eid);
if (!currentMenuActionFuncs[func][1] && !currentMenuInSubmenu) {
ContextMenu_DeleteMenu();
}
}
} catch (e) {}
if (!(CONTEXT_MENU_SETTINGS.noSfx ?? false)) { Audio.playSystemSound(SOUND_CLICK); }
}
function ContextMenu_EntityHover(eid, _event) {
if (!currentMenuEntities.has(eid)) { return; }
try {
const data = JSON.parse(Entities.getEntityProperties(eid, "userData").userData);
if (data.actionFunc !== undefined || (data.nextPage !== undefined && data.actionSetName !== undefined)) {
if (!(CONTEXT_MENU_SETTINGS.noSfx ?? false)) { Audio.playSystemSound(SOUND_HOVER); }
}
} catch (e) {}
}
function ContextMenu_FindTarget(hand = 1) {
currentMenuTargetIsAvatar = false;
const ray = Picks.getPrevPickResult(targetingPick[hand]);
if (!ray.intersects) {
currentMenuTarget = Uuid.NULL;
return;
}
if (ray.type === 3) {
currentMenuTarget = ray.objectID;
currentMenuTargetIsAvatar = true;
} else {
currentMenuTarget = ray.objectID;
}
}
function ContextMenu_OpenActions(actionSetName, page = 0) {
currentMenuEntities.forEach((_, e) => Entities.deleteEntity(e));
currentMenuTargetLine = Uuid.NULL;
currentMenuActionFuncs = [];
currentMenuInSubmenu = true;
mouseWasCaptured = Camera.captureMouse;
Camera.captureMouse = false;
const scale = MyAvatar.sensorToWorldScale;
const myAvatar = MyAvatar.sessionUUID;
// https://github.com/overte-org/overte/issues/1668
const sensorScaleHack = HMD.active;
const baseActions = registeredActionSets[actionSetName];
let actionEnts = [];
let origin;
if (HMD.active) {
origin = Vec3.sum(
Camera.position,
Vec3.multiplyQbyV(
Camera.orientation,
[0, -0.25 * scale, -1.0 * scale]
)
);
} else {
const remap = (low1, high1, low2, high2, value) => low2 + (value - low1) * (high2 - low2) / (high1 - low1);
const spawnDist = remap(20, 130, 1.2, 0.25, Camera.frustum.fieldOfView);
origin = Vec3.sum(
Camera.position,
Vec3.multiplyQbyV(
Camera.orientation,
[0, 0, -spawnDist * scale]
)
);
}
let angle = Quat.lookAtSimple(Camera.position, origin);
let activeActions = [];
let actions = [...Object.values(baseActions)];
for (const [setName, parent] of Object.entries(registeredActionSetParents)) {
if (parent === actionSetName) {
actions.push(...Object.values(registeredActionSets[setName]));
}
}
for (const action of actions) {
let actionData = action(currentMenuTarget);
if (!actionData || Object.keys(actionData).length === 0) { continue; }
// the menu item is only valid for a target entity
if (actionData.requiredTargets !== undefined && !actionData.requiredTargets.includes(currentMenuTarget)) {
continue;
}
activeActions.push(actionData);
}
// FIXME: in some cases there's one too many pages?
const hasPages = activeActions.length > ACTIONS_PER_PAGE;
const maxPages = Math.floor(activeActions.length / ACTIONS_PER_PAGE);
page = Math.max(0, Math.min(page, maxPages));
activeActions.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
activeActions = activeActions.slice(
page * ACTIONS_PER_PAGE,
(page * ACTIONS_PER_PAGE) + ACTIONS_PER_PAGE
);
let yPos = Math.max(1, activeActions.length) * 0.02 * scale;
let bgHeight = (yPos / 2.0) - (0.03 * scale);
// FIXME: Highlight laser doesn't work on camera or sensor-matrix joint
/*if (!Uuid.isNull(currentMenuTarget)) {
let targetPos;
if (currentMenuTargetIsAvatar) {
targetPos = AvatarList.getAvatar(currentMenuTarget).position;
} else {
targetPos = Entities.getEntityProperties(currentMenuTarget, "position").position;
}
currentMenuTargetLine = Entities.addEntity({
type: "PolyLine",
grab: {grabbable: false},
parentID: (CONTEXT_MENU_SETTINGS.parented ?? true) ? myAvatar : undefined,
parentJointIndex: HMD.active ? SENSOR_TO_WORLD_MATRIX_INDEX : CAMERA_MATRIX_INDEX,
position: origin,
linePoints: [
[0, 0, 0],
[targetPos.x - origin.x, targetPos.y - origin.y, targetPos.z - origin.z],
],
normals: [
[0, 0, 1],
[0, 0, 1],
],
strokeWidths: [0.02, 0],
color: [127, 255, 255],
faceCamera: true,
glow: true,
}, (CONTEXT_MENU_SETTINGS.public ?? false) ? "avatar" : "local");
currentMenuEntities.add(currentMenuTargetLine);
}*/
let titleText;
let descriptionText;
if (currentMenuTargetIsAvatar) {
const data = AvatarList.getAvatar(currentMenuTarget);
titleText = data.displayName;
} else if (currentMenuTarget) {
const data = Entities.getEntityProperties(currentMenuTarget, ["type", "name", "description", "userData"]);
const type = data.type ?? "UNKNOWN";
if (data.name) {
titleText = data.name;
} else {
titleText = `${type}`;
}
descriptionText = data.description;
} else {
titleText = "Self";
}
if (registeredActionSetTitles[actionSetName]?.title) {
titleText = registeredActionSetTitles[actionSetName].title;
}
if (registeredActionSetTitles[actionSetName]?.description) {
descriptionText = registeredActionSetTitles[actionSetName].description;
}
if (descriptionText) {
actionEnts.push({
grab: {grabbable: false},
type: "Text",
position: Vec3.sum(origin, Vec3.multiplyQbyV(angle, [0, yPos, 0])),
rotation: angle,
dimensions: [0.3 * scale, 0.2 * scale, 0.01 * scale],
text: descriptionText,
textColor: [255, 255, 255],
backgroundColor: [0, 0, 0],
backgroundAlpha: 0.9,
unlit: true,
lineHeight: 0.016 * scale,
verticalAlignment: "bottom",
alignment: "center",
triggerable: false,
topMargin: 0.005 * scale,
bottomMargin: 0.005 * scale,
leftMargin: 0.005 * scale,
rightMargin: 0.005 * scale,
textEffect: "outline fill",
textEffectColor: [0, 0, 0],
textEffectThickness: 0.3,
});
yPos -= 0.122 * scale;
bgHeight += 0.122;
}
actionEnts.push({
grab: {grabbable: false},
type: "Text",
position: Vec3.sum(origin, Vec3.multiplyQbyV(angle, [0, yPos, 0])),
rotation: angle,
dimensions: [0.215 * scale, 0.04 * scale, 0.01 * scale],
text: titleText,
textColor: [230, 230, 230],
backgroundColor: [0, 0, 0],
backgroundAlpha: 0.9,
unlit: true,
lineHeight: 0.018 * scale,
verticalAlignment: "center",
alignment: "center",
triggerable: false,
textEffect: "outline fill",
textEffectColor: [0, 0, 0],
textEffectThickness: 0.3,
});
actionEnts.push({
grab: {grabbable: false},
type: "Text",
position: Vec3.sum(origin, Vec3.multiplyQbyV(angle, [-0.13 * scale, yPos, 0])),
rotation: angle,
dimensions: [0.04 * scale, 0.04 * scale, 0.01 * scale],
text: hasPages && page > 0 ? "<" : "",
textColor: [230, 230, 230],
backgroundColor: [0, 0, 0],
backgroundAlpha: 0.9,
unlit: true,
lineHeight: 0.05 * scale,
verticalAlignment: "top",
alignment: "center",
triggerable: false,
leftMargin: 0.003 * scale,
topMargin: -0.008 * scale,
userData: hasPages && page > 0 ? JSON.stringify({nextPage: page - 1, actionSetName: actionSetName}) : undefined,
textEffect: "outline fill",
textEffectColor: [0, 0, 0],
textEffectThickness: 0.3,
});
actionEnts.push({
grab: {grabbable: false},
type: "Text",
position: Vec3.sum(origin, Vec3.multiplyQbyV(angle, [0.13 * scale, yPos, 0])),
rotation: angle,
dimensions: [0.04 * scale, 0.04 * scale, 0.01 * scale],
text: hasPages && page < maxPages ? ">" : "",
textColor: [230, 230, 230],
backgroundColor: [0, 0, 0],
backgroundAlpha: 0.9,
unlit: true,
lineHeight: 0.05 * scale,
verticalAlignment: "top",
alignment: "center",
triggerable: false,
leftMargin: -0.002 * scale,
topMargin: -0.008 * scale,
userData: hasPages && page < maxPages ? JSON.stringify({nextPage: page + 1, actionSetName: actionSetName}) : undefined,
textEffect: "outline fill",
textEffectColor: [0, 0, 0],
textEffectThickness: 0.3,
});
yPos -= 0.047 * scale;
bgHeight += 0.047;
for (let i = 0; i < activeActions.length; i++) {
let action = activeActions[i];
let pos = Vec3.sum(origin, Vec3.multiplyQbyV(angle, [0, yPos, 0]));
actionEnts.push({
grab: {grabbable: false},
type: "Text",
position: pos,
rotation: angle,
dimensions: [0.3 * scale, 0.05 * scale, 0.0001 * scale],
text: action.text,
textColor: action.textColor ?? [255, 255, 255],
backgroundColor: action.backgroundColor ?? [0, 0, 0],
backgroundAlpha: action.backgroundAlpha ?? 0.8,
unlit: true,
lineHeight: 0.025 * scale,
verticalAlignment: "center",
alignment: "left",
triggerable: true,
leftMargin: 0.05 * scale,
rightMargin: 0.05 * scale,
userData: JSON.stringify({actionFunc: i}),
textEffect: "outline fill",
textEffectColor: action.backgroundColor ?? [0, 0, 0],
textEffectThickness: 0.3,
});
if (action.iconImage) {
let pos = Vec3.sum(origin, Vec3.multiplyQbyV(angle, [-0.125 * scale, yPos, 0.01 * scale]));
actionEnts.push({
grab: {grabbable: false},
type: "Image",
position: pos,
rotation: angle,
dimensions: [0.03 * scale, 0.03 * scale, 0.01 * scale],
imageURL: action.iconImage,
emissive: true,
userData: JSON.stringify({actionFunc: i}),
});
}
let clickFunc = EMPTY_FUNCTION;
if (action.submenu) {
if (registeredActionSets[action.submenu]) {
clickFunc = _target => ContextMenu_OpenActions(action.submenu);
} else {
console.error(`Action "${action.text}" referencing unregistered submenu action set "${action.submenu}"`);
}
} else if (action.clickFunc) {
clickFunc = action.clickFunc;
} else if (action.remoteClickFunc) {
clickFunc = target => {
Messages.sendMessage(CLICK_FUNC_CHANNEL, JSON.stringify({
funcName: action.remoteClickFunc,
targetEntity: target,
}));
};
} else if (action.localClickFunc) {
clickFunc = target => {
Messages.sendLocalMessage(CLICK_FUNC_CHANNEL, JSON.stringify({
funcName: action.localClickFunc,
targetEntity: target,
}));
};
}
currentMenuActionFuncs.push([clickFunc, action.keepMenuOpen ?? false]);
yPos -= 0.0525 * scale;
bgHeight += 0.0525;
}
if (activeActions.length === 0) {
let pos = Vec3.sum(origin, Vec3.multiplyQbyV(angle, [0, yPos, 0]));
actionEnts.push({
grab: {grabbable: false},
type: "Text",
position: pos,
rotation: angle,
dimensions: [0.3 * scale, 0.05 * scale, 0.0001 * scale],
text: "(No actions)",
textColor: [230, 230, 230],
backgroundColor: [64, 64, 64],
backgroundAlpha: 0.8,
unlit: true,
lineHeight: 0.025 * scale,
verticalAlignment: "center",
alignment: "left",
triggerable: true,
leftMargin: 0.05 * scale,
rightMargin: 0.05 * scale,
textEffect: "outline fill",
textEffectColor: [0, 0, 0],
textEffectThickness: 0.3,
});
yPos -= 0.0525 * scale;
bgHeight += 0.0525;
}
for (let a of actionEnts) {
if (CONTEXT_MENU_SETTINGS.parented ?? true) {
a.parentID = myAvatar;
a.parentJointIndex = HMD.active ? SENSOR_TO_WORLD_MATRIX_INDEX : CAMERA_MATRIX_INDEX;
}
a.font = CONTEXT_MENU_FONT;
a.canCastShadow = false;
a.isVisibleInSecondaryCamera = false;
a.renderLayer = "front";
if (sensorScaleHack) {
a.dimensions[0] /= MyAvatar.sensorToWorldScale;
a.dimensions[1] /= MyAvatar.sensorToWorldScale;
if (a.rightMargin === undefined) { a.rightMargin = 0; }
if (a.bottomMargin === undefined) { a.bottomMargin = 0; }
a.rightMargin += a.dimensions[0] * -(MyAvatar.sensorToWorldScale - 1);
a.bottomMargin += a.dimensions[1] * -(MyAvatar.sensorToWorldScale - 1);
}
const e = Entities.addEntity(a, (CONTEXT_MENU_SETTINGS.public ?? false) ? "avatar" : "local");
currentMenuEntities.add(e);
}
currentMenuOpen = true;
}
let mouseButtonHeld = false;
function ContextMenu_MousePressEvent(event) {
if (event.isLeftButton) { mouseButtonHeld = true; }
}
function ContextMenu_MouseReleaseEvent(event) {
if (event.isLeftButton) { mouseButtonHeld = false; }
}
function ContextMenu_OpenRoot() {
registeredActionSets["_TARGET"] = [];
delete registeredActionSetTitles["_TARGET"];
// check if the target is worth selecting, so we don't
// start highlighting something that we can't do anything with
if (currentMenuTarget && !currentMenuTargetIsAvatar) {
const {
userData,
cloneable,
cloneOriginID,
} = Entities.getEntityProperties(currentMenuTarget, [
"userData",
"cloneable",
"cloneOriginID",
]);
let data = {};
try {
data = JSON.parse(userData);
} catch (e) {}
if (
data?.contextMenu === undefined &&
!cloneable &&
Uuid.isNull(cloneOriginID)
) {
// doesn't do anything interesting, don't bother highlighting
currentMenuTarget = undefined;
}
// don't target UI elements
if (
Keyboard.containsID(currentMenuTarget) ||
currentMenuTarget === HMD.tabletID ||
currentMenuTarget === HMD.tabletScreenID ||
currentMenuTarget === HMD.homeButtonID ||
currentMenuTarget === HMD.homeButtonHighlightID ||
currentMenuTarget === HMD.miniTabletID ||
currentMenuTarget === HMD.miniTabletScreenID
) {
currentMenuTarget = undefined;
}
}
if (currentMenuTarget && !currentMenuTargetIsAvatar) {
try {
const { userData } = Entities.getEntityProperties(currentMenuTarget, "userData");
const data = userData ? JSON.parse(userData) : undefined;
if (data?.contextMenu?.actions) {
if (Array.isArray(data.contextMenu.actions)) {
// objects with custom context menu actions
for (const action of data.contextMenu.actions) {
registeredActionSets["_TARGET"].push(_entity => action);
}
registeredActionSetTitles["_TARGET"] = {
title: data.contextMenu.title,
description: data.contextMenu.description,
};
if (data.contextMenu.root ?? true) {
ContextMenu_OpenActions("_TARGET");
} else {
ContextMenu_OpenActions("_ROOT");
}
if (!(CONTEXT_MENU_SETTINGS.noSfx)) { Audio.playSystemSound(SOUND_OPEN); }
} else {
// objects with only one implicit action that's triggered by the context menu button
const remoteFunc = data?.contextMenu?.actions?.remoteClickFunc;
if (remoteFunc) {
Messages.sendMessage(CLICK_FUNC_CHANNEL, JSON.stringify({
funcName: remoteFunc,
targetEntity: currentMenuTarget,
isTargetAvatar: false,
}));
}
const localFunc = data?.contextMenu?.actions?.localClickFunc;
if (localFunc) {
Messages.sendLocalMessage(CLICK_FUNC_CHANNEL, JSON.stringify({
funcName: localFunc,
targetEntity: currentMenuTarget,
isTargetAvatar: false,
}));
currentMenuTarget = undefined;
}
}
return;
}
} catch (e) {
console.error(`ContextMenu_OpenRoot: ${e}`);
}
}
ContextMenu_OpenActions("_ROOT");
if (!(CONTEXT_MENU_SETTINGS.noSfx ?? false)) { Audio.playSystemSound(SOUND_OPEN); }
}
function ContextMenu_KeyEvent(event) {
if (event.text === MENU_TOGGLE_KEY && !event.isAutoRepeat) {
if (currentMenuOpen) {
ContextMenu_DeleteMenu();
if (!(CONTEXT_MENU_SETTINGS.noSfx ?? false)) { Audio.playSystemSound(SOUND_CLOSE); }
} else if (disableCounter === 0) {
if (mouseButtonHeld) { ContextMenu_FindTarget(); }
ContextMenu_OpenRoot();
}
}
}
let controllerHovering = [false, false];
function ContextMenu_ActionEvent(action, value) {
if (TARGET_HOVER_ACTION[0] === action) {
controllerHovering[0] = value > 0.5;
} else if (TARGET_HOVER_ACTION[1] === action) {
controllerHovering[1] = value > 0.5;
} else if (MENU_TOGGLE_ACTION.includes(action) && value > 0.5) {
if (currentMenuOpen) {
ContextMenu_DeleteMenu();
if (!(CONTEXT_MENU_SETTINGS.noSfx ?? false)) { Audio.playSystemSound(SOUND_CLOSE); }
} else if (disableCounter === 0) {
if (controllerHovering[1]) { ContextMenu_FindTarget(1); }
else if (controllerHovering[0]) { ContextMenu_FindTarget(0); }
ContextMenu_OpenRoot();
}
}
}
// FIXME: Highlight laser doesn't work on camera or sensor-matrix joint
/*function ContextMenu_Update() {
if (!Uuid.isNull(currentMenuTargetLine)) {
if (currentMenuTargetIsAvatar) {
targetPos = AvatarList.getAvatar(currentMenuTarget).position;
} else {
targetPos = Entities.getEntityProperties(currentMenuTarget, "position").position;
}
const myPos = Entities.getEntityProperties(currentMenuTargetLine, "position").position;
Entities.editEntity(currentMenuTargetLine, {
linePoints: [
[0, 0, 0],
[targetPos.x - myPos.x, targetPos.y - myPos.y, targetPos.z - myPos.z],
],
rotation: Quat.IDENTITY,
});
}
}*/
function ContextMenu_Message(channel, msg, _senderID, _localOnly) {
if (channel === MAIN_CHANNEL) {
let data; try { data = JSON.parse(msg); } catch (e) {}
if (data?.func === "disable") {
disableCounter += 1;
} else if (data?.func === "enable" && disableCounter > 0) {
disableCounter -= 1;
}
return;
}
if (channel !== ACTIONS_CHANNEL) { return; }
let data; try { data = JSON.parse(msg); } catch (e) {}
if (data?.func === "register") {
let tmp = {};
for (const [k, v] of Object.entries(data.actionSet)) {
tmp[k] = _entity => v;
}
registeredActionSets[data.name] = tmp;
if (data.parent) {
registeredActionSetParents[data.name] = data.parent;
}
registeredActionSetTitles[data.name] = {title: data?.title, description: data?.description};
} else if (data?.func === "unregister") {
delete registeredActionSets[data.name];
delete registeredActionSetParents[data.name];
delete registeredActionSetTitles[data.name];
} else if (data?.func === "edit") {
if (!(data.name in registeredActionSets)) {
console.error(`ContextMenu_Message: tried to edit unregistered action set "${data.name}"`);
return;
}
for (const [k, v] of Object.entries(data.actionSet)) {
registeredActionSets[data.name][k] = _entity => v;
}
}
}
Messages.messageReceived.connect(ContextMenu_Message);
Controller.keyPressEvent.connect(ContextMenu_KeyEvent);
Controller.inputEvent.connect(ContextMenu_ActionEvent);
Controller.mousePressEvent.connect(ContextMenu_MousePressEvent);
Controller.mouseReleaseEvent.connect(ContextMenu_MouseReleaseEvent);
Entities.mousePressOnEntity.connect(ContextMenu_EntityClick);
Entities.hoverEnterEntity.connect(ContextMenu_EntityHover);
//Script.update.connect(ContextMenu_Update);
for (const pick of targetingPick) {
Picks.setIgnoreItems(pick, [MyAvatar.sessionUUID]);
}
MyAvatar.sessionUUIDChanged.connect(() => {
for (const pick of targetingPick) {
Picks.setIgnoreItems(pick, [MyAvatar.sessionUUID]);
}
});
Messages.sendLocalMessage(MAIN_CHANNEL, JSON.stringify({func: "startup"}));
Script.scriptEnding.connect(() => {
Messages.messageReceived.disconnect(ContextMenu_Message);
Controller.keyPressEvent.disconnect(ContextMenu_KeyEvent);
Controller.inputEvent.disconnect(ContextMenu_ActionEvent);
Controller.mousePressEvent.disconnect(ContextMenu_MousePressEvent);
Controller.mouseReleaseEvent.disconnect(ContextMenu_MouseReleaseEvent);
Entities.mousePressOnEntity.disconnect(ContextMenu_EntityClick);
Entities.hoverEnterEntity.disconnect(ContextMenu_EntityHover);
//Script.update.disconnect(ContextMenu_Update);
Picks.removePick(targetingPick[0]);
Picks.removePick(targetingPick[1]);
ContextMenu_DeleteMenu();
Messages.sendLocalMessage(MAIN_CHANNEL, JSON.stringify({func: "shutdown"}));
});