const Cast = require('../../util/cast'); const Clone = require('../../util/clone'); const ExtensionInfo = require("./info"); const Three = require("three"); const BufferGeometryUtils = require('three/examples/jsm/utils/BufferGeometryUtils'); const { ConvexGeometry } = require('three/examples/jsm/geometries/ConvexGeometry'); const { OBJLoader } = require('three/examples/jsm/loaders/OBJLoader.js'); const { GLTFLoader } = require('three/examples/jsm/loaders/GLTFLoader.js'); const { FBXLoader } = require('three/examples/jsm/loaders/FBXLoader.js'); const Color = require('../../util/color'); const MeshLoaders = { OBJ: new OBJLoader(), GLTF: new GLTFLoader(), FBX: new FBXLoader(), } function toRad(deg) { return deg * (Math.PI / 180); } function toDeg(rad) { return rad * (180 / Math.PI); } function normalize(vec) { const length = Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z); return new Three.Vector3(vec.x / length, vec.y / length, vec.z / length); } 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 blocks * @constructor */ class Jg3DBlocks { constructor(runtime) { /** * The runtime instantiating this block package. * @type {Runtime} */ this.runtime = runtime; this.three = Three; // expose addons and that for the funnis this.BufferGeometryUtils = BufferGeometryUtils; this.ConvexGeometry = ConvexGeometry; this.OBJLoader = OBJLoader; this.GLTFLoader = GLTFLoader; this.FBXLoader = FBXLoader; // prism has screenshots, lets tell it to use OUR canvas for them this.runtime.prism_screenshot_checkForExternalCanvas = true; this.runtime.prism_screenshot_externalCanvas = null; // Three.js requirements /** * @type {Three.Scene} */ this.scene = null; /** * @type {Three.Camera} */ this.camera = null; /** * @type {Three.WebGLRenderer} */ this.renderer = null; this.existingSceneObjects = []; this.existingSceneLights = []; // extras this.lastStageSizeWhenRendering = { width: 0, height: 0 } this.savedMeshes = {}; this.sceneLayer = "front"; this.lastStageColor = [255, 255, 255, 0]; // event recievers // stop button clicked or project restarted, dispose of all objects this.runtime.on('PROJECT_STOP_ALL', () => { this.dispose(); this.sceneLayer = "front"; this.updateScratchCanvasRelayering(); }); } /** * Dispose of the scene, camera & renderer (and any objects) */ dispose() { this.existingSceneObjects = []; this.existingSceneLights = []; if (this.scene) { this.scene.remove(); this.scene = null; } if (this.camera) { this.camera.remove(); this.camera = null; } if (this.renderer) { if (this.renderer.domElement) { this.renderer.domElement.remove(); } this.renderer.dispose(); this.renderer = null; this.runtime.prism_screenshot_externalCanvas = null; } } /** * Displays a message for stack blocks. * @param {BlockUtility} util */ stackWarning(util, message) { if (!util) return; if (!util.thread) return; if (!util.thread.stackClick) return; const block = util.thread.blockGlowInFrame; this.runtime.visualReport(block, message); } /** * @returns {object} metadata for this extension and its blocks. */ getInfo() { return ExtensionInfo; } // utilities getScratchCanvas() { return this.runtime.renderer.canvas; } restyleExternalCanvas(canvas) { canvas.style.position = "absolute"; // position above canvas without pushing it down canvas.style.width = "100%"; canvas.style.height = "100%"; // we have no reason to register clicks on the three.js canvas, // so make it click on the scratch canvas instead canvas.style.pointerEvents = "none"; } appendElementAboveScratchCanvas(element) { element.style.zIndex = 450; if (this.sceneLayer === 'back') { element.style.zIndex = 0; } this.getScratchCanvas().parentElement.prepend(element); } updateScratchCanvasRelayering() { const canvas = this.getScratchCanvas(); canvas.style.backgroundColor = "transparent"; canvas.style.position = "relative"; // allows zIndex changes if (Cast.toNumber(canvas.style.zIndex) < 1) { canvas.style.zIndex = 1; } // _backgroundColor4f[3] controls opacity let lastOpacity = this.runtime.renderer._backgroundColor4f[3]; if (this.sceneLayer === 'front') { this.runtime.renderer.setBackgroundColor( this.lastStageColor[0], this.lastStageColor[1], this.lastStageColor[2], 1 ); } if (this.sceneLayer === 'back') { if ( this.runtime.renderer._backgroundColor4f[0] !== this.lastStageColor[0] || this.runtime.renderer._backgroundColor4f[1] !== this.lastStageColor[1] || this.runtime.renderer._backgroundColor4f[2] !== this.lastStageColor[2] ) { // color likely changed to sum else console.log("updated stage color"); this.lastStageColor = this.runtime.renderer._backgroundColor4f; } this.runtime.renderer.setBackgroundColor(0, 0, 0, 0); } // update if changed if (lastOpacity !== this.runtime.renderer._backgroundColor4f[3]) { this.runtime.renderer.dirty = true; } } needsToResizeCanvas() { const stage = { width: this.runtime.stageWidth, height: this.runtime.stageHeight } return stage !== this.lastStageSizeWhenRendering; } mergeVertices(geometry) { const vertices = geometry.attributes.position.array; const vertexMap = {}; const mergedVertices = []; const newIndices = []; let newIndex = 0; for (let i = 0; i < vertices.length; i += 3) { const x = vertices[i]; const y = vertices[i + 1]; const z = vertices[i + 2]; const key = `${x},${y},${z}`; if (vertexMap[key] === undefined) { vertexMap[key] = newIndex; mergedVertices.push(x, y, z); newIndices.push(newIndex); newIndex++; } else { newIndices.push(vertexMap[key]); } } geometry.setAttribute('position', new Three.Float32BufferAttribute(mergedVertices, 3)); geometry.setIndex(new Three.Uint32BufferAttribute(newIndices, 1)); return geometry; } performRaycast(raycaster, object) { const geometry = object.geometry; const mergedGeometry = this.mergeVertices(geometry); const boundingGeometry = new Three.BufferGeometry().copy(mergedGeometry); boundingGeometry.computeBoundingBox(); boundingGeometry.boundingBox.applyMatrix4(object.matrixWorld); const intersection = raycaster.intersectObject(object, true); return intersection.length > 0; } initialize() { // dispose of the previous scene this.dispose(); this.scene = new Three.Scene(); this.renderer = new Three.WebGLRenderer({ preserveDrawingBuffer: true, alpha: true }); this.renderer.penguinMod = { backgroundColor: 0x000000, backgroundOpacity: 1 } this.renderer.setClearColor(0x000000, 1); // add renderer canvas ontop of scratch canvas const canvas = this.renderer.domElement; this.runtime.prism_screenshot_externalCanvas = canvas; this.restyleExternalCanvas(canvas); this.appendElementAboveScratchCanvas(canvas); this.updateScratchCanvasRelayering(); /* dev: test rendering by drawing a cube and see if it appears // const geometry = new Three.BoxGeometry(1, 1, 1); // const material = new Three.MeshBasicMaterial({ color: 0x00ff00 }); // const cube = new Three.Mesh(geometry, material); // this.scene.add(cube) dev update: it worked W */ } render() { if (!this.renderer) return; if (!this.scene) return; if (!this.camera) return; if (this.needsToResizeCanvas()) { this.lastStageSizeWhenRendering = { width: this.runtime.stageWidth, height: this.runtime.stageHeight } /* multiply sizes because the stage looks like doo doo xd we dont need to worry about multiplying 1920 * 2 since projects shouldnt be using that large of a stage but instead a smaller size with the same aspect ratio, penguinmod even says that */ this.renderer.setSize(this.lastStageSizeWhenRendering.width * 2, this.lastStageSizeWhenRendering.height * 2); this.restyleExternalCanvas(this.renderer.domElement); } // when switching between project page & editor, we need to move the canvas again since it gets lost /* todo: create layers so that iframe appears above 3d every time this is done */ this.appendElementAboveScratchCanvas(this.renderer.domElement); this.updateScratchCanvasRelayering(); return new Promise((resolve) => { // we do this to avoid HUGE lag when not waiting 1 tick // and because it waits if the tab isnt focused requestAnimationFrame(() => { // renderer might not exist anymore if (!this.renderer) return; resolve(this.renderer.render(this.scene, this.camera)); }) }) } setCameraPerspective2(args) { if (this.camera) { // remove existing camera this.camera.remove(); this.camera = null; } const fov = Cast.toNumber(args.FOV); const aspect = Cast.toNumber(args.AR); const near = Cast.toNumber(args.NEAR); const far = Cast.toNumber(args.FAR); this.camera = new Three.PerspectiveCamera(fov, aspect, near, far); } setCameraPerspective1(args) { /* todo: make near and far be the same as the existing camera if there is one */ const near = 0.1; const far = 1000; return this.setCameraPerspective2({ FOV: args.FOV, AR: args.AR, NEAR: near, FAR: far }) } setCameraPerspective0(args) { /* todo: make ar, near and far be the same as the existing camera if there is one */ const ar = this.runtime.stageWidth / this.runtime.stageHeight; const near = 0.1; const far = 1000; return this.setCameraPerspective2({ FOV: args.FOV, AR: ar, NEAR: near, FAR: far }) } setCameraPosition(args) { if (!this.camera) return; const position = { x: Cast.toNumber(args.X), y: Cast.toNumber(args.Y), z: Cast.toNumber(args.Z), } this.camera.position.set(position.x, position.y, position.z); } setCameraRotation(args) { if (!this.camera) return; const rotation = { x: Cast.toNumber(args.X), y: Cast.toNumber(args.Y), z: Cast.toNumber(args.Z), } // const euler = new Three.Euler(toRad(rotation.x), toRad(rotation.y), toRad(rotation.z)); // this.camera.setRotationFromEuler(euler); const euler = new Three.Euler(0, 0, 0); this.camera.setRotationFromEuler(euler); this.camera.rotateY(toRad(rotation.y)); this.camera.rotateX(toRad(rotation.x)); this.camera.rotateZ(toRad(rotation.z)); } getCameraPosition(args) { if (!this.camera) return ""; const v = args.VECTOR3; if (!v) return ""; if (!["x", "y", "z"].includes(v)) return ""; return Cast.toNumber(this.camera.position[v]); } getCameraRotation(args) { if (!this.camera) return ""; const v = args.VECTOR3; if (!v) return ""; if (!["x", "y", "z"].includes(v)) return ""; const rotation = Cast.toNumber(this.camera.rotation[v]); // rotation is in radians, convert to degrees but round it // a bit so that we get 46 instead of 45.999999999999996 return toDegRounding(rotation); } setSceneLayer(args) { if (!this.renderer) return; let lastSceneLayer = this.sceneLayer; this.sceneLayer = "front"; if (Cast.toString(args.SIDE) === 'back') { this.sceneLayer = "back"; } if (this.sceneLayer !== lastSceneLayer) { this.lastStageColor = this.runtime.renderer._backgroundColor4f; } this.appendElementAboveScratchCanvas(this.renderer.domElement); this.updateScratchCanvasRelayering(); } setSceneBackgroundColor(args) { if (!this.renderer) return; const rgb = Cast.toRgbColorObject(args.COLOR); const color = Color.rgbToDecimal(rgb); this.renderer.penguinMod.backgroundColor = color; this.renderer.setClearColor(color, this.renderer.penguinMod.backgroundOpacity); } setSceneBackgroundOpacity(args) { if (!this.renderer) return; let opacity = Cast.toNumber(args.OPACITY); if (opacity > 100) opacity = 100; if (opacity < 0) opacity = 0; const backgroundOpac = 1 - (opacity / 100); this.renderer.penguinMod.backgroundOpacity = backgroundOpac; this.renderer.setClearColor(this.renderer.penguinMod.backgroundColor, backgroundOpac); } show3d() { this.renderer.domElement.style.display = "" } hide3d() { this.renderer.domElement.style.display = "none" } is3dVisible() { return this.renderer.domElement.style.display === "" || this.renderer.domElement.style.display === "absolute" } getCameraZoom() { if (!this.camera) return ""; return Cast.toNumber(this.camera.zoom) * 100; } setCameraZoom(args) { if (!this.camera) return; this.camera.zoom = Cast.toNumber(args.ZOOM) / 100; this.camera.updateProjectionMatrix(); } getCameraClipPlane(args) { if (!this.camera) return ""; const plane = args.CLIPPLANE; if (!["near", "far"].includes(plane)) return ""; return this.camera[plane]; } getCameraAspectRatio() { if (!this.camera) return ""; return Cast.toNumber(this.camera.aspect); } getCameraFov() { if (!this.camera) return ""; return Cast.toNumber(this.camera.fov); } isCameraPerspective() { if (!this.camera) return false; return Cast.toBoolean(this.camera.isPerspectiveCamera); } isCameraOrthographic() { if (!this.camera) return false; return Cast.toBoolean(!this.camera.isPerspectiveCamera); } doesObjectExist(args) { if (!this.scene) return false; const name = Cast.toString(args.NAME); // !! is easier to type than if (...) { return true; } return false; return !!this.scene.getObjectByName(name); } createGameObject(args, util, type) { if (!this.scene) return; const name = Cast.toString(args.NAME); if (this.scene.getObjectByName(name)) return this.stackWarning(util, 'An object with this name already exists!'); const position = { x: Cast.toNumber(args.X), y: Cast.toNumber(args.Y), z: Cast.toNumber(args.Z), }; let object; switch (type) { case 'sphere': { const geometry = new Three.SphereGeometry(1); const material = new Three.MeshStandardMaterial({ color: 0xffffff }); const sphere = new Three.Mesh(geometry, material); object = sphere; break; } case 'plane': { const geometry = new Three.PlaneGeometry(1, 1); const material = new Three.MeshStandardMaterial({ color: 0xffffff }); const plane = new Three.Mesh(geometry, material); object = plane; break; } case 'mesh': { const url = Cast.toString(args.URL); // switch loaders based on file type let fileType = 'obj'; switch (Cast.toString(args.FILETYPE)) { case '.glb / .gltf': fileType = 'glb'; break; case '.fbx': fileType = 'fbx'; break; } // we need to do a promise here so that stack continues on load return new Promise((resolve) => { let loader = MeshLoaders.OBJ; switch (fileType) { case 'glb': loader = MeshLoaders.GLTF; break; case 'fbx': loader = MeshLoaders.FBX; break; } if (url in this.savedMeshes) { const mesh = this.savedMeshes[url]; object = mesh.clone(); object.name = name; this.existingSceneObjects.push(name); object.isPenguinMod = true; object.isMeshObj = true; object.position.set(position.x, position.y, position.z); this.scene.add(object); resolve(); return; } else { loader.load(url, (object) => { // success if (loader === MeshLoaders.GLTF) { object = object.scene; } if (loader === MeshLoaders.OBJ) { const material = new Three.MeshStandardMaterial({ color: 0xffffff }); material.wireframe = false; this.updateMaterialOfObjObject(object, material); this.savedMeshes[url] = object; } object.name = name; console.log(object); this.existingSceneObjects.push(name); object.isPenguinMod = true; object.isMeshObj = true; object.position.set(position.x, position.y, position.z); this.scene.add(object); resolve(); }, () => { }, (error) => { console.warn('Failed to load 3D mesh obj;', error); this.stackWarning(util, 'Failed to get the 3D mesh!'); resolve(); }) } }); } case 'light': { const type = Cast.toString(args.LIGHTTYPE); // switch type because there are different types of lights let light; switch (type) { default: { light = new Three.PointLight(0xffffff, 1, 100); break; } } object = light; this.existingSceneLights.push(name); break; } default: { const geometry = new Three.BoxGeometry(1, 1, 1); const material = new Three.MeshStandardMaterial({ color: 0xffffff }); const cube = new Three.Mesh(geometry, material); object = cube; break; } } object.name = name; this.existingSceneObjects.push(name); object.isPenguinMod = true; object.position.set(position.x, position.y, position.z); this.scene.add(object); } createCubeObject(args, util) { this.createGameObject(args, util, 'cube'); } createSphereObject(args, util) { this.createGameObject(args, util, 'sphere'); } createPlaneObject(args, util) { this.createGameObject(args, util, 'plane'); } createMeshObject(args, util) { this.createGameObject(args, util, 'mesh'); } createMeshObjectFileTyped(args, util) { this.createGameObject(args, util, 'mesh'); } createLightObject(args, util) { this.createGameObject(args, util, 'light'); } getMaterialOfObjObject(object) { let material; object.traverse((child) => { if (child instanceof Three.Mesh) { material = child.material; } }); return material; } updateMaterialOfObjObject(object, material) { object.traverse((child) => { if (child instanceof Three.Mesh) { child.material = material; } }); } setObjectPosition(args) { if (!this.scene) return; const name = Cast.toString(args.NAME); const position = { x: Cast.toNumber(args.X), y: Cast.toNumber(args.Y), z: Cast.toNumber(args.Z), }; const object = this.scene.getObjectByName(name); if (!object) return; object.position.set(position.x, position.y, position.z); } setObjectRotation(args) { if (!this.scene) return; const name = Cast.toString(args.NAME); const rotation = { x: Cast.toNumber(args.X), y: Cast.toNumber(args.Y), z: Cast.toNumber(args.Z), }; const object = this.scene.getObjectByName(name); if (!object) return; // const euler = new Three.Euler(toRad(rotation.x), toRad(rotation.y), toRad(rotation.z)); // object.setRotationFromEuler(euler); const euler = new Three.Euler(0, 0, 0); object.setRotationFromEuler(euler); object.rotateY(toRad(rotation.y)); object.rotateX(toRad(rotation.x)); object.rotateZ(toRad(rotation.z)); } setObjectSize(args) { if (!this.scene) return; const name = Cast.toString(args.NAME); const size = { x: Cast.toNumber(args.X) / 100, y: Cast.toNumber(args.Y) / 100, z: Cast.toNumber(args.Z) / 100, }; const object = this.scene.getObjectByName(name); if (!object) return; object.scale.set(size.x, size.y, size.z); } moveObjectUnits(args) { if (!this.scene) return; const name = Cast.toString(args.NAME); const object = this.scene.getObjectByName(name); if (!object) return; const amount = Cast.toNumber(args.AMOUNT); const direction = new Three.Vector3(); object.getWorldDirection(direction); object.position.add(direction.multiplyScalar(amount)); } getObjectPosition(args) { if (!this.scene) return ""; const name = Cast.toString(args.NAME); const object = this.scene.getObjectByName(name); if (!object) return ''; const v = args.VECTOR3; if (!v) return ""; if (!["x", "y", "z"].includes(v)) return ""; return Cast.toNumber(object.position[v]); } getObjectRotation(args) { if (!this.scene) return ""; const name = Cast.toString(args.NAME); const object = this.scene.getObjectByName(name); if (!object) return ''; const v = args.VECTOR3; if (!v) return ""; if (!["x", "y", "z"].includes(v)) return ""; const rotation = Cast.toNumber(object.rotation[v]); // rotation is in radians, convert to degrees but round it // a bit so that we get 46 instead of 45.999999999999996 return toDegRounding(rotation); } getObjectSize(args) { if (!this.scene) return ""; const name = Cast.toString(args.NAME); const object = this.scene.getObjectByName(name); if (!object) return ''; const v = args.VECTOR3; if (!v) return ""; if (!["x", "y", "z"].includes(v)) return ""; return Cast.toNumber(object.scale[v]) * 100; } getObjectColor(args) { if (!this.scene) return ""; const name = Cast.toString(args.NAME); const object = this.scene.getObjectByName(name); if (!object) return ''; return "#" + object.material.color.getHexString() } deleteObject(args) { if (!this.scene) return; const name = Cast.toString(args.NAME); const object = this.scene.getObjectByName(name); if (!object) return; const isLight = object.isLight; object.clear(); this.scene.remove(object); const idx = this.existingSceneObjects.indexOf(name); this.existingSceneObjects.splice(idx, 1); if (isLight) { const lidx = this.existingSceneLights.indexOf(name); this.existingSceneLights.splice(lidx, 1); } } setObjectColor(args) { if (!this.scene) return; const name = Cast.toString(args.NAME); const rgb = Cast.toRgbColorObject(args.COLOR); const color = Color.rgbToDecimal(rgb); const object = this.scene.getObjectByName(name); if (!object) return; if (object.isLight) { object.color.set(color); return } if (object.isMeshObj) { const material = this.getMaterialOfObjObject(object); if (!material) return; material.color.set(color); this.updateMaterialOfObjObject(object, material); return; } object.material.color.set(color); } setObjectShading(args) { if (!this.scene) return; const name = Cast.toString(args.NAME); const on = Cast.toString(args.ONOFF) === 'on'; const object = this.scene.getObjectByName(name); if (!object) return; if (object.isLight) return; if (object.isMeshObj) { const material = this.getMaterialOfObjObject(object); if (!material) return; const color = '#' + material.color.getHexString(); let newMat; if (on) { newMat = new Three.MeshStandardMaterial({ color: color }); } else { newMat = new Three.MeshBasicMaterial({ color: color }); } newMat.color.set(color); this.updateMaterialOfObjObject(object, newMat); return; } const color = '#' + object.material.color.getHexString(); if (on) { object.material = new Three.MeshStandardMaterial({ color: color }); } else { object.material = new Three.MeshBasicMaterial({ color: color }); } } setObjectWireframe(args) { if (!this.scene) return; const name = Cast.toString(args.NAME); const on = Cast.toString(args.ONOFF) === 'on'; const object = this.scene.getObjectByName(name); if (!object) return; if (object.isLight) return; if (object.isMeshObj) { const material = this.getMaterialOfObjObject(object); if (!material) return; material.wireframe = on; this.updateMaterialOfObjObject(object, material); return; } object.material.wireframe = on; } existingObjectsArray(args) { const listType = Cast.toString(args.OBJECTLIST); const validOptions = ["objects", "physical objects", "lights"]; if (!validOptions.includes(listType)) return '[]'; switch (listType) { case 'objects': return JSON.stringify(this.existingSceneObjects); case 'lights': return JSON.stringify(this.existingSceneLights); case 'physical objects': { const physical = this.existingSceneObjects.filter(objectName => { return !this.existingSceneLights.includes(objectName); }); return JSON.stringify(physical); } default: return '[]'; } } objectTouchingObject(args) { if (!this.scene) return false; const name1 = Cast.toString(args.NAME1); const name2 = Cast.toString(args.NAME2); const object1 = this.scene.getObjectByName(name1); const object2 = this.scene.getObjectByName(name2); if (!object1) return false; if (!object2) return false; if (object1.isLight) return false; // currently lights are not supported for collisions if (object2.isLight) return false; // currently lights are not supported for collisions const box1 = new Three.Box3().setFromObject(object1); const box2 = new Three.Box3().setFromObject(object2); const collision = box1.intersectsBox(box2); return collision; } pointTowardsObject(args) { if (!this.scene) return false; const name1 = Cast.toString(args.NAME1); const name2 = Cast.toString(args.NAME2); const object1 = this.scene.getObjectByName(name1); const object2 = this.scene.getObjectByName(name2); if (!object1) return false; if (!object2) return false; object1.lookAt(object2.position); } pointTowardsXYZ(args) { if (!this.scene) return false; const name = Cast.toString(args.NAME); const object = this.scene.getObjectByName(name); if (!object) return false; const position = { x: Cast.toNumber(args.X), y: Cast.toNumber(args.Y), z: Cast.toNumber(args.Z) }; object.lookAt(position.x, position.y, position.z); } MoveCameraBy(args) { if (!this.camera) return; const amount = Cast.toNumber(args.AMOUNT); // comment so it updates bc github was having problems const direction = new Three.Vector3(); this.camera.getWorldDirection(direction); this.camera.position.add(direction.multiplyScalar(amount)); } changeCameraPosition(args) { if (!this.camera) return; this.camera.position.x += Cast.toNumber(args.X); this.camera.position.y += Cast.toNumber(args.Y); this.camera.position.z += Cast.toNumber(args.Z); } changeCameraRotation(args) { if (!this.camera) return; this.camera.rotation.x += Cast.toNumber(args.X); this.camera.rotation.y += Cast.toNumber(args.Y); this.camera.rotation.z += Cast.toNumber(args.Z); } raycastResultToReadable(result) { const newResult = Clone.simple(result).map((intersection) => { console.log(intersection.object.object.name); return intersection.object.object.name; }) return newResult; } rayCollision(args) { if (!this.scene) return ''; const ray = new Three.Raycaster(); const origin = { x: Cast.toNumber(args.X), y: Cast.toNumber(args.Y), z: Cast.toNumber(args.Z), }; const direction = normalize({ x: toRad(Cast.toNumber(args.DX)), y: toRad(Cast.toNumber(args.DY)), z: toRad(Cast.toNumber(args.DZ)), }); ray.set(new Three.Vector3(origin.x, origin.y, origin.z), new Three.Vector3(direction.x, direction.y, direction.z)); const intersects = ray.intersectObjects(this.scene.children, true); if (intersects.length === 0) return ''; const first = intersects[0]; return first.object.name; } rayCollisionCamera() { if (!this.scene) return ''; if (!this.camera) return ''; const ray = new Three.Raycaster(); ray.setFromCamera(new Three.Vector2(), this.camera); const intersects = ray.intersectObjects(this.scene.children, true); if (intersects.length === 0) return ''; const first = intersects[0]; return first.object.name; } rayCollisionArray(args) { if (!this.scene) return '[]'; const ray = new Three.Raycaster(); const origin = { x: Cast.toNumber(args.X), y: Cast.toNumber(args.Y), z: Cast.toNumber(args.Z), }; const direction = normalize({ x: toRad(Cast.toNumber(args.DX)), y: toRad(Cast.toNumber(args.DY)), z: toRad(Cast.toNumber(args.DZ)), }); ray.set(new Three.Vector3(origin.x, origin.y, origin.z), new Three.Vector3(direction.x, direction.y, direction.z)); const intersects = ray.intersectObjects(this.scene.children, true); if (intersects.length === 0) return '[]'; //const result = this.raycastResultToReadable(intersects); return JSON.stringify(intersects); } rayCollisionCameraArray() { if (!this.scene) return '[]'; if (!this.camera) return '[]'; const ray = new Three.Raycaster(); ray.setFromCamera(new Three.Vector2(), this.camera); const intersects = ray.intersectObjects(this.scene.children, true); if (intersects.length === 0) return '[]'; //const result = this.raycastResultToReadable(intersects); return JSON.stringify(intersects); } rayCollisionDistance(args) { if (!this.scene) return ''; const origin = new Three.Vector3( Cast.toNumber(args.X), Cast.toNumber(args.Y), Cast.toNumber(args.Z), ); const direction = normalize(new Three.Vector3( toRad(Cast.toNumber(args.DX)), toRad(Cast.toNumber(args.DY)), toRad(Cast.toNumber(args.DZ)), )); const ray = new Three.Raycaster(origin, direction, 0, args.DIS); const intersects = ray.intersectObjects(this.scene.children, true); if (intersects.length === 0) return ''; const first = intersects[0]; return first.object.name; } rayCollisionArrayDistance(args) { if (!this.scene) return '[]'; const origin = new Three.Vector3( Cast.toNumber(args.X), Cast.toNumber(args.Y), Cast.toNumber(args.Z), ); const direction = normalize(new Three.Vector3( toRad(Cast.toNumber(args.DX)), toRad(Cast.toNumber(args.DY)), toRad(Cast.toNumber(args.DZ)), )); const ray = new Three.Raycaster(origin, direction, 0, args.DIS); const intersects = ray.intersectObjects(this.scene.children, true); if (intersects.length === 0) return '[]'; const result = this.raycastResultToReadable(intersects); return JSON.stringify(result); } getObjectParent(args) { if (!this.scene) return ''; const name = Cast.toString(args.NAME); const object = this.scene.getObjectByName(name); if (!object) return ''; if (!object.parent) return ''; return object.parent.name; } } module.exports = Jg3DBlocks;