mirror of
https://github.com/overte-org/overte.git
synced 2025-08-29 19:26:35 +02:00
843 lines
24 KiB
JavaScript
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"}));
|
|
});
|