Spaces:
Runtime error
Runtime error
| 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; | |