soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
22.6 kB
const formatMessage = require('format-message');
const BlockType = require('../../extension-support/block-type');
const ArgumentType = require('../../extension-support/argument-type');
const Cast = require('../../util/cast');
const Icon = require('./icon.png');
const IconController = require('./controller.png');
const SESSION_TYPE = "immersive-vr";
// thanks to twoerner94 for quaternion-to-euler on npm
function quaternionToEuler(quat) {
const q0 = quat[0];
const q1 = quat[1];
const q2 = quat[2];
const q3 = quat[3];
const Rx = Math.atan2(2 * (q0 * q1 + q2 * q3), 1 - (2 * (q1 * q1 + q2 * q2)));
const Ry = Math.asin(2 * (q0 * q2 - q3 * q1));
const Rz = Math.atan2(2 * (q0 * q3 + q1 * q2), 1 - (2 * (q2 * q2 + q3 * q3)));
return [Rx, Ry, Rz];
};
function toRad(deg) {
return deg * (Math.PI / 180);
}
function toDeg(rad) {
return rad * (180 / Math.PI);
}
function toDegRounding(rad) {
const result = toDeg(rad);
if (!String(result).includes('.')) return result;
const split = String(result).split('.');
const endingDecimals = split[1].substring(0, 3);
if ((endingDecimals === '999') && (split[1].charAt(3) === '9')) return Number(split[0]) + 1;
return Number(split[0] + '.' + endingDecimals);
}
/**
* Class for 3D VR blocks
*/
class Jg3DVrBlocks {
constructor(runtime) {
/**
* The runtime instantiating this block package.
*/
this.runtime = runtime;
this.open = false;
this._3d = {};
this.three = {};
// We'll store a wake lock reference here:
this.wakeLock = null;
if (!this.runtime.ext_jg3d) {
vm.extensionManager.loadExtensionURL('jg3d')
.then(() => {
this._3d = this.runtime.ext_jg3d;
this.three = this._3d.three;
});
} else {
this._3d = this.runtime.ext_jg3d;
this.three = this._3d.three;
}
}
/**
* Metadata for this extension and its blocks.
* @returns {object}
*/
getInfo() {
return {
id: 'jg3dVr',
name: '3D VR',
color1: '#B100FE',
color2: '#8000BC',
blockIconURI: Icon,
blocks: [
// CORE
{
opcode: 'isSupported',
text: 'is vr supported?',
blockType: BlockType.BOOLEAN,
disableMonitor: true
},
{
opcode: 'createSession',
text: 'create vr session',
blockType: BlockType.COMMAND
},
{
opcode: 'closeSession',
text: 'close vr session',
blockType: BlockType.COMMAND
},
{
opcode: 'isOpened',
text: 'is vr open?',
blockType: BlockType.BOOLEAN,
disableMonitor: true
},
'---',
{
opcode: 'attachObject',
text: 'attach camera to object named [OBJECT]',
blockType: BlockType.COMMAND,
arguments: {
OBJECT: {
type: ArgumentType.STRING,
defaultValue: "Object1"
}
}
},
{
opcode: 'detachObject',
text: 'detach camera from object',
blockType: BlockType.COMMAND
},
'---',
{
opcode: 'getControllerPosition',
text: 'controller #[INDEX] position [VECTOR3]',
blockType: BlockType.REPORTER,
blockIconURI: IconController,
disableMonitor: true,
arguments: {
INDEX: {
type: ArgumentType.NUMBER,
menu: 'count'
},
VECTOR3: {
type: ArgumentType.STRING,
menu: 'vector3'
}
}
},
{
opcode: 'getControllerRotation',
text: 'controller #[INDEX] rotation [VECTOR3]',
blockType: BlockType.REPORTER,
blockIconURI: IconController,
disableMonitor: true,
arguments: {
INDEX: {
type: ArgumentType.NUMBER,
menu: 'count'
},
VECTOR3: {
type: ArgumentType.STRING,
menu: 'vector3'
}
}
},
{
opcode: 'getControllerSide',
text: 'side of controller #[INDEX]',
blockType: BlockType.REPORTER,
blockIconURI: IconController,
disableMonitor: true,
arguments: {
INDEX: {
type: ArgumentType.NUMBER,
menu: 'count'
}
}
},
'---',
{
opcode: 'getControllerStick',
text: 'joystick axis [XY] of controller #[INDEX]',
blockType: BlockType.REPORTER,
blockIconURI: IconController,
disableMonitor: true,
arguments: {
XY: {
type: ArgumentType.STRING,
menu: 'vector2'
},
INDEX: {
type: ArgumentType.NUMBER,
menu: 'count'
}
}
},
{
opcode: 'getControllerTrig',
text: 'analog value of [TRIGGER] trigger on controller #[INDEX]',
blockType: BlockType.REPORTER,
blockIconURI: IconController,
disableMonitor: true,
arguments: {
TRIGGER: {
type: ArgumentType.STRING,
menu: 'trig'
},
INDEX: {
type: ArgumentType.NUMBER,
menu: 'count'
}
}
},
{
opcode: 'getControllerButton',
text: 'button [BUTTON] on controller #[INDEX] pressed?',
blockType: BlockType.BOOLEAN,
blockIconURI: IconController,
disableMonitor: true,
arguments: {
BUTTON: {
type: ArgumentType.STRING,
menu: 'butt'
},
INDEX: {
type: ArgumentType.NUMBER,
menu: 'count'
}
}
},
{
opcode: 'getControllerTouching',
text: '[BUTTON] on controller #[INDEX] touched?',
blockType: BlockType.BOOLEAN,
blockIconURI: IconController,
disableMonitor: true,
arguments: {
BUTTON: {
type: ArgumentType.STRING,
menu: 'buttAll'
},
INDEX: {
type: ArgumentType.NUMBER,
menu: 'count'
}
}
},
],
menus: {
vector3: {
acceptReporters: true,
items: [
"x",
"y",
"z",
].map(item => ({ text: item, value: item }))
},
vector2: {
acceptReporters: true,
items: [
"x",
"y",
].map(item => ({ text: item, value: item }))
},
butt: {
acceptReporters: true,
items: [
"a",
"b",
"x",
"y",
"joystick",
].map(item => ({ text: item, value: item }))
},
trig: {
acceptReporters: true,
items: [
"back",
"side",
].map(item => ({ text: item, value: item }))
},
buttAll: {
acceptReporters: true,
items: [
"a button",
"b button",
"x button",
"y button",
"joystick",
"back trigger",
"side trigger",
].map(item => ({ text: item, value: item }))
},
count: {
acceptReporters: true,
items: [
"1",
"2",
].map(item => ({ text: item, value: item }))
},
}
};
}
// util
_getRenderer() {
if (!this._3d) return;
return this._3d.renderer;
}
_getGamepad(indexFrom1) {
const index = Cast.toNumber(indexFrom1) - 1;
const three = this._3d;
if (!three.scene) return;
const renderer = this._getRenderer();
if (!renderer) return;
const session = renderer.xr.getSession();
if (!session) return;
const sources = session.inputSources;
const controller = sources[index];
if (!controller) return;
const gamepad = controller.gamepad;
return gamepad;
}
_getController(index) {
const renderer = this._getRenderer();
if (!renderer) return null;
// try to use grip first (which typically has position/quaternion)
const grip = renderer.xr.getControllerGrip(index);
return grip || renderer.xr.getController(index);
}
_getInputSource(index) {
const renderer = this._getRenderer();
if (!renderer) return null;
const session = renderer.xr.getSession();
if (!session) return null;
const sources = session.inputSources;
return sources[index] || null;
}
_disposeImmersive() {
this.session = null;
const renderer = this._getRenderer();
if (!renderer) return;
renderer.xr.enabled = false;
// Clear the animation loop so Three.js stops calling it
renderer.setAnimationLoop(null);
}
async _requestWakeLock() {
if ('wakeLock' in navigator) {
try {
// Request a screen wake lock to prevent idling
this.wakeLock = await navigator.wakeLock.request('screen');
this.wakeLock.addEventListener('release', () => {
console.log('Wake Lock was released');
});
console.log('Wake Lock is active');
} catch (err) {
console.error('Failed to acquire wake lock:', err);
}
}
}
_releaseWakeLock() {
if (this.wakeLock) {
this.wakeLock.release();
this.wakeLock = null;
}
}
async _createImmersive() {
if (!('xr' in navigator)) return false;
const renderer = this._getRenderer();
if (!renderer) return false;
const sessionInit = {
optionalFeatures: ['local-floor', 'bounded-floor', 'hand-tracking', 'layers']
};
const session = await navigator.xr.requestSession(SESSION_TYPE, sessionInit);
this.session = session;
this.open = true;
renderer.xr.enabled = true;
await renderer.xr.setSession(session);
// Request the wake lock so that the session keeps updating even when idle
await this._requestWakeLock();
// When session ends, reset state and release wake lock.
session.addEventListener("end", () => {
this.open = false;
this._disposeImmersive();
this._releaseWakeLock();
});
// Request a reference space (store it so we can use it for the poses)
session.requestReferenceSpace("local").then(space => {
this.localSpace = space;
});
// Use Three.js's setAnimationLoop to drive the render loop
renderer.setAnimationLoop((time, frame) => {
if (!this.open) return;
const threed = this._3d;
if (!threed.camera || !threed.scene) return;
// Render the scene
renderer.render(threed.scene, threed.camera);
// Update controller poses if available
if (this.localSpace && frame) {
this.controllerPoses = {};
const sources = session.inputSources;
for (let i = 0; i < sources.length; i++) {
const inputSource = sources[i];
const pose = frame.getPose(inputSource.targetRaySpace, this.localSpace);
if (pose) {
this.controllerPoses[i] = {
position: pose.transform.position, // {x, y, z}
orientation: pose.transform.orientation // {x, y, z, w}
};
}
}
}
});
return session;
}
// blocks
isSupported() {
if (!('xr' in navigator)) return false;
return navigator.xr.isSessionSupported(SESSION_TYPE);
}
isOpened() {
return this.open;
}
createSession() {
if (this.open) return;
if (this.session) return;
return this._createImmersive();
}
closeSession() {
this.open = false;
if (!this.session) return;
return this.session.end();
}
// extra: attach/detach camera to/from an object in the scene
attachObject(args) {
const three = this._3d;
if (!three.scene) return;
if (!three.camera) return;
const name = Cast.toString(args.OBJECT);
const object = three.scene.getObjectByName(name);
if (!object) return;
object.add(three.camera);
}
detachObject() {
const three = this._3d;
if (!three.scene) return;
if (!three.camera) return;
three.scene.add(three.camera);
}
// Controller input blocks follow
getControllerPosition(args) {
if (!this._3d || !this._3d.scene) return "";
const index = Cast.toNumber(args.INDEX) - 1;
const v = args.VECTOR3;
if (!v || !["x", "y", "z"].includes(v)) return "";
// Use stored pose information if available
if (this.controllerPoses && this.controllerPoses[index]) {
return Cast.toNumber(this.controllerPoses[index].position[v]);
}
const renderer = this._getRenderer();
if (!renderer) return "";
const controller = this._getController(index);
if (!controller) return "";
controller.updateMatrixWorld(true);
// Fallback: get world position via Three.js
const Vector3 = (this.three && this.three.three && this.three.three.Vector3)
? this.three.three.Vector3
: this.three.Vector3;
const position = new Vector3();
controller.getWorldPosition(position);
return Cast.toNumber(position[v]);
}
getControllerRotation(args) {
if (!this._3d || !this._3d.scene) return "";
const index = Cast.toNumber(args.INDEX) - 1;
const v = args.VECTOR3;
if (!v || !["x", "y", "z"].includes(v)) return "";
// Use stored orientation if available
if (this.controllerPoses && this.controllerPoses[index]) {
const o = this.controllerPoses[index].orientation;
const Quaternion = (this.three && this.three.three && this.three.three.Quaternion)
? this.three.three.Quaternion
: this.three.Quaternion;
const quaternion = new Quaternion(o.x, o.y, o.z, o.w);
const Euler = (this.three && this.three.three && this.three.three.Euler)
? this.three.three.Euler
: this.three.Euler;
const euler = new Euler(0, 0, 0, 'YXZ');
euler.setFromQuaternion(quaternion, 'YXZ');
return toDegRounding(euler[v]);
}
const renderer = this._getRenderer();
if (!renderer) return "";
const controller = this._getController(index);
if (!controller) return "";
controller.updateMatrixWorld(true);
const Quaternion = (this.three && this.three.three && this.three.three.Quaternion)
? this.three.three.Quaternion
: this.three.Quaternion;
const quaternion = new Quaternion();
controller.getWorldQuaternion(quaternion);
const Euler = (this.three && this.three.three && this.three.three.Euler)
? this.three.three.Euler
: this.three.Euler;
const euler = new Euler(0, 0, 0, 'YXZ');
euler.setFromQuaternion(quaternion, 'YXZ');
return toDegRounding(euler[v]);
}
getControllerSide(args) {
const three = this._3d;
if (!three.scene) return "";
const renderer = this._getRenderer();
if (!renderer) return "";
const session = renderer.xr.getSession();
if (!session) return "";
const sources = session.inputSources;
const index = Cast.toNumber(args.INDEX) - 1;
const controller = sources[index];
if (!controller) return "";
return controller.handedness;
}
getControllerStick(args) {
const gamepad = this._getGamepad(args.INDEX);
if (!gamepad) return 0;
// For 'y', use axis index 3, otherwise default to index 2.
if (Cast.toString(args.XY) === "y") {
return gamepad.axes[3];
} else {
return gamepad.axes[2];
}
}
getControllerTrig(args) {
const gamepad = this._getGamepad(args.INDEX);
if (!gamepad) return 0;
if (Cast.toString(args.TRIGGER) === "side") {
return gamepad.buttons[1] ? gamepad.buttons[1].value : 0;
} else {
return gamepad.buttons[0] ? gamepad.buttons[0].value : 0;
}
}
getControllerButton(args) {
const gamepad = this._getGamepad(args.INDEX);
if (!gamepad) return false;
const inputSource = this._getInputSource(Cast.toNumber(args.INDEX) - 1);
let handedness = 'right';
if (inputSource && inputSource.handedness) {
handedness = inputSource.handedness;
}
const button = Cast.toString(args.BUTTON).toLowerCase();
if (handedness === 'right') {
switch (button) {
case 'a':
return gamepad.buttons[4] && gamepad.buttons[4].pressed;
case 'b':
return gamepad.buttons[5] && gamepad.buttons[5].pressed;
case 'joystick':
return gamepad.buttons[3] && gamepad.buttons[3].pressed;
}
} else if (handedness === 'left') {
switch (button) {
case 'x':
return gamepad.buttons[4] && gamepad.buttons[4].pressed;
case 'y':
return gamepad.buttons[5] && gamepad.buttons[5].pressed;
case 'joystick':
return gamepad.buttons[3] && gamepad.buttons[3].pressed;
}
}
return false;
}
getControllerTouching(args) {
const gamepad = this._getGamepad(args.INDEX);
if (!gamepad) return false;
const inputSource = this._getInputSource(Cast.toNumber(args.INDEX) - 1);
let handedness = 'right';
if (inputSource && inputSource.handedness) {
handedness = inputSource.handedness;
}
const button = Cast.toString(args.BUTTON).toLowerCase();
if (handedness === 'right') {
switch (button) {
case 'a button':
return gamepad.buttons[4] && gamepad.buttons[4].touched;
case 'b button':
return gamepad.buttons[5] && gamepad.buttons[5].touched;
case 'joystick':
return gamepad.buttons[3] && gamepad.buttons[3].touched;
case 'back trigger':
return gamepad.buttons[0] && gamepad.buttons[0].touched;
case 'side trigger':
return gamepad.buttons[1] && gamepad.buttons[1].touched;
}
} else if (handedness === 'left') {
switch (button) {
case 'x button':
return gamepad.buttons[4] && gamepad.buttons[4].touched;
case 'y button':
return gamepad.buttons[5] && gamepad.buttons[5].touched;
case 'joystick':
return gamepad.buttons[3] && gamepad.buttons[3].touched;
case 'back trigger':
return gamepad.buttons[0] && gamepad.buttons[0].touched;
case 'side trigger':
return gamepad.buttons[1] && gamepad.buttons[1].touched;
}
}
return false;
}
}
module.exports = Jg3DVrBlocks;