From 97809674baf95621adf79db781c3c91a388460c6 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Mon, 15 Dec 2025 08:55:05 -0600 Subject: [PATCH 01/32] Update threejsD.js --- threejsD.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/threejsD.js b/threejsD.js index 043a521..ced1928 100644 --- a/threejsD.js +++ b/threejsD.js @@ -642,6 +642,11 @@ Promise.resolve(load()).then(() => { Scratch.extensions.register(new ThreeRenderer()) class ThreeScene { + constructor() { + this.THREE = THREE; + this.scenes = {}; + } + getInfo() { return { id: "threeScene", @@ -673,7 +678,7 @@ Promise.resolve(load()).then(() => { scene.name = args.NAME scene.background = new THREE.Color("#222") //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - + this.scenes = {...this.scenes, {scene}}; resetor(0) } From e4a038b0005a296b2239648206d77a9810069f8f Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:06:46 -0600 Subject: [PATCH 02/32] Update threejsD.js --- threejsD.js | 6517 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 4560 insertions(+), 1957 deletions(-) diff --git a/threejsD.js b/threejsD.js index ced1928..447584f 100644 --- a/threejsD.js +++ b/threejsD.js @@ -1,88 +1,102 @@ +/* jshint esversion: 11 */ // Name: Extra 3D // ID: threejsExtension -// Description: Use three js inside Turbowarp! A 3D graphics library. +// Description: Use three js inside Turbowarp! A 3D graphics library. // By: Civero // License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors -(function (Scratch) { +(function(Scratch) { "use strict"; if (!Scratch.extensions.unsandboxed) { throw new Error("Three-D extension must run unsandboxed"); } - if (Scratch.vm.runtime.isPackaged) {alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); return} + if (Scratch.vm.runtime.isPackaged) { + alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); + return; + } //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return const vm = Scratch.vm; - const runtime = vm.runtime + const runtime = vm.runtime; const renderer = Scratch.renderer; - const canvas = renderer.canvas + const canvas = renderer.canvas; const Cast = Scratch.Cast; - const menuIconURI = "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; - - let alerts = false - console.log("alerts are "+ (alerts ? "enabled" : "disabled")) + const menuIconURI = + "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; + + let alerts = false; + console.log("alerts are " + (alerts ? "enabled" : "disabled")); - let isMouseDown = { left: false, middle: false, right: false } - let prevMouse = { left: false, middle: false, right: false } + let isMouseDown = { + left: false, + middle: false, + right: false, + }; + let prevMouse = { + left: false, + middle: false, + right: false, + }; - let lastWidth = 0 - let lastHeight = 0 + let lastWidth = 0; + let lastHeight = 0; - let THREE - let clock - let running - let loopId + let THREE; + let clock; + let running; + let loopId; //Addons - let GLTFLoader - let gltf - let OrbitControls - let controls - let BufferGeometryUtils - let TextGeometry - let fontLoad + let GLTFLoader; + let gltf; + let OrbitControls; + let controls; + let BufferGeometryUtils; + let TextGeometry; + let fontLoad; //Physics - let RAPIER - let physicsWorld - - let threeRenderer - let scene - let camera - let eulerOrder = "YXZ" - - let composer - let passes = {} - let customEffects = [] - let renderTargets = {} - - let materials = {} - let geometries = {} - let lights = {} - let models = {} - - let assets = { //should i place materials, geometries; inside too? + let RAPIER; + let physicsWorld; + + let threeRenderer; + let scene; + let camera; + let eulerOrder = "YXZ"; + + let composer; + let passes = {}; + let customEffects = []; + let renderTargets = {}; + + let materials = {}; + let geometries = {}; + let lights = {}; + let models = {}; + + let assets = { + //should i place materials, geometries; inside too? textures: {}, colors: {}, fogs: {}, curves: {}, renderTargets: {}, //not the same as the global one! this one only stores textures - } + }; - let raycastResult = [] + let raycastResult = []; function resetor(level) { - camera = undefined - composer.reset() + camera = undefined; + composer.reset(); - passes = {} - customEffects = [] - renderTargets = {} + passes = {}; + customEffects = []; + renderTargets = {}; - materials = {} - geometries = {} - lights = {} - models = {} + materials = {}; + geometries = {}; + lights = {}; + models = {}; if (level > 0) { assets = { @@ -91,124 +105,140 @@ fogs: {}, curves: {}, renderTargets: {}, - } + }; } - updateComposers() + updateComposers(); } - -//utility - function vector3ToString(prop) { - if (!prop) return "0,0,0"; - const x = (typeof(prop.x) === "number") ? prop.x : (typeof(prop._x) === "number") ? prop._x : (JSON.stringify(prop).includes("X")) ? prop : 0 - const y = (typeof(prop.y) === "number") ? prop.y : (typeof(prop.y) === "number") ? prop._y : 0 - const z = (typeof(prop.z) === "number") ? prop.z : (typeof(prop.z) === "number") ? prop.z : 0 + //utility + function vector3ToString(prop) { + if (!prop) return "0,0,0"; + + const x = + typeof prop.x === "number" ? + prop.x : + typeof prop._x === "number" ? + prop._x : + JSON.stringify(prop).includes("X") ? + prop : + 0; + const y = typeof prop.y === "number" ? prop.y : typeof prop.y === "number" ? prop._y : 0; + const z = typeof prop.z === "number" ? prop.z : typeof prop.z === "number" ? prop.z : 0; + + return [x, y, z]; + } - return [x, y, z] + //objects + function createObject(name, content, parentName) { + let object = getObject(name, true); + if (object) { + removeObject(name); + alerts ? alert(name + " already exsisted, will replace!") : null; } + content.name = name; + content.rotation._order = eulerOrder; + parentName === scene.name ? (object = scene) : (object = getObject(parentName)); + content.physics = false; -//objects - function createObject(name, content, parentName) { - let object = getObject(name, true) - if (object) { - removeObject(name) - alerts ? alert(name + " already exsisted, will replace!") : null - } - content.name = name - content.rotation._order = eulerOrder - parentName === scene.name ? object = scene : object = getObject(parentName) - content.physics = false + object.add(content); + } - object.add(content) - } - function removeObject(name) { - let object = getObject(name) - if (!object) return + function removeObject(name) { + let object = getObject(name); + if (!object) return; - scene.remove(object) + scene.remove(object); - if (object.rigidBody) { - physicsWorld.removeCollider(object.collider, true) - physicsWorld.removeRigidBody(object.rigidBody, true) - object.rigidBody = null - object.collider = null - } - if (object.isLight) { - delete(lights[name]) - } + if (object.rigidBody) { + physicsWorld.removeCollider(object.collider, true); + physicsWorld.removeRigidBody(object.rigidBody, true); + object.rigidBody = null; + object.collider = null; } - function getObject(name, isNew) { - let object = null - if (!scene) { - alerts ? alert("Can not get " + name + ". Create a scene first!") : null; return;} - object = scene.getObjectByName(name) - if (!object && !isNew) {alerts ? alert(name + " does not exist! Add it to scene"):null; return;} - return object + if (object.isLight) { + delete lights[name]; } + } -//materials - function encodeCostume (name) { - if (name.startsWith("data:image/")) return name - return Scratch.vm.editingTarget.sprite.costumes.find(c => c.name === name).asset.encodeDataURI() + function getObject(name, isNew) { + let object = null; + if (!scene) { + alerts ? alert("Can not get " + name + ". Create a scene first!") : null; + return; + } + object = scene.getObjectByName(name); + if (!object && !isNew) { + alerts ? alert(name + " does not exist! Add it to scene") : null; + return; } - function setTexutre (texture, mode, style, x, y) { - texture.colorSpace = THREE.SRGBColorSpace + return object; + } + + //materials + function encodeCostume(name) { + if (name.startsWith("data:image/")) return name; + return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); + } + + function setTexutre(texture, mode, style, x, y) { + texture.colorSpace = THREE.SRGBColorSpace; - if (mode === "Pixelate") { + if (mode === "Pixelate") { texture.minFilter = THREE.NearestFilter; texture.magFilter = THREE.NearestFilter; - } else { //Blur - texture.minFilter = THREE.NearestMipmapLinearFilter - texture.magFilter = THREE.NearestMipmapLinearFilter - } - - if (style === "Repeat") { - texture.wrapS = THREE.RepeatWrapping - texture.wrapT = THREE.RepeatWrapping - texture.repeat.set(x, y) - } + } else { + //Blur + texture.minFilter = THREE.NearestMipmapLinearFilter; + texture.magFilter = THREE.NearestMipmapLinearFilter; + } - texture.generateMipmaps = true; + if (style === "Repeat") { + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(x, y); } - async function resizeImageToSquare(uri, size = 256) { + + texture.generateMipmaps = true; + } + async function resizeImageToSquare(uri, size = 256) { return new Promise((resolve) => { - const img = new Image() - img.onload = () => { - const canvas = document.createElement('canvas') - canvas.width = size - canvas.height = size - const ctx = canvas.getContext('2d') - - // clear + draw image scaled to fit canvas - ctx.clearRect(0, 0, size, size) - ctx.drawImage(img, 0, 0, size, size) - - resolve(canvas.toDataURL()) // return normalized Data URI - //delete canvas? - }; - img.src = uri - }); -} -//light -function updateShadowFrustum(light, focusPos) { - if (light.type !== "DirectionalLight") return + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + + // clear + draw image scaled to fit canvas + ctx.clearRect(0, 0, size, size); + ctx.drawImage(img, 0, 0, size, size); + + resolve(canvas.toDataURL()); // return normalized Data URI + //delete canvas? + }; + img.src = uri; + }); + } + //light + function updateShadowFrustum(light, focusPos) { + if (light.type !== "DirectionalLight") return; // Frustum Size - Increase this value to cover a larger area. const d = 50; // Update Orthographic Shadow Camera Frustum const shadowCamera = light.shadow.camera; - + // Set the width/height of the frustum shadowCamera.left = -d; shadowCamera.right = d; shadowCamera.top = d; shadowCamera.bottom = -d; - + // Determine ranges - shadowCamera.near = 0.1 - shadowCamera.far = 500 + shadowCamera.near = 0.1; + shadowCamera.far = 500; // Position the Light and its Target light.target.position.copy(focusPos); @@ -217,69 +247,79 @@ function updateShadowFrustum(light, focusPos) { // Ensure matrices are updated. light.target.updateMatrixWorld(); - light.shadow.camera.updateProjectionMatrix() - light.shadow.needsUpdate = true; -} -//composer -function updateComposers() { - if (!camera || !scene) return; // nothing to do yet - - // always recreate the RenderPass to point to the current scene/camera - passes["Render"] = new RenderPass(scene, camera); - - // ensure composer has a RenderPass as the first pass - const hasRender = composer.passes.some(p => p && p.scene); - if (!hasRender) composer.addPass(passes["Render"]); - else { - // if composer already has one, replace it so it references current scene/camera - const idx = composer.passes.findIndex(p => p && p.scene); - composer.passes[idx] = passes["Render"]; + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; } -} -//utility -function getMouseNDC(event) { - // Use threeRenderer.domElement for correct offset - const rect = threeRenderer.domElement.getBoundingClientRect(); - const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - return [x, y]; -} -function checkCanvasSize() { - const { width, height } = canvas - if (width !== lastWidth || height !== lastHeight) { - lastWidth = width - lastHeight = height - resize() + //composer + function updateComposers() { + if (!camera || !scene) return; // nothing to do yet + + // always recreate the RenderPass to point to the current scene/camera + passes["Render"] = new RenderPass(scene, camera); + + // ensure composer has a RenderPass as the first pass + const hasRender = composer.passes.some((p) => p && p.scene); + if (!hasRender) composer.addPass(passes["Render"]); + else { + // if composer already has one, replace it so it references current scene/camera + const idx = composer.passes.findIndex((p) => p && p.scene); + composer.passes[idx] = passes["Render"]; + } + } + //utility + function getMouseNDC(event) { + // Use threeRenderer.domElement for correct offset + const rect = threeRenderer.domElement.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + return [x, y]; + } + + function checkCanvasSize() { + const { + width, + height + } = canvas; + if (width !== lastWidth || height !== lastHeight) { + lastWidth = width; + lastHeight = height; + resize(); + } + requestAnimationFrame(checkCanvasSize); //rerun next frame } - requestAnimationFrame(checkCanvasSize) //rerun next frame -} -//physics -function computeWorldBoundingBox(mesh) { + //physics + function computeWorldBoundingBox(mesh) { // Create a Box3 in world coordinates const box = new THREE.Box3().setFromObject(mesh); const size = new THREE.Vector3(); box.getSize(size); const center = new THREE.Vector3(); box.getCenter(center); - return { size, center }; -} -function createCuboidCollider(mesh) { - const { size } = computeWorldBoundingBox(mesh); - const collider = RAPIER.ColliderDesc.cuboid( - size.x / 2, - size.y / 2, - size.z / 2 - ) + return { + size, + center, + }; + } + + function createCuboidCollider(mesh) { + const { + size + } = computeWorldBoundingBox(mesh); + const collider = RAPIER.ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2); return collider; -} -function createBallCollider(mesh) { - const { size } = computeWorldBoundingBox(mesh); + } + + function createBallCollider(mesh) { + const { + size + } = computeWorldBoundingBox(mesh); // radius = 1/2 of the largest verticie const radius = Math.max(size.x, size.y, size.z) / 2; - const collider = RAPIER.ColliderDesc.ball(radius) - return collider //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) -} -function createConvexHullCollider(mesh) { + const collider = RAPIER.ColliderDesc.ball(radius); + return collider; //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) + } + + function createConvexHullCollider(mesh) { mesh.updateWorldMatrix(true, false); const position = mesh.geometry.attributes.position; @@ -287,1040 +327,2192 @@ function createConvexHullCollider(mesh) { const vertex = new THREE.Vector3(); // Matrix for scale only - const scaleMatrix = new THREE.Matrix4().makeScale( - mesh.scale.x, - mesh.scale.y, - mesh.scale.z - ); + const scaleMatrix = new THREE.Matrix4().makeScale(mesh.scale.x, mesh.scale.y, mesh.scale.z); for (let i = 0; i < position.count; i++) { - vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); - vertices.push(vertex.x, vertex.y, vertex.z); + vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); + vertices.push(vertex.x, vertex.y, vertex.z); } const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); return collider; -} -function TriMesh(mesh) { - // Get the positions array (from your geoPoints function) -const positions = mesh.geometry.attributes.position.array; -const numVertices = positions.length / 3; - -// Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] -const indices = Array.from({ length: numVertices }, (_, i) => i); - -const collider = RAPIER.ColliderDesc.trimesh( - positions, - new Uint32Array(indices) -); - -return collider -} -function getModel(model, name) { - const file = runtime.getTargetForStage().getSounds().find(c => c.name === model) - if (!file) return - -return new Promise((resolve, reject) => { - gltf.parse( - file.asset.data.buffer, - "", - gltf => { - const root = gltf.scene - root.traverse(child => { - if (child.isMesh) { - child.castShadow = true - child.receiveShadow = true - } - }); + } - const mixer = new THREE.AnimationMixer(root) - const actions = {} - gltf.animations.forEach(clip => { - const act = mixer.clipAction(clip) - act.clampWhenFinished = true - actions[clip.name] = act - }); + function TriMesh(mesh) { + // Get the positions array (from your geoPoints function) + const positions = mesh.geometry.attributes.position.array; + const numVertices = positions.length / 3; - models[name] = { root, mixer, actions } - resolve(root) + // Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] + const indices = Array.from({ + length: numVertices, }, - error => { - console.error("Error parsing GLB model:", error) - reject(error) - } - )}) -} -async function openFileExplorer(format) { - return new Promise((resolve) => { - const input = document.createElement("input"); - input.type = "file" - input.accept = format - input.multiple = false + (_, i) => i + ); + + const collider = RAPIER.ColliderDesc.trimesh(positions, new Uint32Array(indices)); + + return collider; + } + + function getModel(model, name) { + const file = runtime + .getTargetForStage() + .getSounds() + .find((c) => c.name === model); + if (!file) return; + + return new Promise((resolve, reject) => { + gltf.parse( + file.asset.data.buffer, + "", + (gltf) => { + const root = gltf.scene; + root.traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + const mixer = new THREE.AnimationMixer(root); + const actions = {}; + gltf.animations.forEach((clip) => { + const act = mixer.clipAction(clip); + act.clampWhenFinished = true; + actions[clip.name] = act; + }); + + models[name] = { + root, + mixer, + actions, + }; + resolve(root); + }, + (error) => { + console.error("Error parsing GLB model:", error); + reject(error); + } + ); + }); + } + async function openFileExplorer(format) { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = format; + input.multiple = false; input.onchange = () => { - resolve(input.files) - input.remove() + resolve(input.files); + input.remove(); }; input.click(); - }) -} -function getMeshesUsingTexture(scene, targetTexture) { - const meshes = [] - - scene.traverse(object => { - if (object.material) { - const materials = Array.isArray(object.material) ? object.material : [object.material] - for (const material of materials) { - if (material.map === targetTexture) { - meshes.push(object) - break - } - } + }); + } + + function getMeshesUsingTexture(scene, targetTexture) { + const meshes = []; + + scene.traverse((object) => { + if (object.material) { + const materials = Array.isArray(object.material) ? object.material : [object.material]; + for (const material of materials) { + if (material.map === targetTexture) { + meshes.push(object); + break; + } } - }) - - return meshes -} -function getAsset(path) { - if (typeof(path) == "string") { //string? - if (path.includes("/")) { //has the /? - const value = path.split("/") - console.log(value[0], value[1]) - return assets[value[0]][value[1]] - } + } + }); + + return meshes; } - return JSON.parse(path) //boolean or number -} + function getAsset(path) { + if (typeof path == "string") { + //string? + if (path.includes("/")) { + //has the /? + const value = path.split("/"); + console.log(value[0], value[1]); + return assets[value[0]][value[1]]; + } + } + + return JSON.parse(path); //boolean or number + } -let mouseNDC = [0, 0] -//loops/init -function stopLoop() { - if (!running) return - running = false + let mouseNDC = [0, 0]; + //loops/init + function stopLoop() { + if (!running) return; + running = false; - if (loopId) { - cancelAnimationFrame(loopId) - loopId = null - if (threeRenderer) threeRenderer.clear(); + if (loopId) { + cancelAnimationFrame(loopId); + loopId = null; + if (threeRenderer) threeRenderer.clear(); + } } -} -async function load() { + async function load() { if (!THREE) { - // @ts-ignore - THREE = await import("https://esm.sh/three@0.180.0") + THREE = await import("https://esm.sh/three@0.180.0"); //Addons - GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js") - OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js") - BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js") - TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js") - const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js") - fontLoad = new FontLoader.FontLoader() - - const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8") + GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js"); + OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js"); + BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js"); + TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js"); + const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js"); + fontLoad = new FontLoader.FontLoader(); + + const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8"); const { EffectComposer, EffectPass, RenderPass, - + Effect, BloomEffect, GodRaysEffect, DotScreenEffect, DepthOfFieldEffect, - BlendFunction - } = POSTPROCESSING - //so i can use them later as global - window.EffectComposer = EffectComposer; - window.EffectPass = EffectPass; - window.RenderPass = RenderPass; - window.Effect = Effect; - window.BloomEffect = BloomEffect; - window.GodRaysEffect = GodRaysEffect; - window.DotScreenEffect = DotScreenEffect; - window.DepthOfFieldEffect = DepthOfFieldEffect; - window.BlendFunction = BlendFunction; - - RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0") - await RAPIER.init() - + BlendFunction, + } = POSTPROCESSING; + //so i can use them later as global + window.EffectComposer = EffectComposer; + window.EffectPass = EffectPass; + window.RenderPass = RenderPass; + window.Effect = Effect; + window.BloomEffect = BloomEffect; + window.GodRaysEffect = GodRaysEffect; + window.DotScreenEffect = DotScreenEffect; + window.DepthOfFieldEffect = DepthOfFieldEffect; + window.BlendFunction = BlendFunction; + + RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0"); + await RAPIER.init(); + threeRenderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", antialias: false, stencil: false, - depth: true - }) - threeRenderer.setPixelRatio(window.devicePixelRatio) - threeRenderer.outputColorSpace = THREE.SRGBColorSpace // correct colors - threeRenderer.toneMapping = THREE.ACESFilmicToneMapping // HDR look (test) + depth: true, + }); + threeRenderer.setPixelRatio(window.devicePixelRatio); + threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors + threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) //threeRenderer.toneMappingExposure = 1.0 //(test) - threeRenderer.shadowMap.enabled = true - threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap // (optional) - threeRenderer.domElement.style.pointerEvents = 'auto' //will disable turbowarp mouse events, but enable threejs's + threeRenderer.shadowMap.enabled = true; + threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap; // (optional) + threeRenderer.domElement.style.pointerEvents = "auto"; //will disable turbowarp mouse events, but enable threejs's + + gltf = new GLTFLoader.GLTFLoader(); + clock = new THREE.Clock(); - gltf = new GLTFLoader.GLTFLoader() - clock = new THREE.Clock() - // Example: create a composer - composer = new EffectComposer(threeRenderer, {frameBufferType: THREE.HalfFloatType}) - - renderer.addOverlay( threeRenderer.domElement, "manual" ) - renderer.addOverlay(canvas, "manual") - renderer.setBackgroundColor(1, 1, 1, 0) - - resize() - - window.addEventListener("mousedown", e => { - if (e.button === 0) isMouseDown.left = true - if (e.button === 1) isMouseDown.middle = true - if (e.button === 2) isMouseDown.right = true - }) - window.addEventListener("mouseup", e => { - if (e.button === 0) isMouseDown.left = false; prevMouse.left = false - if (e.button === 1) isMouseDown.middle = false; prevMouse.middle = false - if (e.button === 2) isMouseDown.right = false; prevMouse.right = false - }) + composer = new EffectComposer(threeRenderer, { + frameBufferType: THREE.HalfFloatType, + }); + + renderer.addOverlay(threeRenderer.domElement, "manual"); + renderer.addOverlay(canvas, "manual"); + renderer.setBackgroundColor(1, 1, 1, 0); + + resize(); + + window.addEventListener("mousedown", (e) => { + if (e.button === 0) isMouseDown.left = true; + if (e.button === 1) isMouseDown.middle = true; + if (e.button === 2) isMouseDown.right = true; + }); + window.addEventListener("mouseup", (e) => { + if (e.button === 0) isMouseDown.left = false; + prevMouse.left = false; + if (e.button === 1) isMouseDown.middle = false; + prevMouse.middle = false; + if (e.button === 2) isMouseDown.right = false; + prevMouse.right = false; + }); // prevent contextmenu on right click - threeRenderer.domElement.addEventListener("contextmenu", e => e.preventDefault()); + threeRenderer.domElement.addEventListener("contextmenu", (e) => e.preventDefault()); - threeRenderer.domElement.addEventListener('mousemove', (event) => { + threeRenderer.domElement.addEventListener("mousemove", (event) => { mouseNDC = getMouseNDC(event); - }) - - running = false - load() - - startRenderLoop() - runtime.on('PROJECT_START', () => startRenderLoop()) - runtime.on('PROJECT_STOP_ALL', () => stopLoop()) - runtime.on('STAGE_SIZE_CHANGED', () => {requestAnimationFrame(() => resize())}) - //if (!runtime.isPackaged) checkCanvasSize() //only in editor + }); + + running = false; + load(); + + startRenderLoop(); + runtime.on("PROJECT_START", () => startRenderLoop()); + runtime.on("PROJECT_STOP_ALL", () => stopLoop()); + runtime.on("STAGE_SIZE_CHANGED", () => { + requestAnimationFrame(() => resize()); + }); + //if (!runtime.isPackaged) checkCanvasSize() //only in editor } } -function startRenderLoop() { - if (running) return - running = true - - const loop = () => { - if (!running) return - //RAPIER - if (physicsWorld && scene) { - physicsWorld.step() - - scene.children.forEach(obj => { - if (!(obj.isMesh) || !(obj.physics)) return - if (obj.rigidBody) { - obj.position.copy(obj.rigidBody.translation()) - obj.quaternion.copy(obj.rigidBody.rotation()) - } - }) - - } - if (scene && camera) { - if (controls) controls.update() - const delta = clock.getDelta() - Object.values(models).forEach( model => { if (model) model.mixer.update(delta) } ) + function startRenderLoop() { + if (running) return; + running = true; + + const loop = () => { + if (!running) return; + //RAPIER + if (physicsWorld && scene) { + physicsWorld.step(); + + scene.children.forEach((obj) => { + if (!obj.isMesh || !obj.physics) return; + if (obj.rigidBody) { + obj.position.copy(obj.rigidBody.translation()); + obj.quaternion.copy(obj.rigidBody.rotation()); + } + }); + } + if (scene && camera) { + if (controls) controls.update(); - Object.values(lights).forEach(light => updateShadowFrustum(light, camera.position)) + const delta = clock.getDelta(); + Object.values(models).forEach((model) => { + if (model) model.mixer.update(delta); + }); - //update custom effects time - customEffects.forEach(e => { - if (e.uniforms.get('time')) { - e.uniforms.get('time').value += delta - } - }) - Object.values(renderTargets).forEach(t => { - if ( t.camera.type == "PerspectiveCamera") { - t.camera.aspect = t.target.width / t.target.height - t.camera.updateProjectionMatrix() - } - // get meshes using the texture associated with this target - const displayMeshes = getMeshesUsingTexture(scene, t.target.texture) + Object.values(lights).forEach((light) => updateShadowFrustum(light, camera.position)); - displayMeshes.forEach(mesh => { - mesh.visible = false - }) + //update custom effects time + customEffects.forEach((e) => { + if (e.uniforms.get("time")) { + e.uniforms.get("time").value += delta; + } + }); + Object.values(renderTargets).forEach((t) => { + if (t.camera.type == "PerspectiveCamera") { + t.camera.aspect = t.target.width / t.target.height; + t.camera.updateProjectionMatrix(); + } + // get meshes using the texture associated with this target + const displayMeshes = getMeshesUsingTexture(scene, t.target.texture); + + displayMeshes.forEach((mesh) => { + mesh.visible = false; + }); + + if (t.camera.type == "PerspectiveCamera") { + threeRenderer.setRenderTarget(t.target); + threeRenderer.clear(true, true, true); + threeRenderer.render(scene, t.camera); + } else { + t.target.clear(threeRenderer); + t.camera.update(threeRenderer, scene); //cubeCamera + } - if (t.camera.type == "PerspectiveCamera") { - threeRenderer.setRenderTarget(t.target) - threeRenderer.clear(true, true, true) - threeRenderer.render(scene, t.camera) - } else { - t.target.clear(threeRenderer) - t.camera.update( threeRenderer, scene ) //cubeCamera - } + displayMeshes.forEach((mesh) => { + mesh.visible = true; + }); + }); - displayMeshes.forEach(mesh => { - mesh.visible = true - }) - }) + camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height; + camera.updateProjectionMatrix(); + threeRenderer.setRenderTarget(null); + composer.render(delta); + } - camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height - camera.updateProjectionMatrix() - threeRenderer.setRenderTarget(null) - composer.render(delta) - } + loopId = requestAnimationFrame(loop); + }; - loopId = requestAnimationFrame(loop) + loopId = requestAnimationFrame(loop); } - loopId = requestAnimationFrame(loop) -} + function resize() { + const w = canvas.width; + const h = canvas.height; -function resize() { - const w = canvas.width - const h = canvas.height + threeRenderer.setSize(w, h); + composer.setSize(w, h); + customEffects.forEach((e) => { + if (e.uniforms.get("resolution")) { + e.uniforms.get("resolution").value.set(w, h); + } + }); - threeRenderer.setSize(w, h) - composer.setSize(w, h) - customEffects.forEach(e => { - if (e.uniforms.get('resolution')) { - e.uniforms.get('resolution').value.set(w,h) + if (camera) { + camera.aspect = w / h; + camera.updateProjectionMatrix(); } - }) - - if (camera) { - camera.aspect = w / h - camera.updateProjectionMatrix() -} -} -//wait until all packages are loaded -Promise.resolve(load()).then(() => { - - class threejsExtension { - getInfo() { - return { - id: "threejsExtension", - name: "Extra 3D", - color1: "#222222", - color2: "#222222", - color3: "#11cc99", - menuIconURI, - blockIconURI: menuIconURI, - - blocks: [ - {blockType: Scratch.BlockType.BUTTON, text: "Show Docs", func: "openDocs"}, - {blockType: Scratch.BlockType.BUTTON, text: "Toggle Alerts", func: "alerts"}, - ], - menus: {} - }} - openDocs(){ - open("https://civ3ro.github.io/extensions/Documentation/") - } - alerts() {alerts = !alerts; alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!")} } - Scratch.extensions.register(new threejsExtension()) - - class ThreeRenderer { - getInfo() { - return { - id: "threeRenderer", - name: "Three Renderer", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "setRendererRatio", blockType: Scratch.BlockType.COMMAND, text: "set Pixel Ratio to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"}}}, - {opcode: "eulerOrder", blockType: Scratch.BlockType.COMMAND, text: "set euler order to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "YXZ"}}}, - ], - menus: {} - }} - - setRendererRatio(args) { - threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE) - } - eulerOrder(args) { - eulerOrder = args.VALUE - console.log("euler order set to", eulerOrder) + //wait until all packages are loaded + Promise.resolve(load()).then(() => { + class threejsExtension { + getInfo() { + return { + id: "threejsExtension", + name: "Extra 3D", + color1: "#222222", + color2: "#222222", + color3: "#11cc99", + menuIconURI, + blockIconURI: menuIconURI, + + blocks: [{ + blockType: Scratch.BlockType.BUTTON, + text: "Show Docs", + func: "openDocs", + }, + { + blockType: Scratch.BlockType.BUTTON, + text: "Toggle Alerts", + func: "alerts", + }, + ], + menus: {}, + }; + } + openDocs() { + open("https://civ3ro.github.io/extensions/Documentation/"); + } + alerts() { + alerts = !alerts; + alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!"); + } } + Scratch.extensions.register(new threejsExtension()); - } - Scratch.extensions.register(new ThreeRenderer()) + class ThreeRenderer { + getInfo() { + return { + id: "threeRenderer", + name: "Three Renderer", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "setRendererRatio", + blockType: Scratch.BlockType.COMMAND, + text: "set Pixel Ratio to [VALUE]", + arguments: { + VALUE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + }, + }, + { + opcode: "eulerOrder", + blockType: Scratch.BlockType.COMMAND, + text: "set euler order to [VALUE]", + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "YXZ", + }, + }, + }, + ], + menus: {}, + }; + } - class ThreeScene { - constructor() { - this.THREE = THREE; - this.scenes = {}; + setRendererRatio(args) { + threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE); + } + eulerOrder(args) { + eulerOrder = args.VALUE; + console.log("euler order set to", eulerOrder); + } } - - getInfo() { - return { - id: "threeScene", - name: "Three Scene", - color1: "#4638c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "newScene", blockType: Scratch.BlockType.COMMAND, text: "new Scene [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}}}, - - {opcode: "setSceneProperty", blockType: Scratch.BlockType.COMMAND, text: "set Scene [PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "sceneProperties", defaultValue: "background"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, + Scratch.extensions.register(new ThreeRenderer()); + + class ThreeScene { + constructor() { + this.THREE = THREE; + this.scenes = {}; + } + + getInfo() { + return { + id: "threeScene", + name: "Three Scene", + color1: "#4638c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "newScene", + blockType: Scratch.BlockType.COMMAND, + text: "new Scene [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + }, + }, + + { + opcode: "setSceneProperty", + blockType: Scratch.BlockType.COMMAND, + text: "set Scene [PROPERTY] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "sceneProperties", + defaultValue: "background", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "new Color()", + exemptFromNormalization: true, + }, + }, + }, "---", - {opcode: "getSceneObjects", blockType: Scratch.BlockType.REPORTER, text: "get Scene [THING]", arguments:{THING: {type: Scratch.ArgumentType.STRING, menu: "sceneThings"}}}, - {opcode: "reset", blockType: Scratch.BlockType.COMMAND, text: "Reset Everything"} + { + opcode: "getSceneObjects", + blockType: Scratch.BlockType.REPORTER, + text: "get Scene [THING]", + arguments: { + THING: { + type: Scratch.ArgumentType.STRING, + menu: "sceneThings", + }, + }, + }, + { + opcode: "reset", + blockType: Scratch.BlockType.COMMAND, + text: "Reset Everything", + }, ], - menus: { - sceneProperties: {acceptReporters: false, items: [ - {text: "Background", value: "background"},{text: "Background Blurriness", value: "backgroundBlurriness"},{text: "Background Intensity", value: "backgroundIntensity"},{text: "Background Rotation", value: "backgroundRotation"}, - {text: "Environment", value: "environment"},{text: "Environment Intensity", value: "environmentIntensity"},{text: "Environment Rotation", value: "environmentRotation"},{text: "Fog", value: "fog"}, - ]}, - sceneThings: {acceptReporters: false, items: ["Objects", "Materials", "Geometries","Lights","Scene Properties","Other assets"]}, - - } - }} - - newScene(args) { - scene = new THREE.Scene(); - scene.name = args.NAME - scene.background = new THREE.Color("#222") - //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - this.scenes = {...this.scenes, {scene}}; - resetor(0) - } + menus: { + sceneProperties: { + acceptReporters: false, + items: [{ + text: "Background", + value: "background", + }, + { + text: "Background Blurriness", + value: "backgroundBlurriness", + }, + { + text: "Background Intensity", + value: "backgroundIntensity", + }, + { + text: "Background Rotation", + value: "backgroundRotation", + }, + { + text: "Environment", + value: "environment", + }, + { + text: "Environment Intensity", + value: "environmentIntensity", + }, + { + text: "Environment Rotation", + value: "environmentRotation", + }, + { + text: "Fog", + value: "fog", + }, + ], + }, + sceneThings: { + acceptReporters: false, + items: ["Objects", "Materials", "Geometries", "Lights", "Scene Properties", "Other assets"], + }, + }, + }; + } - reset() { - resetor(1) - } + newScene(args) { + scene = new THREE.Scene(); + scene.name = args.NAME; + scene.background = new THREE.Color("#222"); + //scene.add(new THREE.GridHelper(16, 16)) //future helper section? + this.scenes = { + ...this.scenes, + ...scene, + }; + resetor(0); + } + + reset() { + resetor(1); + } - async setSceneProperty(args) { + async setSceneProperty(args) { const property = args.PROPERTY; const value = getAsset(args.VALUE); scene[property] = value; + } + getSceneObjects(args) { + const names = []; + if (args.THING === "Objects") { + scene.traverse((obj) => { + if (obj.name) names.push(obj.name); //if it has a name, add to list! + }); + } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); + else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); + else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); + else if (args.THING === "Scene Properties") { + console.log(scene); + return "check console"; + } else if (args.THING === "Other assets") return JSON.stringify(assets); + + return JSON.stringify(names); // if objects + } } - getSceneObjects(args){ - const names = []; - if (args.THING === "Objects") { - scene.traverse(obj => { - if (obj.name) names.push(obj.name); //if it has a name, add to list! - }); - } - else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)) - else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)) - else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)) - else if (args.THING === "Scene Properties") {console.log(scene); return "check console"} - else if (args.THING === "Other assets") return JSON.stringify(assets) - - return JSON.stringify(names); // if objects - } + Scratch.extensions.register(new ThreeScene()); - } - Scratch.extensions.register(new ThreeScene()) - - class ThreeCameras { - getInfo() { - return { - id: "threeCameras", - name: "Three Cameras", - color1: "#38c59bff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addCamera", blockType: Scratch.BlockType.COMMAND, text: "add camera [TYPE] [CAMERA] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "cameraTypes"}}}, - {opcode: "setCamera", blockType: Scratch.BlockType.COMMAND, text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "0.1", exemptFromNormalization: true}}}, - {opcode: "getCamera", blockType: Scratch.BlockType.REPORTER, text: "get camera [PROPERTY] of [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}}}, + class ThreeCameras { + getInfo() { + return { + id: "threeCameras", + name: "Three Cameras", + color1: "#38c59bff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addCamera", + blockType: Scratch.BlockType.COMMAND, + text: "add camera [TYPE] [CAMERA] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "cameraTypes", + }, + }, + }, + { + opcode: "setCamera", + blockType: Scratch.BlockType.COMMAND, + text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "cameraProperties", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "0.1", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "getCamera", + blockType: Scratch.BlockType.REPORTER, + text: "get camera [PROPERTY] of [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "cameraProperties", + }, + }, + }, "---", - {opcode: "renderSceneCamera", blockType: Scratch.BlockType.COMMAND, text: "set rendering camera to [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}}}, + { + opcode: "renderSceneCamera", + blockType: Scratch.BlockType.COMMAND, + text: "set rendering camera to [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + }, + }, "---", - {opcode: "cubeCamera", blockType: Scratch.BlockType.COMMAND, text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "cubeCamera"}, GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, + { + opcode: "cubeCamera", + blockType: Scratch.BlockType.COMMAND, + text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "cubeCamera", + }, + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, "---", - {opcode: "renderTarget", blockType: Scratch.BlockType.COMMAND, text: "set a RenderTarget: [RT] for camera [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, - {opcode: "sizeTarget", blockType: Scratch.BlockType.COMMAND, text: "set RenderTarget [RT] size to [W] [H]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, W: {type: Scratch.ArgumentType.NUMBER, defaultValue: 480}, H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 360},} }, - {opcode: "getTarget", blockType: Scratch.BlockType.REPORTER, text: "get RenderTarget: [RT] texture", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, - {opcode: "removeTarget", blockType: Scratch.BlockType.COMMAND, text: "remove RenderTarget: [RT]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, + { + opcode: "renderTarget", + blockType: Scratch.BlockType.COMMAND, + text: "set a RenderTarget: [RT] for camera [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + { + opcode: "sizeTarget", + blockType: Scratch.BlockType.COMMAND, + text: "set RenderTarget [RT] size to [W] [H]", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + W: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 480, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 360, + }, + }, + }, + { + opcode: "getTarget", + blockType: Scratch.BlockType.REPORTER, + text: "get RenderTarget: [RT] texture", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + { + opcode: "removeTarget", + blockType: Scratch.BlockType.COMMAND, + text: "remove RenderTarget: [RT]", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, ], - menus: { - cameraTypes: {acceptReporters: false, items: [ - {text: "Perspective", value: "PerspectiveCamera"}, - ]}, - cameraProperties: {acceptReporters: false, items: [ - {text: "Near", value: "near"},{text: "Far", value: "far"},{text: "FOV", value: "fov"},{text: "Focus (nothing...)", value: "focus"},{text: "Zoom", value: "zoom"}, - ]}, - } - }} - addCamera(args) { - let v2 = new THREE.Vector2() - threeRenderer.getSize(v2) - const object = new THREE.PerspectiveCamera(90, v2.x / v2.y ) - object.position.z = 3 - - createObject(args.CAMERA, object, args.GROUP) - } - setCamera(args) { - let object = getObject(args.CAMERA) - object[args.PROPERTY] = args.VALUE - object.updateProjectionMatrix() - } - getCamera(args) { - let object = getObject(args.CAMERA) - const value = JSON.stringify(object[args.PROPERTY]) - return value - } - renderSceneCamera(args) { - let object = getObject(args.CAMERA) - if (!object) return - camera = object - //reset composer, else it does not update. - composer.passes = [] - passes = {} - customEffects = [] - updateComposers() - } + menus: { + cameraTypes: { + acceptReporters: false, + items: [{ + text: "Perspective", + value: "PerspectiveCamera", + }, ], + }, + cameraProperties: { + acceptReporters: false, + items: [{ + text: "Near", + value: "near", + }, + { + text: "Far", + value: "far", + }, + { + text: "FOV", + value: "fov", + }, + { + text: "Focus (nothing...)", + value: "focus", + }, + { + text: "Zoom", + value: "zoom", + }, + ], + }, + }, + }; + } + addCamera(args) { + let v2 = new THREE.Vector2(); + threeRenderer.getSize(v2); + const object = new THREE.PerspectiveCamera(90, v2.x / v2.y); + object.position.z = 3; - cubeCamera(args) { - // Create cube render target - const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true } ) - // Create cube camera - const cubeCamera = new THREE.CubeCamera( 0.1, 500, cubeRenderTarget ) - createObject(args.CAMERA, cubeCamera, args.GROUP) + createObject(args.CAMERA, object, args.GROUP); + } + setCamera(args) { + let object = getObject(args.CAMERA); + object[args.PROPERTY] = args.VALUE; + object.updateProjectionMatrix(); + } + getCamera(args) { + let object = getObject(args.CAMERA); + const value = JSON.stringify(object[args.PROPERTY]); + return value; + } + renderSceneCamera(args) { + let object = getObject(args.CAMERA); + if (!object) return; + camera = object; + //reset composer, else it does not update. + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } - renderTargets[args.RT] = {target: cubeRenderTarget, camera: cubeCamera} - assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture - } + cubeCamera(args) { + // Create cube render target + const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, { + generateMipmaps: true, + }); + // Create cube camera + const cubeCamera = new THREE.CubeCamera(0.1, 500, cubeRenderTarget); + createObject(args.CAMERA, cubeCamera, args.GROUP); - renderTarget(args) { - let object = getObject(args.CAMERA) - const renderTarget = new THREE.WebGLRenderTarget( - 360, - 360, - { - generateMipmaps: false - } - ) + renderTargets[args.RT] = { + target: cubeRenderTarget, + camera: cubeCamera, + }; + assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture; + } - renderTargets[args.RT] = {target: renderTarget, camera: object} - assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture - } - sizeTarget(args) { - renderTargets[args.RT].target.setSize(args.W, args.H) - } - getTarget(args) { - const t = renderTargets[args.RT].target.texture - console.log(t, renderTargets[args.RT]) - return `renderTargets/${t.uuid}` - } - removeTarget(args) { - delete(assets.renderTargets[renderTargets[args.RT].target.texture.uuid]) - renderTargets[args.RT].target.dispose() - delete(renderTargets[args.RT]) + renderTarget(args) { + let object = getObject(args.CAMERA); + const renderTarget = new THREE.WebGLRenderTarget(360, 360, { + generateMipmaps: false, + }); + + renderTargets[args.RT] = { + target: renderTarget, + camera: object, + }; + assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; + } + sizeTarget(args) { + renderTargets[args.RT].target.setSize(args.W, args.H); + } + getTarget(args) { + const t = renderTargets[args.RT].target.texture; + console.log(t, renderTargets[args.RT]); + return `renderTargets/${t.uuid}`; + } + removeTarget(args) { + delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; + renderTargets[args.RT].target.dispose(); + delete renderTargets[args.RT]; + } } - } - Scratch.extensions.register(new ThreeCameras()) - - class ThreeObjects { - getInfo() { - return { - id: "threeObjects", - name: "Three Objects", - color1: "#38c567ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addObject", blockType: Scratch.BlockType.COMMAND, text: "add object [OBJECT3D] [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},TYPE: {type: Scratch.ArgumentType.STRING, menu: "objectTypes"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "cloneObject", blockType: Scratch.BlockType.COMMAND, text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myClone"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + Scratch.extensions.register(new ThreeCameras()); + + class ThreeObjects { + getInfo() { + return { + id: "threeObjects", + name: "Three Objects", + color1: "#38c567ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addObject", + blockType: Scratch.BlockType.COMMAND, + text: "add object [OBJECT3D] [TYPE] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "objectTypes", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "cloneObject", + blockType: Scratch.BlockType.COMMAND, + text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myClone", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "setObject", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - {opcode: "getObject", blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of object [OBJECT3D]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}}}, - {opcode: "objectE", blockType: Scratch.BlockType.BOOLEAN, text: "is there an object [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + { + opcode: "setObject", + blockType: Scratch.BlockType.COMMAND, + text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectProperties", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + { + opcode: "getObject", + blockType: Scratch.BlockType.REPORTER, + text: "get [PROPERTY] of object [OBJECT3D]", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectProperties", + }, + }, + }, + { + opcode: "objectE", + blockType: Scratch.BlockType.BOOLEAN, + text: "is there an object [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "removeObject", blockType: Scratch.BlockType.COMMAND, text: "remove object [OBJECT3D] from scene", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - - {blockType: Scratch.BlockType.LABEL, text: " ↳ Transforms"}, - {opcode: "setObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, + { + opcode: "removeObject", + blockType: Scratch.BlockType.COMMAND, + text: "remove object [OBJECT3D] from scene", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: " ↳ Transforms", + }, + { + opcode: "setObjectV3", + extensions: ["colours_motion"], + blockType: Scratch.BlockType.COMMAND, + text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectVector3", + defaultValue: "position", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of [OBJECT3D]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - - {blockType: Scratch.BlockType.LABEL, text: "↳ Materials"}, - {opcode: "newMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "new material [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "materialTypes", defaultValue: "MeshStandardMaterial"}}}, - {opcode: "materialE",extensions: ["colours_looks"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a material [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, - {opcode: "removeMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "remove material [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, - {opcode: "setMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [PROPERTY] of [NAME] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "materialProperties", defaultValue: "color"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, - {opcode: "setBlending",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] blending to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "blendModes"}}}, - {opcode: "setDepth",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] depth to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "depthModes"}}}, - - {blockType: Scratch.BlockType.LABEL, text: "↳ Geometries"}, - {opcode: "newGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new geometry [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "geometryTypes", defaultValue: "BoxGeometry"}}}, - {opcode: "geometryE",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a geometry [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - {opcode: "removeGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "remove geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - "---", - {opcode: "newGeo",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new empty geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, - {opcode: "geoPoints",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] vertex points to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, - {opcode: "geoUVs",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] UVs to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[UVs]"}}}, - "---", - {opcode: "splines", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create spline [NAME] from curve [CURVE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}}}, - {opcode: "splineModel", extensions: ["colours_operators"], blockType: Scratch.BlockType.COMMAND, text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, MODEL: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}, SPACING: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, - "---", - {blockType: Scratch.BlockType.BUTTON, text: "Convert font to JSON", func: "openConv"}, - {blockType: Scratch.BlockType.BUTTON, text: "Load JSON font file", func: "loadFont"}, - {opcode: "text", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myText"}, TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "C-369"}, FONT: {type: Scratch.ArgumentType.STRING, menu: "fonts"}, S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, D: {type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1}, CS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 6}}}, - ], - menus: { - objectVector3: {acceptReporters: false, items: [ - {text: "Positon", value: "position"},{text: "Rotation", value: "rotation"},{text: "Scale", value: "scale"},{text: "Facing Direction (.up)", value: "up"} - ]}, - objectProperties: {acceptReporters: false, items: [ - {text: "Geometry", value: "geometry"},{text: "Material", value: "material"},{text: "Visible (true/false)", value: "visible"}, - ]}, - objectTypes: { acceptReporters: false, items: [ - { text: "Mesh", value: "Mesh" }, { text: "Sprite", value: "Sprite" }, { text: "Points", value: "Points" }, { text: "Line", value: "Line" }, { text: "Group", value: "Group" } - ]}, - XYZ: {acceptReporters: false, items: [{text: "X", value: "x"},{text: "Y", value: "y"},{text: "Z", value: "z"}]}, - materialProperties: {acceptReporters: false, items: [ - "|GENERAL| <-- not a property", - { text: "Color", value: "color" }, - { text: "Map", value: "map" }, - { text: "Opacity", value: "opacity" }, - { text: "Transparent", value: "transparent" }, - { text: "Alpha Map", value: "alphaMap" }, - { text: "Alpha Test", value: "alphaTest" }, - { text: "Depth Test", value: "depthTest" }, - { text: "Depth Write", value: "depthWrite" }, - { text: "Color Write", value: "colorWrite" }, - { text: "Side", value: "side" }, - { text: "Visible", value: "visible" },/* - { text: "Blending", value: "blending" }, - { text: "Blend Src", value: "blendSrc" }, - { text: "Blend Dst", value: "blendDst" }, - { text: "Blend Equation", value: "blendEquation" }, - { text: "Blend Src Alpha", value: "blendSrcAlpha" }, - { text: "Blend Dst Alpha", value: "blendDstAlpha" }, - { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ - { text: "Blend Aplha", value: "blendAplha" }, - { text: "Blend Color", value: "blendColor" }, - { text: "Alpha Hash", value: "alphaHash" }, - { text: "Premultiplied Alpha", value: "premultipliedAlpha" }, - - { text: "Tone Mapped", value: "toneMapped" }, - { text: "Fog", value: "fog" }, - { text: "Flat Shading", value: "flatShading" }, - - "|MESH Standard / Physical| <-- not a property", - { text: "Metalness", value: "metalness" }, - { text: "Metalness Map", value: "metalnessMap" }, - { text: "Roughness", value: "roughness" }, - { text: "Reflectivity", value: "reflectivity" }, - { text: "Roughness Map", value: "roughnessMap" }, - { text: "Emissive", value: "emissive" }, - { text: "Emissive Intensity", value: "emissiveIntensity" }, - { text: "Emissive Map", value: "emissiveMap" }, - { text: "Env Map", value: "envMap" }, - { text: "Env Map Intensity", value: "envMapIntensity" }, - { text: "Env Map Rotation", value: "envMapRotation" }, - { text: "Ior", value: "ior" }, - { text: "Refraction Ratio", value: "refractionRatio" }, - { text: "Clearcoat", value: "clearcoat" }, - { text: "Clearcoat Map", value: "clearcoatMap" }, - { text: "Clearcoat Roughness", value: "clearcoatRoughness" }, - { text: "Clearcoat Roughness Map", value: "clearcoatRoughnessMap" }, - { text: "Dispersion", value: "dispersion" }, - { text: "Sheen", value: "sheen" }, - { text: "Sheen Color", value: "sheenColor" }, - { text: "Sheen Color Map", value: "sheenColorMap" }, - { text: "Sheen Roughness", value: "sheenRoughness" }, - { text: "Sheen Roughness Map", value: "sheenRoughnessMap" }, - { text: "Specular Color", value: "specularColor" }, - { text: "Specular Color Map", value: "specularColorMap" }, - { text: "Specular Intensity", value: "specularIntensity" }, - { text: "Specular Intensity Map", value: "specularIntensityMap" }, - { text: "Transmission", value: "transmission" }, - { text: "Transmission Map", value: "transmissionMap" }, - { text: "Thickness", value: "thickness" }, - { text: "Thickness Map", value: "thicknessMap" }, - { text: "Anisotropy", value: "anisotropy" }, - { text: "Anisotropy Map", value: "anisotropyMap" }, - { text: "Anisotropy Rotation", value: "anisotropyRotation" }, - { text: "Attenuation Distance", value: "attenuationDistance" }, - { text: "Attenuation Color", value: "attenuationColor" }, - { text: "Thickness", value: "thickness" }, - { text: "Iridescence", value: "iridescence" }, - { text: "Iridescence Ior", value: "iridescenceIOR" }, - { text: "Iridescence Map", value: "iridescenceMap" }, - { text: "Iridescence Thickness Range", value: "iridescenceThicknessRange" }, - - "|MESH Displacement / Normal / Bump| <-- not a property", - { text: "Displacement Map", value: "displacementMap" }, - { text: "Displacement Scale", value: "displacementScale" }, - { text: "Displacement Bias", value: "displacementBias" }, - { text: "Bump Map", value: "bumpMap" }, - { text: "Bump Scale", value: "bumpScale" }, - { text: "Normal Map Type", value: "normalMapType" }, - - "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", - { text: "Shininess", value: "shininess" }, - - { text: "Wireframe", value: "wireframe" }, - { text: "Wireframe Linewidth", value: "wireframeLinewidth" }, - { text: "Wireframe Linecap", value: "wireframeLinecap" }, - { text: "Wireframe Linejoin", value: "wireframeLinejoin" }, - - "|POINTS| <-- not a property", - { text: "Size", value: "size" }, - { text: "Size Attenuation", value: "sizeAttenuation" }, - - "|LINES| <-- not a property", - { text: "Scale", value: "scale" }, - { text: "Dash Size", value: "dashSize" }, - { text: "Gap Size", value: "gapSize" }, - - "|SPRITES| <-- not a property", - { text: "Rotation", value: "rotation" } -]}, - blendModes: {acceptReporters: false, items: [ - { text: "No Blending", value: "NoBlending" },{ text: "Normal Blending", value: "NormalBlending" },{ text: "Additive Blending", value: "AdditiveBlending" },{ text: "Subtractive Blending", value: "SubtractiveBlending" },{ text: "Multiply Blending", value: "MultiplyBlending" },{ text: "Custom Blending", value: "CustomBlending" } - ]}, - depthModes: {acceptReporters: false, items: [ - { text: "Never Depth", value: "NeverDepth" },{ text: "Always Depth", value: "AlwaysDepth" },{ text: "Equal Depth", value: "EqualDepth" },{ text: "Less Depth", value: "LessDepth" },{ text: "Less Equal Depth", value: "LessEqualDepth" },{ text: "Greater Equal Depth", value: "GreaterEqualDepth" },{ text: "Greater Depth", value: "GreaterDepth" },{ text: "Not Equal Depth", value: "NotEqualDepth" } - ]}, - materialTypes:{acceptReporters: false, items: [ - {text:"Mesh Basic Material",value:"MeshBasicMaterial"},{text:"Mesh Standard Material",value:"MeshStandardMaterial"},{text:"Mesh Physical Material",value:"MeshPhysicalMaterial"},{text:"Mesh Lambert Material",value:"MeshLambertMaterial"},{text:"Mesh Phong Material",value:"MeshPhongMaterial"},{text:"Mesh Depth Material",value:"MeshDepthMaterial"},{text:"Mesh Normal Material",value:"MeshNormalMaterial"},{text:"Mesh Matcap Material",value:"MeshMatcapMaterial"},{text:"Mesh Toon Material",value:"MeshToonMaterial"},{text:"Line Basic Material",value:"LineBasicMaterial"},{text:"Line Dashed Material",value:"LineDashedMaterial"},{text:"Points Material",value:"PointsMaterial"},{text:"Sprite Material",value:"SpriteMaterial"},{text:"Shadow Material",value:"ShadowMaterial"} - ]}, - textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, - textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, - geometryTypes: {acceptReporters: false, items: [ - {text: "Box Geometry", value: "BoxGeometry"},{text: "Sphere Geometry", value: "SphereGeometry"},{text: "Cylinder Geometry", value: "CylinderGeometry"},{text: "Plane Geometry", value: "PlaneGeometry"},{text: "Circle Geometry", value: "CircleGeometry"},{text: "Torus Geometry", value: "TorusGeometry"},{text: "Torus Knot Geometry", value: "TorusKnotGeometry"}, - ]}, - modelsList: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) - if (models.length < 1) return [["Load a model! (GLB Loader category)"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - fonts: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.json')) - if (models.length < 1) return [["Load a font!"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - - } - }} - - addObject(args) { - const object = new THREE[args.TYPE](); - - object.castShadow = true - object.receiveShadow = true - - createObject(args.OBJECT3D, object, args.GROUP) - } - cloneObject(args) { - let object = getObject(args.OBJECT3D) - const clone = object.clone(true) - clone.name - createObject(args.NAME, clone, args.GROUP) - } - setObjectV3(args) { - let object = getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) + { + opcode: "getObjectV3", + extensions: ["colours_motion"], + blockType: Scratch.BlockType.REPORTER, + text: "get [PROPERTY] of [OBJECT3D]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectVector3", + defaultValue: "position", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Materials", + }, + { + opcode: "newMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "new material [NAME] [TYPE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "materialTypes", + defaultValue: "MeshStandardMaterial", + }, + }, + }, + { + opcode: "materialE", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.BOOLEAN, + text: "is there a material [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + }, + }, + { + opcode: "removeMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "remove material [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + }, + }, + { + opcode: "setMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [PROPERTY] of [NAME] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "materialProperties", + defaultValue: "color", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "new Color()", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "setBlending", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [NAME] blending to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + }, + }, + }, + { + opcode: "setDepth", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [NAME] depth to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "depthModes", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Geometries", + }, + { + opcode: "newGeometry", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "new geometry [NAME] [TYPE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "geometryTypes", + defaultValue: "BoxGeometry", + }, + }, + }, + { + opcode: "geometryE", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.BOOLEAN, + text: "is there a geometry [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + { + opcode: "removeGeometry", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "remove geometry [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + "---", + { + opcode: "newGeo", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "new empty geometry [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[points]", + }, + }, + }, + { + opcode: "geoPoints", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "set geometry [NAME] vertex points to [POINTS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[points]", + }, + }, + }, + { + opcode: "geoUVs", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "set geometry [NAME] UVs to [POINTS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[UVs]", + }, + }, + }, + "---", + { + opcode: "splines", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "create spline [NAME] from curve [CURVE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySpline", + }, + CURVE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[curve]", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "splineModel", + extensions: ["colours_operators"], + blockType: Scratch.BlockType.COMMAND, + text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySpline", + }, + MODEL: { + type: Scratch.ArgumentType.STRING, + menu: "modelsList", + }, + CURVE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[curve]", + exemptFromNormalization: true, + }, + SPACING: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.BUTTON, + text: "Convert font to JSON", + func: "openConv", + }, + { + blockType: Scratch.BlockType.BUTTON, + text: "Load JSON font file", + func: "loadFont", + }, + { + opcode: "text", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myText", + }, + TEXT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "C-369", + }, + FONT: { + type: Scratch.ArgumentType.STRING, + menu: "fonts", + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + D: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.1, + }, + CS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 6, + }, + }, + }, + ], + menus: { + objectVector3: { + acceptReporters: false, + items: [{ + text: "Positon", + value: "position", + }, + { + text: "Rotation", + value: "rotation", + }, + { + text: "Scale", + value: "scale", + }, + { + text: "Facing Direction (.up)", + value: "up", + }, + ], + }, + objectProperties: { + acceptReporters: false, + items: [{ + text: "Geometry", + value: "geometry", + }, + { + text: "Material", + value: "material", + }, + { + text: "Visible (true/false)", + value: "visible", + }, + ], + }, + objectTypes: { + acceptReporters: false, + items: [{ + text: "Mesh", + value: "Mesh", + }, + { + text: "Sprite", + value: "Sprite", + }, + { + text: "Points", + value: "Points", + }, + { + text: "Line", + value: "Line", + }, + { + text: "Group", + value: "Group", + }, + ], + }, + XYZ: { + acceptReporters: false, + items: [{ + text: "X", + value: "x", + }, + { + text: "Y", + value: "y", + }, + { + text: "Z", + value: "z", + }, + ], + }, + materialProperties: { + acceptReporters: false, + items: [ + "|GENERAL| <-- not a property", + { + text: "Color", + value: "color", + }, + { + text: "Map", + value: "map", + }, + { + text: "Opacity", + value: "opacity", + }, + { + text: "Transparent", + value: "transparent", + }, + { + text: "Alpha Map", + value: "alphaMap", + }, + { + text: "Alpha Test", + value: "alphaTest", + }, + { + text: "Depth Test", + value: "depthTest", + }, + { + text: "Depth Write", + value: "depthWrite", + }, + { + text: "Color Write", + value: "colorWrite", + }, + { + text: "Side", + value: "side", + }, + { + text: "Visible", + value: "visible", + }, + /* + { text: "Blending", value: "blending" }, + { text: "Blend Src", value: "blendSrc" }, + { text: "Blend Dst", value: "blendDst" }, + { text: "Blend Equation", value: "blendEquation" }, + { text: "Blend Src Alpha", value: "blendSrcAlpha" }, + { text: "Blend Dst Alpha", value: "blendDstAlpha" }, + { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ + { + text: "Blend Aplha", + value: "blendAplha", + }, + { + text: "Blend Color", + value: "blendColor", + }, + { + text: "Alpha Hash", + value: "alphaHash", + }, + { + text: "Premultiplied Alpha", + value: "premultipliedAlpha", + }, + + { + text: "Tone Mapped", + value: "toneMapped", + }, + { + text: "Fog", + value: "fog", + }, + { + text: "Flat Shading", + value: "flatShading", + }, + + "|MESH Standard / Physical| <-- not a property", + { + text: "Metalness", + value: "metalness", + }, + { + text: "Metalness Map", + value: "metalnessMap", + }, + { + text: "Roughness", + value: "roughness", + }, + { + text: "Reflectivity", + value: "reflectivity", + }, + { + text: "Roughness Map", + value: "roughnessMap", + }, + { + text: "Emissive", + value: "emissive", + }, + { + text: "Emissive Intensity", + value: "emissiveIntensity", + }, + { + text: "Emissive Map", + value: "emissiveMap", + }, + { + text: "Env Map", + value: "envMap", + }, + { + text: "Env Map Intensity", + value: "envMapIntensity", + }, + { + text: "Env Map Rotation", + value: "envMapRotation", + }, + { + text: "Ior", + value: "ior", + }, + { + text: "Refraction Ratio", + value: "refractionRatio", + }, + { + text: "Clearcoat", + value: "clearcoat", + }, + { + text: "Clearcoat Map", + value: "clearcoatMap", + }, + { + text: "Clearcoat Roughness", + value: "clearcoatRoughness", + }, + { + text: "Clearcoat Roughness Map", + value: "clearcoatRoughnessMap", + }, + { + text: "Dispersion", + value: "dispersion", + }, + { + text: "Sheen", + value: "sheen", + }, + { + text: "Sheen Color", + value: "sheenColor", + }, + { + text: "Sheen Color Map", + value: "sheenColorMap", + }, + { + text: "Sheen Roughness", + value: "sheenRoughness", + }, + { + text: "Sheen Roughness Map", + value: "sheenRoughnessMap", + }, + { + text: "Specular Color", + value: "specularColor", + }, + { + text: "Specular Color Map", + value: "specularColorMap", + }, + { + text: "Specular Intensity", + value: "specularIntensity", + }, + { + text: "Specular Intensity Map", + value: "specularIntensityMap", + }, + { + text: "Transmission", + value: "transmission", + }, + { + text: "Transmission Map", + value: "transmissionMap", + }, + { + text: "Thickness", + value: "thickness", + }, + { + text: "Thickness Map", + value: "thicknessMap", + }, + { + text: "Anisotropy", + value: "anisotropy", + }, + { + text: "Anisotropy Map", + value: "anisotropyMap", + }, + { + text: "Anisotropy Rotation", + value: "anisotropyRotation", + }, + { + text: "Attenuation Distance", + value: "attenuationDistance", + }, + { + text: "Attenuation Color", + value: "attenuationColor", + }, + { + text: "Thickness", + value: "thickness", + }, + { + text: "Iridescence", + value: "iridescence", + }, + { + text: "Iridescence Ior", + value: "iridescenceIOR", + }, + { + text: "Iridescence Map", + value: "iridescenceMap", + }, + { + text: "Iridescence Thickness Range", + value: "iridescenceThicknessRange", + }, + + "|MESH Displacement / Normal / Bump| <-- not a property", + { + text: "Displacement Map", + value: "displacementMap", + }, + { + text: "Displacement Scale", + value: "displacementScale", + }, + { + text: "Displacement Bias", + value: "displacementBias", + }, + { + text: "Bump Map", + value: "bumpMap", + }, + { + text: "Bump Scale", + value: "bumpScale", + }, + { + text: "Normal Map Type", + value: "normalMapType", + }, + + "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", + { + text: "Shininess", + value: "shininess", + }, + + { + text: "Wireframe", + value: "wireframe", + }, + { + text: "Wireframe Linewidth", + value: "wireframeLinewidth", + }, + { + text: "Wireframe Linecap", + value: "wireframeLinecap", + }, + { + text: "Wireframe Linejoin", + value: "wireframeLinejoin", + }, + + "|POINTS| <-- not a property", + { + text: "Size", + value: "size", + }, + { + text: "Size Attenuation", + value: "sizeAttenuation", + }, + + "|LINES| <-- not a property", + { + text: "Scale", + value: "scale", + }, + { + text: "Dash Size", + value: "dashSize", + }, + { + text: "Gap Size", + value: "gapSize", + }, + + "|SPRITES| <-- not a property", + { + text: "Rotation", + value: "rotation", + }, + ], + }, + blendModes: { + acceptReporters: false, + items: [{ + text: "No Blending", + value: "NoBlending", + }, + { + text: "Normal Blending", + value: "NormalBlending", + }, + { + text: "Additive Blending", + value: "AdditiveBlending", + }, + { + text: "Subtractive Blending", + value: "SubtractiveBlending", + }, + { + text: "Multiply Blending", + value: "MultiplyBlending", + }, + { + text: "Custom Blending", + value: "CustomBlending", + }, + ], + }, + depthModes: { + acceptReporters: false, + items: [{ + text: "Never Depth", + value: "NeverDepth", + }, + { + text: "Always Depth", + value: "AlwaysDepth", + }, + { + text: "Equal Depth", + value: "EqualDepth", + }, + { + text: "Less Depth", + value: "LessDepth", + }, + { + text: "Less Equal Depth", + value: "LessEqualDepth", + }, + { + text: "Greater Equal Depth", + value: "GreaterEqualDepth", + }, + { + text: "Greater Depth", + value: "GreaterDepth", + }, + { + text: "Not Equal Depth", + value: "NotEqualDepth", + }, + ], + }, + materialTypes: { + acceptReporters: false, + items: [{ + text: "Mesh Basic Material", + value: "MeshBasicMaterial", + }, + { + text: "Mesh Standard Material", + value: "MeshStandardMaterial", + }, + { + text: "Mesh Physical Material", + value: "MeshPhysicalMaterial", + }, + { + text: "Mesh Lambert Material", + value: "MeshLambertMaterial", + }, + { + text: "Mesh Phong Material", + value: "MeshPhongMaterial", + }, + { + text: "Mesh Depth Material", + value: "MeshDepthMaterial", + }, + { + text: "Mesh Normal Material", + value: "MeshNormalMaterial", + }, + { + text: "Mesh Matcap Material", + value: "MeshMatcapMaterial", + }, + { + text: "Mesh Toon Material", + value: "MeshToonMaterial", + }, + { + text: "Line Basic Material", + value: "LineBasicMaterial", + }, + { + text: "Line Dashed Material", + value: "LineDashedMaterial", + }, + { + text: "Points Material", + value: "PointsMaterial", + }, + { + text: "Sprite Material", + value: "SpriteMaterial", + }, + { + text: "Shadow Material", + value: "ShadowMaterial", + }, + ], + }, + textureModes: { + acceptReporters: false, + items: ["Pixelate", "Blur"], + }, + textureStyles: { + acceptReporters: false, + items: ["Repeat", "Clamp"], + }, + geometryTypes: { + acceptReporters: false, + items: [{ + text: "Box Geometry", + value: "BoxGeometry", + }, + { + text: "Sphere Geometry", + value: "SphereGeometry", + }, + { + text: "Cylinder Geometry", + value: "CylinderGeometry", + }, + { + text: "Plane Geometry", + value: "PlaneGeometry", + }, + { + text: "Circle Geometry", + value: "CircleGeometry", + }, + { + text: "Torus Geometry", + value: "TorusGeometry", + }, + { + text: "Torus Knot Geometry", + value: "TorusKnotGeometry", + }, + ], + }, + modelsList: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".glb")); + if (models.length < 1) return [ + ["Load a model! (GLB Loader category)"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + fonts: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".json")); + if (models.length < 1) return [ + ["Load a font!"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + }, + }; + } + + addObject(args) { + const object = new THREE[args.TYPE](); + + object.castShadow = true; + object.receiveShadow = true; + + createObject(args.OBJECT3D, object, args.GROUP); + } + cloneObject(args) { + let object = getObject(args.OBJECT3D); + const clone = object.clone(true); + clone.name; + createObject(args.NAME, clone, args.GROUP); + } + setObjectV3(args) { + let object = getObject(args.OBJECT3D); + let values = JSON.parse(args.VALUE); function degToRad(deg) { - return deg * Math.PI / 180; + return (deg * Math.PI) / 180; } - if (object.rigidBody) { - const x = values[0] - const y = values[1] - const z = values[2] + const x = values[0]; + const y = values[1]; + const z = values[2]; if (args.PROPERTY === "rotation") { - const euler = new THREE.Euler( - degToRad(x), - degToRad(y), - degToRad(z), - 'YXZ' - ) - const quaternion = new THREE.Quaternion() - quaternion.setFromEuler(euler) + const euler = new THREE.Euler(degToRad(x), degToRad(y), degToRad(z), "YXZ"); + const quaternion = new THREE.Quaternion(); + quaternion.setFromEuler(euler); object.rigidBody.setRotation({ x: quaternion.x, y: quaternion.y, z: quaternion.z, - w: quaternion.w + w: quaternion.w, }); } else if (args.PROPERTY === "position") { - object.rigidBody.setTranslation({ x: x, y: y, z: z }, true) + object.rigidBody.setTranslation({ + x: x, + y: y, + z: z, + }, + true + ); } - return + return; } - if (object.isCamera == true && controls) { - - } + if (object.isCamera == true && controls) {} if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.set(0,0,0) + values = values.map((v) => (v * Math.PI) / 180); + object.rotation.set(0, 0, 0); + } + if (object.isDirectionalLight == true) { + object.pos = new THREE.Vector3(...values); + console.log(true, values, object.pos); + return; } - if (object.isDirectionalLight == true) {object.pos = new THREE.Vector3(...values); console.log(true, values, object.pos); return} - object[args.PROPERTY].set(...values); + object[args.PROPERTY].set(...values); - if (object.type == "CubeCamera") object.updateCoordinateSystem() - } - /* - changeObjectV3(args) { - getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) + if (object.type == "CubeCamera") object.updateCoordinateSystem(); + } + /* + changeObjectV3(args) { + getObject(args.OBJECT3D) + let values = JSON.parse(args.VALUE) - if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.x += values[0] - object.rotation.y += values[1] - object.rotation.z += values[2] - } - else { - object[args.PROPERTY].add(...values); - } - } - changeObjectXV3(args) { - getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "rotation") value = value * Math.PI / 180 + if (args.PROPERTY === "rotation") { + values = values.map(v => v * Math.PI / 180); + object.rotation.x += values[0] + object.rotation.y += values[1] + object.rotation.z += values[2] + } + else { + object[args.PROPERTY].add(...values); + } + } + changeObjectXV3(args) { + getObject(args.OBJECT3D) + let value = args.VALUE + if (args.PROPERTY === "rotation") value = value * Math.PI / 180 - object[args.PROPERTY][args.X] += value - } - */ - getObjectV3(args) { - let object = getObject(args.OBJECT3D) - if (!object) return - let values = vector3ToString(object[args.PROPERTY]) + object[args.PROPERTY][args.X] += value + } + */ + getObjectV3(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let values = vector3ToString(object[args.PROPERTY]); if (args.PROPERTY === "rotation") { - const toDeg = Math.PI/180 - values = [values[0]/toDeg,values[1]/toDeg,values[2]/toDeg,] + const toDeg = Math.PI / 180; + values = [values[0] / toDeg, values[1] / toDeg, values[2] / toDeg]; } - return JSON.stringify(values) - } - setObject(args){ - let object = getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "material") {const mat = materials[args.NAME]; if (mat) value = mat; else value = undefined} - else if (args.PROPERTY === "geometry") {const geo = geometries[args.NAME]; if (geo) value = geo; else value = undefined} - else value = !!value - - if (value == undefined) return //invalid geo/mat - object[args.PROPERTY] = value - } - getObject(args){ - let object = getObject(args.OBJECT3D) - if (!object) return - let value - if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; - else value = object.visible; - - return value - } - removeObject(args) { - removeObject(args.OBJECT3D) - } - objectE(args) { - return scene.children.map(o => o.name).includes(args.NAME) - } + return JSON.stringify(values); + } + setObject(args) { + let object = getObject(args.OBJECT3D); + let value = args.VALUE; + if (args.PROPERTY === "material") { + const mat = materials[args.NAME]; + if (mat) value = mat; + else value = undefined; + } else if (args.PROPERTY === "geometry") { + const geo = geometries[args.NAME]; + if (geo) value = geo; + else value = undefined; + } else value = !!value; + + if (value == undefined) return; //invalid geo/mat + object[args.PROPERTY] = value; + } + getObject(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let value; + if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; + else value = object.visible; + + return value; + } + removeObject(args) { + removeObject(args.OBJECT3D); + } + objectE(args) { + return scene.children.map((o) => o.name).includes(args.NAME); + } -//defines - newMaterial(args) { - if (materials[args.NAME] && alerts) alert ("material already exists! will replace...") - const mat = new THREE[args.TYPE](); - mat.name = args.NAME; + //defines + newMaterial(args) { + if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); + const mat = new THREE[args.TYPE](); + mat.name = args.NAME; - materials[args.NAME] = mat; - } - async setMaterial(args) { - if (typeof(args.VALUE) == "string" && args.VALUE.at(0) == "|") return - const mat = materials[args.NAME] - - let value = args.VALUE - - if (args.VALUE == "false") value = false - - if (args.PROPERTY == "side") {value = (args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide)} - else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)) - else value = getAsset(value) - - - console.log("o:", args.VALUE, typeof(args.VALUE)) - console.log("r:", value, typeof(value)) - - mat[args.PROPERTY] = await (value) //await incase its a texture - mat.needsUpdate = true - } - setBlending(args) { - const mat = materials[args.NAME] - mat.blending = THREE[args.VALUE] - mat.premultipliedAlpha = true - mat.needsUpdate = true - } - setDepth(args) { - const mat = materials[args.NAME] - mat.depthFunc = THREE[args.VALUE] - mat.needsUpdate = true - } - removeMaterial(args){ - const mat = materials[args.NAME] - mat.dispose() - delete(materials[args.NAME]) - } - materialE(args) { - return materials[args.NAME] ? true : false - } + materials[args.NAME] = mat; + } + async setMaterial(args) { + if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; + const mat = materials[args.NAME]; - newGeometry(args) { - if (geometries[args.NAME] && alerts) alert ("geometry already exists! will replace...") - const geo = new THREE[args.TYPE]() - geo.name = args.NAME + let value = args.VALUE; - geometries[args.NAME] = geo - } - setGeometry(args) { - const geo = geometries[args.NAME] - geo[args.PROPERTY] = (args.VALUE) + if (args.VALUE == "false") value = false; - geo.needsUpdate = true; - } - removeGeometry(args){ - const geo = geometries[args.NAME] - geo.dispose() - delete(geometries[args.NAME]) - } - geometryE(args) { - return geometries[args.NAME] ? true : false - } + if (args.PROPERTY == "side") { + value = args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide; + } else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)); + else value = getAsset(value); - newGeo(args) { - const geometry = new THREE.BufferGeometry() - geometry.name = args.NAME - geometries[args.NAME] = geometry - } - async geoPoints(args) { - const geometry = geometries[args.NAME] - const positions = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v3 of each vertex of each triangle + console.log("o:", args.VALUE, typeof args.VALUE); + console.log("r:", value, typeof value); - geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3)) - geometry.computeVertexNormals() + mat[args.PROPERTY] = await value; //await incase its a texture + mat.needsUpdate = true; + } + setBlending(args) { + const mat = materials[args.NAME]; + mat.blending = THREE[args.VALUE]; + mat.premultipliedAlpha = true; + mat.needsUpdate = true; + } + setDepth(args) { + const mat = materials[args.NAME]; + mat.depthFunc = THREE[args.VALUE]; + mat.needsUpdate = true; + } + removeMaterial(args) { + const mat = materials[args.NAME]; + mat.dispose(); + delete materials[args.NAME]; + } + materialE(args) { + return materials[args.NAME] ? true : false; + } - geometry.needsUpdate = true - } - geoUVs(args) { - const geometry = geometries[args.NAME] - const UVs = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v2 of each UV of each triangle + newGeometry(args) { + if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); + const geo = new THREE[args.TYPE](); + geo.name = args.NAME; - geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(UVs), 2)) - geometry.needsUpdate = true - } + geometries[args.NAME] = geo; + } + setGeometry(args) { + const geo = geometries[args.NAME]; + geo[args.PROPERTY] = args.VALUE; - splines(args) { - const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)) - geometry.name = args.NAME + geo.needsUpdate = true; + } + removeGeometry(args) { + const geo = geometries[args.NAME]; + geo.dispose(); + delete geometries[args.NAME]; + } + geometryE(args) { + return geometries[args.NAME] ? true : false; + } - geometries[args.NAME] = geometry - } + newGeo(args) { + const geometry = new THREE.BufferGeometry(); + geometry.name = args.NAME; + geometries[args.NAME] = geometry; + } + async geoPoints(args) { + const geometry = geometries[args.NAME]; + const positions = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v3 of each vertex of each triangle + + geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3)); + geometry.computeVertexNormals(); - async splineModel(args) { - const model = await getModel(args.MODEL, args.NAME) - if (!model) return console.warn("Model not found:", args.MODEL) + geometry.needsUpdate = true; + } + geoUVs(args) { + const geometry = geometries[args.NAME]; + const UVs = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v2 of each UV of each triangle + + geometry.setAttribute("uv", new THREE.BufferAttribute(new Float32Array(UVs), 2)); + geometry.needsUpdate = true; + } + + splines(args) { + const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); + geometry.name = args.NAME; + + geometries[args.NAME] = geometry; + } - const curve = getAsset(args.CURVE) - const spacing = parseFloat(args.SPACING) || 1 - const curveLength = curve.getLength() - const divisions = Math.floor(curveLength / spacing) + async splineModel(args) { + const model = await getModel(args.MODEL, args.NAME); + if (!model) return console.warn("Model not found:", args.MODEL); - const geomList = [] - const matList = [] + const curve = getAsset(args.CURVE); + const spacing = parseFloat(args.SPACING) || 1; + const curveLength = curve.getLength(); + const divisions = Math.floor(curveLength / spacing); - for (let i = 0; i <= divisions; i++) { - const t = i / divisions - const pos = curve.getPointAt(t) - const tangent = curve.getTangentAt(t) + const geomList = []; + const matList = []; - const temp = model.clone(true) - temp.position.copy(pos) + for (let i = 0; i <= divisions; i++) { + const t = i / divisions; + const pos = curve.getPointAt(t); + const tangent = curve.getTangentAt(t); - const up = new THREE.Vector3(0, 0, 1) - const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()) - temp.quaternion.copy(quat) + const temp = model.clone(true); + temp.position.copy(pos); - temp.updateMatrixWorld(true) + const up = new THREE.Vector3(0, 0, 1); + const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()); + temp.quaternion.copy(quat); - temp.traverse(child => { - if (child.isMesh && child.geometry) { - const geom = child.geometry.clone() - geom.applyMatrix4(child.matrixWorld) - geomList.push(geom) - matList.push(child.material) //.clone() ? + temp.updateMatrixWorld(true); + + temp.traverse((child) => { + if (child.isMesh && child.geometry) { + const geom = child.geometry.clone(); + geom.applyMatrix4(child.matrixWorld); + geomList.push(geom); + matList.push(child.material); //.clone() ? + } + }); + } + + const validGeoms = geomList.filter((g) => { + const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position; + if (!ok) console.warn("geometry skipped:", g); + return ok; + }); + + const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true); + merged.computeBoundingBox(); + merged.computeBoundingSphere(); + + merged.name = args.NAME; + geometries[args.NAME] = merged; + matList.name = args.NAME; + materials[args.NAME] = matList; } - }) - } - const validGeoms = geomList.filter(g => { - const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position - if (!ok) console.warn("geometry skipped:", g) - return ok - }) + async text(args) { + const fontFile = runtime + .getTargetForStage() + .getSounds() + .find((c) => c.name === args.FONT); + if (!fontFile) return; - const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true) - merged.computeBoundingBox() - merged.computeBoundingSphere() + const json = new TextDecoder().decode(fontFile.asset.data.buffer); + const fontData = JSON.parse(json); - merged.name = args.NAME - geometries[args.NAME] = merged - matList.name = args.NAME - materials[args.NAME] = matList - } - - async text(args) { - const fontFile = runtime.getTargetForStage().getSounds().find(c => c.name === args.FONT) - if (!fontFile) return + const font = fontLoad.parse(fontData); - const json = new TextDecoder().decode(fontFile.asset.data.buffer) - const fontData = JSON.parse(json) + const params = { + font: font, + size: JSON.parse(args.S), + height: JSON.parse(args.D), + curveSegments: JSON.parse(args.CS), + bevelEnabled: false, + }; + const geometry = new TextGeometry.TextGeometry(args.TEXT, params); + geometry.computeVertexNormals(); + geometry.center(); // optional, recenters the text - const font = fontLoad.parse(fontData) + geometry.name = args.NAME; - const params = {font: font, size: JSON.parse(args.S), height: JSON.parse(args.D), curveSegments: JSON.parse(args.CS), bevelEnabled: false} - const geometry = new TextGeometry.TextGeometry(args.TEXT, params) - geometry.computeVertexNormals() - geometry.center() // optional, recenters the text - + geometries[args.NAME] = geometry; + } - geometry.name = args.NAME + async loadFont() { + openFileExplorer(".json").then((files) => { + const file = files[0]; + const reader = new FileReader(); - geometries[args.NAME] = geometry - } + reader.onload = async (e) => { + const arrayBuffer = e.target.result; - async loadFont() { - openFileExplorer(".json").then(files => { - const file = files[0] - const reader = new FileReader() + // From lily's assets + // // Thank you PenguinMod for providing this code. - reader.onload = async (e) => { - const arrayBuffer = e.target.result - - // From lily's assets - // // Thank you PenguinMod for providing this code. - - const targetId = runtime.getTargetForStage().id //util.target.id not working! - const assetName = Cast.toString(file.name) + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); - const buffer = arrayBuffer + const buffer = arrayBuffer; - const storage = runtime.storage + const storage = runtime.storage; const asset = storage.createAsset( storage.AssetType.Sound, storage.DataFormat.MP3, @@ -1328,7 +2520,7 @@ Promise.resolve(load()).then(() => { new Uint8Array(buffer), null, true - ) + ); try { await vm.addSound( @@ -1339,615 +2531,1386 @@ Promise.resolve(load()).then(() => { name: assetName, }, targetId - ) - alert("Font loaded successfully!") + ); + alert("Font loaded successfully!"); } catch (e) { - console.error(e) - alert("Error loading font.") + console.error(e); + alert("Error loading font."); } - - // End of PenguinMod + + // End of PenguinMod + }; + + reader.readAsArrayBuffer(file); + }); + } + openConv() { + { + open("https://gero3.github.io/facetype.js/"); } + } + } + Scratch.extensions.register(new ThreeObjects()); + + class ThreeLights { + getInfo() { + return { + id: "threeLights", + name: "Three Lights", + color1: "#c7a22aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addLight", + blockType: Scratch.BlockType.COMMAND, + text: "add light [NAME] type [TYPE] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myLight", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "lightTypes", + }, + }, + }, + { + opcode: "setLight", + blockType: Scratch.BlockType.COMMAND, + text: "set light [NAME][PROPERTY] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "lightProperties", + defaultValue: "intensity", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myLight", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + exemptFromNormalization: true, + }, + }, + }, + ], + menus: { + lightTypes: { + acceptReporters: false, + items: [{ + text: "Ambient Light", + value: "AmbientLight", + }, + { + text: "Directional Light", + value: "DirectionalLight", + }, + { + text: "Point Light", + value: "PointLight", + }, + { + text: "Hemisphere Light", + value: "HemisphereLight", + }, + { + text: "Spot Light", + value: "SpotLight", + }, + ], + }, + lightProperties: { + acceptReporters: false, + items: [{ + text: "Color", + value: "color", + }, + { + text: "Intensity", + value: "intensity", + }, + { + text: "Cast Shadow?", + value: "castShadow", + }, + { + text: "Ground Color (HemisphereLight)", + value: "groundColor", + }, + { + text: "Map (SpotLight)", + value: "map", + }, + { + text: "Distance (SpotLight)", + value: "distance", + }, + { + text: "Decay (SpotLight)", + value: "decay", + }, + { + text: "Penumbra (SpotLight)", + value: "penumbra", + }, + { + text: "Angle/Size (SpotLight)", + value: "angle", + }, + { + text: "Power (SpotLight)", + value: "power", + }, + { + text: "Target Position (Directional/SpotLight)", + value: "target", + }, + ], + }, + }, + }; + } - reader.readAsArrayBuffer(file); - }) - } - openConv() {{open("https://gero3.github.io/facetype.js/")}} + addLight(args) { + const light = new THREE[args.TYPE](0xffffff, 1); - } - Scratch.extensions.register(new ThreeObjects()) - - class ThreeLights { - getInfo() { - return { - id: "threeLights", - name: "Three Lights", - color1: "#c7a22aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addLight", blockType: Scratch.BlockType.COMMAND, text: "add light [NAME] type [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "lightTypes"}}}, - {opcode: "setLight", blockType: Scratch.BlockType.COMMAND, text: "set light [NAME][PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lightProperties", defaultValue: "intensity"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1", exemptFromNormalization: true}}}, - ], - menus: { - lightTypes: {acceptReporters: false, items: [ - {text: "Ambient Light", value: "AmbientLight"},{text: "Directional Light", value: "DirectionalLight"},{text: "Point Light", value: "PointLight"},{text: "Hemisphere Light", value: "HemisphereLight"},{text: "Spot Light", value: "SpotLight"}, - ]}, - lightProperties: {acceptReporters: false, items: [ - {text: "Color", value: "color"},{text: "Intensity", value: "intensity"},{text: "Cast Shadow?", value: "castShadow"}, - {text: "Ground Color (HemisphereLight)", value: "groundColor"}, - {text: "Map (SpotLight)", value: "map"},{text: "Distance (SpotLight)", value: "distance"},{text: "Decay (SpotLight)", value: "decay"},{text: "Penumbra (SpotLight)", value: "penumbra"},{text: "Angle/Size (SpotLight)", value: "angle"},{text: "Power (SpotLight)", value: "power"}, - {text: "Target Position (Directional/SpotLight)", value: "target"}, - ]}, - } - }} + createObject(args.NAME, light, args.GROUP); + lights[args.NAME] = light; + if (light.type === "AmbientLight" || "HemisphereLight") return; - addLight(args) { - const light = new THREE[args.TYPE](0xffffff, 1) - - createObject(args.NAME, light, args.GROUP) - lights[args.NAME] = light - if (light.type === "AmbientLight" || "HemisphereLight") return - - light.castShadow = true - if (light.type === "PointLight") return - //Directional & Spot Light - light.target.position.set(0, 0, 0) - scene.add(light.target) - - light.pos = new THREE.Vector3(0,0,0) - - light.shadow.mapSize.width = 4096 - light.shadow.mapSize.height = 2048 - - if (light.type === "SpotLight") { - light.decay = 0 - light.shadow.camera.near = 500; - light.shadow.camera.far = 4000; - light.shadow.camera.fov = 30; - } - light.shadow.needsUpdate = true - light.needsUpdate = true - } + light.castShadow = true; + if (light.type === "PointLight") return; + //Directional & Spot Light + light.target.position.set(0, 0, 0); + scene.add(light.target); - setLight(args) { - const light = lights[args.NAME] - if (!args.PROPERTY) return - if (args.PROPERTY === "target") { - light.target.position.set(...JSON.parse(args.VALUE)) //vector3 - light.target.updateMatrixWorld(); - } - else { - light[args.PROPERTY] = getAsset(args.VALUE) + light.pos = new THREE.Vector3(0, 0, 0); + + light.shadow.mapSize.width = 4096; + light.shadow.mapSize.height = 2048; + + if (light.type === "SpotLight") { + light.decay = 0; + light.shadow.camera.near = 500; + light.shadow.camera.far = 4000; + light.shadow.camera.fov = 30; + } + light.shadow.needsUpdate = true; + light.needsUpdate = true; } - light.needsUpdate = true - if (light.type === "AmbientLight" || "HemisphereLight") return + setLight(args) { + const light = lights[args.NAME]; + if (!args.PROPERTY) return; + if (args.PROPERTY === "target") { + light.target.position.set(...JSON.parse(args.VALUE)); //vector3 + light.target.updateMatrixWorld(); + } else { + light[args.PROPERTY] = getAsset(args.VALUE); + } + light.needsUpdate = true; + + if (light.type === "AmbientLight" || "HemisphereLight") return; - light.shadow.camera.updateProjectionMatrix(); - light.shadow.needsUpdate = true + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; + } } + Scratch.extensions.register(new ThreeLights()); - } - Scratch.extensions.register(new ThreeLights()) - - class ThreeUtilities { - getInfo() { - return { - id: "threeUtility", - name: "Three Utilities", - color1: "#3875c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "newVector2", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}}}, - {opcode: "newVector3", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y] [Z]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}, Z: {type: Scratch.ArgumentType.NUMBER}}}, + class ThreeUtilities { + getInfo() { + return { + id: "threeUtility", + name: "Three Utilities", + color1: "#3875c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "newVector2", + blockType: Scratch.BlockType.REPORTER, + text: "New Vector [X] [Y]", + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "newVector3", + blockType: Scratch.BlockType.REPORTER, + text: "New Vector [X] [Y] [Z]", + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + }, + Z: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, "---", - {opcode: "operateV3", blockType: Scratch.BlockType.REPORTER, text: "do [V3] [O] [V32]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, O: {type: Scratch.ArgumentType.STRING, menu: "operators"}, V32: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, - {opcode: "moveVector3", blockType: Scratch.BlockType.REPORTER, text: "move [S] steps in vector [V3] in direction [D3]", arguments: {S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, - {opcode: "directionTo", blockType: Scratch.BlockType.REPORTER, text: "direction from [V3] to [T3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, T3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, + { + opcode: "operateV3", + blockType: Scratch.BlockType.REPORTER, + text: "do [V3] [O] [V32]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + O: { + type: Scratch.ArgumentType.STRING, + menu: "operators", + }, + V32: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + { + opcode: "moveVector3", + blockType: Scratch.BlockType.REPORTER, + text: "move [S] steps in vector [V3] in direction [D3]", + arguments: { + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + D3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + { + opcode: "directionTo", + blockType: Scratch.BlockType.REPORTER, + text: "direction from [V3] to [T3]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,3]", + }, + T3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, "---", - {opcode: "newColor", blockType: Scratch.BlockType.REPORTER, text: "New Color [HEX]", arguments: {HEX: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff"}}}, - {opcode: "newFog", blockType: Scratch.BlockType.REPORTER, text: "New Fog [COLOR] [NEAR] [FAR]", arguments: {COLOR: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff", exemptFromNormalization: true}, NEAR: {type: Scratch.ArgumentType.NUMBER}, FAR: {type: Scratch.ArgumentType.NUMBER, defaultValue: 10}}}, - {opcode: "newTexture", blockType: Scratch.BlockType.REPORTER, text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, - {opcode: "newCubeTexture", blockType: Scratch.BlockType.REPORTER, text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", arguments: {"COSTUMEX0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEX1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ1": {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, - {opcode: "newEquirectangularTexture", blockType: Scratch.BlockType.REPORTER, text: "New Equirectangular Texture [COSTUME] [MODE]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"}}}, + { + opcode: "newColor", + blockType: Scratch.BlockType.REPORTER, + text: "New Color [HEX]", + arguments: { + HEX: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#9966ff", + }, + }, + }, + { + opcode: "newFog", + blockType: Scratch.BlockType.REPORTER, + text: "New Fog [COLOR] [NEAR] [FAR]", + arguments: { + COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#9966ff", + exemptFromNormalization: true, + }, + NEAR: { + type: Scratch.ArgumentType.NUMBER, + }, + FAR: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10, + }, + }, + }, + { + opcode: "newTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "textureStyles", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "newCubeTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", + arguments: { + COSTUMEX0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEX1: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEY0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEY1: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEZ0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEZ1: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "textureStyles", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "newEquirectangularTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Equirectangular Texture [COSTUME] [MODE]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + }, + }, "---", - {opcode: "curve", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", arguments: {TYPE: {type: Scratch.ArgumentType.STRING, menu: "curveTypes"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]"}, CLOSED: {type: Scratch.ArgumentType.STRING, defaultValue: "true"}}}, + { + opcode: "curve", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.REPORTER, + text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", + arguments: { + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "curveTypes", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]", + }, + CLOSED: { + type: Scratch.ArgumentType.STRING, + defaultValue: "true", + }, + }, + }, "---", - {opcode:"mouseDown",extensions: ["colours_sensing"], blockType: Scratch.BlockType.BOOLEAN, text: "mouse [BUTTON] [action]?", arguments: {BUTTON: {type: Scratch.ArgumentType.STRING, menu: "mouseButtons"},action: {type: Scratch.ArgumentType.STRING, menu: "mouseAction"}}}, - {opcode: "mousePos",extensions: ["colours_sensing"], blockType: Scratch.BlockType.REPORTER, text: "mouse position", arguments: {}}, + { + opcode: "mouseDown", + extensions: ["colours_sensing"], + blockType: Scratch.BlockType.BOOLEAN, + text: "mouse [BUTTON] [action]?", + arguments: { + BUTTON: { + type: Scratch.ArgumentType.STRING, + menu: "mouseButtons", + }, + action: { + type: Scratch.ArgumentType.STRING, + menu: "mouseAction", + }, + }, + }, + { + opcode: "mousePos", + extensions: ["colours_sensing"], + blockType: Scratch.BlockType.REPORTER, + text: "mouse position", + arguments: {}, + }, "---", - {opcode: "getItem",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "get item [ITEM] of [ARRAY]", arguments: {ITEM: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}, ARRAY: {type: Scratch.ArgumentType.STRING, defaultValue: `["myObject", "myLight"]`}}}, - {blockType: Scratch.BlockType.LABEL, text: "↳ Raycasting"}, - {opcode: "raycast", blockType: Scratch.BlockType.COMMAND, text: "Raycast from [V3] in direction [D3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,1]"}}}, - {opcode: "getRaycast", blockType: Scratch.BlockType.REPORTER, text: "get raycast [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "raycastProperties"}}}, - - ], - menus: { - materialProperties: {acceptReporters: false, items: [ - {text: "Color", value: "color"},{text: "Map (texture)", value: "map"},{text: "Alpha Map (texture)", value: "alphaMap"},{text: "Alpha Test (0-1)", value: "alphaTest"},{text: "Side (front/back/double)", value: "side"},{text: "Bump Map (texture)", value: "bumpMap"},{text: "Bump Scale", value: "bumpScale"}, - ]}, - textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, - textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, - raycastProperties: {acceptReporters: false, items: [ - {text: "Intersected Object Names", value: "name"},{text: "Number of Objects", value: "number"},{text: "Intersected Objects distances", value: "distance"}, - ]}, - mouseButtons: {acceptReporters: false, items: ["left","middle","right"]}, - mouseAction: {acceptReporters: false, items: ["Down","Clicked"]}, - curveTypes: {acceptReporters: false, items: ["CatmullRomCurve3"]}, - operators: {acceptReporters: false, items: [ - "+","-","*","/","=","max","min","dot","cross","distance to","angle to","apply euler", - ]} + { + opcode: "getItem", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.REPORTER, + text: "get item [ITEM] of [ARRAY]", + arguments: { + ITEM: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + ARRAY: { + type: Scratch.ArgumentType.STRING, + defaultValue: `["myObject", "myLight"]`, + }, + }, + }, + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Raycasting", + }, + { + opcode: "raycast", + blockType: Scratch.BlockType.COMMAND, + text: "Raycast from [V3] in direction [D3]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,3]", + }, + D3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,1]", + }, + }, + }, + { + opcode: "getRaycast", + blockType: Scratch.BlockType.REPORTER, + text: "get raycast [PROPERTY]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "raycastProperties", + }, + }, + }, + ], + menus: { + materialProperties: { + acceptReporters: false, + items: [{ + text: "Color", + value: "color", + }, + { + text: "Map (texture)", + value: "map", + }, + { + text: "Alpha Map (texture)", + value: "alphaMap", + }, + { + text: "Alpha Test (0-1)", + value: "alphaTest", + }, + { + text: "Side (front/back/double)", + value: "side", + }, + { + text: "Bump Map (texture)", + value: "bumpMap", + }, + { + text: "Bump Scale", + value: "bumpScale", + }, + ], + }, + textureModes: { + acceptReporters: false, + items: ["Pixelate", "Blur"], + }, + textureStyles: { + acceptReporters: false, + items: ["Repeat", "Clamp"], + }, + raycastProperties: { + acceptReporters: false, + items: [{ + text: "Intersected Object Names", + value: "name", + }, + { + text: "Number of Objects", + value: "number", + }, + { + text: "Intersected Objects distances", + value: "distance", + }, + ], + }, + mouseButtons: { + acceptReporters: false, + items: ["left", "middle", "right"], + }, + mouseAction: { + acceptReporters: false, + items: ["Down", "Clicked"], + }, + curveTypes: { + acceptReporters: false, + items: ["CatmullRomCurve3"], + }, + operators: { + acceptReporters: false, + items: ["+", "-", "*", "/", "=", "max", "min", "dot", "cross", "distance to", "angle to", "apply euler"], + }, + }, + }; + } + mouseDown(args) { + if (args.action === "Down") return isMouseDown[args.BUTTON]; + if (args.action === "Clicked") { + if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false; + else prevMouse[args.BUTTON] = true; + return true; } - }} - mouseDown(args) { - if (args.action === "Down") return isMouseDown[args.BUTTON] - if (args.action === "Clicked") { - if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false - else prevMouse[args.BUTTON] = true; return true } - } - mousePos(event) { - return JSON.stringify(mouseNDC) - } - newVector3(args) { - return JSON.stringify([args.X, args.Y, args.Z]) - } - operateV3(args){ - const v3 = new THREE.Vector3(...JSON.parse(args.V3)) - const v32 = new THREE.Vector3(...JSON.parse(args.V32)) - - let r - if (args.O == "+") r = v3.add(v32) - else if (args.O == "-") r = v3.sub(v32) - else if (args.O == "*") r = v3.multiply(v32) - else if (args.O == "/") r = v3.divide(v32) - else if (args.O == "=") r = v3.equals(v32) - else if (args.O == "max") r = v3.max(v32) - else if (args.O == "min") r = v3.min(v32) - else if (args.O == "dot") r = v3.dot(v32) - else if (args.O == "cross") r = v3.cross(v32) - else if (args.O == "distance to") r = v3.distanceTo(v32) - else if (args.O == "angle to") r = v3.angleTo(v32) - else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)) - - if (typeof(r) == "object") return JSON.stringify([r.x, r.y, r.z]) - else return JSON.stringify(r) - } - - newVector2(args) { - return JSON.stringify([args.X, args.Y]) - } + mousePos(event) { + return JSON.stringify(mouseNDC); + } + newVector3(args) { + return JSON.stringify([args.X, args.Y, args.Z]); + } + operateV3(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)); + const v32 = new THREE.Vector3(...JSON.parse(args.V32)); + + let r; + if (args.O == "+") r = v3.add(v32); + else if (args.O == "-") r = v3.sub(v32); + else if (args.O == "*") r = v3.multiply(v32); + else if (args.O == "/") r = v3.divide(v32); + else if (args.O == "=") r = v3.equals(v32); + else if (args.O == "max") r = v3.max(v32); + else if (args.O == "min") r = v3.min(v32); + else if (args.O == "dot") r = v3.dot(v32); + else if (args.O == "cross") r = v3.cross(v32); + else if (args.O == "distance to") r = v3.distanceTo(v32); + else if (args.O == "angle to") r = v3.angleTo(v32); + else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)); + + if (typeof r == "object") return JSON.stringify([r.x, r.y, r.z]); + else return JSON.stringify(r); + } - moveVector3(args) { - const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); - const steps = Number(args.S); + newVector2(args) { + return JSON.stringify([args.X, args.Y]); + } - const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); + moveVector3(args) { + const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); + const steps = Number(args.S); - const yaw = THREE.MathUtils.degToRad(yawInputDeg); - const pitch = THREE.MathUtils.degToRad(pitchInputDeg); - const roll = THREE.MathUtils.degToRad(rollInputDeg); + const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); - const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); + const yaw = THREE.MathUtils.degToRad(yawInputDeg); + const pitch = THREE.MathUtils.degToRad(pitchInputDeg); + const roll = THREE.MathUtils.degToRad(rollInputDeg); - const forwardVector = new THREE.Vector3(0, 0, -1); - const direction = forwardVector.applyEuler(euler).normalize(); + const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); - const newPos = currentPos.add(direction.multiplyScalar(steps)); - return JSON.stringify([newPos.x, newPos.y, newPos.z]); - } + const forwardVector = new THREE.Vector3(0, 0, -1); + const direction = forwardVector.applyEuler(euler).normalize(); - directionTo(args) { - const v3 = new THREE.Vector3(...JSON.parse(args.V3)) - const toV3 = new THREE.Vector3(...JSON.parse(args.T3)) + const newPos = currentPos.add(direction.multiplyScalar(steps)); + return JSON.stringify([newPos.x, newPos.y, newPos.z]); + } - const direction = toV3.clone().sub(v3).normalize(); - // Pitch (X) - const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x*direction.x + direction.z*direction.z)); - // Yaw (Y) - const yaw = Math.atan2(direction.x, direction.z); + directionTo(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)); + const toV3 = new THREE.Vector3(...JSON.parse(args.T3)); - // Roll always 0 - return JSON.stringify([180+THREE.MathUtils.radToDeg(pitch),THREE.MathUtils.radToDeg(yaw),0]) - } + const direction = toV3.clone().sub(v3).normalize(); + // Pitch (X) + const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)); + // Yaw (Y) + const yaw = Math.atan2(direction.x, direction.z); - newColor(args) { - const color = new THREE.Color(args.HEX) - const uuid = crypto.randomUUID() - assets.colors[uuid] = color - return `colors/${uuid}` - } - newFog(args) { - const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR) - const uuid = crypto.randomUUID() - assets.fogs[uuid] = fog - return `fogs/${uuid}` - } - async newTexture(args) { - const textureURI = encodeCostume(args.COSTUME) - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - async newCubeTexture(args) { - const uris = [encodeCostume(args.COSTUMEX0),encodeCostume(args.COSTUMEX1), encodeCostume(args.COSTUMEY0),encodeCostume(args.COSTUMEY1), encodeCostume(args.COSTUMEZ0),encodeCostume(args.COSTUMEZ1)] - const normalized = await Promise.all(uris.map(uri => resizeImageToSquare(uri, 256))); - const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); - - texture.name = "CubeTexture" + args.COSTUMEX0; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - async newEquirectangularTexture(args) { - const textureURI = encodeCostume(args.COSTUME) - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME - texture.mapping = THREE.EquirectangularReflectionMapping - - setTexutre(texture, args.MODE) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } + // Roll always 0 + return JSON.stringify([180 + THREE.MathUtils.radToDeg(pitch), THREE.MathUtils.radToDeg(yaw), 0]); + } - curve(args) { - function parsePoints(input) { - // Match all [x,y,z] groups - const matches = input.match(/\[([^\]]+)\]/g) - if (!matches) return [] - - return matches.map(str => { - const nums = str - .replace(/[\[\]\s]/g, '') - .split(',') - .map(Number) - return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0) - }) - } - const points = parsePoints(args.POINTS) - const curve = new THREE[args.TYPE](points) - curve.closed = JSON.parse(args.CLOSED) - - const uuid = crypto.randomUUID() - assets.curves[uuid] = curve - return `curves/${uuid}` - } + newColor(args) { + const color = new THREE.Color(args.HEX); + const uuid = crypto.randomUUID(); + assets.colors[uuid] = color; + return `colors/${uuid}`; + } + newFog(args) { + const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR); + const uuid = crypto.randomUUID(); + assets.fogs[uuid] = fog; + return `fogs/${uuid}`; + } + async newTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newCubeTexture(args) { + const uris = [ + encodeCostume(args.COSTUMEX0), + encodeCostume(args.COSTUMEX1), + encodeCostume(args.COSTUMEY0), + encodeCostume(args.COSTUMEY1), + encodeCostume(args.COSTUMEZ0), + encodeCostume(args.COSTUMEZ1), + ]; + const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); + const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); + + texture.name = "CubeTexture" + args.COSTUMEX0; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newEquirectangularTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + texture.mapping = THREE.EquirectangularReflectionMapping; + + setTexutre(texture, args.MODE); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } - getItem(args) { - const items = JSON.parse(args.ARRAY) - const item = items[args.ITEM - 1] - if (!item) return "0" - return item - } + curve(args) { + function parsePoints(input) { + // Match all [x,y,z] groups + const matches = input.match(/\[([^\]]+)\]/g); + if (!matches) return []; + + return matches.map((str) => { + const nums = str + .replace(/[\[\]\s]/g, "") + .split(",") + .map(Number); + return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0); + }); + } + const points = parsePoints(args.POINTS); + const curve = new THREE[args.TYPE](points); + curve.closed = JSON.parse(args.CLOSED); + + const uuid = crypto.randomUUID(); + assets.curves[uuid] = curve; + return `curves/${uuid}`; + } + + getItem(args) { + const items = JSON.parse(args.ARRAY); + const item = items[args.ITEM - 1]; + if (!item) return "0"; + return item; + } - raycast(args) { - const origin = new THREE.Vector3(...JSON.parse(args.V3)) + raycast(args) { + const origin = new THREE.Vector3(...JSON.parse(args.V3)); // rotation is in degrees => convert to radians first - const rot = JSON.parse(args.D3).map(v => v * Math.PI / 180) + const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); - const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder) - const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize() + const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder); + const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize(); - const raycaster = new THREE.Raycaster() - //const camera = getObject(args.CAMERA) - raycaster.set( origin, direction ); + const raycaster = new THREE.Raycaster(); + //const camera = getObject(args.CAMERA) + raycaster.set(origin, direction); - const intersects = raycaster.intersectObjects( scene.children, true ) + const intersects = raycaster.intersectObjects(scene.children, true); - raycastResult = intersects - } - getRaycast(args) { - if (args.PROPERTY === "number") return raycastResult.length - if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map(i => i.distance)) - return JSON.stringify(raycastResult.map(i => i.object[args.PROPERTY])) + raycastResult = intersects; + } + getRaycast(args) { + if (args.PROPERTY === "number") return raycastResult.length; + if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map((i) => i.distance)); + return JSON.stringify(raycastResult.map((i) => i.object[args.PROPERTY])); + } } + Scratch.extensions.register(new ThreeUtilities()); - } - Scratch.extensions.register(new ThreeUtilities()) - - class ThreeGLB { - getInfo() { - return { - id: "threeGLB", - name: "Three GLB Loader", - color1: "#c53838ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {blockType: Scratch.BlockType.BUTTON, text: "Load GLB File", func: "loadModelFile"}, - {opcode: "addModel", blockType: Scratch.BlockType.COMMAND, text: "add [ITEM] as [NAME] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},ITEM: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}}}, - {opcode: "getModel", blockType: Scratch.BlockType.REPORTER, text: "get object [PROPERTY] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "modelProperties"}}}, - {opcode: "playAnimation", blockType: Scratch.BlockType.COMMAND, text: "play animation [ANAME] of [NAME], [TIMES] times", arguments: {TIMES: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - {opcode: "pauseAnimation", blockType: Scratch.BlockType.COMMAND, text: "set [TOGGLE] animation [ANAME] of [NAME]", arguments: {TOGGLE: {type: Scratch.ArgumentType.NUMBER, menu: "pauseUn"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - {opcode: "stopAnimation", blockType: Scratch.BlockType.COMMAND, text: "stop animation [ANAME] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - - ], - menus: { - modelProperties: {acceptReporters: false, items: [ - {text: "Animations", value: "animations"}, - ]}, - pauseUn: {acceptReporters: true, items: [{text: "Pause", value: "true"},{text: "Unpasue", value: "false"},]}, - modelsList: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; + class ThreeGLB { + getInfo() { + return { + id: "threeGLB", + name: "Three GLB Loader", + color1: "#c53838ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + blockType: Scratch.BlockType.BUTTON, + text: "Load GLB File", + func: "loadModelFile", + }, + { + opcode: "addModel", + blockType: Scratch.BlockType.COMMAND, + text: "add [ITEM] as [NAME] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + ITEM: { + type: Scratch.ArgumentType.STRING, + menu: "modelsList", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + }, + }, + { + opcode: "getModel", + blockType: Scratch.BlockType.REPORTER, + text: "get object [PROPERTY] of [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "modelProperties", + }, + }, + }, + { + opcode: "playAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "play animation [ANAME] of [NAME], [TIMES] times", + arguments: { + TIMES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "pauseAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "set [TOGGLE] animation [ANAME] of [NAME]", + arguments: { + TOGGLE: { + type: Scratch.ArgumentType.NUMBER, + menu: "pauseUn", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "stopAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "stop animation [ANAME] of [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + ], + menus: { + modelProperties: { + acceptReporters: false, + items: [{ + text: "Animations", + value: "animations", + }, ], + }, + pauseUn: { + acceptReporters: true, + items: [{ + text: "Pause", + value: "true", + }, + { + text: "Unpasue", + value: "false", + }, + ], + }, + modelsList: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) - if (models.length < 1) return [["Load a model!"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - } - }} + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".glb")); + if (models.length < 1) return [ + ["Load a model!"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + }, + }; + } - async loadModelFile() { + async loadModelFile() { + openFileExplorer(".glb").then((files) => { + const file = files[0]; + const reader = new FileReader(); - openFileExplorer(".glb").then(files => { - const file = files[0]; - const reader = new FileReader(); + reader.onload = async (e) => { + const arrayBuffer = e.target.result; - reader.onload = async (e) => { - const arrayBuffer = e.target.result; - - { // From lily's assets + { + // From lily's assets // Thank you PenguinMod for providing this code. - { - const targetId = runtime.getTargetForStage().id; //util.target.id not working! - const assetName = Cast.toString(file.name); + { + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); + + //const res = await Scratch.fetch(args.URL); + //const buffer = await res.arrayBuffer(); + const buffer = arrayBuffer; + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + alert("Model loaded successfully!"); + } catch (e) { + console.error(e); + alert("Error loading model."); + } + } + // End of PenguinMod + } + }; - //const res = await Scratch.fetch(args.URL); - //const buffer = await res.arrayBuffer(); - const buffer = arrayBuffer + reader.readAsArrayBuffer(file); + }); + } + async addModel(args) { + const group = await getModel(args.ITEM, args.NAME); - const storage = runtime.storage; - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ); + createObject(args.NAME, group, args.GROUP); + } + getModel(args) { + if (!models[args.NAME]) return; + return Object.keys(models[args.NAME].actions).toString(); + } - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ); - alert("Model loaded successfully!"); - } catch (e) { - console.error(e); - alert("Error loading model."); - } - } - // End of PenguinMod + playAnimation(args) { + const model = models[args.NAME]; + if (!model) { + console.log("no model!"); + return; } - }; - reader.readAsArrayBuffer(file); - }) - - } - async addModel(args) { - const group = await getModel(args.ITEM, args.NAME) - - createObject(args.NAME, group, args.GROUP) - } - getModel(args){ - if (!models[args.NAME]) return; - return Object.keys(models[args.NAME].actions).toString() - } + const action = model.actions[args.ANAME]; //clones of models dont have a stored actions! + if (!action) { + console.log("no action!"); + return; + } - playAnimation(args) { - const model = models[args.NAME] - if (!model) {console.log("no model!"); return} + args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity); - const action = model.actions[args.ANAME] //clones of models dont have a stored actions! - if (!action) { - console.log("no action!") - return + action.reset().play(); } + stopAnimation(args) { + const model = models[args.NAME]; + if (!model) return; - args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity) - - action.reset() - .play() - } - stopAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.stop(); - } - pauseAnimation(args) { - const model = models[args.NAME]; - if (!model) return; + const action = model.actions[args.ANAME]; + if (action) action.stop(); + } + pauseAnimation(args) { + const model = models[args.NAME]; + if (!model) return; - const action = model.actions[args.ANAME]; - if (action) action.paused = args.TOGGLE + const action = model.actions[args.ANAME]; + if (action) action.paused = args.TOGGLE; + } } + Scratch.extensions.register(new ThreeGLB()); - } - Scratch.extensions.register(new ThreeGLB()) - - class ThreeAddons { - getInfo() { - return { - id: "threeAddons", - name: "Three Addons", - color1: "#c538a2ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {blockType: Scratch.BlockType.LABEL, text: "Orbit Control"}, - {opcode: "OrbitControl", blockType: Scratch.BlockType.COMMAND, text: "set addon Orbit Control [STATE]", arguments: {STATE: {type: Scratch.ArgumentType.STRING, menu: "onoff"},}}, - - {blockType: Scratch.BlockType.LABEL, text: "Post Processing"}, - {opcode: "resetComposer", blockType: Scratch.BlockType.COMMAND, text: "reset composer"}, - {opcode: "bloom", blockType: Scratch.BlockType.COMMAND, text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},I: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, T:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, - {opcode: "godRays", blockType: Scratch.BlockType.COMMAND, text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}, DEC:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.95}, DENS:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},EXP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1},WEI:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.4},RES:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},SAMP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 64},}}, - {opcode: "dots", blockType: Scratch.BlockType.COMMAND, text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, A: {type: Scratch.ArgumentType.ANGLE, defaultValue: 0},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, - {opcode: "depth", blockType: Scratch.BlockType.COMMAND, text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", arguments: {FD: {type: Scratch.ArgumentType.NUMBER, defaultValue: (3)},FL: {type: Scratch.ArgumentType.NUMBER, defaultValue: (0.001)},BS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 4},H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 240},OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}}}, + class ThreeAddons { + getInfo() { + return { + id: "threeAddons", + name: "Three Addons", + color1: "#c538a2ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + blockType: Scratch.BlockType.LABEL, + text: "Orbit Control", + }, + { + opcode: "OrbitControl", + blockType: Scratch.BlockType.COMMAND, + text: "set addon Orbit Control [STATE]", + arguments: { + STATE: { + type: Scratch.ArgumentType.STRING, + menu: "onoff", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "Post Processing", + }, + { + opcode: "resetComposer", + blockType: Scratch.BlockType.COMMAND, + text: "reset composer", + }, + { + opcode: "bloom", + blockType: Scratch.BlockType.COMMAND, + text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + I: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.5, + }, + T: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.5, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + }, + }, + { + opcode: "godRays", + blockType: Scratch.BlockType.COMMAND, + text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + DEC: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.95, + }, + DENS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + EXP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.1, + }, + WEI: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.4, + }, + RES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + SAMP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 64, + }, + }, + }, + { + opcode: "dots", + blockType: Scratch.BlockType.COMMAND, + text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + A: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: 0, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + }, + }, + { + opcode: "depth", + blockType: Scratch.BlockType.COMMAND, + text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", + arguments: { + FD: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 3, + }, + FL: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.001, + }, + BS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 4, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 240, + }, + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "NORMAL", + }, + }, + }, "---", - {opcode: "custom", blockType: Scratch.BlockType.COMMAND, text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myShader"}, FRA: {type: Scratch.ArgumentType.STRING}, VER: {type: Scratch.ArgumentType.STRING}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}, OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, + { + opcode: "custom", + blockType: Scratch.BlockType.COMMAND, + text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myShader", + }, + FRA: { + type: Scratch.ArgumentType.STRING, + }, + VER: { + type: Scratch.ArgumentType.STRING, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "NORMAL", + }, + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, ], - menus: { - onoff: {acceptReporters: true, items: [{text: "enabled", value: "1"},{text: "disabled", value: "0"},]}, - blendModes: {acceptReporters: false, items: [ - "SKIP","SET","ADD","ALPHA","AVERAGE","COLOR","COLOR_BURN","COLOR_DODGE", - "DARKEN","DIFFERENCE","DIVIDE","DST","EXCLUSION","HARD_LIGHT","HARD_MIX", - "HUE","INVERT","INVERT_RGB","LIGHTEN","LINEAR_BURN","LINEAR_DODGE", - "LINEAR_LIGHT","LUMINOSITY","MULTIPLY","NEGATION","NORMAL","OVERLAY", - "PIN_LIGHT","REFLECT","SCREEN","SRC","SATURATION","SOFT_LIGHT","SUBTRACT", - "VIVID_LIGHT" - ]}, - } - }} + menus: { + onoff: { + acceptReporters: true, + items: [{ + text: "enabled", + value: "1", + }, + { + text: "disabled", + value: "0", + }, + ], + }, + blendModes: { + acceptReporters: false, + items: [ + "SKIP", + "SET", + "ADD", + "ALPHA", + "AVERAGE", + "COLOR", + "COLOR_BURN", + "COLOR_DODGE", + "DARKEN", + "DIFFERENCE", + "DIVIDE", + "DST", + "EXCLUSION", + "HARD_LIGHT", + "HARD_MIX", + "HUE", + "INVERT", + "INVERT_RGB", + "LIGHTEN", + "LINEAR_BURN", + "LINEAR_DODGE", + "LINEAR_LIGHT", + "LUMINOSITY", + "MULTIPLY", + "NEGATION", + "NORMAL", + "OVERLAY", + "PIN_LIGHT", + "REFLECT", + "SCREEN", + "SRC", + "SATURATION", + "SOFT_LIGHT", + "SUBTRACT", + "VIVID_LIGHT", + ], + }, + }, + }; + } OrbitControl(args) { - if (controls) controls.dispose() + if (controls) controls.dispose(); - console.log("creating...", OrbitControls) + console.log("creating...", OrbitControls); controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); - controls.enableDamping = true - - controls.enabled = !!args.STATE - console.log(controls) - } + controls.enableDamping = true; - resetComposer() { - composer.passes = [] - passes = {} - customEffects = [] - updateComposers() - } + controls.enabled = !!args.STATE; + console.log(controls); + } - bloom(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const bloomEffect = new BloomEffect({ - intensity: args.I, - luminanceThreshold: args.T, // ← correct key - luminanceSmoothing: args.S, - blendFunction: BlendFunction[args.BLEND], - }) - bloomEffect.blendMode.opacity.value = args.OP + resetComposer() { + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } - const pass = new EffectPass(camera, bloomEffect) + bloom(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const bloomEffect = new BloomEffect({ + intensity: args.I, + luminanceThreshold: args.T, // ← correct key + luminanceSmoothing: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + bloomEffect.blendMode.opacity.value = args.OP; - composer.addPass(pass) - } + const pass = new EffectPass(camera, bloomEffect); - godRays(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - let object = getObject(args.NAME) - const sun = object - - const godRays = new GodRaysEffect(camera, sun, { - resolutionScale: args.RES, - density: args.DENS, // ray density - decay: args.DEC, // fade out - weight: args.WEI, // brightness of rays - exposure: args.EXP, - samples: args.SAMP, - blendFunction: BlendFunction[args.BLEND], - }) - godRays.blendMode.opacity.value = args.OP - const pass = new EffectPass(camera, godRays) - composer.addPass(pass) - } + composer.addPass(pass); + } - dots(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const dot = new DotScreenEffect({ - angle: args.A, - scale: args.S, - blendFunction: BlendFunction[args.BLEND], - }) - dot.blendMode.opacity.value = args.OP - const pass = new EffectPass(camera, dot) - composer.addPass(pass) - } + godRays(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + let object = getObject(args.NAME); + const sun = object; + + const godRays = new GodRaysEffect(camera, sun, { + resolutionScale: args.RES, + density: args.DENS, // ray density + decay: args.DEC, // fade out + weight: args.WEI, // brightness of rays + exposure: args.EXP, + samples: args.SAMP, + blendFunction: BlendFunction[args.BLEND], + }); + godRays.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, godRays); + composer.addPass(pass); + } - depth(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const dofEffect = new DepthOfFieldEffect(camera, { - focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) - focalLength: args.FL, // lens focal length in meters - bokehScale: args.BS, // strength/size of the blur circles - height: args.H, // resolution hint (affects quality/perf) - blendFunction: BlendFunction[args.BLEND], - }) - dofEffect.blendMode.opacity.value = args.OP - - const dofPass = new EffectPass(camera, dofEffect) - composer.addPass(dofPass) - } + dots(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dot = new DotScreenEffect({ + angle: args.A, + scale: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + dot.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, dot); + composer.addPass(pass); + } - async custom(args) { - function cleanGLSL(glslCode) { - //delete multilines comments - let cleanedCode = glslCode.replace(/\/\*[\s\S]*?\*\//g, ' ') - .replace(/ /g, '\n') - .replace(/\/\/.*$/gm, ' ') - .replace(/; /g, ';\n') - - return cleanedCode; + depth(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dofEffect = new DepthOfFieldEffect(camera, { + focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) + focalLength: args.FL, // lens focal length in meters + bokehScale: args.BS, // strength/size of the blur circles + height: args.H, // resolution hint (affects quality/perf) + blendFunction: BlendFunction[args.BLEND], + }); + dofEffect.blendMode.opacity.value = args.OP; + + const dofPass = new EffectPass(camera, dofEffect); + composer.addPass(dofPass); } - let fs = cleanGLSL(` + async custom(args) { + function cleanGLSL(glslCode) { + //delete multilines comments + let cleanedCode = glslCode + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/ /g, "\n") + .replace(/\/\/.*$/gm, " ") + .replace(/; /g, ";\n"); + + return cleanedCode; + } + + let fs = cleanGLSL(` ${args.FRA} - `) - if (!args.FRA.trim()) {fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`} - const vs = cleanGLSL(` + `); + if (!args.FRA.trim()) { + fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`; + } + const vs = cleanGLSL(` ${args.VER} - `) - console.log(fs) - console.log(vs) + `); + console.log(fs); + console.log(vs); - const effect = new Effect( - "Custom", - fs, - { + const effect = new Effect("Custom", fs, { blendFunction: BlendFunction[args.BLEND], vertexShader: vs, - uniforms: new Map([ //uniforms usually in shaders... open to more! - ['time', new THREE.Uniform(0.0)], - ['resolution', new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height))] + uniforms: new Map([ + //uniforms usually in shaders... open to more! + ["time", new THREE.Uniform(0.0)], + [ + "resolution", + new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height)), + ], ]), - defines: new Map([['USE_TIME', '1'], ['USE_VERTEX_TRANSFORM', '']]), - } - ); + defines: new Map([ + ["USE_TIME", "1"], + ["USE_VERTEX_TRANSFORM", ""], + ]), + }); - effect.blendMode.opacity.value = args.OP + effect.blendMode.opacity.value = args.OP; - const pass = new EffectPass(camera, effect); - composer.addPass(pass); + const pass = new EffectPass(camera, effect); + composer.addPass(pass); - customEffects.push(effect); + customEffects.push(effect); + } } + Scratch.extensions.register(new ThreeAddons()); - } - Scratch.extensions.register(new ThreeAddons()) - - class RapierPhysics { + class RapierPhysics { getInfo() { return { id: "rapierPhysics", @@ -1955,119 +3918,679 @@ Promise.resolve(load()).then(() => { color1: "#222222", color2: "#203024ff", color3: "#78f07eff", - blocks: [ - {opcode: "createWorld", blockType: Scratch.BlockType.COMMAND, text: "create world | gravity:[G]", arguments: {G: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,-9.81,0]"}}}, - {opcode: "getWorld", blockType: Scratch.BlockType.REPORTER, text: "get world [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "wProp"}}}, + blocks: [{ + opcode: "createWorld", + blockType: Scratch.BlockType.COMMAND, + text: "create world | gravity:[G]", + arguments: { + G: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,-9.81,0]", + }, + }, + }, + { + opcode: "getWorld", + blockType: Scratch.BlockType.REPORTER, + text: "get world [PROPERTY]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "wProp", + }, + }, + }, "---", - {opcode: "objectPhysics", blockType: Scratch.BlockType.COMMAND, text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", arguments: {state2: {type: Scratch.ArgumentType.STRING, menu: "state2"},state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"}, type: {type: Scratch.ArgumentType.STRING, menu: "objectTypes", defaultValue: "dynamic"}, collider: {type: Scratch.ArgumentType.STRING, menu: "colliderTypes", defaultValue: "cuboid"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},mass: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},density: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},friction: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0.5"}}}, + { + opcode: "objectPhysics", + blockType: Scratch.BlockType.COMMAND, + text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", + arguments: { + state2: { + type: Scratch.ArgumentType.STRING, + menu: "state2", + }, + state: { + type: Scratch.ArgumentType.STRING, + menu: "state", + defaultValue: "true", + }, + type: { + type: Scratch.ArgumentType.STRING, + menu: "objectTypes", + defaultValue: "dynamic", + }, + collider: { + type: Scratch.ArgumentType.STRING, + menu: "colliderTypes", + defaultValue: "cuboid", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + mass: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + density: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + friction: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0.5", + }, + }, + }, "---", - {blockType: Scratch.BlockType.LABEL, text: "- RigidBody"}, - {opcode: "setRB", blockType: Scratch.BlockType.COMMAND, text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodySets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getRB", blockType: Scratch.BlockType.REPORTER, text: "get rigidbody [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodyProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + { + blockType: Scratch.BlockType.LABEL, + text: "- RigidBody", + }, + { + opcode: "setRB", + blockType: Scratch.BlockType.COMMAND, + text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "rigidBodySets", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "getRB", + blockType: Scratch.BlockType.REPORTER, + text: "get rigidbody [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "rigidBodyProperties", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "lockObjectAxis", blockType: Scratch.BlockType.COMMAND, text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", arguments: {OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lockAxes"}, X: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Y: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Z: {type: Scratch.ArgumentType.STRING, menu: "tf"}}}, + { + opcode: "lockObjectAxis", + blockType: Scratch.BlockType.COMMAND, + text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", + arguments: { + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "lockAxes", + }, + X: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + Y: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + Z: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + }, + }, "---", - {opcode: "addForce", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,10,0]"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "forces", defaultValue: "addForce"},SPACE: {type: Scratch.ArgumentType.STRING, menu: "spaces", defaultValue: "world"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "resetForces", blockType: Scratch.BlockType.COMMAND, text: "reset [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "resetF", defaultValue: "resetForces"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + { + opcode: "addForce", + blockType: Scratch.BlockType.COMMAND, + text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,10,0]", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "forces", + defaultValue: "addForce", + }, + SPACE: { + type: Scratch.ArgumentType.STRING, + menu: "spaces", + defaultValue: "world", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "resetForces", + blockType: Scratch.BlockType.COMMAND, + text: "reset [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "resetF", + defaultValue: "resetForces", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "enableCCD", blockType: Scratch.BlockType.COMMAND, text: "enable Continuous Collision Detection for [OBJECT] [state]", arguments: {state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "oPropS", defaultValue: "physics"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + { + opcode: "enableCCD", + blockType: Scratch.BlockType.COMMAND, + text: "enable Continuous Collision Detection for [OBJECT] [state]", + arguments: { + state: { + type: Scratch.ArgumentType.STRING, + menu: "state", + defaultValue: "true", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "oPropS", + defaultValue: "physics", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "fixedJoint", blockType: Scratch.BlockType.COMMAND, text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},RA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},RB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},}}, - {opcode: "sphericalJoint", blockType: Scratch.BlockType.COMMAND, text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},}}, - {opcode: "revoluteJoint", blockType: Scratch.BlockType.COMMAND, text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},X: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"},}}, + { + opcode: "fixedJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + RA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + RB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + { + opcode: "sphericalJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + }, + }, + { + opcode: "revoluteJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + X: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, "---", - {blockType: Scratch.BlockType.LABEL, text: "- Collider"}, - {opcode: "setC", blockType: Scratch.BlockType.COMMAND, text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderSets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getC", blockType: Scratch.BlockType.REPORTER, text: "get collider [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + { + blockType: Scratch.BlockType.LABEL, + text: "- Collider", + }, + { + opcode: "setC", + blockType: Scratch.BlockType.COMMAND, + text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "colliderSets", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "getC", + blockType: Scratch.BlockType.REPORTER, + text: "get collider [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "colliderProperties", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "sensorSingle", blockType: Scratch.BlockType.BOOLEAN, text: "is sensor [SENSOR] touching [OBJECT]?", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "sensorAll", blockType: Scratch.BlockType.REPORTER, text: "objects touching sensor [SENSOR]", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}}} + { + opcode: "sensorSingle", + blockType: Scratch.BlockType.BOOLEAN, + text: "is sensor [SENSOR] touching [OBJECT]?", + arguments: { + SENSOR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySensor", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "sensorAll", + blockType: Scratch.BlockType.REPORTER, + text: "objects touching sensor [SENSOR]", + arguments: { + SENSOR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySensor", + }, + }, + }, ], menus: { - wProp: {acceptReporters: false, items: [ - {text: "Gravity", value: "gravity"}, {text: "log to console", value: "log"} - ]}, - tf: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true", value: "true"}]}, - lockAxes: {acceptReporters: false, items: [ - {text: "Translation", value: "setEnabledTranslations"}, {text: "Rotation", value: "setEnabledRotations"} - ]}, - rigidBodyProperties: {acceptReporters: false, items: [ - {text: "Type", value: "bodyType"}, - {text: "Linear Velocity", value: "linvel"}, - {text: "Angular Velocity", value: "angvel"}, - {text: "Translation (position)", value: "translation"}, - {text: "Rotation (quaternion)", value: "rotation"}, - {text: "Mass", value: "mass"}, - //{text: "Center of Mass", value: "centerOfMass"}, - {text: "Linear Damping", value: "linearDamping"}, - {text: "Angular Damping", value: "angularDamping"}, - {text: "Is Sleeping?", value: "isSleeping"}, - //{text: "Can Sleep?", value: "isCanSleep"}, - {text: "Gravity Scale", value: "gravityScale"}, - {text: "Is Fixed?", value: "isFixed"}, - {text: "Is Dynamic?", value: "isDynamic"}, - {text: "Is Kinematic?", value: "isKinematic"}, - //{text: "Sleeping", value: "sleeping"} - ]}, - rigidBodySets: {acceptReporters: false, items: [ - //{text: "Linear Velocity", value: "setLinvel"}, - //{text: "Angular Velocity", value: "setAngvel"}, - //{text: "Mass", value: "setMass"}, - {text: "Gravity Scale", value: "setGravityScale"}, - //{text: "Can Sleep?", value: "setCanSleep"}, - //{text: "Sleeping", value: "sleeping"}, - {text: "Linear Damping", value: "setLinearDamping"}, - {text: "Angular Damping", value: "setAngularDamping"}, - {text: "Is Fixed?", value: "isFixed"}, - {text: "Is Dynamic?", value: "isDynamic"}, - {text: "Is Kinematic?", value: "isKinematic"} - ]}, - colliderProperties: {acceptReporters: false, items: [ - //{text: "Collider Type", value: "type"}, - {text: "Is Sensor?", value: "isSensor"}, - {text: "Friction", value: "friction"}, - {text: "Restitution", value: "restitution"}, - {text: "Density", value: "density"}, - {text: "Mass", value: "mass"}, - {text: "Position", value: "translation"}, - {text: "Rotation", value: "rotation"}, - //{text: "Area", value: "area"}, - {text: "Volume", value: "volume"}, - {text: "Collision Groups", value: "collisionGroups"}, - //{text: "Collision Mask", value: "collisionMask"}, - //{text: "Is Enabled?", value: "enabled"}, - //{text: "Contact Count", value: "contactCount"}, - //{text: "RigidBody Handle", value: "rigidBody"} - ]}, - colliderSets: {acceptReporters: false, items: [ - {text: "Friction", value: "setFriction"}, - {text: "Restitution", value: "setRestitution"}, - {text: "Density", value: "setDensity"}, - {text: "Is Sensor?", value: "setSensor"}, - {text: "Collision Groups", value: "setCollisionGroups"}, - //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool - //{text: "Position Offset", value: "setTranslation"}, - //{text: "Rotation Offset", value: "setRotation"} - ]}, - state: {acceptReporters: true, items: [{text: "on", value: "true"},{text: "off", value: "false"}]}, - state2: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true (must be fixed)", value: "true"}]}, - spaces: {acceptReporters: false, items: [{text: "World", value: "world"},{text: "Local", value: "local"}]}, - objectTypes: {acceptReporters: false, items: [{text: "Dynamic", value: "dynamic"},{text: "Fixed", value: "fixed"},{text: "Kinematic Position Based",value: "kinematicPositionBased"}]}, - colliderTypes: {acceptReporters: false, items: [{text: "Box, Rectangle, cuboid", value: "cuboid"},{text: "Sphere, ball", value: "ball"},{text: "Custom, complex simple shapes, convexHull", value: "convexHull"},{text:"Precision, TriMesh",value:"trimesh"}]}, - forces: {acceptReporters: false, items: [{text: "Force", value: "addForce"},{text: "Torque (rotation)", value: "addTorque"},{text: "Apply Impulse", value: "applyImpulse"},{text: "Apply Torque Impulse (rotation)", value: "applyTorqueImpulse"},{text: "Linear Velocity", value: "setLinvel"},{text: "Angular Velocity", value: "setAngvel"},]}, - resetF: {acceptReporters: false, items: [{text:"Forces", value: "resetForces"},{text:"Torques", value: "resetTorques"},]} - } - } + wProp: { + acceptReporters: false, + items: [{ + text: "Gravity", + value: "gravity", + }, + { + text: "log to console", + value: "log", + }, + ], + }, + tf: { + acceptReporters: true, + items: [{ + text: "false", + value: "false", + }, + { + text: "true", + value: "true", + }, + ], + }, + lockAxes: { + acceptReporters: false, + items: [{ + text: "Translation", + value: "setEnabledTranslations", + }, + { + text: "Rotation", + value: "setEnabledRotations", + }, + ], + }, + rigidBodyProperties: { + acceptReporters: false, + items: [{ + text: "Type", + value: "bodyType", + }, + { + text: "Linear Velocity", + value: "linvel", + }, + { + text: "Angular Velocity", + value: "angvel", + }, + { + text: "Translation (position)", + value: "translation", + }, + { + text: "Rotation (quaternion)", + value: "rotation", + }, + { + text: "Mass", + value: "mass", + }, + //{text: "Center of Mass", value: "centerOfMass"}, + { + text: "Linear Damping", + value: "linearDamping", + }, + { + text: "Angular Damping", + value: "angularDamping", + }, + { + text: "Is Sleeping?", + value: "isSleeping", + }, + //{text: "Can Sleep?", value: "isCanSleep"}, + { + text: "Gravity Scale", + value: "gravityScale", + }, + { + text: "Is Fixed?", + value: "isFixed", + }, + { + text: "Is Dynamic?", + value: "isDynamic", + }, + { + text: "Is Kinematic?", + value: "isKinematic", + }, + //{text: "Sleeping", value: "sleeping"} + ], + }, + rigidBodySets: { + acceptReporters: false, + items: [ + //{text: "Linear Velocity", value: "setLinvel"}, + //{text: "Angular Velocity", value: "setAngvel"}, + //{text: "Mass", value: "setMass"}, + { + text: "Gravity Scale", + value: "setGravityScale", + }, + //{text: "Can Sleep?", value: "setCanSleep"}, + //{text: "Sleeping", value: "sleeping"}, + { + text: "Linear Damping", + value: "setLinearDamping", + }, + { + text: "Angular Damping", + value: "setAngularDamping", + }, + { + text: "Is Fixed?", + value: "isFixed", + }, + { + text: "Is Dynamic?", + value: "isDynamic", + }, + { + text: "Is Kinematic?", + value: "isKinematic", + }, + ], + }, + colliderProperties: { + acceptReporters: false, + items: [ + //{text: "Collider Type", value: "type"}, + { + text: "Is Sensor?", + value: "isSensor", + }, + { + text: "Friction", + value: "friction", + }, + { + text: "Restitution", + value: "restitution", + }, + { + text: "Density", + value: "density", + }, + { + text: "Mass", + value: "mass", + }, + { + text: "Position", + value: "translation", + }, + { + text: "Rotation", + value: "rotation", + }, + //{text: "Area", value: "area"}, + { + text: "Volume", + value: "volume", + }, + { + text: "Collision Groups", + value: "collisionGroups", + }, + //{text: "Collision Mask", value: "collisionMask"}, + //{text: "Is Enabled?", value: "enabled"}, + //{text: "Contact Count", value: "contactCount"}, + //{text: "RigidBody Handle", value: "rigidBody"} + ], + }, + colliderSets: { + acceptReporters: false, + items: [{ + text: "Friction", + value: "setFriction", + }, + { + text: "Restitution", + value: "setRestitution", + }, + { + text: "Density", + value: "setDensity", + }, + { + text: "Is Sensor?", + value: "setSensor", + }, + { + text: "Collision Groups", + value: "setCollisionGroups", + }, + //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool + //{text: "Position Offset", value: "setTranslation"}, + //{text: "Rotation Offset", value: "setRotation"} + ], + }, + state: { + acceptReporters: true, + items: [{ + text: "on", + value: "true", + }, + { + text: "off", + value: "false", + }, + ], + }, + state2: { + acceptReporters: true, + items: [{ + text: "false", + value: "false", + }, + { + text: "true (must be fixed)", + value: "true", + }, + ], + }, + spaces: { + acceptReporters: false, + items: [{ + text: "World", + value: "world", + }, + { + text: "Local", + value: "local", + }, + ], + }, + objectTypes: { + acceptReporters: false, + items: [{ + text: "Dynamic", + value: "dynamic", + }, + { + text: "Fixed", + value: "fixed", + }, + { + text: "Kinematic Position Based", + value: "kinematicPositionBased", + }, + ], + }, + colliderTypes: { + acceptReporters: false, + items: [{ + text: "Box, Rectangle, cuboid", + value: "cuboid", + }, + { + text: "Sphere, ball", + value: "ball", + }, + { + text: "Custom, complex simple shapes, convexHull", + value: "convexHull", + }, + { + text: "Precision, TriMesh", + value: "trimesh", + }, + ], + }, + forces: { + acceptReporters: false, + items: [{ + text: "Force", + value: "addForce", + }, + { + text: "Torque (rotation)", + value: "addTorque", + }, + { + text: "Apply Impulse", + value: "applyImpulse", + }, + { + text: "Apply Torque Impulse (rotation)", + value: "applyTorqueImpulse", + }, + { + text: "Linear Velocity", + value: "setLinvel", + }, + { + text: "Angular Velocity", + value: "setAngvel", + }, + ], + }, + resetF: { + acceptReporters: false, + items: [{ + text: "Forces", + value: "resetForces", + }, + { + text: "Torques", + value: "resetTorques", + }, + ], + }, + }, + }; } joint(jointData, bodyA, bodyB) { - physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true) + physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); } fixedJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - let RA = JSON.parse(args.RA).map(Number) - let RB = JSON.parse(args.RB).map(Number) + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + let RA = JSON.parse(args.RA).map(Number); + let RB = JSON.parse(args.RB).map(Number); RA = new THREE.Quaternion().setFromEuler( new THREE.Euler( @@ -2075,339 +4598,419 @@ Promise.resolve(load()).then(() => { THREE.MathUtils.degToRad(RA[1]), THREE.MathUtils.degToRad(RA[2]) ) - ) + ); RB = new THREE.Quaternion().setFromEuler( new THREE.Euler( THREE.MathUtils.degToRad(RB[0]), THREE.MathUtils.degToRad(RB[1]), THREE.MathUtils.degToRad(RB[2]) ) - ) - - const data = RAPIER.JointData.fixed( - { x: VA[0], y: VA[1], z: VA[2] }, RA, - { x: VB[0], y: VB[1], z: VB[2] }, RB - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) + ); + + const data = RAPIER.JointData.fixed({ + x: VA[0], + y: VA[1], + z: VA[2], + }, + RA, { + x: VB[0], + y: VB[1], + z: VB[2], + }, + RB + ); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); } sphericalJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - - const data = RAPIER.JointData.spherical( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] } - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + + const data = RAPIER.JointData.spherical({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); } revoluteJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - const x = JSON.parse(args.X).map(Number) - - const data = RAPIER.JointData.revolute( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + const x = JSON.parse(args.X).map(Number); + + const data = RAPIER.JointData.revolute({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }, { + x: x[0], + y: x[1], + z: x[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); } prismaticJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - const x = JSON.parse(args.X).map(Number) - - const data = RAPIER.JointData.prismatic( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + const x = JSON.parse(args.X).map(Number); + + const data = RAPIER.JointData.prismatic({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }, { + x: x[0], + y: x[1], + z: x[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); } createWorld(args) { - const v3 = JSON.parse(args.G).map(Number) - const gravity = { x: v3[0], y: v3[1], z: v3[2]} - physicsWorld = new RAPIER.World(gravity) + const v3 = JSON.parse(args.G).map(Number); + const gravity = { + x: v3[0], + y: v3[1], + z: v3[2], + }; + physicsWorld = new RAPIER.World(gravity); - console.log(physicsWorld) + console.log(physicsWorld); } getWorld(args) { - if (args.PROPERTY === "log") {console.log(physicsWorld); return "logged"} - return JSON.stringify(physicsWorld[args.PROPERTY]) + if (args.PROPERTY === "log") { + console.log(physicsWorld); + return "logged"; + } + return JSON.stringify(physicsWorld[args.PROPERTY]); } setRB(args) { - let value = args.VALUE - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE - let object = getObject(args.OBJECT) - object.rigidBody[args.PROPERTY](value) + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.rigidBody[args.PROPERTY](value); } setC(args) { - let value = args.VALUE - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE - let object = getObject(args.OBJECT) - object.collider[args.PROPERTY](value) + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.collider[args.PROPERTY](value); } getRB(args) { - let object = getObject(args.OBJECT) - return JSON.stringify(object.rigidBody[args.PROPERTY]()) + let object = getObject(args.OBJECT); + return JSON.stringify(object.rigidBody[args.PROPERTY]()); } getC(args) { - let object = getObject(args.OBJECT) - return JSON.stringify(object.collider[args.PROPERTY]()) + let object = getObject(args.OBJECT); + return JSON.stringify(object.collider[args.PROPERTY]()); } lockObjectAxis(args) { - let object = getObject(args.OBJECT) - const x = !JSON.parse(args.X) - const y = !JSON.parse(args.Y) - const z = !JSON.parse(args.Z) - object.rigidBody[args.PROPERTY](x,y,z,true) //changes is xyz, wake up + let object = getObject(args.OBJECT); + const x = !JSON.parse(args.X); + const y = !JSON.parse(args.Y); + const z = !JSON.parse(args.Z); + object.rigidBody[args.PROPERTY](x, y, z, true); //changes is xyz, wake up } objectPhysics(args) { - let object = getObject(args.OBJECT) - object.physics = JSON.parse(args.state) + let object = getObject(args.OBJECT); + object.physics = JSON.parse(args.state); if (JSON.parse(args.state)) { //if already exists delete: if (object.rigidBody) { - physicsWorld.removeRigidBody(object.rigidBody) - object.rigidBody = null - object.collider = null + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; } /*asing a rigidbody and collider to object and add them to physicsWorld*/ - let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() + let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() .setTranslation(object.position.x, object.position.y, object.position.z) - .setRotation({w: object.quaternion._w, x: object.quaternion._x, y: object.quaternion._y, z: object.quaternion._z}) - - let colliderDesc - switch(args.collider) { - case "cuboid": colliderDesc = createCuboidCollider(object,); break - case "ball": colliderDesc = createBallCollider(object); break - case "convexHull": colliderDesc = createConvexHullCollider(object); break - case "trimesh": colliderDesc = TriMesh(object); break - } - colliderDesc.setSensor(JSON.parse(args.state2)).setMass(args.mass).setDensity(args.density).setFriction(args.friction) + .setRotation({ + w: object.quaternion._w, + x: object.quaternion._x, + y: object.quaternion._y, + z: object.quaternion._z, + }); + + let colliderDesc; + switch (args.collider) { + case "cuboid": + colliderDesc = createCuboidCollider(object); + break; + case "ball": + colliderDesc = createBallCollider(object); + break; + case "convexHull": + colliderDesc = createConvexHullCollider(object); + break; + case "trimesh": + colliderDesc = TriMesh(object); + break; + } + colliderDesc + .setSensor(JSON.parse(args.state2)) + .setMass(args.mass) + .setDensity(args.density) + .setFriction(args.friction); - let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc) - let collider = physicsWorld.createCollider(colliderDesc, rigidBody) + let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc); + let collider = physicsWorld.createCollider(colliderDesc, rigidBody); - object.rigidBody = rigidBody - object.collider = collider + object.rigidBody = rigidBody; + object.collider = collider; } else { /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ - physicsWorld.removeRigidBody(object.rigidBody) - object.rigidBody = null - object.collider = null + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; } - } enableCCD(args) { - let object = getObject(args.OBJECT) + let object = getObject(args.OBJECT); if (object.physics) { - let rigidBody = object.rigidBody - rigidBody.enableCcd(JSON.parse(args.state)) + let rigidBody = object.rigidBody; + rigidBody.enableCcd(JSON.parse(args.state)); } - } + } - addForce(args) { - let object = getObject(args.OBJECT) - const vector = JSON.parse(args.VALUE).map(Number) - - let force = new THREE.Vector3(vector[0],vector[1],vector[2]) + addForce(args) { + let object = getObject(args.OBJECT); + const vector = JSON.parse(args.VALUE).map(Number); + + let force = new THREE.Vector3(vector[0], vector[1], vector[2]); if (args.SPACE === "local") { force.applyQuaternion(object.quaternion); } - object.rigidBody[args.PROPERTY](force,true) - } - - resetForces(args) { - rigidBody[args.PROPERTY](true) - } + object.rigidBody[args.PROPERTY](force, true); + } - sensorSingle(args) { - const sensor = getObject(args.SENSOR) + resetForces(args) { + rigidBody[args.PROPERTY](true); + } - let object = getObject(args.OBJECT) + sensorSingle(args) { + const sensor = getObject(args.SENSOR); - let touching = false - physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { - if (otherCollider === object.collider) touching = true - }) + let object = getObject(args.OBJECT); - return touching - } + let touching = false; + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + if (otherCollider === object.collider) touching = true; + }); - sensorAll(args) { - const sensor = getObject(args.SENSOR) + return touching; + } - const touchedObjects = [] + sensorAll(args) { + const sensor = getObject(args.SENSOR); - // loop thruogh every collider touching sensor - physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { - // find owner of collider - const otherObject = scene.children.find(o => o.collider === otherCollider) - console.log(otherCollider) - if (otherObject) touchedObjects.push(otherObject.name) - }) + const touchedObjects = []; - return JSON.stringify(touchedObjects) - } + // loop thruogh every collider touching sensor + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + // find owner of collider + const otherObject = scene.children.find((o) => o.collider === otherCollider); + console.log(otherCollider); + if (otherObject) touchedObjects.push(otherObject.name); + }); + return JSON.stringify(touchedObjects); + } } - Scratch.extensions.register(new RapierPhysics()) - - //Thanks to the PointerLock extension of Turbowarp - const mouse = vm.runtime.ioDevices.mouse; - let isLocked = false; - let isPointerLockEnabled = false; - - let rect = threeRenderer.domElement.getBoundingClientRect(); - document.addEventListener("resize", () => { - rect = threeRenderer.domElement.getBoundingClientRect(); - }); + Scratch.extensions.register(new RapierPhysics()); - const postMouseData = (e, isDown) => { - const { movementX, movementY } = e; - const { width, height } = rect; - const x = mouse._clientX + movementX; - const y = mouse._clientY - movementY; - mouse._clientX = x; - mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); - mouse._clientY = y; - mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); - if (typeof isDown === "boolean") { - const data = { - button: e.button, - isDown, - }; - originalPostIOData(data); - } - }; + //Thanks to the PointerLock extension of Turbowarp + const mouse = vm.runtime.ioDevices.mouse; + let isLocked = false; + let isPointerLockEnabled = false; - const mouseDevice = vm.runtime.ioDevices.mouse; - const originalPostIOData = mouseDevice.postData.bind(mouseDevice); - mouseDevice.postData = (data) => { - if (!isPointerLockEnabled) { - return originalPostIOData(data); - } - }; + let rect = threeRenderer.domElement.getBoundingClientRect(); + document.addEventListener("resize", () => { + rect = threeRenderer.domElement.getBoundingClientRect(); + }); + + const postMouseData = (e, isDown) => { + const { + movementX, + movementY + } = e; + const { + width, + height + } = rect; + const x = mouse._clientX + movementX; + const y = mouse._clientY - movementY; + mouse._clientX = x; + mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); + mouse._clientY = y; + mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); + if (typeof isDown === "boolean") { + const data = { + button: e.button, + isDown, + }; + originalPostIOData(data); + } + }; + + const mouseDevice = vm.runtime.ioDevices.mouse; + const originalPostIOData = mouseDevice.postData.bind(mouseDevice); + mouseDevice.postData = (data) => { + if (!isPointerLockEnabled) { + return originalPostIOData(data); + } + }; - document.addEventListener( - "mousedown", - (e) => { - // @ts-expect-error - if (threeRenderer.domElement.contains(e.target)) { + document.addEventListener( + "mousedown", + (e) => { + // @ts-expect-error + if (threeRenderer.domElement.contains(e.target)) { + if (isLocked) { + postMouseData(e, true); + } else if (isPointerLockEnabled) { + threeRenderer.domElement.requestPointerLock(); + } + } + }, + true + ); + document.addEventListener( + "mouseup", + (e) => { if (isLocked) { - postMouseData(e, true); - } else if (isPointerLockEnabled) { + postMouseData(e, false); + // @ts-expect-error + } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { threeRenderer.domElement.requestPointerLock(); } + }, + true + ); + document.addEventListener( + "mousemove", + (e) => { + if (isLocked) { + postMouseData(e); + } + }, + true + ); + + document.addEventListener("pointerlockchange", () => { + isLocked = document.pointerLockElement === threeRenderer.domElement; + }); + document.addEventListener("pointerlockerror", (e) => { + console.error("Pointer lock error", e); + }); + + const oldStep = vm.runtime._step; + vm.runtime._step = function(...args) { + const ret = oldStep.call(this, ...args); + if (isPointerLockEnabled) { + const { + width, + height + } = rect; + mouse._clientX = width / 2; + mouse._clientY = height / 2; + mouse._scratchX = 0; + mouse._scratchY = 0; } - }, - true - ); - document.addEventListener( - "mouseup", - (e) => { - if (isLocked) { - postMouseData(e, false); - // @ts-expect-error - } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { - threeRenderer.domElement.requestPointerLock(); - } - }, - true - ); - document.addEventListener( - "mousemove", - (e) => { + return ret; + }; + + vm.runtime.on("PROJECT_LOADED", () => { + isPointerLockEnabled = false; if (isLocked) { - postMouseData(e); + document.exitPointerLock(); } - }, - true - ); - - document.addEventListener("pointerlockchange", () => { - isLocked = document.pointerLockElement === threeRenderer.domElement; - }); - document.addEventListener("pointerlockerror", (e) => { - console.error("Pointer lock error", e); - }); - - const oldStep = vm.runtime._step; - vm.runtime._step = function (...args) { - const ret = oldStep.call(this, ...args); - if (isPointerLockEnabled) { - const { width, height } = rect; - mouse._clientX = width / 2; - mouse._clientY = height / 2; - mouse._scratchX = 0; - mouse._scratchY = 0; - } - return ret; - }; + }); - vm.runtime.on("PROJECT_LOADED", () => { - isPointerLockEnabled = false; - if (isLocked) { - document.exitPointerLock(); - } - }); - - class Pointerlock { - getInfo() { - return { - id: "threepointerlockmod", - name: "Pointerlock for Extra 3D", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "setLocked", blockType: Scratch.BlockType.COMMAND, text: "set pointer lock [enabled]", arguments: { enabled: { type: Scratch.ArgumentType.STRING, defaultValue: "true", menu: "enabled"}},}, - {opcode: "isLocked", blockType: Scratch.BlockType.BOOLEAN, text: "pointer locked?",}, - ], - menus: { - enabled: {acceptReporters: true, items: [ - {text: "enabled", value: "true"},{text: "disabled", value: "false"}, - ]} - }, + class Pointerlock { + getInfo() { + return { + id: "threepointerlockmod", + name: "Pointerlock for Extra 3D", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "setLocked", + blockType: Scratch.BlockType.COMMAND, + text: "set pointer lock [enabled]", + arguments: { + enabled: { + type: Scratch.ArgumentType.STRING, + defaultValue: "true", + menu: "enabled", + }, + }, + }, + { + opcode: "isLocked", + blockType: Scratch.BlockType.BOOLEAN, + text: "pointer locked?", + }, + ], + menus: { + enabled: { + acceptReporters: true, + items: [{ + text: "enabled", + value: "true", + }, + { + text: "disabled", + value: "false", + }, + ], + }, + }, + }; } - } - setLocked(args) { - isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; - if (!isPointerLockEnabled && isLocked) { - document.exitPointerLock(); + setLocked(args) { + isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; + if (!isPointerLockEnabled && isLocked) { + document.exitPointerLock(); + } } - } - isLocked() { - return isLocked; + isLocked() { + return isLocked; + } } - } -Scratch.extensions.register(new Pointerlock()) - - }) - - - - + Scratch.extensions.register(new Pointerlock()); + }); })(Scratch); From 6b1cf7d4b26e8cd8f914e07fceaa2724196cbe7d Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:15:08 -0600 Subject: [PATCH 03/32] Update threejsD.js --- threejsD.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/threejsD.js b/threejsD.js index 447584f..81f3447 100644 --- a/threejsD.js +++ b/threejsD.js @@ -731,6 +731,7 @@ class ThreeScene { constructor() { + // expose threejs and the scenes, so other extensions and javascript can do stuff manually this.THREE = THREE; this.scenes = {}; } @@ -836,17 +837,17 @@ } newScene(args) { - scene = new THREE.Scene(); + const scene = new THREE.Scene(); scene.name = args.NAME; scene.background = new THREE.Color("#222"); - //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - this.scenes = { - ...this.scenes, - ...scene, - }; + + this.scenes[scene.name] = scene; // this can overwrite an existing scene of that name, which is okay + resetor(0); } + + reset() { resetor(1); } From 9f8d403b77877337dc6bc4763c0f0cc8adaae2f3 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:16:19 -0600 Subject: [PATCH 04/32] Update threejsD.js --- threejsD.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/threejsD.js b/threejsD.js index 81f3447..0ede9da 100644 --- a/threejsD.js +++ b/threejsD.js @@ -838,10 +838,10 @@ newScene(args) { const scene = new THREE.Scene(); - scene.name = args.NAME; + scene.name = Scratch.Cast.toString(args.NAME); scene.background = new THREE.Color("#222"); - this.scenes[scene.name] = scene; // this can overwrite an existing scene of that name, which is okay + this.scenes[Scratch.Cast.toString(args.NAME)] = scene; // this can overwrite an existing scene of that name, which is okay resetor(0); } From ab32d4ff76065689f252b472e38ad7104b65dad8 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:40:47 -0600 Subject: [PATCH 05/32] Update threejsD.js --- threejsD.js | 212 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 178 insertions(+), 34 deletions(-) diff --git a/threejsD.js b/threejsD.js index 0ede9da..fbef9c7 100644 --- a/threejsD.js +++ b/threejsD.js @@ -60,7 +60,6 @@ let physicsWorld; let threeRenderer; - let scene; let camera; let eulerOrder = "YXZ"; @@ -87,7 +86,7 @@ function resetor(level) { camera = undefined; - composer.reset(); + if (composer) composer.reset(); passes = {}; customEffects = []; @@ -129,8 +128,36 @@ return [x, y, z]; } + // Helper function to get current scene + function getCurrentScene() { + const threeScene = runtime.ext_threeScene; + if (!threeScene) return null; + const currentSceneName = threeScene.currentSceneName; + return currentSceneName ? threeScene.scenes[currentSceneName] : null; + } + + // Helper function to get scene by name + function getSceneByName(sceneName) { + const threeScene = runtime.ext_threeScene; + if (!threeScene) return null; + return threeScene.scenes[sceneName]; + } + + // Helper function to set current scene + function setCurrentScene(sceneName) { + const threeScene = runtime.ext_threeScene; + if (!threeScene) return; + threeScene.currentSceneName = sceneName; + } + //objects function createObject(name, content, parentName) { + const scene = getCurrentScene(); + if (!scene) { + alerts ? alert("No active scene! Create a scene first!") : null; + return; + } + let object = getObject(name, true); if (object) { removeObject(name); @@ -138,13 +165,25 @@ } content.name = name; content.rotation._order = eulerOrder; - parentName === scene.name ? (object = scene) : (object = getObject(parentName)); + + let parentObject; + if (parentName === scene.name) { + parentObject = scene; + } else { + parentObject = getObject(parentName); + if (!parentObject) { + parentObject = scene; + } + } + content.physics = false; - - object.add(content); + parentObject.add(content); } function removeObject(name) { + const scene = getCurrentScene(); + if (!scene) return; + let object = getObject(name); if (!object) return; @@ -162,15 +201,16 @@ } function getObject(name, isNew) { - let object = null; + const scene = getCurrentScene(); if (!scene) { alerts ? alert("Can not get " + name + ". Create a scene first!") : null; - return; + return null; } - object = scene.getObjectByName(name); + + let object = scene.getObjectByName(name); if (!object && !isNew) { alerts ? alert(name + " does not exist! Add it to scene") : null; - return; + return null; } return object; } @@ -178,7 +218,10 @@ //materials function encodeCostume(name) { if (name.startsWith("data:image/")) return name; - return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); + const editingTarget = vm.editingTarget; + if (!editingTarget) return null; + const costume = editingTarget.sprite.costumes.find((c) => c.name === name); + return costume ? costume.asset.encodeDataURI() : null; } function setTexutre(texture, mode, style, x, y) { @@ -252,6 +295,7 @@ } //composer function updateComposers() { + const scene = getCurrentScene(); if (!camera || !scene) return; // nothing to do yet // always recreate the RenderPass to point to the current scene/camera @@ -563,8 +607,15 @@ const loop = () => { if (!running) return; + + const scene = getCurrentScene(); + if (!scene) { + loopId = requestAnimationFrame(loop); + return; + } + //RAPIER - if (physicsWorld && scene) { + if (physicsWorld) { physicsWorld.step(); scene.children.forEach((obj) => { @@ -575,7 +626,7 @@ } }); } - if (scene && camera) { + if (camera) { if (controls) controls.update(); const delta = clock.getDelta(); @@ -634,7 +685,7 @@ const h = canvas.height; threeRenderer.setSize(w, h); - composer.setSize(w, h); + if (composer) composer.setSize(w, h); customEffects.forEach((e) => { if (e.uniforms.get("resolution")) { e.uniforms.get("resolution").value.set(w, h); @@ -734,6 +785,7 @@ // expose threejs and the scenes, so other extensions and javascript can do stuff manually this.THREE = THREE; this.scenes = {}; + this.currentSceneName = null; } getInfo() { @@ -838,27 +890,42 @@ newScene(args) { const scene = new THREE.Scene(); - scene.name = Scratch.Cast.toString(args.NAME); + const sceneName = Scratch.Cast.toString(args.NAME); + scene.name = sceneName; scene.background = new THREE.Color("#222"); - this.scenes[Scratch.Cast.toString(args.NAME)] = scene; // this can overwrite an existing scene of that name, which is okay + this.scenes[sceneName] = scene; + this.currentSceneName = sceneName; // Set as current scene resetor(0); } - - reset() { resetor(1); + this.scenes = {}; + this.currentSceneName = null; } async setSceneProperty(args) { + const scene = getCurrentScene(); + if (!scene) { + alerts ? alert("No active scene! Create a scene first!") : null; + return; + } + const property = args.PROPERTY; const value = getAsset(args.VALUE); scene[property] = value; } + getSceneObjects(args) { + const scene = getCurrentScene(); + if (!scene) { + alerts ? alert("No active scene! Create a scene first!") : null; + return "[]"; + } + const names = []; if (args.THING === "Objects") { scene.traverse((obj) => { @@ -866,7 +933,7 @@ }); } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); - else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); + else if (args.THING === "Lights") return JSON.stringify(Object.keys(lights)); else if (args.THING === "Scene Properties") { console.log(scene); return "check console"; @@ -1075,11 +1142,13 @@ } setCamera(args) { let object = getObject(args.CAMERA); + if (!object) return; object[args.PROPERTY] = args.VALUE; object.updateProjectionMatrix(); } getCamera(args) { let object = getObject(args.CAMERA); + if (!object) return "null"; const value = JSON.stringify(object[args.PROPERTY]); return value; } @@ -1112,6 +1181,8 @@ renderTarget(args) { let object = getObject(args.CAMERA); + if (!object) return; + const renderTarget = new THREE.WebGLRenderTarget(360, 360, { generateMipmaps: false, }); @@ -1120,19 +1191,25 @@ target: renderTarget, camera: object, }; - assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; + assets.renderTargets[renderTarget.texture.uuid] = renderTarget.texture; } sizeTarget(args) { - renderTargets[args.RT].target.setSize(args.W, args.H); + const target = renderTargets[args.RT]; + if (!target) return; + target.target.setSize(args.W, args.H); } getTarget(args) { - const t = renderTargets[args.RT].target.texture; + const target = renderTargets[args.RT]; + if (!target) return "null"; + const t = target.target.texture; console.log(t, renderTargets[args.RT]); return `renderTargets/${t.uuid}`; } removeTarget(args) { - delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; - renderTargets[args.RT].target.dispose(); + const target = renderTargets[args.RT]; + if (!target) return; + delete assets.renderTargets[target.target.texture.uuid]; + target.target.dispose(); delete renderTargets[args.RT]; } } @@ -2195,12 +2272,15 @@ } cloneObject(args) { let object = getObject(args.OBJECT3D); + if (!object) return; const clone = object.clone(true); clone.name; createObject(args.NAME, clone, args.GROUP); } setObjectV3(args) { let object = getObject(args.OBJECT3D); + if (!object) return; + let values = JSON.parse(args.VALUE); function degToRad(deg) { @@ -2274,7 +2354,7 @@ */ getObjectV3(args) { let object = getObject(args.OBJECT3D); - if (!object) return; + if (!object) return "[0,0,0]"; let values = vector3ToString(object[args.PROPERTY]); if (args.PROPERTY === "rotation") { const toDeg = Math.PI / 180; @@ -2285,6 +2365,8 @@ } setObject(args) { let object = getObject(args.OBJECT3D); + if (!object) return; + let value = args.VALUE; if (args.PROPERTY === "material") { const mat = materials[args.NAME]; @@ -2301,7 +2383,7 @@ } getObject(args) { let object = getObject(args.OBJECT3D); - if (!object) return; + if (!object) return "null"; let value; if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; else value = object.visible; @@ -2312,6 +2394,8 @@ removeObject(args.OBJECT3D); } objectE(args) { + const scene = getCurrentScene(); + if (!scene) return false; return scene.children.map((o) => o.name).includes(args.NAME); } @@ -2326,6 +2410,7 @@ async setMaterial(args) { if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; const mat = materials[args.NAME]; + if (!mat) return; let value = args.VALUE; @@ -2344,17 +2429,20 @@ } setBlending(args) { const mat = materials[args.NAME]; + if (!mat) return; mat.blending = THREE[args.VALUE]; mat.premultipliedAlpha = true; mat.needsUpdate = true; } setDepth(args) { const mat = materials[args.NAME]; + if (!mat) return; mat.depthFunc = THREE[args.VALUE]; mat.needsUpdate = true; } removeMaterial(args) { const mat = materials[args.NAME]; + if (!mat) return; mat.dispose(); delete materials[args.NAME]; } @@ -2371,12 +2459,14 @@ } setGeometry(args) { const geo = geometries[args.NAME]; + if (!geo) return; geo[args.PROPERTY] = args.VALUE; geo.needsUpdate = true; } removeGeometry(args) { const geo = geometries[args.NAME]; + if (!geo) return; geo.dispose(); delete geometries[args.NAME]; } @@ -2391,6 +2481,7 @@ } async geoPoints(args) { const geometry = geometries[args.NAME]; + if (!geometry) return; const positions = args.POINTS.split(" ") .map((v) => JSON.parse(v)) .flat(); //array of v3 of each vertex of each triangle @@ -2402,6 +2493,7 @@ } geoUVs(args) { const geometry = geometries[args.NAME]; + if (!geometry) return; const UVs = args.POINTS.split(" ") .map((v) => JSON.parse(v)) .flat(); //array of v2 of each UV of each triangle @@ -2691,7 +2783,9 @@ if (light.type === "PointLight") return; //Directional & Spot Light light.target.position.set(0, 0, 0); - scene.add(light.target); + + const scene = getCurrentScene(); + if (scene) scene.add(light.target); light.pos = new THREE.Vector3(0, 0, 0); @@ -2710,7 +2804,8 @@ setLight(args) { const light = lights[args.NAME]; - if (!args.PROPERTY) return; + if (!light || !args.PROPERTY) return; + if (args.PROPERTY === "target") { light.target.position.set(...JSON.parse(args.VALUE)); //vector3 light.target.updateMatrixWorld(); @@ -3187,6 +3282,7 @@ } async newTexture(args) { const textureURI = encodeCostume(args.COSTUME); + if (!textureURI) return "null"; const texture = await new THREE.TextureLoader().loadAsync(textureURI); texture.name = args.COSTUME; @@ -3203,6 +3299,9 @@ encodeCostume(args.COSTUMEZ0), encodeCostume(args.COSTUMEZ1), ]; + // Check if all URIs are valid + if (uris.some(uri => !uri)) return "null"; + const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); @@ -3214,6 +3313,7 @@ } async newEquirectangularTexture(args) { const textureURI = encodeCostume(args.COSTUME); + if (!textureURI) return "null"; const texture = await new THREE.TextureLoader().loadAsync(textureURI); texture.name = args.COSTUME; texture.mapping = THREE.EquirectangularReflectionMapping; @@ -3254,6 +3354,9 @@ } raycast(args) { + const scene = getCurrentScene(); + if (!scene) return; + const origin = new THREE.Vector3(...JSON.parse(args.V3)); // rotation is in degrees => convert to radians first const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); @@ -3484,7 +3587,7 @@ createObject(args.NAME, group, args.GROUP); } getModel(args) { - if (!models[args.NAME]) return; + if (!models[args.NAME]) return "null"; return Object.keys(models[args.NAME].actions).toString(); } @@ -3788,6 +3891,7 @@ } bloom(args) { + const scene = getCurrentScene(); if (!camera || !scene) { if (alerts) alert("set a camera!"); return; @@ -3806,11 +3910,13 @@ } godRays(args) { + const scene = getCurrentScene(); if (!camera || !scene) { if (alerts) alert("set a camera!"); return; } let object = getObject(args.NAME); + if (!object) return; const sun = object; const godRays = new GodRaysEffect(camera, sun, { @@ -3828,6 +3934,7 @@ } dots(args) { + const scene = getCurrentScene(); if (!camera || !scene) { if (alerts) alert("set a camera!"); return; @@ -3843,6 +3950,7 @@ } depth(args) { + const scene = getCurrentScene(); if (!camera || !scene) { if (alerts) alert("set a camera!"); return; @@ -4584,6 +4692,7 @@ }; } joint(jointData, bodyA, bodyB) { + if (!physicsWorld || !bodyA || !bodyB) return; physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); } @@ -4702,6 +4811,7 @@ } getWorld(args) { + if (!physicsWorld) return "null"; if (args.PROPERTY === "log") { console.log(physicsWorld); return "logged"; @@ -4713,26 +4823,32 @@ let value = args.VALUE; if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; let object = getObject(args.OBJECT); + if (!object || !object.rigidBody) return; object.rigidBody[args.PROPERTY](value); } setC(args) { let value = args.VALUE; if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; let object = getObject(args.OBJECT); + if (!object || !object.collider) return; object.collider[args.PROPERTY](value); } getRB(args) { let object = getObject(args.OBJECT); + if (!object || !object.rigidBody) return "null"; return JSON.stringify(object.rigidBody[args.PROPERTY]()); } getC(args) { let object = getObject(args.OBJECT); + if (!object || !object.collider) return "null"; return JSON.stringify(object.collider[args.PROPERTY]()); } lockObjectAxis(args) { let object = getObject(args.OBJECT); + if (!object || !object.rigidBody) return; + const x = !JSON.parse(args.X); const y = !JSON.parse(args.Y); const z = !JSON.parse(args.Z); @@ -4741,15 +4857,25 @@ objectPhysics(args) { let object = getObject(args.OBJECT); + if (!object) return; + object.physics = JSON.parse(args.state); if (JSON.parse(args.state)) { //if already exists delete: if (object.rigidBody) { - physicsWorld.removeRigidBody(object.rigidBody); + if (physicsWorld) { + physicsWorld.removeRigidBody(object.rigidBody); + } object.rigidBody = null; object.collider = null; } + + if (!physicsWorld) { + console.error("Physics world not created!"); + return; + } + /*asing a rigidbody and collider to object and add them to physicsWorld*/ let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() .setTranslation(object.position.x, object.position.y, object.position.z) @@ -4774,7 +4900,13 @@ case "trimesh": colliderDesc = TriMesh(object); break; + default: + console.error("Unknown collider type:", args.collider); + return; } + + if (!colliderDesc) return; + colliderDesc .setSensor(JSON.parse(args.state2)) .setMass(args.mass) @@ -4788,7 +4920,9 @@ object.collider = collider; } else { /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ - physicsWorld.removeRigidBody(object.rigidBody); + if (physicsWorld && object.rigidBody) { + physicsWorld.removeRigidBody(object.rigidBody); + } object.rigidBody = null; object.collider = null; } @@ -4796,7 +4930,7 @@ enableCCD(args) { let object = getObject(args.OBJECT); - if (object.physics) { + if (object && object.physics && object.rigidBody) { let rigidBody = object.rigidBody; rigidBody.enableCcd(JSON.parse(args.state)); } @@ -4804,6 +4938,8 @@ addForce(args) { let object = getObject(args.OBJECT); + if (!object || !object.rigidBody) return; + const vector = JSON.parse(args.VALUE).map(Number); let force = new THREE.Vector3(vector[0], vector[1], vector[2]); @@ -4815,13 +4951,18 @@ } resetForces(args) { - rigidBody[args.PROPERTY](true); + let object = getObject(args.OBJECT); + if (!object || !object.rigidBody) return; + + object.rigidBody[args.PROPERTY](true); } sensorSingle(args) { const sensor = getObject(args.SENSOR); + if (!sensor || !sensor.collider || !physicsWorld) return false; let object = getObject(args.OBJECT); + if (!object || !object.collider) return false; let touching = false; physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { @@ -4833,14 +4974,17 @@ sensorAll(args) { const sensor = getObject(args.SENSOR); + if (!sensor || !sensor.collider || !physicsWorld) return "[]"; const touchedObjects = []; - // loop thruogh every collider touching sensor + // loop through every collider touching sensor physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { // find owner of collider + const scene = getCurrentScene(); + if (!scene) return; + const otherObject = scene.children.find((o) => o.collider === otherCollider); - console.log(otherCollider); if (otherObject) touchedObjects.push(otherObject.name); }); From dec00ad042c865607fac0e24f34d37ab14005a2a Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:44:19 -0600 Subject: [PATCH 06/32] Update threejsD.js --- threejsD.js | 1 + 1 file changed, 1 insertion(+) diff --git a/threejsD.js b/threejsD.js index fbef9c7..90ff344 100644 --- a/threejsD.js +++ b/threejsD.js @@ -545,6 +545,7 @@ stencil: false, depth: true, }); + window.__THREE_RENDERER__ = threeRenderer; threeRenderer.setPixelRatio(window.devicePixelRatio); threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) From 23f2ee20b4aaa7037b00e02cf3a61f2020f9a8e5 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:52:47 -0600 Subject: [PATCH 07/32] Update threejsD.js --- threejsD.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/threejsD.js b/threejsD.js index 90ff344..186548c 100644 --- a/threejsD.js +++ b/threejsD.js @@ -491,6 +491,7 @@ //loops/init function stopLoop() { if (!running) return; + vm.renderer.canvas.style.visibility="visible"; running = false; if (loopId) { @@ -604,6 +605,7 @@ function startRenderLoop() { if (running) return; + vm.renderer.canvas.style.visibility="hidden"; running = true; const loop = () => { From 930b4e847983db736f94706e1dd4da1889bce73d Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:07:48 -0600 Subject: [PATCH 08/32] Update threejsD.js --- threejsD.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/threejsD.js b/threejsD.js index 186548c..e628135 100644 --- a/threejsD.js +++ b/threejsD.js @@ -69,7 +69,9 @@ let renderTargets = {}; let materials = {}; + window.__THREE_MATERIALS__ = materials; let geometries = {}; + window.__THREE_GEOMETRIES__ = geometries; let lights = {}; let models = {}; From 8a85ec952dc23ef7cfab5015119c1b46d5769e5b Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:11:17 -0600 Subject: [PATCH 09/32] Update threejsD.js --- threejsD.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/threejsD.js b/threejsD.js index e628135..d08c71e 100644 --- a/threejsD.js +++ b/threejsD.js @@ -95,7 +95,9 @@ renderTargets = {}; materials = {}; + window.__THREE_MATERIALS__ = materials; geometries = {}; + window.__THREE_GEOMETRIES__ = geometries; lights = {}; models = {}; From 293ed4d41da5de52bf711403747ed7278b49bee8 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:14:25 -0600 Subject: [PATCH 10/32] Update threejsD.js --- threejsD.js | 44 +++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/threejsD.js b/threejsD.js index d08c71e..c0e07de 100644 --- a/threejsD.js +++ b/threejsD.js @@ -67,11 +67,8 @@ let passes = {}; let customEffects = []; let renderTargets = {}; - - let materials = {}; - window.__THREE_MATERIALS__ = materials; - let geometries = {}; - window.__THREE_GEOMETRIES__ = geometries; + window.__THREE_MATERIALS__ = {}; + window.__THREE_GEOMETRIES__ = {} let lights = {}; let models = {}; @@ -93,11 +90,8 @@ passes = {}; customEffects = []; renderTargets = {}; - - materials = {}; - window.__THREE_MATERIALS__ = materials; - geometries = {}; - window.__THREE_GEOMETRIES__ = geometries; + window.__THREE_MATERIALS__ = {}; + window.__THREE_GEOMETRIES__ = {}; lights = {}; models = {}; @@ -938,8 +932,8 @@ scene.traverse((obj) => { if (obj.name) names.push(obj.name); //if it has a name, add to list! }); - } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); - else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); + } else if (args.THING === "Materials") return JSON.stringify(Object.keys(window.__THREE_MATERIALS__)); + else if (args.THING === "Geometries") return JSON.stringify(Object.keys(window.__THREE_GEOMETRIES__)); else if (args.THING === "Lights") return JSON.stringify(Object.keys(lights)); else if (args.THING === "Scene Properties") { console.log(scene); @@ -2380,7 +2374,7 @@ if (mat) value = mat; else value = undefined; } else if (args.PROPERTY === "geometry") { - const geo = geometries[args.NAME]; + const geo = window.__THREE_GEOMETRIES__[args.NAME]; if (geo) value = geo; else value = undefined; } else value = !!value; @@ -2458,36 +2452,36 @@ } newGeometry(args) { - if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); + if (window.__THREE_GEOMETRIES__[args.NAME] && alerts) alert("geometry already exists! will replace..."); const geo = new THREE[args.TYPE](); geo.name = args.NAME; - geometries[args.NAME] = geo; + window.__THREE_GEOMETRIES__[args.NAME] = geo; } setGeometry(args) { - const geo = geometries[args.NAME]; + const geo = window.__THREE_GEOMETRIES__[args.NAME]; if (!geo) return; geo[args.PROPERTY] = args.VALUE; geo.needsUpdate = true; } removeGeometry(args) { - const geo = geometries[args.NAME]; + const geo = window.__THREE_GEOMETRIES__[args.NAME]; if (!geo) return; geo.dispose(); - delete geometries[args.NAME]; + delete window.__THREE_GEOMETRIES__[args.NAME]; } geometryE(args) { - return geometries[args.NAME] ? true : false; + return window.__THREE_GEOMETRIES__[args.NAME] ? true : false; } newGeo(args) { const geometry = new THREE.BufferGeometry(); geometry.name = args.NAME; - geometries[args.NAME] = geometry; + window.__THREE_GEOMETRIES__[args.NAME] = geometry; } async geoPoints(args) { - const geometry = geometries[args.NAME]; + const geometry = window.__THREE_GEOMETRIES__[args.NAME]; if (!geometry) return; const positions = args.POINTS.split(" ") .map((v) => JSON.parse(v)) @@ -2499,7 +2493,7 @@ geometry.needsUpdate = true; } geoUVs(args) { - const geometry = geometries[args.NAME]; + const geometry = window.__THREE_GEOMETRIES__[args.NAME]; if (!geometry) return; const UVs = args.POINTS.split(" ") .map((v) => JSON.parse(v)) @@ -2513,7 +2507,7 @@ const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); geometry.name = args.NAME; - geometries[args.NAME] = geometry; + window.__THREE_GEOMETRIES__[args.NAME] = geometry; } async splineModel(args) { @@ -2563,7 +2557,7 @@ merged.computeBoundingSphere(); merged.name = args.NAME; - geometries[args.NAME] = merged; + window.__THREE_GEOMETRIES__[args.NAME] = merged; matList.name = args.NAME; materials[args.NAME] = matList; } @@ -2593,7 +2587,7 @@ geometry.name = args.NAME; - geometries[args.NAME] = geometry; + window.__THREE_GEOMETRIES__[args.NAME] = geometry; } async loadFont() { From bcb905793defdadf8a482d3925fca113bdea985a Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:15:36 -0600 Subject: [PATCH 11/32] Update threejsD.js --- threejsD.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/threejsD.js b/threejsD.js index c0e07de..ad7673c 100644 --- a/threejsD.js +++ b/threejsD.js @@ -2370,7 +2370,7 @@ let value = args.VALUE; if (args.PROPERTY === "material") { - const mat = materials[args.NAME]; + const mat = window.__THREE_MATERIALS__[args.NAME]; if (mat) value = mat; else value = undefined; } else if (args.PROPERTY === "geometry") { @@ -2402,15 +2402,15 @@ //defines newMaterial(args) { - if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); + if (window.__THREE_MATERIALS__[args.NAME] && alerts) alert("material already exists! will replace..."); const mat = new THREE[args.TYPE](); mat.name = args.NAME; - materials[args.NAME] = mat; + window.__THREE_MATERIALS__[args.NAME] = mat; } async setMaterial(args) { if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; - const mat = materials[args.NAME]; + const mat = window.__THREE_MATERIALS__[args.NAME]; if (!mat) return; let value = args.VALUE; @@ -2429,26 +2429,26 @@ mat.needsUpdate = true; } setBlending(args) { - const mat = materials[args.NAME]; + const mat = window.__THREE_MATERIALS__[args.NAME]; if (!mat) return; mat.blending = THREE[args.VALUE]; mat.premultipliedAlpha = true; mat.needsUpdate = true; } setDepth(args) { - const mat = materials[args.NAME]; + const mat = window.__THREE_MATERIALS__[args.NAME]; if (!mat) return; mat.depthFunc = THREE[args.VALUE]; mat.needsUpdate = true; } removeMaterial(args) { - const mat = materials[args.NAME]; + const mat = window.__THREE_MATERIALS__[args.NAME]; if (!mat) return; mat.dispose(); - delete materials[args.NAME]; + delete window.__THREE_MATERIALS__[args.NAME]; } materialE(args) { - return materials[args.NAME] ? true : false; + return window.__THREE_MATERIALS__[args.NAME] ? true : false; } newGeometry(args) { @@ -2559,7 +2559,7 @@ merged.name = args.NAME; window.__THREE_GEOMETRIES__[args.NAME] = merged; matList.name = args.NAME; - materials[args.NAME] = matList; + window.__THREE_MATERIALS__[args.NAME] = matList; } async text(args) { From 3db54fdb1219ad5561bdf3bb3de6fd8d73a269c4 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Mon, 15 Dec 2025 08:55:05 -0600 Subject: [PATCH 12/32] Update threejsD.js --- threejsD.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/threejsD.js b/threejsD.js index 2d9823c..27ba380 100644 --- a/threejsD.js +++ b/threejsD.js @@ -643,6 +643,11 @@ Promise.resolve(load()).then(() => { Scratch.extensions.register(new ThreeRenderer()) class ThreeScene { + constructor() { + this.THREE = THREE; + this.scenes = {}; + } + getInfo() { return { id: "threeScene", @@ -674,7 +679,7 @@ Promise.resolve(load()).then(() => { scene.name = args.NAME scene.background = new THREE.Color("#222") //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - + this.scenes = {...this.scenes, {scene}}; resetor(0) } From b813f957ef146b9defa1e37a696c3253ac164391 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 Date: Tue, 16 Dec 2025 16:59:54 -0600 Subject: [PATCH 13/32] merge --- threejsD.js | 6475 ++++++++++++++++++++++++++------------ threejsD.js.orig | 5029 +++++++++++++++++++++++++++++ threejsD_BACKUP_13852.js | 5029 +++++++++++++++++++++++++++++ threejsD_BASE_13852.js | 2413 ++++++++++++++ threejsD_LOCAL_13852.js | 2414 ++++++++++++++ threejsD_REMOTE_13852.js | 5016 +++++++++++++++++++++++++++++ 6 files changed, 24439 insertions(+), 1937 deletions(-) create mode 100644 threejsD.js.orig create mode 100644 threejsD_BACKUP_13852.js create mode 100644 threejsD_BASE_13852.js create mode 100644 threejsD_LOCAL_13852.js create mode 100644 threejsD_REMOTE_13852.js diff --git a/threejsD.js b/threejsD.js index 27ba380..bc6db57 100644 --- a/threejsD.js +++ b/threejsD.js @@ -1,88 +1,102 @@ +/* jshint esversion: 11 */ // Name: Extra 3D // ID: threejsExtension -// Description: Use three js inside Turbowarp! A 3D graphics library. +// Description: Use three js inside Turbowarp! A 3D graphics library. // By: Civero // License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors -(function (Scratch) { +(function(Scratch) { "use strict"; if (!Scratch.extensions.unsandboxed) { throw new Error("Three-D extension must run unsandboxed"); } - if (Scratch.vm.runtime.isPackaged) {alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); return} + if (Scratch.vm.runtime.isPackaged) { + alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); + return; + } //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return const vm = Scratch.vm; - const runtime = vm.runtime + const runtime = vm.runtime; const renderer = Scratch.renderer; - const canvas = renderer.canvas + const canvas = renderer.canvas; const Cast = Scratch.Cast; - const menuIconURI = "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; - - let alerts = false - console.log("alerts are "+ (alerts ? "enabled" : "disabled")) + const menuIconURI = + "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; + + let alerts = false; + console.log("alerts are " + (alerts ? "enabled" : "disabled")); - let isMouseDown = { left: false, middle: false, right: false } - let prevMouse = { left: false, middle: false, right: false } + let isMouseDown = { + left: false, + middle: false, + right: false, + }; + let prevMouse = { + left: false, + middle: false, + right: false, + }; - let lastWidth = 0 - let lastHeight = 0 + let lastWidth = 0; + let lastHeight = 0; - let THREE - let clock - let running - let loopId + let THREE; + let clock; + let running; + let loopId; //Addons - let GLTFLoader - let gltf - let OrbitControls - let controls - let BufferGeometryUtils - let TextGeometry - let fontLoad + let GLTFLoader; + let gltf; + let OrbitControls; + let controls; + let BufferGeometryUtils; + let TextGeometry; + let fontLoad; //Physics - let RAPIER - let physicsWorld - - let threeRenderer - let scene - let camera - let eulerOrder = "YXZ" - - let composer - let passes = {} - let customEffects = [] - let renderTargets = {} - - let materials = {} - let geometries = {} - let lights = {} - let models = {} - - let assets = { //should i place materials, geometries; inside too? + let RAPIER; + let physicsWorld; + + let threeRenderer; + let scene; + let camera; + let eulerOrder = "YXZ"; + + let composer; + let passes = {}; + let customEffects = []; + let renderTargets = {}; + + let materials = {}; + let geometries = {}; + let lights = {}; + let models = {}; + + let assets = { + //should i place materials, geometries; inside too? textures: {}, colors: {}, fogs: {}, curves: {}, renderTargets: {}, //not the same as the global one! this one only stores textures - } + }; - let raycastResult = [] + let raycastResult = []; function resetor(level) { - camera = undefined - composer.reset() + camera = undefined; + composer.reset(); - passes = {} - customEffects = [] - renderTargets = {} + passes = {}; + customEffects = []; + renderTargets = {}; - materials = {} - geometries = {} - lights = {} - models = {} + materials = {}; + geometries = {}; + lights = {}; + models = {}; if (level > 0) { assets = { @@ -91,124 +105,140 @@ fogs: {}, curves: {}, renderTargets: {}, - } + }; } - updateComposers() + updateComposers(); } - -//utility - function vector3ToString(prop) { - if (!prop) return "0,0,0"; - const x = (typeof(prop.x) === "number") ? prop.x : (typeof(prop._x) === "number") ? prop._x : (JSON.stringify(prop).includes("X")) ? prop : 0 - const y = (typeof(prop.y) === "number") ? prop.y : (typeof(prop.y) === "number") ? prop._y : 0 - const z = (typeof(prop.z) === "number") ? prop.z : (typeof(prop.z) === "number") ? prop.z : 0 + //utility + function vector3ToString(prop) { + if (!prop) return "0,0,0"; + + const x = + typeof prop.x === "number" ? + prop.x : + typeof prop._x === "number" ? + prop._x : + JSON.stringify(prop).includes("X") ? + prop : + 0; + const y = typeof prop.y === "number" ? prop.y : typeof prop.y === "number" ? prop._y : 0; + const z = typeof prop.z === "number" ? prop.z : typeof prop.z === "number" ? prop.z : 0; + + return [x, y, z]; + } - return [x, y, z] + //objects + function createObject(name, content, parentName) { + let object = getObject(name, true); + if (object) { + removeObject(name); + alerts ? alert(name + " already exsisted, will replace!") : null; } + content.name = name; + content.rotation._order = eulerOrder; + parentName === scene.name ? (object = scene) : (object = getObject(parentName)); + content.physics = false; -//objects - function createObject(name, content, parentName) { - let object = getObject(name, true) - if (object) { - removeObject(name) - alerts ? alert(name + " already exsisted, will replace!") : null - } - content.name = name - content.rotation._order = eulerOrder - parentName === scene.name ? object = scene : object = getObject(parentName) - content.physics = false + object.add(content); + } - object.add(content) - } - function removeObject(name) { - let object = getObject(name) - if (!object) return + function removeObject(name) { + let object = getObject(name); + if (!object) return; - scene.remove(object) + scene.remove(object); - if (object.rigidBody) { - physicsWorld.removeCollider(object.collider, true) - physicsWorld.removeRigidBody(object.rigidBody, true) - object.rigidBody = null - object.collider = null - } - if (object.isLight) { - delete(lights[name]) - } + if (object.rigidBody) { + physicsWorld.removeCollider(object.collider, true); + physicsWorld.removeRigidBody(object.rigidBody, true); + object.rigidBody = null; + object.collider = null; } - function getObject(name, isNew) { - let object = null - if (!scene) { - alerts ? alert("Can not get " + name + ". Create a scene first!") : null; return;} - object = scene.getObjectByName(name) - if (!object && !isNew) {alerts ? alert(name + " does not exist! Add it to scene"):null; return;} - return object + if (object.isLight) { + delete lights[name]; } + } -//materials - function encodeCostume (name) { - if (name.startsWith("data:image/")) return name - return Scratch.vm.editingTarget.sprite.costumes.find(c => c.name === name).asset.encodeDataURI() + function getObject(name, isNew) { + let object = null; + if (!scene) { + alerts ? alert("Can not get " + name + ". Create a scene first!") : null; + return; + } + object = scene.getObjectByName(name); + if (!object && !isNew) { + alerts ? alert(name + " does not exist! Add it to scene") : null; + return; } - function setTexutre (texture, mode, style, x, y) { - texture.colorSpace = THREE.SRGBColorSpace + return object; + } + + //materials + function encodeCostume(name) { + if (name.startsWith("data:image/")) return name; + return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); + } + + function setTexutre(texture, mode, style, x, y) { + texture.colorSpace = THREE.SRGBColorSpace; - if (mode === "Pixelate") { + if (mode === "Pixelate") { texture.minFilter = THREE.NearestFilter; texture.magFilter = THREE.NearestFilter; - } else { //Blur - texture.minFilter = THREE.NearestMipmapLinearFilter - texture.magFilter = THREE.NearestMipmapLinearFilter - } - - if (style === "Repeat") { - texture.wrapS = THREE.RepeatWrapping - texture.wrapT = THREE.RepeatWrapping - texture.repeat.set(x, y) - } + } else { + //Blur + texture.minFilter = THREE.NearestMipmapLinearFilter; + texture.magFilter = THREE.NearestMipmapLinearFilter; + } - texture.generateMipmaps = true; + if (style === "Repeat") { + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(x, y); } - async function resizeImageToSquare(uri, size = 256) { + + texture.generateMipmaps = true; + } + async function resizeImageToSquare(uri, size = 256) { return new Promise((resolve) => { - const img = new Image() - img.onload = () => { - const canvas = document.createElement('canvas') - canvas.width = size - canvas.height = size - const ctx = canvas.getContext('2d') - - // clear + draw image scaled to fit canvas - ctx.clearRect(0, 0, size, size) - ctx.drawImage(img, 0, 0, size, size) - - resolve(canvas.toDataURL()) // return normalized Data URI - //delete canvas? - }; - img.src = uri - }); -} -//light -function updateShadowFrustum(light, focusPos) { - if (light.type !== "DirectionalLight") return + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + + // clear + draw image scaled to fit canvas + ctx.clearRect(0, 0, size, size); + ctx.drawImage(img, 0, 0, size, size); + + resolve(canvas.toDataURL()); // return normalized Data URI + //delete canvas? + }; + img.src = uri; + }); + } + //light + function updateShadowFrustum(light, focusPos) { + if (light.type !== "DirectionalLight") return; // Frustum Size - Increase this value to cover a larger area. const d = 50; // Update Orthographic Shadow Camera Frustum const shadowCamera = light.shadow.camera; - + // Set the width/height of the frustum shadowCamera.left = -d; shadowCamera.right = d; shadowCamera.top = d; shadowCamera.bottom = -d; - + // Determine ranges - shadowCamera.near = 0.1 - shadowCamera.far = 500 + shadowCamera.near = 0.1; + shadowCamera.far = 500; // Position the Light and its Target light.target.position.copy(focusPos); @@ -217,69 +247,79 @@ function updateShadowFrustum(light, focusPos) { // Ensure matrices are updated. light.target.updateMatrixWorld(); - light.shadow.camera.updateProjectionMatrix() - light.shadow.needsUpdate = true; -} -//composer -function updateComposers() { - if (!camera || !scene) return; // nothing to do yet - - // always recreate the RenderPass to point to the current scene/camera - passes["Render"] = new RenderPass(scene, camera); - - // ensure composer has a RenderPass as the first pass - const hasRender = composer.passes.some(p => p && p.scene); - if (!hasRender) composer.addPass(passes["Render"]); - else { - // if composer already has one, replace it so it references current scene/camera - const idx = composer.passes.findIndex(p => p && p.scene); - composer.passes[idx] = passes["Render"]; + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; } -} -//utility -function getMouseNDC(event) { - // Use threeRenderer.domElement for correct offset - const rect = threeRenderer.domElement.getBoundingClientRect(); - const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - return [x, y]; -} -function checkCanvasSize() { - const { width, height } = canvas - if (width !== lastWidth || height !== lastHeight) { - lastWidth = width - lastHeight = height - resize() + //composer + function updateComposers() { + if (!camera || !scene) return; // nothing to do yet + + // always recreate the RenderPass to point to the current scene/camera + passes["Render"] = new RenderPass(scene, camera); + + // ensure composer has a RenderPass as the first pass + const hasRender = composer.passes.some((p) => p && p.scene); + if (!hasRender) composer.addPass(passes["Render"]); + else { + // if composer already has one, replace it so it references current scene/camera + const idx = composer.passes.findIndex((p) => p && p.scene); + composer.passes[idx] = passes["Render"]; + } + } + //utility + function getMouseNDC(event) { + // Use threeRenderer.domElement for correct offset + const rect = threeRenderer.domElement.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + return [x, y]; + } + + function checkCanvasSize() { + const { + width, + height + } = canvas; + if (width !== lastWidth || height !== lastHeight) { + lastWidth = width; + lastHeight = height; + resize(); + } + requestAnimationFrame(checkCanvasSize); //rerun next frame } - requestAnimationFrame(checkCanvasSize) //rerun next frame -} -//physics -function computeWorldBoundingBox(mesh) { + //physics + function computeWorldBoundingBox(mesh) { // Create a Box3 in world coordinates const box = new THREE.Box3().setFromObject(mesh); const size = new THREE.Vector3(); box.getSize(size); const center = new THREE.Vector3(); box.getCenter(center); - return { size, center }; -} -function createCuboidCollider(mesh) { - const { size } = computeWorldBoundingBox(mesh); - const collider = RAPIER.ColliderDesc.cuboid( - size.x / 2, - size.y / 2, - size.z / 2 - ) + return { + size, + center, + }; + } + + function createCuboidCollider(mesh) { + const { + size + } = computeWorldBoundingBox(mesh); + const collider = RAPIER.ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2); return collider; -} -function createBallCollider(mesh) { - const { size } = computeWorldBoundingBox(mesh); + } + + function createBallCollider(mesh) { + const { + size + } = computeWorldBoundingBox(mesh); // radius = 1/2 of the largest verticie const radius = Math.max(size.x, size.y, size.z) / 2; - const collider = RAPIER.ColliderDesc.ball(radius) - return collider //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) -} -function createConvexHullCollider(mesh) { + const collider = RAPIER.ColliderDesc.ball(radius); + return collider; //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) + } + + function createConvexHullCollider(mesh) { mesh.updateWorldMatrix(true, false); const position = mesh.geometry.attributes.position; @@ -287,211 +327,226 @@ function createConvexHullCollider(mesh) { const vertex = new THREE.Vector3(); // Matrix for scale only - const scaleMatrix = new THREE.Matrix4().makeScale( - mesh.scale.x, - mesh.scale.y, - mesh.scale.z - ); + const scaleMatrix = new THREE.Matrix4().makeScale(mesh.scale.x, mesh.scale.y, mesh.scale.z); for (let i = 0; i < position.count; i++) { - vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); - vertices.push(vertex.x, vertex.y, vertex.z); + vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); + vertices.push(vertex.x, vertex.y, vertex.z); } const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); return collider; -} -function TriMesh(mesh) { - // Get the positions array (from your geoPoints function) -const positions = mesh.geometry.attributes.position.array; -const numVertices = positions.length / 3; - -// Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] -const indices = Array.from({ length: numVertices }, (_, i) => i); - -const collider = RAPIER.ColliderDesc.trimesh( - positions, - new Uint32Array(indices) -); - -return collider -} -function getModel(model, name) { - const file = runtime.getTargetForStage().getSounds().find(c => c.name === model) - if (!file) return - -return new Promise((resolve, reject) => { - gltf.parse( - file.asset.data.buffer, - "", - gltf => { - const root = gltf.scene - root.traverse(child => { - if (child.isMesh) { - child.castShadow = true - child.receiveShadow = true - } - }); + } - const mixer = new THREE.AnimationMixer(root) - const actions = {} - gltf.animations.forEach(clip => { - const act = mixer.clipAction(clip) - act.clampWhenFinished = true - actions[clip.name] = act - }); + function TriMesh(mesh) { + // Get the positions array (from your geoPoints function) + const positions = mesh.geometry.attributes.position.array; + const numVertices = positions.length / 3; - models[name] = { root, mixer, actions } - resolve(root) + // Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] + const indices = Array.from({ + length: numVertices, }, - error => { - console.error("Error parsing GLB model:", error) - reject(error) - } - )}) -} -async function openFileExplorer(format) { - return new Promise((resolve) => { - const input = document.createElement("input"); - input.type = "file" - input.accept = format - input.multiple = false + (_, i) => i + ); + + const collider = RAPIER.ColliderDesc.trimesh(positions, new Uint32Array(indices)); + + return collider; + } + + function getModel(model, name) { + const file = runtime + .getTargetForStage() + .getSounds() + .find((c) => c.name === model); + if (!file) return; + + return new Promise((resolve, reject) => { + gltf.parse( + file.asset.data.buffer, + "", + (gltf) => { + const root = gltf.scene; + root.traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + const mixer = new THREE.AnimationMixer(root); + const actions = {}; + gltf.animations.forEach((clip) => { + const act = mixer.clipAction(clip); + act.clampWhenFinished = true; + actions[clip.name] = act; + }); + + models[name] = { + root, + mixer, + actions, + }; + resolve(root); + }, + (error) => { + console.error("Error parsing GLB model:", error); + reject(error); + } + ); + }); + } + async function openFileExplorer(format) { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = format; + input.multiple = false; input.onchange = () => { - resolve(input.files) - input.remove() + resolve(input.files); + input.remove(); }; input.click(); - }) -} -function getMeshesUsingTexture(scene, targetTexture) { - const meshes = [] - - scene.traverse(object => { - if (object.material) { - const materials = Array.isArray(object.material) ? object.material : [object.material] - for (const material of materials) { - if (material.map === targetTexture) { - meshes.push(object) - break - } - } + }); + } + + function getMeshesUsingTexture(scene, targetTexture) { + const meshes = []; + + scene.traverse((object) => { + if (object.material) { + const materials = Array.isArray(object.material) ? object.material : [object.material]; + for (const material of materials) { + if (material.map === targetTexture) { + meshes.push(object); + break; + } } - }) - - return meshes -} -function getAsset(path) { - if (typeof(path) == "string") { //string? - if (path.includes("/")) { //has the /? - const value = path.split("/") - console.log(value[0], value[1]) - return assets[value[0]][value[1]] - } + } + }); + + return meshes; } - return JSON.parse(path) //boolean or number -} + function getAsset(path) { + if (typeof path == "string") { + //string? + if (path.includes("/")) { + //has the /? + const value = path.split("/"); + console.log(value[0], value[1]); + return assets[value[0]][value[1]]; + } + } + + return JSON.parse(path); //boolean or number + } -let mouseNDC = [0, 0] -//loops/init -function stopLoop() { - if (!running) return - running = false + let mouseNDC = [0, 0]; + //loops/init + function stopLoop() { + if (!running) return; + running = false; - if (loopId) { - cancelAnimationFrame(loopId) - loopId = null - if (threeRenderer) threeRenderer.clear(); + if (loopId) { + cancelAnimationFrame(loopId); + loopId = null; + if (threeRenderer) threeRenderer.clear(); + } } -} -async function load() { + async function load() { if (!THREE) { - // @ts-ignore THREE = await import("https://esm.sh/three@0.180.0") window._THREE_ = THREE //Addons - GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js") - OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js") - BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js") - TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js") - const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js") - fontLoad = new FontLoader.FontLoader() - - const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8") + GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js"); + OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js"); + BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js"); + TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js"); + const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js"); + fontLoad = new FontLoader.FontLoader(); + + const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8"); const { EffectComposer, EffectPass, RenderPass, - + Effect, BloomEffect, GodRaysEffect, DotScreenEffect, DepthOfFieldEffect, - BlendFunction - } = POSTPROCESSING - //so i can use them later as global - window.EffectComposer = EffectComposer; - window.EffectPass = EffectPass; - window.RenderPass = RenderPass; - window.Effect = Effect; - window.BloomEffect = BloomEffect; - window.GodRaysEffect = GodRaysEffect; - window.DotScreenEffect = DotScreenEffect; - window.DepthOfFieldEffect = DepthOfFieldEffect; - window.BlendFunction = BlendFunction; - - RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0") - await RAPIER.init() - + BlendFunction, + } = POSTPROCESSING; + //so i can use them later as global + window.EffectComposer = EffectComposer; + window.EffectPass = EffectPass; + window.RenderPass = RenderPass; + window.Effect = Effect; + window.BloomEffect = BloomEffect; + window.GodRaysEffect = GodRaysEffect; + window.DotScreenEffect = DotScreenEffect; + window.DepthOfFieldEffect = DepthOfFieldEffect; + window.BlendFunction = BlendFunction; + + RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0"); + await RAPIER.init(); + threeRenderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", antialias: false, stencil: false, - depth: true - }) - threeRenderer.setPixelRatio(window.devicePixelRatio) - threeRenderer.outputColorSpace = THREE.SRGBColorSpace // correct colors - threeRenderer.toneMapping = THREE.ACESFilmicToneMapping // HDR look (test) + depth: true, + }); + threeRenderer.setPixelRatio(window.devicePixelRatio); + threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors + threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) //threeRenderer.toneMappingExposure = 1.0 //(test) - threeRenderer.shadowMap.enabled = true - threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap // (optional) - threeRenderer.domElement.style.pointerEvents = 'auto' //will disable turbowarp mouse events, but enable threejs's + threeRenderer.shadowMap.enabled = true; + threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap; // (optional) + threeRenderer.domElement.style.pointerEvents = "auto"; //will disable turbowarp mouse events, but enable threejs's + + gltf = new GLTFLoader.GLTFLoader(); + clock = new THREE.Clock(); - gltf = new GLTFLoader.GLTFLoader() - clock = new THREE.Clock() - // Example: create a composer - composer = new EffectComposer(threeRenderer, {frameBufferType: THREE.HalfFloatType}) - - renderer.addOverlay( threeRenderer.domElement, "manual" ) - renderer.addOverlay(canvas, "manual") - renderer.setBackgroundColor(1, 1, 1, 0) - - resize() - - window.addEventListener("mousedown", e => { - if (e.button === 0) isMouseDown.left = true - if (e.button === 1) isMouseDown.middle = true - if (e.button === 2) isMouseDown.right = true - }) - window.addEventListener("mouseup", e => { - if (e.button === 0) isMouseDown.left = false; prevMouse.left = false - if (e.button === 1) isMouseDown.middle = false; prevMouse.middle = false - if (e.button === 2) isMouseDown.right = false; prevMouse.right = false - }) + composer = new EffectComposer(threeRenderer, { + frameBufferType: THREE.HalfFloatType, + }); + + renderer.addOverlay(threeRenderer.domElement, "manual"); + renderer.addOverlay(canvas, "manual"); + renderer.setBackgroundColor(1, 1, 1, 0); + + resize(); + + window.addEventListener("mousedown", (e) => { + if (e.button === 0) isMouseDown.left = true; + if (e.button === 1) isMouseDown.middle = true; + if (e.button === 2) isMouseDown.right = true; + }); + window.addEventListener("mouseup", (e) => { + if (e.button === 0) isMouseDown.left = false; + prevMouse.left = false; + if (e.button === 1) isMouseDown.middle = false; + prevMouse.middle = false; + if (e.button === 2) isMouseDown.right = false; + prevMouse.right = false; + }); // prevent contextmenu on right click - threeRenderer.domElement.addEventListener("contextmenu", e => e.preventDefault()); + threeRenderer.domElement.addEventListener("contextmenu", (e) => e.preventDefault()); - threeRenderer.domElement.addEventListener('mousemove', (event) => { + threeRenderer.domElement.addEventListener("mousemove", (event) => { mouseNDC = getMouseNDC(event); - }) + }); - running = false - load() + running = false; + load(); startRenderLoop() runtime.on('PROJECT_START', () => startRenderLoop()) @@ -500,828 +555,1963 @@ async function load() { checkCanvasSize() } } -function startRenderLoop() { - if (running) return - running = true - - const loop = () => { - if (!running) return - //RAPIER - if (physicsWorld && scene) { - physicsWorld.step() - - scene.children.forEach(obj => { - if (!(obj.isMesh) || !(obj.physics)) return - if (obj.rigidBody) { - obj.position.copy(obj.rigidBody.translation()) - obj.quaternion.copy(obj.rigidBody.rotation()) - } - }) - - } - if (scene && camera) { - if (controls) controls.update() - const delta = clock.getDelta() - Object.values(models).forEach( model => { if (model) model.mixer.update(delta) } ) + function startRenderLoop() { + if (running) return; + running = true; + + const loop = () => { + if (!running) return; + //RAPIER + if (physicsWorld && scene) { + physicsWorld.step(); + + scene.children.forEach((obj) => { + if (!obj.isMesh || !obj.physics) return; + if (obj.rigidBody) { + obj.position.copy(obj.rigidBody.translation()); + obj.quaternion.copy(obj.rigidBody.rotation()); + } + }); + } + if (scene && camera) { + if (controls) controls.update(); - Object.values(lights).forEach(light => updateShadowFrustum(light, camera.position)) + const delta = clock.getDelta(); + Object.values(models).forEach((model) => { + if (model) model.mixer.update(delta); + }); - //update custom effects time - customEffects.forEach(e => { - if (e.uniforms.get('time')) { - e.uniforms.get('time').value += delta - } - }) - Object.values(renderTargets).forEach(t => { - if ( t.camera.type == "PerspectiveCamera") { - t.camera.aspect = t.target.width / t.target.height - t.camera.updateProjectionMatrix() - } - // get meshes using the texture associated with this target - const displayMeshes = getMeshesUsingTexture(scene, t.target.texture) + Object.values(lights).forEach((light) => updateShadowFrustum(light, camera.position)); - displayMeshes.forEach(mesh => { - mesh.visible = false - }) + //update custom effects time + customEffects.forEach((e) => { + if (e.uniforms.get("time")) { + e.uniforms.get("time").value += delta; + } + }); + Object.values(renderTargets).forEach((t) => { + if (t.camera.type == "PerspectiveCamera") { + t.camera.aspect = t.target.width / t.target.height; + t.camera.updateProjectionMatrix(); + } + // get meshes using the texture associated with this target + const displayMeshes = getMeshesUsingTexture(scene, t.target.texture); + + displayMeshes.forEach((mesh) => { + mesh.visible = false; + }); + + if (t.camera.type == "PerspectiveCamera") { + threeRenderer.setRenderTarget(t.target); + threeRenderer.clear(true, true, true); + threeRenderer.render(scene, t.camera); + } else { + t.target.clear(threeRenderer); + t.camera.update(threeRenderer, scene); //cubeCamera + } - if (t.camera.type == "PerspectiveCamera") { - threeRenderer.setRenderTarget(t.target) - threeRenderer.clear(true, true, true) - threeRenderer.render(scene, t.camera) - } else { - t.target.clear(threeRenderer) - t.camera.update( threeRenderer, scene ) //cubeCamera - } + displayMeshes.forEach((mesh) => { + mesh.visible = true; + }); + }); - displayMeshes.forEach(mesh => { - mesh.visible = true - }) - }) + camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height; + camera.updateProjectionMatrix(); + threeRenderer.setRenderTarget(null); + composer.render(delta); + } - camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height - camera.updateProjectionMatrix() - threeRenderer.setRenderTarget(null) - composer.render(delta) - } + loopId = requestAnimationFrame(loop); + }; - loopId = requestAnimationFrame(loop) + loopId = requestAnimationFrame(loop); } - loopId = requestAnimationFrame(loop) -} + function resize() { + const w = canvas.width; + const h = canvas.height; -function resize() { - const w = canvas.width - const h = canvas.height + threeRenderer.setSize(w, h); + composer.setSize(w, h); + customEffects.forEach((e) => { + if (e.uniforms.get("resolution")) { + e.uniforms.get("resolution").value.set(w, h); + } + }); - threeRenderer.setSize(w, h) - composer.setSize(w, h) - customEffects.forEach(e => { - if (e.uniforms.get('resolution')) { - e.uniforms.get('resolution').value.set(w,h) - } - }) - - if (camera) { - camera.aspect = w / h - camera.updateProjectionMatrix() -} -} -//wait until all packages are loaded -Promise.resolve(load()).then(() => { - - class threejsExtension { - getInfo() { - return { - id: "threejsExtension", - name: "Extra 3D", - color1: "#222222", - color2: "#222222", - color3: "#11cc99", - menuIconURI, - blockIconURI: menuIconURI, - - blocks: [ - {blockType: Scratch.BlockType.BUTTON, text: "Show Docs", func: "openDocs"}, - {blockType: Scratch.BlockType.BUTTON, text: "Toggle Alerts", func: "alerts"}, - ], - menus: {} - }} - openDocs(){ - open("https://civ3ro.github.io/extensions/Documentation/") + if (camera) { + camera.aspect = w / h; + camera.updateProjectionMatrix(); } - alerts() {alerts = !alerts; alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!")} } - Scratch.extensions.register(new threejsExtension()) - - class ThreeRenderer { - getInfo() { - return { - id: "threeRenderer", - name: "Three Renderer", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "setRendererRatio", blockType: Scratch.BlockType.COMMAND, text: "set Pixel Ratio to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"}}}, - {opcode: "eulerOrder", blockType: Scratch.BlockType.COMMAND, text: "set euler order to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "YXZ"}}}, - ], - menus: {} - }} - - setRendererRatio(args) { - threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE) - } - eulerOrder(args) { - eulerOrder = args.VALUE - console.log("euler order set to", eulerOrder) + //wait until all packages are loaded + Promise.resolve(load()).then(() => { + class threejsExtension { + getInfo() { + return { + id: "threejsExtension", + name: "Extra 3D", + color1: "#222222", + color2: "#222222", + color3: "#11cc99", + menuIconURI, + blockIconURI: menuIconURI, + + blocks: [{ + blockType: Scratch.BlockType.BUTTON, + text: "Show Docs", + func: "openDocs", + }, + { + blockType: Scratch.BlockType.BUTTON, + text: "Toggle Alerts", + func: "alerts", + }, + ], + menus: {}, + }; + } + openDocs() { + open("https://civ3ro.github.io/extensions/Documentation/"); + } + alerts() { + alerts = !alerts; + alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!"); + } } + Scratch.extensions.register(new threejsExtension()); - } - Scratch.extensions.register(new ThreeRenderer()) + class ThreeRenderer { + getInfo() { + return { + id: "threeRenderer", + name: "Three Renderer", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "setRendererRatio", + blockType: Scratch.BlockType.COMMAND, + text: "set Pixel Ratio to [VALUE]", + arguments: { + VALUE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + }, + }, + { + opcode: "eulerOrder", + blockType: Scratch.BlockType.COMMAND, + text: "set euler order to [VALUE]", + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "YXZ", + }, + }, + }, + ], + menus: {}, + }; + } - class ThreeScene { - constructor() { - this.THREE = THREE; - this.scenes = {}; + setRendererRatio(args) { + threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE); + } + eulerOrder(args) { + eulerOrder = args.VALUE; + console.log("euler order set to", eulerOrder); + } } - - getInfo() { - return { - id: "threeScene", - name: "Three Scene", - color1: "#4638c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "newScene", blockType: Scratch.BlockType.COMMAND, text: "new Scene [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}}}, - - {opcode: "setSceneProperty", blockType: Scratch.BlockType.COMMAND, text: "set Scene [PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "sceneProperties", defaultValue: "background"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, + Scratch.extensions.register(new ThreeRenderer()); + + class ThreeScene { + constructor() { + this.THREE = THREE; + this.scenes = {}; + } + + getInfo() { + return { + id: "threeScene", + name: "Three Scene", + color1: "#4638c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "newScene", + blockType: Scratch.BlockType.COMMAND, + text: "new Scene [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + }, + }, + + { + opcode: "setSceneProperty", + blockType: Scratch.BlockType.COMMAND, + text: "set Scene [PROPERTY] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "sceneProperties", + defaultValue: "background", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "new Color()", + exemptFromNormalization: true, + }, + }, + }, "---", - {opcode: "getSceneObjects", blockType: Scratch.BlockType.REPORTER, text: "get Scene [THING]", arguments:{THING: {type: Scratch.ArgumentType.STRING, menu: "sceneThings"}}}, - {opcode: "reset", blockType: Scratch.BlockType.COMMAND, text: "Reset Everything"} + { + opcode: "getSceneObjects", + blockType: Scratch.BlockType.REPORTER, + text: "get Scene [THING]", + arguments: { + THING: { + type: Scratch.ArgumentType.STRING, + menu: "sceneThings", + }, + }, + }, + { + opcode: "reset", + blockType: Scratch.BlockType.COMMAND, + text: "Reset Everything", + }, ], - menus: { - sceneProperties: {acceptReporters: false, items: [ - {text: "Background", value: "background"},{text: "Background Blurriness", value: "backgroundBlurriness"},{text: "Background Intensity", value: "backgroundIntensity"},{text: "Background Rotation", value: "backgroundRotation"}, - {text: "Environment", value: "environment"},{text: "Environment Intensity", value: "environmentIntensity"},{text: "Environment Rotation", value: "environmentRotation"},{text: "Fog", value: "fog"}, - ]}, - sceneThings: {acceptReporters: false, items: ["Objects", "Materials", "Geometries","Lights","Scene Properties","Other assets"]}, - - } - }} - - newScene(args) { - scene = new THREE.Scene(); - scene.name = args.NAME - scene.background = new THREE.Color("#222") - //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - this.scenes = {...this.scenes, {scene}}; - resetor(0) - } + menus: { + sceneProperties: { + acceptReporters: false, + items: [{ + text: "Background", + value: "background", + }, + { + text: "Background Blurriness", + value: "backgroundBlurriness", + }, + { + text: "Background Intensity", + value: "backgroundIntensity", + }, + { + text: "Background Rotation", + value: "backgroundRotation", + }, + { + text: "Environment", + value: "environment", + }, + { + text: "Environment Intensity", + value: "environmentIntensity", + }, + { + text: "Environment Rotation", + value: "environmentRotation", + }, + { + text: "Fog", + value: "fog", + }, + ], + }, + sceneThings: { + acceptReporters: false, + items: ["Objects", "Materials", "Geometries", "Lights", "Scene Properties", "Other assets"], + }, + }, + }; + } - reset() { - resetor(1) - } + newScene(args) { + scene = new THREE.Scene(); + scene.name = args.NAME; + scene.background = new THREE.Color("#222"); + //scene.add(new THREE.GridHelper(16, 16)) //future helper section? + this.scenes = { + ...this.scenes, + ...scene, + }; + resetor(0); + } + + reset() { + resetor(1); + } - async setSceneProperty(args) { + async setSceneProperty(args) { const property = args.PROPERTY; const value = getAsset(args.VALUE); scene[property] = value; + } + getSceneObjects(args) { + const names = []; + if (args.THING === "Objects") { + scene.traverse((obj) => { + if (obj.name) names.push(obj.name); //if it has a name, add to list! + }); + } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); + else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); + else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); + else if (args.THING === "Scene Properties") { + console.log(scene); + return "check console"; + } else if (args.THING === "Other assets") return JSON.stringify(assets); + + return JSON.stringify(names); // if objects + } } - getSceneObjects(args){ - const names = []; - if (args.THING === "Objects") { - scene.traverse(obj => { - if (obj.name) names.push(obj.name); //if it has a name, add to list! - }); - } - else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)) - else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)) - else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)) - else if (args.THING === "Scene Properties") {console.log(scene); return "check console"} - else if (args.THING === "Other assets") return JSON.stringify(assets) - - return JSON.stringify(names); // if objects - } + Scratch.extensions.register(new ThreeScene()); - } - Scratch.extensions.register(new ThreeScene()) - - class ThreeCameras { - getInfo() { - return { - id: "threeCameras", - name: "Three Cameras", - color1: "#38c59bff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addCamera", blockType: Scratch.BlockType.COMMAND, text: "add camera [TYPE] [CAMERA] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "cameraTypes"}}}, - {opcode: "setCamera", blockType: Scratch.BlockType.COMMAND, text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "0.1", exemptFromNormalization: true}}}, - {opcode: "getCamera", blockType: Scratch.BlockType.REPORTER, text: "get camera [PROPERTY] of [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}}}, + class ThreeCameras { + getInfo() { + return { + id: "threeCameras", + name: "Three Cameras", + color1: "#38c59bff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addCamera", + blockType: Scratch.BlockType.COMMAND, + text: "add camera [TYPE] [CAMERA] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "cameraTypes", + }, + }, + }, + { + opcode: "setCamera", + blockType: Scratch.BlockType.COMMAND, + text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "cameraProperties", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "0.1", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "getCamera", + blockType: Scratch.BlockType.REPORTER, + text: "get camera [PROPERTY] of [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "cameraProperties", + }, + }, + }, "---", - {opcode: "renderSceneCamera", blockType: Scratch.BlockType.COMMAND, text: "set rendering camera to [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}}}, + { + opcode: "renderSceneCamera", + blockType: Scratch.BlockType.COMMAND, + text: "set rendering camera to [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + }, + }, "---", - {opcode: "cubeCamera", blockType: Scratch.BlockType.COMMAND, text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "cubeCamera"}, GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, + { + opcode: "cubeCamera", + blockType: Scratch.BlockType.COMMAND, + text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "cubeCamera", + }, + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, "---", - {opcode: "renderTarget", blockType: Scratch.BlockType.COMMAND, text: "set a RenderTarget: [RT] for camera [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, - {opcode: "sizeTarget", blockType: Scratch.BlockType.COMMAND, text: "set RenderTarget [RT] size to [W] [H]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, W: {type: Scratch.ArgumentType.NUMBER, defaultValue: 480}, H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 360},} }, - {opcode: "getTarget", blockType: Scratch.BlockType.REPORTER, text: "get RenderTarget: [RT] texture", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, - {opcode: "removeTarget", blockType: Scratch.BlockType.COMMAND, text: "remove RenderTarget: [RT]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, + { + opcode: "renderTarget", + blockType: Scratch.BlockType.COMMAND, + text: "set a RenderTarget: [RT] for camera [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + { + opcode: "sizeTarget", + blockType: Scratch.BlockType.COMMAND, + text: "set RenderTarget [RT] size to [W] [H]", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + W: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 480, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 360, + }, + }, + }, + { + opcode: "getTarget", + blockType: Scratch.BlockType.REPORTER, + text: "get RenderTarget: [RT] texture", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + { + opcode: "removeTarget", + blockType: Scratch.BlockType.COMMAND, + text: "remove RenderTarget: [RT]", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, ], - menus: { - cameraTypes: {acceptReporters: false, items: [ - {text: "Perspective", value: "PerspectiveCamera"}, - ]}, - cameraProperties: {acceptReporters: false, items: [ - {text: "Near", value: "near"},{text: "Far", value: "far"},{text: "FOV", value: "fov"},{text: "Focus (nothing...)", value: "focus"},{text: "Zoom", value: "zoom"}, - ]}, - } - }} - addCamera(args) { - let v2 = new THREE.Vector2() - threeRenderer.getSize(v2) - const object = new THREE.PerspectiveCamera(90, v2.x / v2.y ) - object.position.z = 3 - - createObject(args.CAMERA, object, args.GROUP) - } - setCamera(args) { - let object = getObject(args.CAMERA) - object[args.PROPERTY] = args.VALUE - object.updateProjectionMatrix() - } - getCamera(args) { - let object = getObject(args.CAMERA) - const value = JSON.stringify(object[args.PROPERTY]) - return value - } - renderSceneCamera(args) { - let object = getObject(args.CAMERA) - if (!object) return - camera = object - //reset composer, else it does not update. - composer.passes = [] - passes = {} - customEffects = [] - updateComposers() - } + menus: { + cameraTypes: { + acceptReporters: false, + items: [{ + text: "Perspective", + value: "PerspectiveCamera", + }, ], + }, + cameraProperties: { + acceptReporters: false, + items: [{ + text: "Near", + value: "near", + }, + { + text: "Far", + value: "far", + }, + { + text: "FOV", + value: "fov", + }, + { + text: "Focus (nothing...)", + value: "focus", + }, + { + text: "Zoom", + value: "zoom", + }, + ], + }, + }, + }; + } + addCamera(args) { + let v2 = new THREE.Vector2(); + threeRenderer.getSize(v2); + const object = new THREE.PerspectiveCamera(90, v2.x / v2.y); + object.position.z = 3; - cubeCamera(args) { - // Create cube render target - const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true } ) - // Create cube camera - const cubeCamera = new THREE.CubeCamera( 0.1, 500, cubeRenderTarget ) - createObject(args.CAMERA, cubeCamera, args.GROUP) + createObject(args.CAMERA, object, args.GROUP); + } + setCamera(args) { + let object = getObject(args.CAMERA); + object[args.PROPERTY] = args.VALUE; + object.updateProjectionMatrix(); + } + getCamera(args) { + let object = getObject(args.CAMERA); + const value = JSON.stringify(object[args.PROPERTY]); + return value; + } + renderSceneCamera(args) { + let object = getObject(args.CAMERA); + if (!object) return; + camera = object; + //reset composer, else it does not update. + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } - renderTargets[args.RT] = {target: cubeRenderTarget, camera: cubeCamera} - assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture - } + cubeCamera(args) { + // Create cube render target + const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, { + generateMipmaps: true, + }); + // Create cube camera + const cubeCamera = new THREE.CubeCamera(0.1, 500, cubeRenderTarget); + createObject(args.CAMERA, cubeCamera, args.GROUP); - renderTarget(args) { - let object = getObject(args.CAMERA) - const renderTarget = new THREE.WebGLRenderTarget( - 360, - 360, - { - generateMipmaps: false - } - ) + renderTargets[args.RT] = { + target: cubeRenderTarget, + camera: cubeCamera, + }; + assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture; + } - renderTargets[args.RT] = {target: renderTarget, camera: object} - assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture - } - sizeTarget(args) { - renderTargets[args.RT].target.setSize(args.W, args.H) - } - getTarget(args) { - const t = renderTargets[args.RT].target.texture - console.log(t, renderTargets[args.RT]) - return `renderTargets/${t.uuid}` - } - removeTarget(args) { - delete(assets.renderTargets[renderTargets[args.RT].target.texture.uuid]) - renderTargets[args.RT].target.dispose() - delete(renderTargets[args.RT]) + renderTarget(args) { + let object = getObject(args.CAMERA); + const renderTarget = new THREE.WebGLRenderTarget(360, 360, { + generateMipmaps: false, + }); + + renderTargets[args.RT] = { + target: renderTarget, + camera: object, + }; + assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; + } + sizeTarget(args) { + renderTargets[args.RT].target.setSize(args.W, args.H); + } + getTarget(args) { + const t = renderTargets[args.RT].target.texture; + console.log(t, renderTargets[args.RT]); + return `renderTargets/${t.uuid}`; + } + removeTarget(args) { + delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; + renderTargets[args.RT].target.dispose(); + delete renderTargets[args.RT]; + } } - } - Scratch.extensions.register(new ThreeCameras()) - - class ThreeObjects { - getInfo() { - return { - id: "threeObjects", - name: "Three Objects", - color1: "#38c567ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addObject", blockType: Scratch.BlockType.COMMAND, text: "add object [OBJECT3D] [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},TYPE: {type: Scratch.ArgumentType.STRING, menu: "objectTypes"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "cloneObject", blockType: Scratch.BlockType.COMMAND, text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myClone"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + Scratch.extensions.register(new ThreeCameras()); + + class ThreeObjects { + getInfo() { + return { + id: "threeObjects", + name: "Three Objects", + color1: "#38c567ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addObject", + blockType: Scratch.BlockType.COMMAND, + text: "add object [OBJECT3D] [TYPE] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "objectTypes", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "cloneObject", + blockType: Scratch.BlockType.COMMAND, + text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myClone", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "setObject", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - {opcode: "getObject", blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of object [OBJECT3D]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}}}, - {opcode: "objectE", blockType: Scratch.BlockType.BOOLEAN, text: "is there an object [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + { + opcode: "setObject", + blockType: Scratch.BlockType.COMMAND, + text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectProperties", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + { + opcode: "getObject", + blockType: Scratch.BlockType.REPORTER, + text: "get [PROPERTY] of object [OBJECT3D]", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectProperties", + }, + }, + }, + { + opcode: "objectE", + blockType: Scratch.BlockType.BOOLEAN, + text: "is there an object [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "removeObject", blockType: Scratch.BlockType.COMMAND, text: "remove object [OBJECT3D] from scene", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - - {blockType: Scratch.BlockType.LABEL, text: " ↳ Transforms"}, - {opcode: "setObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, + { + opcode: "removeObject", + blockType: Scratch.BlockType.COMMAND, + text: "remove object [OBJECT3D] from scene", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: " ↳ Transforms", + }, + { + opcode: "setObjectV3", + extensions: ["colours_motion"], + blockType: Scratch.BlockType.COMMAND, + text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectVector3", + defaultValue: "position", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of [OBJECT3D]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - - {blockType: Scratch.BlockType.LABEL, text: "↳ Materials"}, - {opcode: "newMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "new material [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "materialTypes", defaultValue: "MeshStandardMaterial"}}}, - {opcode: "materialE",extensions: ["colours_looks"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a material [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, - {opcode: "removeMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "remove material [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, - {opcode: "setMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [PROPERTY] of [NAME] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "materialProperties", defaultValue: "color"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, - {opcode: "setBlending",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] blending to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "blendModes"}}}, - {opcode: "setDepth",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] depth to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "depthModes"}}}, - - {blockType: Scratch.BlockType.LABEL, text: "↳ Geometries"}, - {opcode: "newGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new geometry [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "geometryTypes", defaultValue: "BoxGeometry"}}}, - {opcode: "geometryE",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a geometry [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - {opcode: "removeGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "remove geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, + { + opcode: "getObjectV3", + extensions: ["colours_motion"], + blockType: Scratch.BlockType.REPORTER, + text: "get [PROPERTY] of [OBJECT3D]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectVector3", + defaultValue: "position", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Materials", + }, + { + opcode: "newMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "new material [NAME] [TYPE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "materialTypes", + defaultValue: "MeshStandardMaterial", + }, + }, + }, + { + opcode: "materialE", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.BOOLEAN, + text: "is there a material [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + }, + }, + { + opcode: "removeMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "remove material [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + }, + }, + { + opcode: "setMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [PROPERTY] of [NAME] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "materialProperties", + defaultValue: "color", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "new Color()", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "setBlending", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [NAME] blending to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + }, + }, + }, + { + opcode: "setDepth", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [NAME] depth to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "depthModes", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Geometries", + }, + { + opcode: "newGeometry", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "new geometry [NAME] [TYPE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "geometryTypes", + defaultValue: "BoxGeometry", + }, + }, + }, + { + opcode: "geometryE", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.BOOLEAN, + text: "is there a geometry [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + { + opcode: "removeGeometry", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "remove geometry [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, "---", - {opcode: "newGeo",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new empty geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, - {opcode: "geoPoints",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] vertex points to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, - {opcode: "geoUVs",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] UVs to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[UVs]"}}}, + { + opcode: "newGeo", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "new empty geometry [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[points]", + }, + }, + }, + { + opcode: "geoPoints", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "set geometry [NAME] vertex points to [POINTS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[points]", + }, + }, + }, + { + opcode: "geoUVs", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "set geometry [NAME] UVs to [POINTS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[UVs]", + }, + }, + }, "---", - {opcode: "splines", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create spline [NAME] from curve [CURVE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}}}, - {opcode: "splineModel", extensions: ["colours_operators"], blockType: Scratch.BlockType.COMMAND, text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, MODEL: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}, SPACING: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, + { + opcode: "splines", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "create spline [NAME] from curve [CURVE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySpline", + }, + CURVE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[curve]", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "splineModel", + extensions: ["colours_operators"], + blockType: Scratch.BlockType.COMMAND, + text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySpline", + }, + MODEL: { + type: Scratch.ArgumentType.STRING, + menu: "modelsList", + }, + CURVE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[curve]", + exemptFromNormalization: true, + }, + SPACING: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, "---", - {blockType: Scratch.BlockType.BUTTON, text: "Convert font to JSON", func: "openConv"}, - {blockType: Scratch.BlockType.BUTTON, text: "Load JSON font file", func: "loadFont"}, - {opcode: "text", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myText"}, TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "C-369"}, FONT: {type: Scratch.ArgumentType.STRING, menu: "fonts"}, S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, D: {type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1}, CS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 6}}}, - ], - menus: { - objectVector3: {acceptReporters: false, items: [ - {text: "Positon", value: "position"},{text: "Rotation", value: "rotation"},{text: "Scale", value: "scale"},{text: "Facing Direction (.up)", value: "up"} - ]}, - objectProperties: {acceptReporters: false, items: [ - {text: "Geometry", value: "geometry"},{text: "Material", value: "material"},{text: "Visible (true/false)", value: "visible"}, - ]}, - objectTypes: { acceptReporters: false, items: [ - { text: "Mesh", value: "Mesh" }, { text: "Sprite", value: "Sprite" }, { text: "Points", value: "Points" }, { text: "Line", value: "Line" }, { text: "Group", value: "Group" } - ]}, - XYZ: {acceptReporters: false, items: [{text: "X", value: "x"},{text: "Y", value: "y"},{text: "Z", value: "z"}]}, - materialProperties: {acceptReporters: false, items: [ - "|GENERAL| <-- not a property", - { text: "Color", value: "color" }, - { text: "Map", value: "map" }, - { text: "Opacity", value: "opacity" }, - { text: "Transparent", value: "transparent" }, - { text: "Alpha Map", value: "alphaMap" }, - { text: "Alpha Test", value: "alphaTest" }, - { text: "Depth Test", value: "depthTest" }, - { text: "Depth Write", value: "depthWrite" }, - { text: "Color Write", value: "colorWrite" }, - { text: "Side", value: "side" }, - { text: "Visible", value: "visible" },/* - { text: "Blending", value: "blending" }, - { text: "Blend Src", value: "blendSrc" }, - { text: "Blend Dst", value: "blendDst" }, - { text: "Blend Equation", value: "blendEquation" }, - { text: "Blend Src Alpha", value: "blendSrcAlpha" }, - { text: "Blend Dst Alpha", value: "blendDstAlpha" }, - { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ - { text: "Blend Aplha", value: "blendAplha" }, - { text: "Blend Color", value: "blendColor" }, - { text: "Alpha Hash", value: "alphaHash" }, - { text: "Premultiplied Alpha", value: "premultipliedAlpha" }, - - { text: "Tone Mapped", value: "toneMapped" }, - { text: "Fog", value: "fog" }, - { text: "Flat Shading", value: "flatShading" }, - - "|MESH Standard / Physical| <-- not a property", - { text: "Metalness", value: "metalness" }, - { text: "Metalness Map", value: "metalnessMap" }, - { text: "Roughness", value: "roughness" }, - { text: "Reflectivity", value: "reflectivity" }, - { text: "Roughness Map", value: "roughnessMap" }, - { text: "Emissive", value: "emissive" }, - { text: "Emissive Intensity", value: "emissiveIntensity" }, - { text: "Emissive Map", value: "emissiveMap" }, - { text: "Env Map", value: "envMap" }, - { text: "Env Map Intensity", value: "envMapIntensity" }, - { text: "Env Map Rotation", value: "envMapRotation" }, - { text: "Ior", value: "ior" }, - { text: "Refraction Ratio", value: "refractionRatio" }, - { text: "Clearcoat", value: "clearcoat" }, - { text: "Clearcoat Map", value: "clearcoatMap" }, - { text: "Clearcoat Roughness", value: "clearcoatRoughness" }, - { text: "Clearcoat Roughness Map", value: "clearcoatRoughnessMap" }, - { text: "Dispersion", value: "dispersion" }, - { text: "Sheen", value: "sheen" }, - { text: "Sheen Color", value: "sheenColor" }, - { text: "Sheen Color Map", value: "sheenColorMap" }, - { text: "Sheen Roughness", value: "sheenRoughness" }, - { text: "Sheen Roughness Map", value: "sheenRoughnessMap" }, - { text: "Specular Color", value: "specularColor" }, - { text: "Specular Color Map", value: "specularColorMap" }, - { text: "Specular Intensity", value: "specularIntensity" }, - { text: "Specular Intensity Map", value: "specularIntensityMap" }, - { text: "Transmission", value: "transmission" }, - { text: "Transmission Map", value: "transmissionMap" }, - { text: "Thickness", value: "thickness" }, - { text: "Thickness Map", value: "thicknessMap" }, - { text: "Anisotropy", value: "anisotropy" }, - { text: "Anisotropy Map", value: "anisotropyMap" }, - { text: "Anisotropy Rotation", value: "anisotropyRotation" }, - { text: "Attenuation Distance", value: "attenuationDistance" }, - { text: "Attenuation Color", value: "attenuationColor" }, - { text: "Thickness", value: "thickness" }, - { text: "Iridescence", value: "iridescence" }, - { text: "Iridescence Ior", value: "iridescenceIOR" }, - { text: "Iridescence Map", value: "iridescenceMap" }, - { text: "Iridescence Thickness Range", value: "iridescenceThicknessRange" }, - - "|MESH Displacement / Normal / Bump| <-- not a property", - { text: "Displacement Map", value: "displacementMap" }, - { text: "Displacement Scale", value: "displacementScale" }, - { text: "Displacement Bias", value: "displacementBias" }, - { text: "Bump Map", value: "bumpMap" }, - { text: "Bump Scale", value: "bumpScale" }, - { text: "Normal Map Type", value: "normalMapType" }, - - "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", - { text: "Shininess", value: "shininess" }, - - { text: "Wireframe", value: "wireframe" }, - { text: "Wireframe Linewidth", value: "wireframeLinewidth" }, - { text: "Wireframe Linecap", value: "wireframeLinecap" }, - { text: "Wireframe Linejoin", value: "wireframeLinejoin" }, - - "|POINTS| <-- not a property", - { text: "Size", value: "size" }, - { text: "Size Attenuation", value: "sizeAttenuation" }, - - "|LINES| <-- not a property", - { text: "Scale", value: "scale" }, - { text: "Dash Size", value: "dashSize" }, - { text: "Gap Size", value: "gapSize" }, - - "|SPRITES| <-- not a property", - { text: "Rotation", value: "rotation" } -]}, - blendModes: {acceptReporters: false, items: [ - { text: "No Blending", value: "NoBlending" },{ text: "Normal Blending", value: "NormalBlending" },{ text: "Additive Blending", value: "AdditiveBlending" },{ text: "Subtractive Blending", value: "SubtractiveBlending" },{ text: "Multiply Blending", value: "MultiplyBlending" },{ text: "Custom Blending", value: "CustomBlending" } - ]}, - depthModes: {acceptReporters: false, items: [ - { text: "Never Depth", value: "NeverDepth" },{ text: "Always Depth", value: "AlwaysDepth" },{ text: "Equal Depth", value: "EqualDepth" },{ text: "Less Depth", value: "LessDepth" },{ text: "Less Equal Depth", value: "LessEqualDepth" },{ text: "Greater Equal Depth", value: "GreaterEqualDepth" },{ text: "Greater Depth", value: "GreaterDepth" },{ text: "Not Equal Depth", value: "NotEqualDepth" } - ]}, - materialTypes:{acceptReporters: false, items: [ - {text:"Mesh Basic Material",value:"MeshBasicMaterial"},{text:"Mesh Standard Material",value:"MeshStandardMaterial"},{text:"Mesh Physical Material",value:"MeshPhysicalMaterial"},{text:"Mesh Lambert Material",value:"MeshLambertMaterial"},{text:"Mesh Phong Material",value:"MeshPhongMaterial"},{text:"Mesh Depth Material",value:"MeshDepthMaterial"},{text:"Mesh Normal Material",value:"MeshNormalMaterial"},{text:"Mesh Matcap Material",value:"MeshMatcapMaterial"},{text:"Mesh Toon Material",value:"MeshToonMaterial"},{text:"Line Basic Material",value:"LineBasicMaterial"},{text:"Line Dashed Material",value:"LineDashedMaterial"},{text:"Points Material",value:"PointsMaterial"},{text:"Sprite Material",value:"SpriteMaterial"},{text:"Shadow Material",value:"ShadowMaterial"} - ]}, - textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, - textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, - geometryTypes: {acceptReporters: false, items: [ - {text: "Box Geometry", value: "BoxGeometry"},{text: "Sphere Geometry", value: "SphereGeometry"},{text: "Cylinder Geometry", value: "CylinderGeometry"},{text: "Plane Geometry", value: "PlaneGeometry"},{text: "Circle Geometry", value: "CircleGeometry"},{text: "Torus Geometry", value: "TorusGeometry"},{text: "Torus Knot Geometry", value: "TorusKnotGeometry"}, - ]}, - modelsList: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; + { + blockType: Scratch.BlockType.BUTTON, + text: "Convert font to JSON", + func: "openConv", + }, + { + blockType: Scratch.BlockType.BUTTON, + text: "Load JSON font file", + func: "loadFont", + }, + { + opcode: "text", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myText", + }, + TEXT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "C-369", + }, + FONT: { + type: Scratch.ArgumentType.STRING, + menu: "fonts", + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + D: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.1, + }, + CS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 6, + }, + }, + }, + ], + menus: { + objectVector3: { + acceptReporters: false, + items: [{ + text: "Positon", + value: "position", + }, + { + text: "Rotation", + value: "rotation", + }, + { + text: "Scale", + value: "scale", + }, + { + text: "Facing Direction (.up)", + value: "up", + }, + ], + }, + objectProperties: { + acceptReporters: false, + items: [{ + text: "Geometry", + value: "geometry", + }, + { + text: "Material", + value: "material", + }, + { + text: "Visible (true/false)", + value: "visible", + }, + ], + }, + objectTypes: { + acceptReporters: false, + items: [{ + text: "Mesh", + value: "Mesh", + }, + { + text: "Sprite", + value: "Sprite", + }, + { + text: "Points", + value: "Points", + }, + { + text: "Line", + value: "Line", + }, + { + text: "Group", + value: "Group", + }, + ], + }, + XYZ: { + acceptReporters: false, + items: [{ + text: "X", + value: "x", + }, + { + text: "Y", + value: "y", + }, + { + text: "Z", + value: "z", + }, + ], + }, + materialProperties: { + acceptReporters: false, + items: [ + "|GENERAL| <-- not a property", + { + text: "Color", + value: "color", + }, + { + text: "Map", + value: "map", + }, + { + text: "Opacity", + value: "opacity", + }, + { + text: "Transparent", + value: "transparent", + }, + { + text: "Alpha Map", + value: "alphaMap", + }, + { + text: "Alpha Test", + value: "alphaTest", + }, + { + text: "Depth Test", + value: "depthTest", + }, + { + text: "Depth Write", + value: "depthWrite", + }, + { + text: "Color Write", + value: "colorWrite", + }, + { + text: "Side", + value: "side", + }, + { + text: "Visible", + value: "visible", + }, + /* + { text: "Blending", value: "blending" }, + { text: "Blend Src", value: "blendSrc" }, + { text: "Blend Dst", value: "blendDst" }, + { text: "Blend Equation", value: "blendEquation" }, + { text: "Blend Src Alpha", value: "blendSrcAlpha" }, + { text: "Blend Dst Alpha", value: "blendDstAlpha" }, + { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ + { + text: "Blend Aplha", + value: "blendAplha", + }, + { + text: "Blend Color", + value: "blendColor", + }, + { + text: "Alpha Hash", + value: "alphaHash", + }, + { + text: "Premultiplied Alpha", + value: "premultipliedAlpha", + }, + + { + text: "Tone Mapped", + value: "toneMapped", + }, + { + text: "Fog", + value: "fog", + }, + { + text: "Flat Shading", + value: "flatShading", + }, + + "|MESH Standard / Physical| <-- not a property", + { + text: "Metalness", + value: "metalness", + }, + { + text: "Metalness Map", + value: "metalnessMap", + }, + { + text: "Roughness", + value: "roughness", + }, + { + text: "Reflectivity", + value: "reflectivity", + }, + { + text: "Roughness Map", + value: "roughnessMap", + }, + { + text: "Emissive", + value: "emissive", + }, + { + text: "Emissive Intensity", + value: "emissiveIntensity", + }, + { + text: "Emissive Map", + value: "emissiveMap", + }, + { + text: "Env Map", + value: "envMap", + }, + { + text: "Env Map Intensity", + value: "envMapIntensity", + }, + { + text: "Env Map Rotation", + value: "envMapRotation", + }, + { + text: "Ior", + value: "ior", + }, + { + text: "Refraction Ratio", + value: "refractionRatio", + }, + { + text: "Clearcoat", + value: "clearcoat", + }, + { + text: "Clearcoat Map", + value: "clearcoatMap", + }, + { + text: "Clearcoat Roughness", + value: "clearcoatRoughness", + }, + { + text: "Clearcoat Roughness Map", + value: "clearcoatRoughnessMap", + }, + { + text: "Dispersion", + value: "dispersion", + }, + { + text: "Sheen", + value: "sheen", + }, + { + text: "Sheen Color", + value: "sheenColor", + }, + { + text: "Sheen Color Map", + value: "sheenColorMap", + }, + { + text: "Sheen Roughness", + value: "sheenRoughness", + }, + { + text: "Sheen Roughness Map", + value: "sheenRoughnessMap", + }, + { + text: "Specular Color", + value: "specularColor", + }, + { + text: "Specular Color Map", + value: "specularColorMap", + }, + { + text: "Specular Intensity", + value: "specularIntensity", + }, + { + text: "Specular Intensity Map", + value: "specularIntensityMap", + }, + { + text: "Transmission", + value: "transmission", + }, + { + text: "Transmission Map", + value: "transmissionMap", + }, + { + text: "Thickness", + value: "thickness", + }, + { + text: "Thickness Map", + value: "thicknessMap", + }, + { + text: "Anisotropy", + value: "anisotropy", + }, + { + text: "Anisotropy Map", + value: "anisotropyMap", + }, + { + text: "Anisotropy Rotation", + value: "anisotropyRotation", + }, + { + text: "Attenuation Distance", + value: "attenuationDistance", + }, + { + text: "Attenuation Color", + value: "attenuationColor", + }, + { + text: "Thickness", + value: "thickness", + }, + { + text: "Iridescence", + value: "iridescence", + }, + { + text: "Iridescence Ior", + value: "iridescenceIOR", + }, + { + text: "Iridescence Map", + value: "iridescenceMap", + }, + { + text: "Iridescence Thickness Range", + value: "iridescenceThicknessRange", + }, + + "|MESH Displacement / Normal / Bump| <-- not a property", + { + text: "Displacement Map", + value: "displacementMap", + }, + { + text: "Displacement Scale", + value: "displacementScale", + }, + { + text: "Displacement Bias", + value: "displacementBias", + }, + { + text: "Bump Map", + value: "bumpMap", + }, + { + text: "Bump Scale", + value: "bumpScale", + }, + { + text: "Normal Map Type", + value: "normalMapType", + }, + + "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", + { + text: "Shininess", + value: "shininess", + }, + + { + text: "Wireframe", + value: "wireframe", + }, + { + text: "Wireframe Linewidth", + value: "wireframeLinewidth", + }, + { + text: "Wireframe Linecap", + value: "wireframeLinecap", + }, + { + text: "Wireframe Linejoin", + value: "wireframeLinejoin", + }, + + "|POINTS| <-- not a property", + { + text: "Size", + value: "size", + }, + { + text: "Size Attenuation", + value: "sizeAttenuation", + }, + + "|LINES| <-- not a property", + { + text: "Scale", + value: "scale", + }, + { + text: "Dash Size", + value: "dashSize", + }, + { + text: "Gap Size", + value: "gapSize", + }, + + "|SPRITES| <-- not a property", + { + text: "Rotation", + value: "rotation", + }, + ], + }, + blendModes: { + acceptReporters: false, + items: [{ + text: "No Blending", + value: "NoBlending", + }, + { + text: "Normal Blending", + value: "NormalBlending", + }, + { + text: "Additive Blending", + value: "AdditiveBlending", + }, + { + text: "Subtractive Blending", + value: "SubtractiveBlending", + }, + { + text: "Multiply Blending", + value: "MultiplyBlending", + }, + { + text: "Custom Blending", + value: "CustomBlending", + }, + ], + }, + depthModes: { + acceptReporters: false, + items: [{ + text: "Never Depth", + value: "NeverDepth", + }, + { + text: "Always Depth", + value: "AlwaysDepth", + }, + { + text: "Equal Depth", + value: "EqualDepth", + }, + { + text: "Less Depth", + value: "LessDepth", + }, + { + text: "Less Equal Depth", + value: "LessEqualDepth", + }, + { + text: "Greater Equal Depth", + value: "GreaterEqualDepth", + }, + { + text: "Greater Depth", + value: "GreaterDepth", + }, + { + text: "Not Equal Depth", + value: "NotEqualDepth", + }, + ], + }, + materialTypes: { + acceptReporters: false, + items: [{ + text: "Mesh Basic Material", + value: "MeshBasicMaterial", + }, + { + text: "Mesh Standard Material", + value: "MeshStandardMaterial", + }, + { + text: "Mesh Physical Material", + value: "MeshPhysicalMaterial", + }, + { + text: "Mesh Lambert Material", + value: "MeshLambertMaterial", + }, + { + text: "Mesh Phong Material", + value: "MeshPhongMaterial", + }, + { + text: "Mesh Depth Material", + value: "MeshDepthMaterial", + }, + { + text: "Mesh Normal Material", + value: "MeshNormalMaterial", + }, + { + text: "Mesh Matcap Material", + value: "MeshMatcapMaterial", + }, + { + text: "Mesh Toon Material", + value: "MeshToonMaterial", + }, + { + text: "Line Basic Material", + value: "LineBasicMaterial", + }, + { + text: "Line Dashed Material", + value: "LineDashedMaterial", + }, + { + text: "Points Material", + value: "PointsMaterial", + }, + { + text: "Sprite Material", + value: "SpriteMaterial", + }, + { + text: "Shadow Material", + value: "ShadowMaterial", + }, + ], + }, + textureModes: { + acceptReporters: false, + items: ["Pixelate", "Blur"], + }, + textureStyles: { + acceptReporters: false, + items: ["Repeat", "Clamp"], + }, + geometryTypes: { + acceptReporters: false, + items: [{ + text: "Box Geometry", + value: "BoxGeometry", + }, + { + text: "Sphere Geometry", + value: "SphereGeometry", + }, + { + text: "Cylinder Geometry", + value: "CylinderGeometry", + }, + { + text: "Plane Geometry", + value: "PlaneGeometry", + }, + { + text: "Circle Geometry", + value: "CircleGeometry", + }, + { + text: "Torus Geometry", + value: "TorusGeometry", + }, + { + text: "Torus Knot Geometry", + value: "TorusKnotGeometry", + }, + ], + }, + modelsList: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) - if (models.length < 1) return [["Load a model! (GLB Loader category)"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - fonts: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".glb")); + if (models.length < 1) return [ + ["Load a model! (GLB Loader category)"] + ]; // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.json')) - if (models.length < 1) return [["Load a font!"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, + return models.map((m) => [m.name]); + }, + }, + fonts: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; - } - }} + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".json")); + if (models.length < 1) return [ + ["Load a font!"] + ]; - addObject(args) { + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + }, + }; + } + + addObject(args) { const object = new THREE[args.TYPE](); - object.castShadow = true - object.receiveShadow = true + object.castShadow = true; + object.receiveShadow = true; - createObject(args.OBJECT3D, object, args.GROUP) - } - cloneObject(args) { - let object = getObject(args.OBJECT3D) - const clone = object.clone(true) - clone.name - createObject(args.NAME, clone, args.GROUP) - } - setObjectV3(args) { - let object = getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) + createObject(args.OBJECT3D, object, args.GROUP); + } + cloneObject(args) { + let object = getObject(args.OBJECT3D); + const clone = object.clone(true); + clone.name; + createObject(args.NAME, clone, args.GROUP); + } + setObjectV3(args) { + let object = getObject(args.OBJECT3D); + let values = JSON.parse(args.VALUE); function degToRad(deg) { - return deg * Math.PI / 180; + return (deg * Math.PI) / 180; } - if (object.rigidBody) { - const x = values[0] - const y = values[1] - const z = values[2] + const x = values[0]; + const y = values[1]; + const z = values[2]; if (args.PROPERTY === "rotation") { - const euler = new THREE.Euler( - degToRad(x), - degToRad(y), - degToRad(z), - 'YXZ' - ) - const quaternion = new THREE.Quaternion() - quaternion.setFromEuler(euler) + const euler = new THREE.Euler(degToRad(x), degToRad(y), degToRad(z), "YXZ"); + const quaternion = new THREE.Quaternion(); + quaternion.setFromEuler(euler); object.rigidBody.setRotation({ x: quaternion.x, y: quaternion.y, z: quaternion.z, - w: quaternion.w + w: quaternion.w, }); } else if (args.PROPERTY === "position") { - object.rigidBody.setTranslation({ x: x, y: y, z: z }, true) + object.rigidBody.setTranslation({ + x: x, + y: y, + z: z, + }, + true + ); } - return + return; } - if (object.isCamera == true && controls) { - - } + if (object.isCamera == true && controls) {} if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.set(0,0,0) + values = values.map((v) => (v * Math.PI) / 180); + object.rotation.set(0, 0, 0); + } + if (object.isDirectionalLight == true) { + object.pos = new THREE.Vector3(...values); + console.log(true, values, object.pos); + return; } - if (object.isDirectionalLight == true) {object.pos = new THREE.Vector3(...values); console.log(true, values, object.pos); return} - object[args.PROPERTY].set(...values); + object[args.PROPERTY].set(...values); - if (object.type == "CubeCamera") object.updateCoordinateSystem() - } - /* - changeObjectV3(args) { - getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) + if (object.type == "CubeCamera") object.updateCoordinateSystem(); + } + /* + changeObjectV3(args) { + getObject(args.OBJECT3D) + let values = JSON.parse(args.VALUE) - if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.x += values[0] - object.rotation.y += values[1] - object.rotation.z += values[2] - } - else { - object[args.PROPERTY].add(...values); - } - } - changeObjectXV3(args) { - getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "rotation") value = value * Math.PI / 180 + if (args.PROPERTY === "rotation") { + values = values.map(v => v * Math.PI / 180); + object.rotation.x += values[0] + object.rotation.y += values[1] + object.rotation.z += values[2] + } + else { + object[args.PROPERTY].add(...values); + } + } + changeObjectXV3(args) { + getObject(args.OBJECT3D) + let value = args.VALUE + if (args.PROPERTY === "rotation") value = value * Math.PI / 180 - object[args.PROPERTY][args.X] += value - } - */ - getObjectV3(args) { - let object = getObject(args.OBJECT3D) - if (!object) return - let values = vector3ToString(object[args.PROPERTY]) + object[args.PROPERTY][args.X] += value + } + */ + getObjectV3(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let values = vector3ToString(object[args.PROPERTY]); if (args.PROPERTY === "rotation") { - const toDeg = Math.PI/180 - values = [values[0]/toDeg,values[1]/toDeg,values[2]/toDeg,] + const toDeg = Math.PI / 180; + values = [values[0] / toDeg, values[1] / toDeg, values[2] / toDeg]; } - return JSON.stringify(values) - } - setObject(args){ - let object = getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "material") {const mat = materials[args.NAME]; if (mat) value = mat; else value = undefined} - else if (args.PROPERTY === "geometry") {const geo = geometries[args.NAME]; if (geo) value = geo; else value = undefined} - else value = !!value - - if (value == undefined) return //invalid geo/mat - object[args.PROPERTY] = value - } - getObject(args){ - let object = getObject(args.OBJECT3D) - if (!object) return - let value - if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; - else value = object.visible; - - return value - } - removeObject(args) { - removeObject(args.OBJECT3D) - } - objectE(args) { - return scene.children.map(o => o.name).includes(args.NAME) - } + return JSON.stringify(values); + } + setObject(args) { + let object = getObject(args.OBJECT3D); + let value = args.VALUE; + if (args.PROPERTY === "material") { + const mat = materials[args.NAME]; + if (mat) value = mat; + else value = undefined; + } else if (args.PROPERTY === "geometry") { + const geo = geometries[args.NAME]; + if (geo) value = geo; + else value = undefined; + } else value = !!value; + + if (value == undefined) return; //invalid geo/mat + object[args.PROPERTY] = value; + } + getObject(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let value; + if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; + else value = object.visible; + + return value; + } + removeObject(args) { + removeObject(args.OBJECT3D); + } + objectE(args) { + return scene.children.map((o) => o.name).includes(args.NAME); + } -//defines - newMaterial(args) { - if (materials[args.NAME] && alerts) alert ("material already exists! will replace...") - const mat = new THREE[args.TYPE](); - mat.name = args.NAME; + //defines + newMaterial(args) { + if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); + const mat = new THREE[args.TYPE](); + mat.name = args.NAME; - materials[args.NAME] = mat; - } - async setMaterial(args) { - if (typeof(args.VALUE) == "string" && args.VALUE.at(0) == "|") return - const mat = materials[args.NAME] - - let value = args.VALUE - - if (args.VALUE == "false") value = false - - if (args.PROPERTY == "side") {value = (args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide)} - else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)) - else value = getAsset(value) - - - console.log("o:", args.VALUE, typeof(args.VALUE)) - console.log("r:", value, typeof(value)) - - mat[args.PROPERTY] = await (value) //await incase its a texture - mat.needsUpdate = true - } - setBlending(args) { - const mat = materials[args.NAME] - mat.blending = THREE[args.VALUE] - mat.premultipliedAlpha = true - mat.needsUpdate = true - } - setDepth(args) { - const mat = materials[args.NAME] - mat.depthFunc = THREE[args.VALUE] - mat.needsUpdate = true - } - removeMaterial(args){ - const mat = materials[args.NAME] - mat.dispose() - delete(materials[args.NAME]) - } - materialE(args) { - return materials[args.NAME] ? true : false - } + materials[args.NAME] = mat; + } + async setMaterial(args) { + if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; + const mat = materials[args.NAME]; - newGeometry(args) { - if (geometries[args.NAME] && alerts) alert ("geometry already exists! will replace...") - const geo = new THREE[args.TYPE]() - geo.name = args.NAME + let value = args.VALUE; - geometries[args.NAME] = geo - } - setGeometry(args) { - const geo = geometries[args.NAME] - geo[args.PROPERTY] = (args.VALUE) + if (args.VALUE == "false") value = false; - geo.needsUpdate = true; - } - removeGeometry(args){ - const geo = geometries[args.NAME] - geo.dispose() - delete(geometries[args.NAME]) - } - geometryE(args) { - return geometries[args.NAME] ? true : false - } + if (args.PROPERTY == "side") { + value = args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide; + } else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)); + else value = getAsset(value); - newGeo(args) { - const geometry = new THREE.BufferGeometry() - geometry.name = args.NAME - geometries[args.NAME] = geometry - } - async geoPoints(args) { - const geometry = geometries[args.NAME] - const positions = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v3 of each vertex of each triangle + console.log("o:", args.VALUE, typeof args.VALUE); + console.log("r:", value, typeof value); - geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3)) - geometry.computeVertexNormals() + mat[args.PROPERTY] = await value; //await incase its a texture + mat.needsUpdate = true; + } + setBlending(args) { + const mat = materials[args.NAME]; + mat.blending = THREE[args.VALUE]; + mat.premultipliedAlpha = true; + mat.needsUpdate = true; + } + setDepth(args) { + const mat = materials[args.NAME]; + mat.depthFunc = THREE[args.VALUE]; + mat.needsUpdate = true; + } + removeMaterial(args) { + const mat = materials[args.NAME]; + mat.dispose(); + delete materials[args.NAME]; + } + materialE(args) { + return materials[args.NAME] ? true : false; + } - geometry.needsUpdate = true - } - geoUVs(args) { - const geometry = geometries[args.NAME] - const UVs = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v2 of each UV of each triangle + newGeometry(args) { + if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); + const geo = new THREE[args.TYPE](); + geo.name = args.NAME; - geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(UVs), 2)) - geometry.needsUpdate = true - } + geometries[args.NAME] = geo; + } + setGeometry(args) { + const geo = geometries[args.NAME]; + geo[args.PROPERTY] = args.VALUE; - splines(args) { - const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)) - geometry.name = args.NAME + geo.needsUpdate = true; + } + removeGeometry(args) { + const geo = geometries[args.NAME]; + geo.dispose(); + delete geometries[args.NAME]; + } + geometryE(args) { + return geometries[args.NAME] ? true : false; + } - geometries[args.NAME] = geometry - } + newGeo(args) { + const geometry = new THREE.BufferGeometry(); + geometry.name = args.NAME; + geometries[args.NAME] = geometry; + } + async geoPoints(args) { + const geometry = geometries[args.NAME]; + const positions = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v3 of each vertex of each triangle - async splineModel(args) { - const model = await getModel(args.MODEL, args.NAME) - if (!model) return console.warn("Model not found:", args.MODEL) + geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3)); + geometry.computeVertexNormals(); - const curve = getAsset(args.CURVE) - const spacing = parseFloat(args.SPACING) || 1 - const curveLength = curve.getLength() - const divisions = Math.floor(curveLength / spacing) + geometry.needsUpdate = true; + } + geoUVs(args) { + const geometry = geometries[args.NAME]; + const UVs = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v2 of each UV of each triangle + + geometry.setAttribute("uv", new THREE.BufferAttribute(new Float32Array(UVs), 2)); + geometry.needsUpdate = true; + } + + splines(args) { + const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); + geometry.name = args.NAME; - const geomList = [] - const matList = [] + geometries[args.NAME] = geometry; + } - for (let i = 0; i <= divisions; i++) { - const t = i / divisions - const pos = curve.getPointAt(t) - const tangent = curve.getTangentAt(t) + async splineModel(args) { + const model = await getModel(args.MODEL, args.NAME); + if (!model) return console.warn("Model not found:", args.MODEL); - const temp = model.clone(true) - temp.position.copy(pos) + const curve = getAsset(args.CURVE); + const spacing = parseFloat(args.SPACING) || 1; + const curveLength = curve.getLength(); + const divisions = Math.floor(curveLength / spacing); - const up = new THREE.Vector3(0, 0, 1) - const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()) - temp.quaternion.copy(quat) + const geomList = []; + const matList = []; - temp.updateMatrixWorld(true) + for (let i = 0; i <= divisions; i++) { + const t = i / divisions; + const pos = curve.getPointAt(t); + const tangent = curve.getTangentAt(t); + + const temp = model.clone(true); + temp.position.copy(pos); + + const up = new THREE.Vector3(0, 0, 1); + const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()); + temp.quaternion.copy(quat); + + temp.updateMatrixWorld(true); + + temp.traverse((child) => { + if (child.isMesh && child.geometry) { + const geom = child.geometry.clone(); + geom.applyMatrix4(child.matrixWorld); + geomList.push(geom); + matList.push(child.material); //.clone() ? + } + }); + } - temp.traverse(child => { - if (child.isMesh && child.geometry) { - const geom = child.geometry.clone() - geom.applyMatrix4(child.matrixWorld) - geomList.push(geom) - matList.push(child.material) //.clone() ? + const validGeoms = geomList.filter((g) => { + const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position; + if (!ok) console.warn("geometry skipped:", g); + return ok; + }); + + const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true); + merged.computeBoundingBox(); + merged.computeBoundingSphere(); + + merged.name = args.NAME; + geometries[args.NAME] = merged; + matList.name = args.NAME; + materials[args.NAME] = matList; } - }) - } - const validGeoms = geomList.filter(g => { - const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position - if (!ok) console.warn("geometry skipped:", g) - return ok - }) + async text(args) { + const fontFile = runtime + .getTargetForStage() + .getSounds() + .find((c) => c.name === args.FONT); + if (!fontFile) return; - const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true) - merged.computeBoundingBox() - merged.computeBoundingSphere() + const json = new TextDecoder().decode(fontFile.asset.data.buffer); + const fontData = JSON.parse(json); - merged.name = args.NAME - geometries[args.NAME] = merged - matList.name = args.NAME - materials[args.NAME] = matList - } - - async text(args) { - const fontFile = runtime.getTargetForStage().getSounds().find(c => c.name === args.FONT) - if (!fontFile) return + const font = fontLoad.parse(fontData); - const json = new TextDecoder().decode(fontFile.asset.data.buffer) - const fontData = JSON.parse(json) + const params = { + font: font, + size: JSON.parse(args.S), + height: JSON.parse(args.D), + curveSegments: JSON.parse(args.CS), + bevelEnabled: false, + }; + const geometry = new TextGeometry.TextGeometry(args.TEXT, params); + geometry.computeVertexNormals(); + geometry.center(); // optional, recenters the text - const font = fontLoad.parse(fontData) + geometry.name = args.NAME; - const params = {font: font, size: JSON.parse(args.S), height: JSON.parse(args.D), curveSegments: JSON.parse(args.CS), bevelEnabled: false} - const geometry = new TextGeometry.TextGeometry(args.TEXT, params) - geometry.computeVertexNormals() - geometry.center() // optional, recenters the text - + geometries[args.NAME] = geometry; + } - geometry.name = args.NAME + async loadFont() { + openFileExplorer(".json").then((files) => { + const file = files[0]; + const reader = new FileReader(); - geometries[args.NAME] = geometry - } + reader.onload = async (e) => { + const arrayBuffer = e.target.result; - async loadFont() { - openFileExplorer(".json").then(files => { - const file = files[0] - const reader = new FileReader() + // From lily's assets + // // Thank you PenguinMod for providing this code. - reader.onload = async (e) => { - const arrayBuffer = e.target.result - - // From lily's assets - // // Thank you PenguinMod for providing this code. - - const targetId = runtime.getTargetForStage().id //util.target.id not working! - const assetName = Cast.toString(file.name) + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); - const buffer = arrayBuffer + const buffer = arrayBuffer; - const storage = runtime.storage + const storage = runtime.storage; const asset = storage.createAsset( storage.AssetType.Sound, storage.DataFormat.MP3, @@ -1329,7 +2519,7 @@ Promise.resolve(load()).then(() => { new Uint8Array(buffer), null, true - ) + ); try { await vm.addSound( @@ -1340,615 +2530,1386 @@ Promise.resolve(load()).then(() => { name: assetName, }, targetId - ) - alert("Font loaded successfully!") + ); + alert("Font loaded successfully!"); } catch (e) { - console.error(e) - alert("Error loading font.") + console.error(e); + alert("Error loading font."); } - - // End of PenguinMod + + // End of PenguinMod + }; + + reader.readAsArrayBuffer(file); + }); + } + openConv() { + { + open("https://gero3.github.io/facetype.js/"); } + } + } + Scratch.extensions.register(new ThreeObjects()); + + class ThreeLights { + getInfo() { + return { + id: "threeLights", + name: "Three Lights", + color1: "#c7a22aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addLight", + blockType: Scratch.BlockType.COMMAND, + text: "add light [NAME] type [TYPE] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myLight", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "lightTypes", + }, + }, + }, + { + opcode: "setLight", + blockType: Scratch.BlockType.COMMAND, + text: "set light [NAME][PROPERTY] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "lightProperties", + defaultValue: "intensity", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myLight", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + exemptFromNormalization: true, + }, + }, + }, + ], + menus: { + lightTypes: { + acceptReporters: false, + items: [{ + text: "Ambient Light", + value: "AmbientLight", + }, + { + text: "Directional Light", + value: "DirectionalLight", + }, + { + text: "Point Light", + value: "PointLight", + }, + { + text: "Hemisphere Light", + value: "HemisphereLight", + }, + { + text: "Spot Light", + value: "SpotLight", + }, + ], + }, + lightProperties: { + acceptReporters: false, + items: [{ + text: "Color", + value: "color", + }, + { + text: "Intensity", + value: "intensity", + }, + { + text: "Cast Shadow?", + value: "castShadow", + }, + { + text: "Ground Color (HemisphereLight)", + value: "groundColor", + }, + { + text: "Map (SpotLight)", + value: "map", + }, + { + text: "Distance (SpotLight)", + value: "distance", + }, + { + text: "Decay (SpotLight)", + value: "decay", + }, + { + text: "Penumbra (SpotLight)", + value: "penumbra", + }, + { + text: "Angle/Size (SpotLight)", + value: "angle", + }, + { + text: "Power (SpotLight)", + value: "power", + }, + { + text: "Target Position (Directional/SpotLight)", + value: "target", + }, + ], + }, + }, + }; + } - reader.readAsArrayBuffer(file); - }) - } - openConv() {{open("https://gero3.github.io/facetype.js/")}} + addLight(args) { + const light = new THREE[args.TYPE](0xffffff, 1); - } - Scratch.extensions.register(new ThreeObjects()) - - class ThreeLights { - getInfo() { - return { - id: "threeLights", - name: "Three Lights", - color1: "#c7a22aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addLight", blockType: Scratch.BlockType.COMMAND, text: "add light [NAME] type [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "lightTypes"}}}, - {opcode: "setLight", blockType: Scratch.BlockType.COMMAND, text: "set light [NAME][PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lightProperties", defaultValue: "intensity"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1", exemptFromNormalization: true}}}, - ], - menus: { - lightTypes: {acceptReporters: false, items: [ - {text: "Ambient Light", value: "AmbientLight"},{text: "Directional Light", value: "DirectionalLight"},{text: "Point Light", value: "PointLight"},{text: "Hemisphere Light", value: "HemisphereLight"},{text: "Spot Light", value: "SpotLight"}, - ]}, - lightProperties: {acceptReporters: false, items: [ - {text: "Color", value: "color"},{text: "Intensity", value: "intensity"},{text: "Cast Shadow?", value: "castShadow"}, - {text: "Ground Color (HemisphereLight)", value: "groundColor"}, - {text: "Map (SpotLight)", value: "map"},{text: "Distance (SpotLight)", value: "distance"},{text: "Decay (SpotLight)", value: "decay"},{text: "Penumbra (SpotLight)", value: "penumbra"},{text: "Angle/Size (SpotLight)", value: "angle"},{text: "Power (SpotLight)", value: "power"}, - {text: "Target Position (Directional/SpotLight)", value: "target"}, - ]}, - } - }} + createObject(args.NAME, light, args.GROUP); + lights[args.NAME] = light; + if (light.type === "AmbientLight" || "HemisphereLight") return; - addLight(args) { - const light = new THREE[args.TYPE](0xffffff, 1) - - createObject(args.NAME, light, args.GROUP) - lights[args.NAME] = light - if (light.type === "AmbientLight" || "HemisphereLight") return - - light.castShadow = true - if (light.type === "PointLight") return - //Directional & Spot Light - light.target.position.set(0, 0, 0) - scene.add(light.target) - - light.pos = new THREE.Vector3(0,0,0) - - light.shadow.mapSize.width = 4096 - light.shadow.mapSize.height = 2048 - - if (light.type === "SpotLight") { - light.decay = 0 - light.shadow.camera.near = 500; - light.shadow.camera.far = 4000; - light.shadow.camera.fov = 30; - } - light.shadow.needsUpdate = true - light.needsUpdate = true - } + light.castShadow = true; + if (light.type === "PointLight") return; + //Directional & Spot Light + light.target.position.set(0, 0, 0); + scene.add(light.target); - setLight(args) { - const light = lights[args.NAME] - if (!args.PROPERTY) return - if (args.PROPERTY === "target") { - light.target.position.set(...JSON.parse(args.VALUE)) //vector3 - light.target.updateMatrixWorld(); - } - else { - light[args.PROPERTY] = getAsset(args.VALUE) + light.pos = new THREE.Vector3(0, 0, 0); + + light.shadow.mapSize.width = 4096; + light.shadow.mapSize.height = 2048; + + if (light.type === "SpotLight") { + light.decay = 0; + light.shadow.camera.near = 500; + light.shadow.camera.far = 4000; + light.shadow.camera.fov = 30; + } + light.shadow.needsUpdate = true; + light.needsUpdate = true; } - light.needsUpdate = true - if (light.type === "AmbientLight" || "HemisphereLight") return + setLight(args) { + const light = lights[args.NAME]; + if (!args.PROPERTY) return; + if (args.PROPERTY === "target") { + light.target.position.set(...JSON.parse(args.VALUE)); //vector3 + light.target.updateMatrixWorld(); + } else { + light[args.PROPERTY] = getAsset(args.VALUE); + } + light.needsUpdate = true; + + if (light.type === "AmbientLight" || "HemisphereLight") return; - light.shadow.camera.updateProjectionMatrix(); - light.shadow.needsUpdate = true + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; + } } + Scratch.extensions.register(new ThreeLights()); - } - Scratch.extensions.register(new ThreeLights()) - - class ThreeUtilities { - getInfo() { - return { - id: "threeUtility", - name: "Three Utilities", - color1: "#3875c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "newVector2", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}}}, - {opcode: "newVector3", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y] [Z]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}, Z: {type: Scratch.ArgumentType.NUMBER}}}, + class ThreeUtilities { + getInfo() { + return { + id: "threeUtility", + name: "Three Utilities", + color1: "#3875c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "newVector2", + blockType: Scratch.BlockType.REPORTER, + text: "New Vector [X] [Y]", + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "newVector3", + blockType: Scratch.BlockType.REPORTER, + text: "New Vector [X] [Y] [Z]", + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + }, + Z: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, "---", - {opcode: "operateV3", blockType: Scratch.BlockType.REPORTER, text: "do [V3] [O] [V32]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, O: {type: Scratch.ArgumentType.STRING, menu: "operators"}, V32: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, - {opcode: "moveVector3", blockType: Scratch.BlockType.REPORTER, text: "move [S] steps in vector [V3] in direction [D3]", arguments: {S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, - {opcode: "directionTo", blockType: Scratch.BlockType.REPORTER, text: "direction from [V3] to [T3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, T3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, + { + opcode: "operateV3", + blockType: Scratch.BlockType.REPORTER, + text: "do [V3] [O] [V32]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + O: { + type: Scratch.ArgumentType.STRING, + menu: "operators", + }, + V32: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + { + opcode: "moveVector3", + blockType: Scratch.BlockType.REPORTER, + text: "move [S] steps in vector [V3] in direction [D3]", + arguments: { + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + D3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + { + opcode: "directionTo", + blockType: Scratch.BlockType.REPORTER, + text: "direction from [V3] to [T3]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,3]", + }, + T3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, "---", - {opcode: "newColor", blockType: Scratch.BlockType.REPORTER, text: "New Color [HEX]", arguments: {HEX: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff"}}}, - {opcode: "newFog", blockType: Scratch.BlockType.REPORTER, text: "New Fog [COLOR] [NEAR] [FAR]", arguments: {COLOR: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff", exemptFromNormalization: true}, NEAR: {type: Scratch.ArgumentType.NUMBER}, FAR: {type: Scratch.ArgumentType.NUMBER, defaultValue: 10}}}, - {opcode: "newTexture", blockType: Scratch.BlockType.REPORTER, text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, - {opcode: "newCubeTexture", blockType: Scratch.BlockType.REPORTER, text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", arguments: {"COSTUMEX0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEX1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ1": {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, - {opcode: "newEquirectangularTexture", blockType: Scratch.BlockType.REPORTER, text: "New Equirectangular Texture [COSTUME] [MODE]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"}}}, + { + opcode: "newColor", + blockType: Scratch.BlockType.REPORTER, + text: "New Color [HEX]", + arguments: { + HEX: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#9966ff", + }, + }, + }, + { + opcode: "newFog", + blockType: Scratch.BlockType.REPORTER, + text: "New Fog [COLOR] [NEAR] [FAR]", + arguments: { + COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#9966ff", + exemptFromNormalization: true, + }, + NEAR: { + type: Scratch.ArgumentType.NUMBER, + }, + FAR: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10, + }, + }, + }, + { + opcode: "newTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "textureStyles", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "newCubeTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", + arguments: { + COSTUMEX0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEX1: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEY0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEY1: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEZ0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEZ1: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "textureStyles", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "newEquirectangularTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Equirectangular Texture [COSTUME] [MODE]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + }, + }, "---", - {opcode: "curve", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", arguments: {TYPE: {type: Scratch.ArgumentType.STRING, menu: "curveTypes"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]"}, CLOSED: {type: Scratch.ArgumentType.STRING, defaultValue: "true"}}}, + { + opcode: "curve", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.REPORTER, + text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", + arguments: { + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "curveTypes", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]", + }, + CLOSED: { + type: Scratch.ArgumentType.STRING, + defaultValue: "true", + }, + }, + }, "---", - {opcode:"mouseDown",extensions: ["colours_sensing"], blockType: Scratch.BlockType.BOOLEAN, text: "mouse [BUTTON] [action]?", arguments: {BUTTON: {type: Scratch.ArgumentType.STRING, menu: "mouseButtons"},action: {type: Scratch.ArgumentType.STRING, menu: "mouseAction"}}}, - {opcode: "mousePos",extensions: ["colours_sensing"], blockType: Scratch.BlockType.REPORTER, text: "mouse position", arguments: {}}, + { + opcode: "mouseDown", + extensions: ["colours_sensing"], + blockType: Scratch.BlockType.BOOLEAN, + text: "mouse [BUTTON] [action]?", + arguments: { + BUTTON: { + type: Scratch.ArgumentType.STRING, + menu: "mouseButtons", + }, + action: { + type: Scratch.ArgumentType.STRING, + menu: "mouseAction", + }, + }, + }, + { + opcode: "mousePos", + extensions: ["colours_sensing"], + blockType: Scratch.BlockType.REPORTER, + text: "mouse position", + arguments: {}, + }, "---", - {opcode: "getItem",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "get item [ITEM] of [ARRAY]", arguments: {ITEM: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}, ARRAY: {type: Scratch.ArgumentType.STRING, defaultValue: `["myObject", "myLight"]`}}}, - {blockType: Scratch.BlockType.LABEL, text: "↳ Raycasting"}, - {opcode: "raycast", blockType: Scratch.BlockType.COMMAND, text: "Raycast from [V3] in direction [D3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,1]"}}}, - {opcode: "getRaycast", blockType: Scratch.BlockType.REPORTER, text: "get raycast [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "raycastProperties"}}}, - - ], - menus: { - materialProperties: {acceptReporters: false, items: [ - {text: "Color", value: "color"},{text: "Map (texture)", value: "map"},{text: "Alpha Map (texture)", value: "alphaMap"},{text: "Alpha Test (0-1)", value: "alphaTest"},{text: "Side (front/back/double)", value: "side"},{text: "Bump Map (texture)", value: "bumpMap"},{text: "Bump Scale", value: "bumpScale"}, - ]}, - textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, - textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, - raycastProperties: {acceptReporters: false, items: [ - {text: "Intersected Object Names", value: "name"},{text: "Number of Objects", value: "number"},{text: "Intersected Objects distances", value: "distance"}, - ]}, - mouseButtons: {acceptReporters: false, items: ["left","middle","right"]}, - mouseAction: {acceptReporters: false, items: ["Down","Clicked"]}, - curveTypes: {acceptReporters: false, items: ["CatmullRomCurve3"]}, - operators: {acceptReporters: false, items: [ - "+","-","*","/","=","max","min","dot","cross","distance to","angle to","apply euler", - ]} + { + opcode: "getItem", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.REPORTER, + text: "get item [ITEM] of [ARRAY]", + arguments: { + ITEM: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + ARRAY: { + type: Scratch.ArgumentType.STRING, + defaultValue: `["myObject", "myLight"]`, + }, + }, + }, + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Raycasting", + }, + { + opcode: "raycast", + blockType: Scratch.BlockType.COMMAND, + text: "Raycast from [V3] in direction [D3]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,3]", + }, + D3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,1]", + }, + }, + }, + { + opcode: "getRaycast", + blockType: Scratch.BlockType.REPORTER, + text: "get raycast [PROPERTY]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "raycastProperties", + }, + }, + }, + ], + menus: { + materialProperties: { + acceptReporters: false, + items: [{ + text: "Color", + value: "color", + }, + { + text: "Map (texture)", + value: "map", + }, + { + text: "Alpha Map (texture)", + value: "alphaMap", + }, + { + text: "Alpha Test (0-1)", + value: "alphaTest", + }, + { + text: "Side (front/back/double)", + value: "side", + }, + { + text: "Bump Map (texture)", + value: "bumpMap", + }, + { + text: "Bump Scale", + value: "bumpScale", + }, + ], + }, + textureModes: { + acceptReporters: false, + items: ["Pixelate", "Blur"], + }, + textureStyles: { + acceptReporters: false, + items: ["Repeat", "Clamp"], + }, + raycastProperties: { + acceptReporters: false, + items: [{ + text: "Intersected Object Names", + value: "name", + }, + { + text: "Number of Objects", + value: "number", + }, + { + text: "Intersected Objects distances", + value: "distance", + }, + ], + }, + mouseButtons: { + acceptReporters: false, + items: ["left", "middle", "right"], + }, + mouseAction: { + acceptReporters: false, + items: ["Down", "Clicked"], + }, + curveTypes: { + acceptReporters: false, + items: ["CatmullRomCurve3"], + }, + operators: { + acceptReporters: false, + items: ["+", "-", "*", "/", "=", "max", "min", "dot", "cross", "distance to", "angle to", "apply euler"], + }, + }, + }; + } + mouseDown(args) { + if (args.action === "Down") return isMouseDown[args.BUTTON]; + if (args.action === "Clicked") { + if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false; + else prevMouse[args.BUTTON] = true; + return true; } - }} - mouseDown(args) { - if (args.action === "Down") return isMouseDown[args.BUTTON] - if (args.action === "Clicked") { - if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false - else prevMouse[args.BUTTON] = true; return true } - } - mousePos(event) { - return JSON.stringify(mouseNDC) - } - newVector3(args) { - return JSON.stringify([args.X, args.Y, args.Z]) - } - operateV3(args){ - const v3 = new THREE.Vector3(...JSON.parse(args.V3)) - const v32 = new THREE.Vector3(...JSON.parse(args.V32)) - - let r - if (args.O == "+") r = v3.add(v32) - else if (args.O == "-") r = v3.sub(v32) - else if (args.O == "*") r = v3.multiply(v32) - else if (args.O == "/") r = v3.divide(v32) - else if (args.O == "=") r = v3.equals(v32) - else if (args.O == "max") r = v3.max(v32) - else if (args.O == "min") r = v3.min(v32) - else if (args.O == "dot") r = v3.dot(v32) - else if (args.O == "cross") r = v3.cross(v32) - else if (args.O == "distance to") r = v3.distanceTo(v32) - else if (args.O == "angle to") r = v3.angleTo(v32) - else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)) - - if (typeof(r) == "object") return JSON.stringify([r.x, r.y, r.z]) - else return JSON.stringify(r) - } - - newVector2(args) { - return JSON.stringify([args.X, args.Y]) - } + mousePos(event) { + return JSON.stringify(mouseNDC); + } + newVector3(args) { + return JSON.stringify([args.X, args.Y, args.Z]); + } + operateV3(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)); + const v32 = new THREE.Vector3(...JSON.parse(args.V32)); + + let r; + if (args.O == "+") r = v3.add(v32); + else if (args.O == "-") r = v3.sub(v32); + else if (args.O == "*") r = v3.multiply(v32); + else if (args.O == "/") r = v3.divide(v32); + else if (args.O == "=") r = v3.equals(v32); + else if (args.O == "max") r = v3.max(v32); + else if (args.O == "min") r = v3.min(v32); + else if (args.O == "dot") r = v3.dot(v32); + else if (args.O == "cross") r = v3.cross(v32); + else if (args.O == "distance to") r = v3.distanceTo(v32); + else if (args.O == "angle to") r = v3.angleTo(v32); + else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)); + + if (typeof r == "object") return JSON.stringify([r.x, r.y, r.z]); + else return JSON.stringify(r); + } - moveVector3(args) { - const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); - const steps = Number(args.S); + newVector2(args) { + return JSON.stringify([args.X, args.Y]); + } - const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); + moveVector3(args) { + const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); + const steps = Number(args.S); - const yaw = THREE.MathUtils.degToRad(yawInputDeg); - const pitch = THREE.MathUtils.degToRad(pitchInputDeg); - const roll = THREE.MathUtils.degToRad(rollInputDeg); + const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); - const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); + const yaw = THREE.MathUtils.degToRad(yawInputDeg); + const pitch = THREE.MathUtils.degToRad(pitchInputDeg); + const roll = THREE.MathUtils.degToRad(rollInputDeg); - const forwardVector = new THREE.Vector3(0, 0, -1); - const direction = forwardVector.applyEuler(euler).normalize(); + const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); - const newPos = currentPos.add(direction.multiplyScalar(steps)); - return JSON.stringify([newPos.x, newPos.y, newPos.z]); - } + const forwardVector = new THREE.Vector3(0, 0, -1); + const direction = forwardVector.applyEuler(euler).normalize(); - directionTo(args) { - const v3 = new THREE.Vector3(...JSON.parse(args.V3)) - const toV3 = new THREE.Vector3(...JSON.parse(args.T3)) + const newPos = currentPos.add(direction.multiplyScalar(steps)); + return JSON.stringify([newPos.x, newPos.y, newPos.z]); + } - const direction = toV3.clone().sub(v3).normalize(); - // Pitch (X) - const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x*direction.x + direction.z*direction.z)); - // Yaw (Y) - const yaw = Math.atan2(direction.x, direction.z); + directionTo(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)); + const toV3 = new THREE.Vector3(...JSON.parse(args.T3)); - // Roll always 0 - return JSON.stringify([180+THREE.MathUtils.radToDeg(pitch),THREE.MathUtils.radToDeg(yaw),0]) - } + const direction = toV3.clone().sub(v3).normalize(); + // Pitch (X) + const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)); + // Yaw (Y) + const yaw = Math.atan2(direction.x, direction.z); - newColor(args) { - const color = new THREE.Color(args.HEX) - const uuid = crypto.randomUUID() - assets.colors[uuid] = color - return `colors/${uuid}` - } - newFog(args) { - const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR) - const uuid = crypto.randomUUID() - assets.fogs[uuid] = fog - return `fogs/${uuid}` - } - async newTexture(args) { - const textureURI = encodeCostume(args.COSTUME) - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - async newCubeTexture(args) { - const uris = [encodeCostume(args.COSTUMEX0),encodeCostume(args.COSTUMEX1), encodeCostume(args.COSTUMEY0),encodeCostume(args.COSTUMEY1), encodeCostume(args.COSTUMEZ0),encodeCostume(args.COSTUMEZ1)] - const normalized = await Promise.all(uris.map(uri => resizeImageToSquare(uri, 256))); - const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); - - texture.name = "CubeTexture" + args.COSTUMEX0; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - async newEquirectangularTexture(args) { - const textureURI = encodeCostume(args.COSTUME) - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME - texture.mapping = THREE.EquirectangularReflectionMapping - - setTexutre(texture, args.MODE) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } + // Roll always 0 + return JSON.stringify([180 + THREE.MathUtils.radToDeg(pitch), THREE.MathUtils.radToDeg(yaw), 0]); + } - curve(args) { - function parsePoints(input) { - // Match all [x,y,z] groups - const matches = input.match(/\[([^\]]+)\]/g) - if (!matches) return [] - - return matches.map(str => { - const nums = str - .replace(/[\[\]\s]/g, '') - .split(',') - .map(Number) - return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0) - }) - } - const points = parsePoints(args.POINTS) - const curve = new THREE[args.TYPE](points) - curve.closed = JSON.parse(args.CLOSED) - - const uuid = crypto.randomUUID() - assets.curves[uuid] = curve - return `curves/${uuid}` - } + newColor(args) { + const color = new THREE.Color(args.HEX); + const uuid = crypto.randomUUID(); + assets.colors[uuid] = color; + return `colors/${uuid}`; + } + newFog(args) { + const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR); + const uuid = crypto.randomUUID(); + assets.fogs[uuid] = fog; + return `fogs/${uuid}`; + } + async newTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newCubeTexture(args) { + const uris = [ + encodeCostume(args.COSTUMEX0), + encodeCostume(args.COSTUMEX1), + encodeCostume(args.COSTUMEY0), + encodeCostume(args.COSTUMEY1), + encodeCostume(args.COSTUMEZ0), + encodeCostume(args.COSTUMEZ1), + ]; + const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); + const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); + + texture.name = "CubeTexture" + args.COSTUMEX0; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newEquirectangularTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + texture.mapping = THREE.EquirectangularReflectionMapping; + + setTexutre(texture, args.MODE); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } - getItem(args) { - const items = JSON.parse(args.ARRAY) - const item = items[args.ITEM - 1] - if (!item) return "0" - return item - } + curve(args) { + function parsePoints(input) { + // Match all [x,y,z] groups + const matches = input.match(/\[([^\]]+)\]/g); + if (!matches) return []; + + return matches.map((str) => { + const nums = str + .replace(/[\[\]\s]/g, "") + .split(",") + .map(Number); + return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0); + }); + } + const points = parsePoints(args.POINTS); + const curve = new THREE[args.TYPE](points); + curve.closed = JSON.parse(args.CLOSED); + + const uuid = crypto.randomUUID(); + assets.curves[uuid] = curve; + return `curves/${uuid}`; + } + + getItem(args) { + const items = JSON.parse(args.ARRAY); + const item = items[args.ITEM - 1]; + if (!item) return "0"; + return item; + } - raycast(args) { - const origin = new THREE.Vector3(...JSON.parse(args.V3)) + raycast(args) { + const origin = new THREE.Vector3(...JSON.parse(args.V3)); // rotation is in degrees => convert to radians first - const rot = JSON.parse(args.D3).map(v => v * Math.PI / 180) + const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); - const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder) - const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize() + const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder); + const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize(); - const raycaster = new THREE.Raycaster() - //const camera = getObject(args.CAMERA) - raycaster.set( origin, direction ); + const raycaster = new THREE.Raycaster(); + //const camera = getObject(args.CAMERA) + raycaster.set(origin, direction); - const intersects = raycaster.intersectObjects( scene.children, true ) + const intersects = raycaster.intersectObjects(scene.children, true); - raycastResult = intersects - } - getRaycast(args) { - if (args.PROPERTY === "number") return raycastResult.length - if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map(i => i.distance)) - return JSON.stringify(raycastResult.map(i => i.object[args.PROPERTY])) + raycastResult = intersects; + } + getRaycast(args) { + if (args.PROPERTY === "number") return raycastResult.length; + if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map((i) => i.distance)); + return JSON.stringify(raycastResult.map((i) => i.object[args.PROPERTY])); + } } + Scratch.extensions.register(new ThreeUtilities()); - } - Scratch.extensions.register(new ThreeUtilities()) - - class ThreeGLB { - getInfo() { - return { - id: "threeGLB", - name: "Three GLB Loader", - color1: "#c53838ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {blockType: Scratch.BlockType.BUTTON, text: "Load GLB File", func: "loadModelFile"}, - {opcode: "addModel", blockType: Scratch.BlockType.COMMAND, text: "add [ITEM] as [NAME] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},ITEM: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}}}, - {opcode: "getModel", blockType: Scratch.BlockType.REPORTER, text: "get object [PROPERTY] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "modelProperties"}}}, - {opcode: "playAnimation", blockType: Scratch.BlockType.COMMAND, text: "play animation [ANAME] of [NAME], [TIMES] times", arguments: {TIMES: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - {opcode: "pauseAnimation", blockType: Scratch.BlockType.COMMAND, text: "set [TOGGLE] animation [ANAME] of [NAME]", arguments: {TOGGLE: {type: Scratch.ArgumentType.NUMBER, menu: "pauseUn"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - {opcode: "stopAnimation", blockType: Scratch.BlockType.COMMAND, text: "stop animation [ANAME] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - - ], - menus: { - modelProperties: {acceptReporters: false, items: [ - {text: "Animations", value: "animations"}, - ]}, - pauseUn: {acceptReporters: true, items: [{text: "Pause", value: "true"},{text: "Unpasue", value: "false"},]}, - modelsList: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; + class ThreeGLB { + getInfo() { + return { + id: "threeGLB", + name: "Three GLB Loader", + color1: "#c53838ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + blockType: Scratch.BlockType.BUTTON, + text: "Load GLB File", + func: "loadModelFile", + }, + { + opcode: "addModel", + blockType: Scratch.BlockType.COMMAND, + text: "add [ITEM] as [NAME] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + ITEM: { + type: Scratch.ArgumentType.STRING, + menu: "modelsList", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + }, + }, + { + opcode: "getModel", + blockType: Scratch.BlockType.REPORTER, + text: "get object [PROPERTY] of [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "modelProperties", + }, + }, + }, + { + opcode: "playAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "play animation [ANAME] of [NAME], [TIMES] times", + arguments: { + TIMES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "pauseAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "set [TOGGLE] animation [ANAME] of [NAME]", + arguments: { + TOGGLE: { + type: Scratch.ArgumentType.NUMBER, + menu: "pauseUn", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "stopAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "stop animation [ANAME] of [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + ], + menus: { + modelProperties: { + acceptReporters: false, + items: [{ + text: "Animations", + value: "animations", + }, ], + }, + pauseUn: { + acceptReporters: true, + items: [{ + text: "Pause", + value: "true", + }, + { + text: "Unpasue", + value: "false", + }, + ], + }, + modelsList: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) - if (models.length < 1) return [["Load a model!"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - } - }} + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".glb")); + if (models.length < 1) return [ + ["Load a model!"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + }, + }; + } - async loadModelFile() { + async loadModelFile() { + openFileExplorer(".glb").then((files) => { + const file = files[0]; + const reader = new FileReader(); - openFileExplorer(".glb").then(files => { - const file = files[0]; - const reader = new FileReader(); + reader.onload = async (e) => { + const arrayBuffer = e.target.result; - reader.onload = async (e) => { - const arrayBuffer = e.target.result; - - { // From lily's assets + { + // From lily's assets // Thank you PenguinMod for providing this code. - { - const targetId = runtime.getTargetForStage().id; //util.target.id not working! - const assetName = Cast.toString(file.name); + { + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); + + //const res = await Scratch.fetch(args.URL); + //const buffer = await res.arrayBuffer(); + const buffer = arrayBuffer; + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + alert("Model loaded successfully!"); + } catch (e) { + console.error(e); + alert("Error loading model."); + } + } + // End of PenguinMod + } + }; - //const res = await Scratch.fetch(args.URL); - //const buffer = await res.arrayBuffer(); - const buffer = arrayBuffer + reader.readAsArrayBuffer(file); + }); + } + async addModel(args) { + const group = await getModel(args.ITEM, args.NAME); - const storage = runtime.storage; - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ); + createObject(args.NAME, group, args.GROUP); + } + getModel(args) { + if (!models[args.NAME]) return; + return Object.keys(models[args.NAME].actions).toString(); + } - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ); - alert("Model loaded successfully!"); - } catch (e) { - console.error(e); - alert("Error loading model."); - } - } - // End of PenguinMod + playAnimation(args) { + const model = models[args.NAME]; + if (!model) { + console.log("no model!"); + return; } - }; - reader.readAsArrayBuffer(file); - }) - - } - async addModel(args) { - const group = await getModel(args.ITEM, args.NAME) - - createObject(args.NAME, group, args.GROUP) - } - getModel(args){ - if (!models[args.NAME]) return; - return Object.keys(models[args.NAME].actions).toString() - } + const action = model.actions[args.ANAME]; //clones of models dont have a stored actions! + if (!action) { + console.log("no action!"); + return; + } - playAnimation(args) { - const model = models[args.NAME] - if (!model) {console.log("no model!"); return} + args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity); - const action = model.actions[args.ANAME] //clones of models dont have a stored actions! - if (!action) { - console.log("no action!") - return + action.reset().play(); } + stopAnimation(args) { + const model = models[args.NAME]; + if (!model) return; - args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity) - - action.reset() - .play() - } - stopAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.stop(); - } - pauseAnimation(args) { - const model = models[args.NAME]; - if (!model) return; + const action = model.actions[args.ANAME]; + if (action) action.stop(); + } + pauseAnimation(args) { + const model = models[args.NAME]; + if (!model) return; - const action = model.actions[args.ANAME]; - if (action) action.paused = args.TOGGLE + const action = model.actions[args.ANAME]; + if (action) action.paused = args.TOGGLE; + } } + Scratch.extensions.register(new ThreeGLB()); - } - Scratch.extensions.register(new ThreeGLB()) - - class ThreeAddons { - getInfo() { - return { - id: "threeAddons", - name: "Three Addons", - color1: "#c538a2ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {blockType: Scratch.BlockType.LABEL, text: "Orbit Control"}, - {opcode: "OrbitControl", blockType: Scratch.BlockType.COMMAND, text: "set addon Orbit Control [STATE]", arguments: {STATE: {type: Scratch.ArgumentType.STRING, menu: "onoff"},}}, - - {blockType: Scratch.BlockType.LABEL, text: "Post Processing"}, - {opcode: "resetComposer", blockType: Scratch.BlockType.COMMAND, text: "reset composer"}, - {opcode: "bloom", blockType: Scratch.BlockType.COMMAND, text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},I: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, T:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, - {opcode: "godRays", blockType: Scratch.BlockType.COMMAND, text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}, DEC:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.95}, DENS:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},EXP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1},WEI:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.4},RES:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},SAMP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 64},}}, - {opcode: "dots", blockType: Scratch.BlockType.COMMAND, text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, A: {type: Scratch.ArgumentType.ANGLE, defaultValue: 0},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, - {opcode: "depth", blockType: Scratch.BlockType.COMMAND, text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", arguments: {FD: {type: Scratch.ArgumentType.NUMBER, defaultValue: (3)},FL: {type: Scratch.ArgumentType.NUMBER, defaultValue: (0.001)},BS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 4},H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 240},OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}}}, + class ThreeAddons { + getInfo() { + return { + id: "threeAddons", + name: "Three Addons", + color1: "#c538a2ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + blockType: Scratch.BlockType.LABEL, + text: "Orbit Control", + }, + { + opcode: "OrbitControl", + blockType: Scratch.BlockType.COMMAND, + text: "set addon Orbit Control [STATE]", + arguments: { + STATE: { + type: Scratch.ArgumentType.STRING, + menu: "onoff", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "Post Processing", + }, + { + opcode: "resetComposer", + blockType: Scratch.BlockType.COMMAND, + text: "reset composer", + }, + { + opcode: "bloom", + blockType: Scratch.BlockType.COMMAND, + text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + I: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.5, + }, + T: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.5, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + }, + }, + { + opcode: "godRays", + blockType: Scratch.BlockType.COMMAND, + text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + DEC: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.95, + }, + DENS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + EXP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.1, + }, + WEI: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.4, + }, + RES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + SAMP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 64, + }, + }, + }, + { + opcode: "dots", + blockType: Scratch.BlockType.COMMAND, + text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + A: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: 0, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + }, + }, + { + opcode: "depth", + blockType: Scratch.BlockType.COMMAND, + text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", + arguments: { + FD: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 3, + }, + FL: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.001, + }, + BS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 4, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 240, + }, + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "NORMAL", + }, + }, + }, "---", - {opcode: "custom", blockType: Scratch.BlockType.COMMAND, text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myShader"}, FRA: {type: Scratch.ArgumentType.STRING}, VER: {type: Scratch.ArgumentType.STRING}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}, OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, + { + opcode: "custom", + blockType: Scratch.BlockType.COMMAND, + text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myShader", + }, + FRA: { + type: Scratch.ArgumentType.STRING, + }, + VER: { + type: Scratch.ArgumentType.STRING, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "NORMAL", + }, + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, ], - menus: { - onoff: {acceptReporters: true, items: [{text: "enabled", value: "1"},{text: "disabled", value: "0"},]}, - blendModes: {acceptReporters: false, items: [ - "SKIP","SET","ADD","ALPHA","AVERAGE","COLOR","COLOR_BURN","COLOR_DODGE", - "DARKEN","DIFFERENCE","DIVIDE","DST","EXCLUSION","HARD_LIGHT","HARD_MIX", - "HUE","INVERT","INVERT_RGB","LIGHTEN","LINEAR_BURN","LINEAR_DODGE", - "LINEAR_LIGHT","LUMINOSITY","MULTIPLY","NEGATION","NORMAL","OVERLAY", - "PIN_LIGHT","REFLECT","SCREEN","SRC","SATURATION","SOFT_LIGHT","SUBTRACT", - "VIVID_LIGHT" - ]}, - } - }} + menus: { + onoff: { + acceptReporters: true, + items: [{ + text: "enabled", + value: "1", + }, + { + text: "disabled", + value: "0", + }, + ], + }, + blendModes: { + acceptReporters: false, + items: [ + "SKIP", + "SET", + "ADD", + "ALPHA", + "AVERAGE", + "COLOR", + "COLOR_BURN", + "COLOR_DODGE", + "DARKEN", + "DIFFERENCE", + "DIVIDE", + "DST", + "EXCLUSION", + "HARD_LIGHT", + "HARD_MIX", + "HUE", + "INVERT", + "INVERT_RGB", + "LIGHTEN", + "LINEAR_BURN", + "LINEAR_DODGE", + "LINEAR_LIGHT", + "LUMINOSITY", + "MULTIPLY", + "NEGATION", + "NORMAL", + "OVERLAY", + "PIN_LIGHT", + "REFLECT", + "SCREEN", + "SRC", + "SATURATION", + "SOFT_LIGHT", + "SUBTRACT", + "VIVID_LIGHT", + ], + }, + }, + }; + } OrbitControl(args) { - if (controls) controls.dispose() + if (controls) controls.dispose(); - console.log("creating...", OrbitControls) + console.log("creating...", OrbitControls); controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); - controls.enableDamping = true - - controls.enabled = !!args.STATE - console.log(controls) - } + controls.enableDamping = true; - resetComposer() { - composer.passes = [] - passes = {} - customEffects = [] - updateComposers() - } + controls.enabled = !!args.STATE; + console.log(controls); + } - bloom(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const bloomEffect = new BloomEffect({ - intensity: args.I, - luminanceThreshold: args.T, // ← correct key - luminanceSmoothing: args.S, - blendFunction: BlendFunction[args.BLEND], - }) - bloomEffect.blendMode.opacity.value = args.OP + resetComposer() { + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } - const pass = new EffectPass(camera, bloomEffect) + bloom(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const bloomEffect = new BloomEffect({ + intensity: args.I, + luminanceThreshold: args.T, // ← correct key + luminanceSmoothing: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + bloomEffect.blendMode.opacity.value = args.OP; - composer.addPass(pass) - } + const pass = new EffectPass(camera, bloomEffect); - godRays(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - let object = getObject(args.NAME) - const sun = object - - const godRays = new GodRaysEffect(camera, sun, { - resolutionScale: args.RES, - density: args.DENS, // ray density - decay: args.DEC, // fade out - weight: args.WEI, // brightness of rays - exposure: args.EXP, - samples: args.SAMP, - blendFunction: BlendFunction[args.BLEND], - }) - godRays.blendMode.opacity.value = args.OP - const pass = new EffectPass(camera, godRays) - composer.addPass(pass) - } + composer.addPass(pass); + } - dots(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const dot = new DotScreenEffect({ - angle: args.A, - scale: args.S, - blendFunction: BlendFunction[args.BLEND], - }) - dot.blendMode.opacity.value = args.OP - const pass = new EffectPass(camera, dot) - composer.addPass(pass) - } + godRays(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + let object = getObject(args.NAME); + const sun = object; + + const godRays = new GodRaysEffect(camera, sun, { + resolutionScale: args.RES, + density: args.DENS, // ray density + decay: args.DEC, // fade out + weight: args.WEI, // brightness of rays + exposure: args.EXP, + samples: args.SAMP, + blendFunction: BlendFunction[args.BLEND], + }); + godRays.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, godRays); + composer.addPass(pass); + } - depth(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const dofEffect = new DepthOfFieldEffect(camera, { - focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) - focalLength: args.FL, // lens focal length in meters - bokehScale: args.BS, // strength/size of the blur circles - height: args.H, // resolution hint (affects quality/perf) - blendFunction: BlendFunction[args.BLEND], - }) - dofEffect.blendMode.opacity.value = args.OP - - const dofPass = new EffectPass(camera, dofEffect) - composer.addPass(dofPass) - } + dots(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dot = new DotScreenEffect({ + angle: args.A, + scale: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + dot.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, dot); + composer.addPass(pass); + } - async custom(args) { - function cleanGLSL(glslCode) { - //delete multilines comments - let cleanedCode = glslCode.replace(/\/\*[\s\S]*?\*\//g, ' ') - .replace(/ /g, '\n') - .replace(/\/\/.*$/gm, ' ') - .replace(/; /g, ';\n') - - return cleanedCode; + depth(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dofEffect = new DepthOfFieldEffect(camera, { + focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) + focalLength: args.FL, // lens focal length in meters + bokehScale: args.BS, // strength/size of the blur circles + height: args.H, // resolution hint (affects quality/perf) + blendFunction: BlendFunction[args.BLEND], + }); + dofEffect.blendMode.opacity.value = args.OP; + + const dofPass = new EffectPass(camera, dofEffect); + composer.addPass(dofPass); } - let fs = cleanGLSL(` + async custom(args) { + function cleanGLSL(glslCode) { + //delete multilines comments + let cleanedCode = glslCode + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/ /g, "\n") + .replace(/\/\/.*$/gm, " ") + .replace(/; /g, ";\n"); + + return cleanedCode; + } + + let fs = cleanGLSL(` ${args.FRA} - `) - if (!args.FRA.trim()) {fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`} - const vs = cleanGLSL(` + `); + if (!args.FRA.trim()) { + fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`; + } + const vs = cleanGLSL(` ${args.VER} - `) - console.log(fs) - console.log(vs) + `); + console.log(fs); + console.log(vs); - const effect = new Effect( - "Custom", - fs, - { + const effect = new Effect("Custom", fs, { blendFunction: BlendFunction[args.BLEND], vertexShader: vs, - uniforms: new Map([ //uniforms usually in shaders... open to more! - ['time', new THREE.Uniform(0.0)], - ['resolution', new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height))] + uniforms: new Map([ + //uniforms usually in shaders... open to more! + ["time", new THREE.Uniform(0.0)], + [ + "resolution", + new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height)), + ], ]), - defines: new Map([['USE_TIME', '1'], ['USE_VERTEX_TRANSFORM', '']]), - } - ); + defines: new Map([ + ["USE_TIME", "1"], + ["USE_VERTEX_TRANSFORM", ""], + ]), + }); - effect.blendMode.opacity.value = args.OP + effect.blendMode.opacity.value = args.OP; - const pass = new EffectPass(camera, effect); - composer.addPass(pass); + const pass = new EffectPass(camera, effect); + composer.addPass(pass); - customEffects.push(effect); + customEffects.push(effect); + } } + Scratch.extensions.register(new ThreeAddons()); - } - Scratch.extensions.register(new ThreeAddons()) - - class RapierPhysics { + class RapierPhysics { getInfo() { return { id: "rapierPhysics", @@ -1956,119 +3917,679 @@ Promise.resolve(load()).then(() => { color1: "#222222", color2: "#203024ff", color3: "#78f07eff", - blocks: [ - {opcode: "createWorld", blockType: Scratch.BlockType.COMMAND, text: "create world | gravity:[G]", arguments: {G: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,-9.81,0]"}}}, - {opcode: "getWorld", blockType: Scratch.BlockType.REPORTER, text: "get world [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "wProp"}}}, + blocks: [{ + opcode: "createWorld", + blockType: Scratch.BlockType.COMMAND, + text: "create world | gravity:[G]", + arguments: { + G: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,-9.81,0]", + }, + }, + }, + { + opcode: "getWorld", + blockType: Scratch.BlockType.REPORTER, + text: "get world [PROPERTY]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "wProp", + }, + }, + }, "---", - {opcode: "objectPhysics", blockType: Scratch.BlockType.COMMAND, text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", arguments: {state2: {type: Scratch.ArgumentType.STRING, menu: "state2"},state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"}, type: {type: Scratch.ArgumentType.STRING, menu: "objectTypes", defaultValue: "dynamic"}, collider: {type: Scratch.ArgumentType.STRING, menu: "colliderTypes", defaultValue: "cuboid"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},mass: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},density: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},friction: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0.5"}}}, + { + opcode: "objectPhysics", + blockType: Scratch.BlockType.COMMAND, + text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", + arguments: { + state2: { + type: Scratch.ArgumentType.STRING, + menu: "state2", + }, + state: { + type: Scratch.ArgumentType.STRING, + menu: "state", + defaultValue: "true", + }, + type: { + type: Scratch.ArgumentType.STRING, + menu: "objectTypes", + defaultValue: "dynamic", + }, + collider: { + type: Scratch.ArgumentType.STRING, + menu: "colliderTypes", + defaultValue: "cuboid", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + mass: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + density: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + friction: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0.5", + }, + }, + }, "---", - {blockType: Scratch.BlockType.LABEL, text: "- RigidBody"}, - {opcode: "setRB", blockType: Scratch.BlockType.COMMAND, text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodySets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getRB", blockType: Scratch.BlockType.REPORTER, text: "get rigidbody [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodyProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + { + blockType: Scratch.BlockType.LABEL, + text: "- RigidBody", + }, + { + opcode: "setRB", + blockType: Scratch.BlockType.COMMAND, + text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "rigidBodySets", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "getRB", + blockType: Scratch.BlockType.REPORTER, + text: "get rigidbody [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "rigidBodyProperties", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "lockObjectAxis", blockType: Scratch.BlockType.COMMAND, text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", arguments: {OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lockAxes"}, X: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Y: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Z: {type: Scratch.ArgumentType.STRING, menu: "tf"}}}, + { + opcode: "lockObjectAxis", + blockType: Scratch.BlockType.COMMAND, + text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", + arguments: { + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "lockAxes", + }, + X: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + Y: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + Z: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + }, + }, "---", - {opcode: "addForce", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,10,0]"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "forces", defaultValue: "addForce"},SPACE: {type: Scratch.ArgumentType.STRING, menu: "spaces", defaultValue: "world"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "resetForces", blockType: Scratch.BlockType.COMMAND, text: "reset [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "resetF", defaultValue: "resetForces"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + { + opcode: "addForce", + blockType: Scratch.BlockType.COMMAND, + text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,10,0]", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "forces", + defaultValue: "addForce", + }, + SPACE: { + type: Scratch.ArgumentType.STRING, + menu: "spaces", + defaultValue: "world", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "resetForces", + blockType: Scratch.BlockType.COMMAND, + text: "reset [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "resetF", + defaultValue: "resetForces", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "enableCCD", blockType: Scratch.BlockType.COMMAND, text: "enable Continuous Collision Detection for [OBJECT] [state]", arguments: {state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "oPropS", defaultValue: "physics"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + { + opcode: "enableCCD", + blockType: Scratch.BlockType.COMMAND, + text: "enable Continuous Collision Detection for [OBJECT] [state]", + arguments: { + state: { + type: Scratch.ArgumentType.STRING, + menu: "state", + defaultValue: "true", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "oPropS", + defaultValue: "physics", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "fixedJoint", blockType: Scratch.BlockType.COMMAND, text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},RA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},RB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},}}, - {opcode: "sphericalJoint", blockType: Scratch.BlockType.COMMAND, text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},}}, - {opcode: "revoluteJoint", blockType: Scratch.BlockType.COMMAND, text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},X: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"},}}, + { + opcode: "fixedJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + RA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + RB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + { + opcode: "sphericalJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + }, + }, + { + opcode: "revoluteJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + X: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, "---", - {blockType: Scratch.BlockType.LABEL, text: "- Collider"}, - {opcode: "setC", blockType: Scratch.BlockType.COMMAND, text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderSets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getC", blockType: Scratch.BlockType.REPORTER, text: "get collider [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + { + blockType: Scratch.BlockType.LABEL, + text: "- Collider", + }, + { + opcode: "setC", + blockType: Scratch.BlockType.COMMAND, + text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "colliderSets", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "getC", + blockType: Scratch.BlockType.REPORTER, + text: "get collider [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "colliderProperties", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, "---", - {opcode: "sensorSingle", blockType: Scratch.BlockType.BOOLEAN, text: "is sensor [SENSOR] touching [OBJECT]?", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "sensorAll", blockType: Scratch.BlockType.REPORTER, text: "objects touching sensor [SENSOR]", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}}} + { + opcode: "sensorSingle", + blockType: Scratch.BlockType.BOOLEAN, + text: "is sensor [SENSOR] touching [OBJECT]?", + arguments: { + SENSOR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySensor", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "sensorAll", + blockType: Scratch.BlockType.REPORTER, + text: "objects touching sensor [SENSOR]", + arguments: { + SENSOR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySensor", + }, + }, + }, ], menus: { - wProp: {acceptReporters: false, items: [ - {text: "Gravity", value: "gravity"}, {text: "log to console", value: "log"} - ]}, - tf: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true", value: "true"}]}, - lockAxes: {acceptReporters: false, items: [ - {text: "Translation", value: "setEnabledTranslations"}, {text: "Rotation", value: "setEnabledRotations"} - ]}, - rigidBodyProperties: {acceptReporters: false, items: [ - {text: "Type", value: "bodyType"}, - {text: "Linear Velocity", value: "linvel"}, - {text: "Angular Velocity", value: "angvel"}, - {text: "Translation (position)", value: "translation"}, - {text: "Rotation (quaternion)", value: "rotation"}, - {text: "Mass", value: "mass"}, - //{text: "Center of Mass", value: "centerOfMass"}, - {text: "Linear Damping", value: "linearDamping"}, - {text: "Angular Damping", value: "angularDamping"}, - {text: "Is Sleeping?", value: "isSleeping"}, - //{text: "Can Sleep?", value: "isCanSleep"}, - {text: "Gravity Scale", value: "gravityScale"}, - {text: "Is Fixed?", value: "isFixed"}, - {text: "Is Dynamic?", value: "isDynamic"}, - {text: "Is Kinematic?", value: "isKinematic"}, - //{text: "Sleeping", value: "sleeping"} - ]}, - rigidBodySets: {acceptReporters: false, items: [ - //{text: "Linear Velocity", value: "setLinvel"}, - //{text: "Angular Velocity", value: "setAngvel"}, - //{text: "Mass", value: "setMass"}, - {text: "Gravity Scale", value: "setGravityScale"}, - //{text: "Can Sleep?", value: "setCanSleep"}, - //{text: "Sleeping", value: "sleeping"}, - {text: "Linear Damping", value: "setLinearDamping"}, - {text: "Angular Damping", value: "setAngularDamping"}, - {text: "Is Fixed?", value: "isFixed"}, - {text: "Is Dynamic?", value: "isDynamic"}, - {text: "Is Kinematic?", value: "isKinematic"} - ]}, - colliderProperties: {acceptReporters: false, items: [ - //{text: "Collider Type", value: "type"}, - {text: "Is Sensor?", value: "isSensor"}, - {text: "Friction", value: "friction"}, - {text: "Restitution", value: "restitution"}, - {text: "Density", value: "density"}, - {text: "Mass", value: "mass"}, - {text: "Position", value: "translation"}, - {text: "Rotation", value: "rotation"}, - //{text: "Area", value: "area"}, - {text: "Volume", value: "volume"}, - {text: "Collision Groups", value: "collisionGroups"}, - //{text: "Collision Mask", value: "collisionMask"}, - //{text: "Is Enabled?", value: "enabled"}, - //{text: "Contact Count", value: "contactCount"}, - //{text: "RigidBody Handle", value: "rigidBody"} - ]}, - colliderSets: {acceptReporters: false, items: [ - {text: "Friction", value: "setFriction"}, - {text: "Restitution", value: "setRestitution"}, - {text: "Density", value: "setDensity"}, - {text: "Is Sensor?", value: "setSensor"}, - {text: "Collision Groups", value: "setCollisionGroups"}, - //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool - //{text: "Position Offset", value: "setTranslation"}, - //{text: "Rotation Offset", value: "setRotation"} - ]}, - state: {acceptReporters: true, items: [{text: "on", value: "true"},{text: "off", value: "false"}]}, - state2: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true (must be fixed)", value: "true"}]}, - spaces: {acceptReporters: false, items: [{text: "World", value: "world"},{text: "Local", value: "local"}]}, - objectTypes: {acceptReporters: false, items: [{text: "Dynamic", value: "dynamic"},{text: "Fixed", value: "fixed"},{text: "Kinematic Position Based",value: "kinematicPositionBased"}]}, - colliderTypes: {acceptReporters: false, items: [{text: "Box, Rectangle, cuboid", value: "cuboid"},{text: "Sphere, ball", value: "ball"},{text: "Custom, complex simple shapes, convexHull", value: "convexHull"},{text:"Precision, TriMesh",value:"trimesh"}]}, - forces: {acceptReporters: false, items: [{text: "Force", value: "addForce"},{text: "Torque (rotation)", value: "addTorque"},{text: "Apply Impulse", value: "applyImpulse"},{text: "Apply Torque Impulse (rotation)", value: "applyTorqueImpulse"},{text: "Linear Velocity", value: "setLinvel"},{text: "Angular Velocity", value: "setAngvel"},]}, - resetF: {acceptReporters: false, items: [{text:"Forces", value: "resetForces"},{text:"Torques", value: "resetTorques"},]} - } - } + wProp: { + acceptReporters: false, + items: [{ + text: "Gravity", + value: "gravity", + }, + { + text: "log to console", + value: "log", + }, + ], + }, + tf: { + acceptReporters: true, + items: [{ + text: "false", + value: "false", + }, + { + text: "true", + value: "true", + }, + ], + }, + lockAxes: { + acceptReporters: false, + items: [{ + text: "Translation", + value: "setEnabledTranslations", + }, + { + text: "Rotation", + value: "setEnabledRotations", + }, + ], + }, + rigidBodyProperties: { + acceptReporters: false, + items: [{ + text: "Type", + value: "bodyType", + }, + { + text: "Linear Velocity", + value: "linvel", + }, + { + text: "Angular Velocity", + value: "angvel", + }, + { + text: "Translation (position)", + value: "translation", + }, + { + text: "Rotation (quaternion)", + value: "rotation", + }, + { + text: "Mass", + value: "mass", + }, + //{text: "Center of Mass", value: "centerOfMass"}, + { + text: "Linear Damping", + value: "linearDamping", + }, + { + text: "Angular Damping", + value: "angularDamping", + }, + { + text: "Is Sleeping?", + value: "isSleeping", + }, + //{text: "Can Sleep?", value: "isCanSleep"}, + { + text: "Gravity Scale", + value: "gravityScale", + }, + { + text: "Is Fixed?", + value: "isFixed", + }, + { + text: "Is Dynamic?", + value: "isDynamic", + }, + { + text: "Is Kinematic?", + value: "isKinematic", + }, + //{text: "Sleeping", value: "sleeping"} + ], + }, + rigidBodySets: { + acceptReporters: false, + items: [ + //{text: "Linear Velocity", value: "setLinvel"}, + //{text: "Angular Velocity", value: "setAngvel"}, + //{text: "Mass", value: "setMass"}, + { + text: "Gravity Scale", + value: "setGravityScale", + }, + //{text: "Can Sleep?", value: "setCanSleep"}, + //{text: "Sleeping", value: "sleeping"}, + { + text: "Linear Damping", + value: "setLinearDamping", + }, + { + text: "Angular Damping", + value: "setAngularDamping", + }, + { + text: "Is Fixed?", + value: "isFixed", + }, + { + text: "Is Dynamic?", + value: "isDynamic", + }, + { + text: "Is Kinematic?", + value: "isKinematic", + }, + ], + }, + colliderProperties: { + acceptReporters: false, + items: [ + //{text: "Collider Type", value: "type"}, + { + text: "Is Sensor?", + value: "isSensor", + }, + { + text: "Friction", + value: "friction", + }, + { + text: "Restitution", + value: "restitution", + }, + { + text: "Density", + value: "density", + }, + { + text: "Mass", + value: "mass", + }, + { + text: "Position", + value: "translation", + }, + { + text: "Rotation", + value: "rotation", + }, + //{text: "Area", value: "area"}, + { + text: "Volume", + value: "volume", + }, + { + text: "Collision Groups", + value: "collisionGroups", + }, + //{text: "Collision Mask", value: "collisionMask"}, + //{text: "Is Enabled?", value: "enabled"}, + //{text: "Contact Count", value: "contactCount"}, + //{text: "RigidBody Handle", value: "rigidBody"} + ], + }, + colliderSets: { + acceptReporters: false, + items: [{ + text: "Friction", + value: "setFriction", + }, + { + text: "Restitution", + value: "setRestitution", + }, + { + text: "Density", + value: "setDensity", + }, + { + text: "Is Sensor?", + value: "setSensor", + }, + { + text: "Collision Groups", + value: "setCollisionGroups", + }, + //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool + //{text: "Position Offset", value: "setTranslation"}, + //{text: "Rotation Offset", value: "setRotation"} + ], + }, + state: { + acceptReporters: true, + items: [{ + text: "on", + value: "true", + }, + { + text: "off", + value: "false", + }, + ], + }, + state2: { + acceptReporters: true, + items: [{ + text: "false", + value: "false", + }, + { + text: "true (must be fixed)", + value: "true", + }, + ], + }, + spaces: { + acceptReporters: false, + items: [{ + text: "World", + value: "world", + }, + { + text: "Local", + value: "local", + }, + ], + }, + objectTypes: { + acceptReporters: false, + items: [{ + text: "Dynamic", + value: "dynamic", + }, + { + text: "Fixed", + value: "fixed", + }, + { + text: "Kinematic Position Based", + value: "kinematicPositionBased", + }, + ], + }, + colliderTypes: { + acceptReporters: false, + items: [{ + text: "Box, Rectangle, cuboid", + value: "cuboid", + }, + { + text: "Sphere, ball", + value: "ball", + }, + { + text: "Custom, complex simple shapes, convexHull", + value: "convexHull", + }, + { + text: "Precision, TriMesh", + value: "trimesh", + }, + ], + }, + forces: { + acceptReporters: false, + items: [{ + text: "Force", + value: "addForce", + }, + { + text: "Torque (rotation)", + value: "addTorque", + }, + { + text: "Apply Impulse", + value: "applyImpulse", + }, + { + text: "Apply Torque Impulse (rotation)", + value: "applyTorqueImpulse", + }, + { + text: "Linear Velocity", + value: "setLinvel", + }, + { + text: "Angular Velocity", + value: "setAngvel", + }, + ], + }, + resetF: { + acceptReporters: false, + items: [{ + text: "Forces", + value: "resetForces", + }, + { + text: "Torques", + value: "resetTorques", + }, + ], + }, + }, + }; } joint(jointData, bodyA, bodyB) { - physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true) + physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); } fixedJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - let RA = JSON.parse(args.RA).map(Number) - let RB = JSON.parse(args.RB).map(Number) + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + let RA = JSON.parse(args.RA).map(Number); + let RB = JSON.parse(args.RB).map(Number); RA = new THREE.Quaternion().setFromEuler( new THREE.Euler( @@ -2076,339 +4597,419 @@ Promise.resolve(load()).then(() => { THREE.MathUtils.degToRad(RA[1]), THREE.MathUtils.degToRad(RA[2]) ) - ) + ); RB = new THREE.Quaternion().setFromEuler( new THREE.Euler( THREE.MathUtils.degToRad(RB[0]), THREE.MathUtils.degToRad(RB[1]), THREE.MathUtils.degToRad(RB[2]) ) - ) - - const data = RAPIER.JointData.fixed( - { x: VA[0], y: VA[1], z: VA[2] }, RA, - { x: VB[0], y: VB[1], z: VB[2] }, RB - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) + ); + + const data = RAPIER.JointData.fixed({ + x: VA[0], + y: VA[1], + z: VA[2], + }, + RA, { + x: VB[0], + y: VB[1], + z: VB[2], + }, + RB + ); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); } sphericalJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - - const data = RAPIER.JointData.spherical( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] } - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + + const data = RAPIER.JointData.spherical({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); } revoluteJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - const x = JSON.parse(args.X).map(Number) - - const data = RAPIER.JointData.revolute( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + const x = JSON.parse(args.X).map(Number); + + const data = RAPIER.JointData.revolute({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }, { + x: x[0], + y: x[1], + z: x[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); } prismaticJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - const x = JSON.parse(args.X).map(Number) - - const data = RAPIER.JointData.prismatic( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + const x = JSON.parse(args.X).map(Number); + + const data = RAPIER.JointData.prismatic({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }, { + x: x[0], + y: x[1], + z: x[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); } createWorld(args) { - const v3 = JSON.parse(args.G).map(Number) - const gravity = { x: v3[0], y: v3[1], z: v3[2]} - physicsWorld = new RAPIER.World(gravity) + const v3 = JSON.parse(args.G).map(Number); + const gravity = { + x: v3[0], + y: v3[1], + z: v3[2], + }; + physicsWorld = new RAPIER.World(gravity); - console.log(physicsWorld) + console.log(physicsWorld); } getWorld(args) { - if (args.PROPERTY === "log") {console.log(physicsWorld); return "logged"} - return JSON.stringify(physicsWorld[args.PROPERTY]) + if (args.PROPERTY === "log") { + console.log(physicsWorld); + return "logged"; + } + return JSON.stringify(physicsWorld[args.PROPERTY]); } setRB(args) { - let value = args.VALUE - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE - let object = getObject(args.OBJECT) - object.rigidBody[args.PROPERTY](value) + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.rigidBody[args.PROPERTY](value); } setC(args) { - let value = args.VALUE - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE - let object = getObject(args.OBJECT) - object.collider[args.PROPERTY](value) + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.collider[args.PROPERTY](value); } getRB(args) { - let object = getObject(args.OBJECT) - return JSON.stringify(object.rigidBody[args.PROPERTY]()) + let object = getObject(args.OBJECT); + return JSON.stringify(object.rigidBody[args.PROPERTY]()); } getC(args) { - let object = getObject(args.OBJECT) - return JSON.stringify(object.collider[args.PROPERTY]()) + let object = getObject(args.OBJECT); + return JSON.stringify(object.collider[args.PROPERTY]()); } lockObjectAxis(args) { - let object = getObject(args.OBJECT) - const x = !JSON.parse(args.X) - const y = !JSON.parse(args.Y) - const z = !JSON.parse(args.Z) - object.rigidBody[args.PROPERTY](x,y,z,true) //changes is xyz, wake up + let object = getObject(args.OBJECT); + const x = !JSON.parse(args.X); + const y = !JSON.parse(args.Y); + const z = !JSON.parse(args.Z); + object.rigidBody[args.PROPERTY](x, y, z, true); //changes is xyz, wake up } objectPhysics(args) { - let object = getObject(args.OBJECT) - object.physics = JSON.parse(args.state) + let object = getObject(args.OBJECT); + object.physics = JSON.parse(args.state); if (JSON.parse(args.state)) { //if already exists delete: if (object.rigidBody) { - physicsWorld.removeRigidBody(object.rigidBody) - object.rigidBody = null - object.collider = null + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; } /*asing a rigidbody and collider to object and add them to physicsWorld*/ - let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() + let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() .setTranslation(object.position.x, object.position.y, object.position.z) - .setRotation({w: object.quaternion._w, x: object.quaternion._x, y: object.quaternion._y, z: object.quaternion._z}) - - let colliderDesc - switch(args.collider) { - case "cuboid": colliderDesc = createCuboidCollider(object,); break - case "ball": colliderDesc = createBallCollider(object); break - case "convexHull": colliderDesc = createConvexHullCollider(object); break - case "trimesh": colliderDesc = TriMesh(object); break - } - colliderDesc.setSensor(JSON.parse(args.state2)).setMass(args.mass).setDensity(args.density).setFriction(args.friction) + .setRotation({ + w: object.quaternion._w, + x: object.quaternion._x, + y: object.quaternion._y, + z: object.quaternion._z, + }); + + let colliderDesc; + switch (args.collider) { + case "cuboid": + colliderDesc = createCuboidCollider(object); + break; + case "ball": + colliderDesc = createBallCollider(object); + break; + case "convexHull": + colliderDesc = createConvexHullCollider(object); + break; + case "trimesh": + colliderDesc = TriMesh(object); + break; + } + colliderDesc + .setSensor(JSON.parse(args.state2)) + .setMass(args.mass) + .setDensity(args.density) + .setFriction(args.friction); - let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc) - let collider = physicsWorld.createCollider(colliderDesc, rigidBody) + let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc); + let collider = physicsWorld.createCollider(colliderDesc, rigidBody); - object.rigidBody = rigidBody - object.collider = collider + object.rigidBody = rigidBody; + object.collider = collider; } else { /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ - physicsWorld.removeRigidBody(object.rigidBody) - object.rigidBody = null - object.collider = null + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; } - } enableCCD(args) { - let object = getObject(args.OBJECT) + let object = getObject(args.OBJECT); if (object.physics) { - let rigidBody = object.rigidBody - rigidBody.enableCcd(JSON.parse(args.state)) + let rigidBody = object.rigidBody; + rigidBody.enableCcd(JSON.parse(args.state)); } - } + } - addForce(args) { - let object = getObject(args.OBJECT) - const vector = JSON.parse(args.VALUE).map(Number) - - let force = new THREE.Vector3(vector[0],vector[1],vector[2]) + addForce(args) { + let object = getObject(args.OBJECT); + const vector = JSON.parse(args.VALUE).map(Number); + + let force = new THREE.Vector3(vector[0], vector[1], vector[2]); if (args.SPACE === "local") { force.applyQuaternion(object.quaternion); } - object.rigidBody[args.PROPERTY](force,true) - } - - resetForces(args) { - rigidBody[args.PROPERTY](true) - } + object.rigidBody[args.PROPERTY](force, true); + } - sensorSingle(args) { - const sensor = getObject(args.SENSOR) + resetForces(args) { + rigidBody[args.PROPERTY](true); + } - let object = getObject(args.OBJECT) + sensorSingle(args) { + const sensor = getObject(args.SENSOR); - let touching = false - physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { - if (otherCollider === object.collider) touching = true - }) + let object = getObject(args.OBJECT); - return touching - } + let touching = false; + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + if (otherCollider === object.collider) touching = true; + }); - sensorAll(args) { - const sensor = getObject(args.SENSOR) + return touching; + } - const touchedObjects = [] + sensorAll(args) { + const sensor = getObject(args.SENSOR); - // loop thruogh every collider touching sensor - physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { - // find owner of collider - const otherObject = scene.children.find(o => o.collider === otherCollider) - console.log(otherCollider) - if (otherObject) touchedObjects.push(otherObject.name) - }) + const touchedObjects = []; - return JSON.stringify(touchedObjects) - } + // loop thruogh every collider touching sensor + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + // find owner of collider + const otherObject = scene.children.find((o) => o.collider === otherCollider); + console.log(otherCollider); + if (otherObject) touchedObjects.push(otherObject.name); + }); + return JSON.stringify(touchedObjects); + } } - Scratch.extensions.register(new RapierPhysics()) - - //Thanks to the PointerLock extension of Turbowarp - const mouse = vm.runtime.ioDevices.mouse; - let isLocked = false; - let isPointerLockEnabled = false; - - let rect = threeRenderer.domElement.getBoundingClientRect(); - document.addEventListener("resize", () => { - rect = threeRenderer.domElement.getBoundingClientRect(); - }); + Scratch.extensions.register(new RapierPhysics()); - const postMouseData = (e, isDown) => { - const { movementX, movementY } = e; - const { width, height } = rect; - const x = mouse._clientX + movementX; - const y = mouse._clientY - movementY; - mouse._clientX = x; - mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); - mouse._clientY = y; - mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); - if (typeof isDown === "boolean") { - const data = { - button: e.button, - isDown, - }; - originalPostIOData(data); - } - }; + //Thanks to the PointerLock extension of Turbowarp + const mouse = vm.runtime.ioDevices.mouse; + let isLocked = false; + let isPointerLockEnabled = false; - const mouseDevice = vm.runtime.ioDevices.mouse; - const originalPostIOData = mouseDevice.postData.bind(mouseDevice); - mouseDevice.postData = (data) => { - if (!isPointerLockEnabled) { - return originalPostIOData(data); - } - }; + let rect = threeRenderer.domElement.getBoundingClientRect(); + document.addEventListener("resize", () => { + rect = threeRenderer.domElement.getBoundingClientRect(); + }); + + const postMouseData = (e, isDown) => { + const { + movementX, + movementY + } = e; + const { + width, + height + } = rect; + const x = mouse._clientX + movementX; + const y = mouse._clientY - movementY; + mouse._clientX = x; + mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); + mouse._clientY = y; + mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); + if (typeof isDown === "boolean") { + const data = { + button: e.button, + isDown, + }; + originalPostIOData(data); + } + }; + + const mouseDevice = vm.runtime.ioDevices.mouse; + const originalPostIOData = mouseDevice.postData.bind(mouseDevice); + mouseDevice.postData = (data) => { + if (!isPointerLockEnabled) { + return originalPostIOData(data); + } + }; - document.addEventListener( - "mousedown", - (e) => { - // @ts-expect-error - if (threeRenderer.domElement.contains(e.target)) { + document.addEventListener( + "mousedown", + (e) => { + // @ts-expect-error + if (threeRenderer.domElement.contains(e.target)) { + if (isLocked) { + postMouseData(e, true); + } else if (isPointerLockEnabled) { + threeRenderer.domElement.requestPointerLock(); + } + } + }, + true + ); + document.addEventListener( + "mouseup", + (e) => { if (isLocked) { - postMouseData(e, true); - } else if (isPointerLockEnabled) { + postMouseData(e, false); + // @ts-expect-error + } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { threeRenderer.domElement.requestPointerLock(); } + }, + true + ); + document.addEventListener( + "mousemove", + (e) => { + if (isLocked) { + postMouseData(e); + } + }, + true + ); + + document.addEventListener("pointerlockchange", () => { + isLocked = document.pointerLockElement === threeRenderer.domElement; + }); + document.addEventListener("pointerlockerror", (e) => { + console.error("Pointer lock error", e); + }); + + const oldStep = vm.runtime._step; + vm.runtime._step = function(...args) { + const ret = oldStep.call(this, ...args); + if (isPointerLockEnabled) { + const { + width, + height + } = rect; + mouse._clientX = width / 2; + mouse._clientY = height / 2; + mouse._scratchX = 0; + mouse._scratchY = 0; } - }, - true - ); - document.addEventListener( - "mouseup", - (e) => { - if (isLocked) { - postMouseData(e, false); - // @ts-expect-error - } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { - threeRenderer.domElement.requestPointerLock(); - } - }, - true - ); - document.addEventListener( - "mousemove", - (e) => { + return ret; + }; + + vm.runtime.on("PROJECT_LOADED", () => { + isPointerLockEnabled = false; if (isLocked) { - postMouseData(e); + document.exitPointerLock(); } - }, - true - ); - - document.addEventListener("pointerlockchange", () => { - isLocked = document.pointerLockElement === threeRenderer.domElement; - }); - document.addEventListener("pointerlockerror", (e) => { - console.error("Pointer lock error", e); - }); - - const oldStep = vm.runtime._step; - vm.runtime._step = function (...args) { - const ret = oldStep.call(this, ...args); - if (isPointerLockEnabled) { - const { width, height } = rect; - mouse._clientX = width / 2; - mouse._clientY = height / 2; - mouse._scratchX = 0; - mouse._scratchY = 0; - } - return ret; - }; + }); - vm.runtime.on("PROJECT_LOADED", () => { - isPointerLockEnabled = false; - if (isLocked) { - document.exitPointerLock(); - } - }); - - class Pointerlock { - getInfo() { - return { - id: "threepointerlockmod", - name: "Pointerlock for Extra 3D", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "setLocked", blockType: Scratch.BlockType.COMMAND, text: "set pointer lock [enabled]", arguments: { enabled: { type: Scratch.ArgumentType.STRING, defaultValue: "true", menu: "enabled"}},}, - {opcode: "isLocked", blockType: Scratch.BlockType.BOOLEAN, text: "pointer locked?",}, - ], - menus: { - enabled: {acceptReporters: true, items: [ - {text: "enabled", value: "true"},{text: "disabled", value: "false"}, - ]} - }, + class Pointerlock { + getInfo() { + return { + id: "threepointerlockmod", + name: "Pointerlock for Extra 3D", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "setLocked", + blockType: Scratch.BlockType.COMMAND, + text: "set pointer lock [enabled]", + arguments: { + enabled: { + type: Scratch.ArgumentType.STRING, + defaultValue: "true", + menu: "enabled", + }, + }, + }, + { + opcode: "isLocked", + blockType: Scratch.BlockType.BOOLEAN, + text: "pointer locked?", + }, + ], + menus: { + enabled: { + acceptReporters: true, + items: [{ + text: "enabled", + value: "true", + }, + { + text: "disabled", + value: "false", + }, + ], + }, + }, + }; } - } - setLocked(args) { - isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; - if (!isPointerLockEnabled && isLocked) { - document.exitPointerLock(); + setLocked(args) { + isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; + if (!isPointerLockEnabled && isLocked) { + document.exitPointerLock(); + } } - } - isLocked() { - return isLocked; + isLocked() { + return isLocked; + } } - } -Scratch.extensions.register(new Pointerlock()) - - }) - - - - + Scratch.extensions.register(new Pointerlock()); + }); })(Scratch); diff --git a/threejsD.js.orig b/threejsD.js.orig new file mode 100644 index 0000000..d1db442 --- /dev/null +++ b/threejsD.js.orig @@ -0,0 +1,5029 @@ +/* jshint esversion: 11 */ +// Name: Extra 3D +// ID: threejsExtension +// Description: Use three js inside Turbowarp! A 3D graphics library. +// By: Civero +// License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors + +(function(Scratch) { + "use strict"; + + if (!Scratch.extensions.unsandboxed) { + throw new Error("Three-D extension must run unsandboxed"); + } + + if (Scratch.vm.runtime.isPackaged) { + alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); + return; + } + //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return + + const vm = Scratch.vm; + const runtime = vm.runtime; + const renderer = Scratch.renderer; + const canvas = renderer.canvas; + const Cast = Scratch.Cast; + const menuIconURI = + "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; + + let alerts = false; + console.log("alerts are " + (alerts ? "enabled" : "disabled")); + + let isMouseDown = { + left: false, + middle: false, + right: false, + }; + let prevMouse = { + left: false, + middle: false, + right: false, + }; + + let lastWidth = 0; + let lastHeight = 0; + + let THREE; + let clock; + let running; + let loopId; + //Addons + let GLTFLoader; + let gltf; + let OrbitControls; + let controls; + let BufferGeometryUtils; + let TextGeometry; + let fontLoad; + //Physics + let RAPIER; + let physicsWorld; + + let threeRenderer; + let scene; + let camera; + let eulerOrder = "YXZ"; + + let composer; + let passes = {}; + let customEffects = []; + let renderTargets = {}; + + let materials = {}; + let geometries = {}; + let lights = {}; + let models = {}; + + let assets = { + //should i place materials, geometries; inside too? + textures: {}, + colors: {}, + fogs: {}, + curves: {}, + renderTargets: {}, //not the same as the global one! this one only stores textures + }; + + let raycastResult = []; + + function resetor(level) { + camera = undefined; + composer.reset(); + + passes = {}; + customEffects = []; + renderTargets = {}; + + materials = {}; + geometries = {}; + lights = {}; + models = {}; + + if (level > 0) { + assets = { + textures: {}, + colors: {}, + fogs: {}, + curves: {}, + renderTargets: {}, + }; + } + + updateComposers(); + } + + //utility + function vector3ToString(prop) { + if (!prop) return "0,0,0"; + + const x = + typeof prop.x === "number" ? + prop.x : + typeof prop._x === "number" ? + prop._x : + JSON.stringify(prop).includes("X") ? + prop : + 0; + const y = typeof prop.y === "number" ? prop.y : typeof prop.y === "number" ? prop._y : 0; + const z = typeof prop.z === "number" ? prop.z : typeof prop.z === "number" ? prop.z : 0; + + return [x, y, z]; + } + + //objects + function createObject(name, content, parentName) { + let object = getObject(name, true); + if (object) { + removeObject(name); + alerts ? alert(name + " already exsisted, will replace!") : null; + } + content.name = name; + content.rotation._order = eulerOrder; + parentName === scene.name ? (object = scene) : (object = getObject(parentName)); + content.physics = false; + + object.add(content); + } + + function removeObject(name) { + let object = getObject(name); + if (!object) return; + + scene.remove(object); + + if (object.rigidBody) { + physicsWorld.removeCollider(object.collider, true); + physicsWorld.removeRigidBody(object.rigidBody, true); + object.rigidBody = null; + object.collider = null; + } + if (object.isLight) { + delete lights[name]; + } + } + + function getObject(name, isNew) { + let object = null; + if (!scene) { + alerts ? alert("Can not get " + name + ". Create a scene first!") : null; + return; + } + object = scene.getObjectByName(name); + if (!object && !isNew) { + alerts ? alert(name + " does not exist! Add it to scene") : null; + return; + } + return object; + } + + //materials + function encodeCostume(name) { + if (name.startsWith("data:image/")) return name; + return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); + } + + function setTexutre(texture, mode, style, x, y) { + texture.colorSpace = THREE.SRGBColorSpace; + + if (mode === "Pixelate") { + texture.minFilter = THREE.NearestFilter; + texture.magFilter = THREE.NearestFilter; + } else { + //Blur + texture.minFilter = THREE.NearestMipmapLinearFilter; + texture.magFilter = THREE.NearestMipmapLinearFilter; + } + + if (style === "Repeat") { + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(x, y); + } + + texture.generateMipmaps = true; + } + async function resizeImageToSquare(uri, size = 256) { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + + // clear + draw image scaled to fit canvas + ctx.clearRect(0, 0, size, size); + ctx.drawImage(img, 0, 0, size, size); + + resolve(canvas.toDataURL()); // return normalized Data URI + //delete canvas? + }; + img.src = uri; + }); + } + //light + function updateShadowFrustum(light, focusPos) { + if (light.type !== "DirectionalLight") return; + + // Frustum Size - Increase this value to cover a larger area. + const d = 50; + + // Update Orthographic Shadow Camera Frustum + const shadowCamera = light.shadow.camera; + + // Set the width/height of the frustum + shadowCamera.left = -d; + shadowCamera.right = d; + shadowCamera.top = d; + shadowCamera.bottom = -d; + + // Determine ranges + shadowCamera.near = 0.1; + shadowCamera.far = 500; + + // Position the Light and its Target + light.target.position.copy(focusPos); + const direction = light.position.clone().sub(light.target.position).normalize(); + light.position.copy(focusPos.clone().add(direction.multiplyScalar(100))); + + // Ensure matrices are updated. + light.target.updateMatrixWorld(); + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; + } + //composer + function updateComposers() { + if (!camera || !scene) return; // nothing to do yet + + // always recreate the RenderPass to point to the current scene/camera + passes["Render"] = new RenderPass(scene, camera); + + // ensure composer has a RenderPass as the first pass + const hasRender = composer.passes.some((p) => p && p.scene); + if (!hasRender) composer.addPass(passes["Render"]); + else { + // if composer already has one, replace it so it references current scene/camera + const idx = composer.passes.findIndex((p) => p && p.scene); + composer.passes[idx] = passes["Render"]; + } + } + //utility + function getMouseNDC(event) { + // Use threeRenderer.domElement for correct offset + const rect = threeRenderer.domElement.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + return [x, y]; + } + + function checkCanvasSize() { + const { + width, + height + } = canvas; + if (width !== lastWidth || height !== lastHeight) { + lastWidth = width; + lastHeight = height; + resize(); + } + requestAnimationFrame(checkCanvasSize); //rerun next frame + } + //physics + function computeWorldBoundingBox(mesh) { + // Create a Box3 in world coordinates + const box = new THREE.Box3().setFromObject(mesh); + const size = new THREE.Vector3(); + box.getSize(size); + const center = new THREE.Vector3(); + box.getCenter(center); + return { + size, + center, + }; + } + + function createCuboidCollider(mesh) { + const { + size + } = computeWorldBoundingBox(mesh); + const collider = RAPIER.ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2); + return collider; + } + + function createBallCollider(mesh) { + const { + size + } = computeWorldBoundingBox(mesh); + // radius = 1/2 of the largest verticie + const radius = Math.max(size.x, size.y, size.z) / 2; + const collider = RAPIER.ColliderDesc.ball(radius); + return collider; //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) + } + + function createConvexHullCollider(mesh) { + mesh.updateWorldMatrix(true, false); + + const position = mesh.geometry.attributes.position; + const vertices = []; + const vertex = new THREE.Vector3(); + + // Matrix for scale only + const scaleMatrix = new THREE.Matrix4().makeScale(mesh.scale.x, mesh.scale.y, mesh.scale.z); + + for (let i = 0; i < position.count; i++) { + vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); + vertices.push(vertex.x, vertex.y, vertex.z); + } + + const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); + return collider; + } + + function TriMesh(mesh) { + // Get the positions array (from your geoPoints function) + const positions = mesh.geometry.attributes.position.array; + const numVertices = positions.length / 3; + + // Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] + const indices = Array.from({ + length: numVertices, + }, + (_, i) => i + ); + + const collider = RAPIER.ColliderDesc.trimesh(positions, new Uint32Array(indices)); + + return collider; + } + + function getModel(model, name) { + const file = runtime + .getTargetForStage() + .getSounds() + .find((c) => c.name === model); + if (!file) return; + + return new Promise((resolve, reject) => { + gltf.parse( + file.asset.data.buffer, + "", + (gltf) => { + const root = gltf.scene; + root.traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + const mixer = new THREE.AnimationMixer(root); + const actions = {}; + gltf.animations.forEach((clip) => { + const act = mixer.clipAction(clip); + act.clampWhenFinished = true; + actions[clip.name] = act; + }); + + models[name] = { + root, + mixer, + actions, + }; + resolve(root); + }, + (error) => { + console.error("Error parsing GLB model:", error); + reject(error); + } + ); + }); + } + async function openFileExplorer(format) { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = format; + input.multiple = false; + input.onchange = () => { + resolve(input.files); + input.remove(); + }; + input.click(); + }); + } + + function getMeshesUsingTexture(scene, targetTexture) { + const meshes = []; + + scene.traverse((object) => { + if (object.material) { + const materials = Array.isArray(object.material) ? object.material : [object.material]; + for (const material of materials) { + if (material.map === targetTexture) { + meshes.push(object); + break; + } + } + } + }); + + return meshes; + } + + function getAsset(path) { + if (typeof path == "string") { + //string? + if (path.includes("/")) { + //has the /? + const value = path.split("/"); + console.log(value[0], value[1]); + return assets[value[0]][value[1]]; + } + } + + return JSON.parse(path); //boolean or number + } + + let mouseNDC = [0, 0]; + //loops/init + function stopLoop() { + if (!running) return; + running = false; + + if (loopId) { + cancelAnimationFrame(loopId); + loopId = null; + if (threeRenderer) threeRenderer.clear(); + } + } + async function load() { + if (!THREE) { + // @ts-ignore +<<<<<<< HEAD + THREE = await import("https://esm.sh/three@0.180.0") + window._THREE_ = THREE +======= + THREE = await import("https://esm.sh/three@0.180.0"); +>>>>>>> e4a038b (Update threejsD.js) + //Addons + GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js"); + OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js"); + BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js"); + TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js"); + const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js"); + fontLoad = new FontLoader.FontLoader(); + + const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8"); + const { + EffectComposer, + EffectPass, + RenderPass, + + Effect, + BloomEffect, + GodRaysEffect, + DotScreenEffect, + DepthOfFieldEffect, + + BlendFunction, + } = POSTPROCESSING; + //so i can use them later as global + window.EffectComposer = EffectComposer; + window.EffectPass = EffectPass; + window.RenderPass = RenderPass; + window.Effect = Effect; + window.BloomEffect = BloomEffect; + window.GodRaysEffect = GodRaysEffect; + window.DotScreenEffect = DotScreenEffect; + window.DepthOfFieldEffect = DepthOfFieldEffect; + window.BlendFunction = BlendFunction; + + RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0"); + await RAPIER.init(); + + threeRenderer = new THREE.WebGLRenderer({ + powerPreference: "high-performance", + antialias: false, + stencil: false, + depth: true, + }); + threeRenderer.setPixelRatio(window.devicePixelRatio); + threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors + threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) + //threeRenderer.toneMappingExposure = 1.0 //(test) + + threeRenderer.shadowMap.enabled = true; + threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap; // (optional) + threeRenderer.domElement.style.pointerEvents = "auto"; //will disable turbowarp mouse events, but enable threejs's + + gltf = new GLTFLoader.GLTFLoader(); + clock = new THREE.Clock(); + + // Example: create a composer + composer = new EffectComposer(threeRenderer, { + frameBufferType: THREE.HalfFloatType, + }); + + renderer.addOverlay(threeRenderer.domElement, "manual"); + renderer.addOverlay(canvas, "manual"); + renderer.setBackgroundColor(1, 1, 1, 0); + + resize(); + + window.addEventListener("mousedown", (e) => { + if (e.button === 0) isMouseDown.left = true; + if (e.button === 1) isMouseDown.middle = true; + if (e.button === 2) isMouseDown.right = true; + }); + window.addEventListener("mouseup", (e) => { + if (e.button === 0) isMouseDown.left = false; + prevMouse.left = false; + if (e.button === 1) isMouseDown.middle = false; + prevMouse.middle = false; + if (e.button === 2) isMouseDown.right = false; + prevMouse.right = false; + }); + // prevent contextmenu on right click + threeRenderer.domElement.addEventListener("contextmenu", (e) => e.preventDefault()); + + threeRenderer.domElement.addEventListener("mousemove", (event) => { + mouseNDC = getMouseNDC(event); + }); + + running = false; + load(); + +<<<<<<< HEAD + startRenderLoop() + runtime.on('PROJECT_START', () => startRenderLoop()) + runtime.on('PROJECT_STOP_ALL', () => stopLoop()) + runtime.on('STAGE_SIZE_CHANGED', () => {requestAnimationFrame(() => resize())}) + checkCanvasSize() +======= + startRenderLoop(); + runtime.on("PROJECT_START", () => startRenderLoop()); + runtime.on("PROJECT_STOP_ALL", () => stopLoop()); + runtime.on("STAGE_SIZE_CHANGED", () => { + requestAnimationFrame(() => resize()); + }); + //if (!runtime.isPackaged) checkCanvasSize() //only in editor +>>>>>>> e4a038b (Update threejsD.js) + } + } + + function startRenderLoop() { + if (running) return; + running = true; + + const loop = () => { + if (!running) return; + //RAPIER + if (physicsWorld && scene) { + physicsWorld.step(); + + scene.children.forEach((obj) => { + if (!obj.isMesh || !obj.physics) return; + if (obj.rigidBody) { + obj.position.copy(obj.rigidBody.translation()); + obj.quaternion.copy(obj.rigidBody.rotation()); + } + }); + } + if (scene && camera) { + if (controls) controls.update(); + + const delta = clock.getDelta(); + Object.values(models).forEach((model) => { + if (model) model.mixer.update(delta); + }); + + Object.values(lights).forEach((light) => updateShadowFrustum(light, camera.position)); + + //update custom effects time + customEffects.forEach((e) => { + if (e.uniforms.get("time")) { + e.uniforms.get("time").value += delta; + } + }); + Object.values(renderTargets).forEach((t) => { + if (t.camera.type == "PerspectiveCamera") { + t.camera.aspect = t.target.width / t.target.height; + t.camera.updateProjectionMatrix(); + } + // get meshes using the texture associated with this target + const displayMeshes = getMeshesUsingTexture(scene, t.target.texture); + + displayMeshes.forEach((mesh) => { + mesh.visible = false; + }); + + if (t.camera.type == "PerspectiveCamera") { + threeRenderer.setRenderTarget(t.target); + threeRenderer.clear(true, true, true); + threeRenderer.render(scene, t.camera); + } else { + t.target.clear(threeRenderer); + t.camera.update(threeRenderer, scene); //cubeCamera + } + + displayMeshes.forEach((mesh) => { + mesh.visible = true; + }); + }); + + camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height; + camera.updateProjectionMatrix(); + threeRenderer.setRenderTarget(null); + composer.render(delta); + } + + loopId = requestAnimationFrame(loop); + }; + + loopId = requestAnimationFrame(loop); + } + + function resize() { + const w = canvas.width; + const h = canvas.height; + + threeRenderer.setSize(w, h); + composer.setSize(w, h); + customEffects.forEach((e) => { + if (e.uniforms.get("resolution")) { + e.uniforms.get("resolution").value.set(w, h); + } + }); + + if (camera) { + camera.aspect = w / h; + camera.updateProjectionMatrix(); + } + } + //wait until all packages are loaded + Promise.resolve(load()).then(() => { + class threejsExtension { + getInfo() { + return { + id: "threejsExtension", + name: "Extra 3D", + color1: "#222222", + color2: "#222222", + color3: "#11cc99", + menuIconURI, + blockIconURI: menuIconURI, + + blocks: [{ + blockType: Scratch.BlockType.BUTTON, + text: "Show Docs", + func: "openDocs", + }, + { + blockType: Scratch.BlockType.BUTTON, + text: "Toggle Alerts", + func: "alerts", + }, + ], + menus: {}, + }; + } + openDocs() { + open("https://civ3ro.github.io/extensions/Documentation/"); + } + alerts() { + alerts = !alerts; + alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!"); + } + } + Scratch.extensions.register(new threejsExtension()); + + class ThreeRenderer { + getInfo() { + return { + id: "threeRenderer", + name: "Three Renderer", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "setRendererRatio", + blockType: Scratch.BlockType.COMMAND, + text: "set Pixel Ratio to [VALUE]", + arguments: { + VALUE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + }, + }, + { + opcode: "eulerOrder", + blockType: Scratch.BlockType.COMMAND, + text: "set euler order to [VALUE]", + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "YXZ", + }, + }, + }, + ], + menus: {}, + }; + } + + setRendererRatio(args) { + threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE); + } + eulerOrder(args) { + eulerOrder = args.VALUE; + console.log("euler order set to", eulerOrder); + } + } + Scratch.extensions.register(new ThreeRenderer()); + + class ThreeScene { + constructor() { + this.THREE = THREE; + this.scenes = {}; + } + + getInfo() { + return { + id: "threeScene", + name: "Three Scene", + color1: "#4638c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "newScene", + blockType: Scratch.BlockType.COMMAND, + text: "new Scene [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + }, + }, + + { + opcode: "setSceneProperty", + blockType: Scratch.BlockType.COMMAND, + text: "set Scene [PROPERTY] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "sceneProperties", + defaultValue: "background", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "new Color()", + exemptFromNormalization: true, + }, + }, + }, + "---", + { + opcode: "getSceneObjects", + blockType: Scratch.BlockType.REPORTER, + text: "get Scene [THING]", + arguments: { + THING: { + type: Scratch.ArgumentType.STRING, + menu: "sceneThings", + }, + }, + }, + { + opcode: "reset", + blockType: Scratch.BlockType.COMMAND, + text: "Reset Everything", + }, + ], + menus: { + sceneProperties: { + acceptReporters: false, + items: [{ + text: "Background", + value: "background", + }, + { + text: "Background Blurriness", + value: "backgroundBlurriness", + }, + { + text: "Background Intensity", + value: "backgroundIntensity", + }, + { + text: "Background Rotation", + value: "backgroundRotation", + }, + { + text: "Environment", + value: "environment", + }, + { + text: "Environment Intensity", + value: "environmentIntensity", + }, + { + text: "Environment Rotation", + value: "environmentRotation", + }, + { + text: "Fog", + value: "fog", + }, + ], + }, + sceneThings: { + acceptReporters: false, + items: ["Objects", "Materials", "Geometries", "Lights", "Scene Properties", "Other assets"], + }, + }, + }; + } + + newScene(args) { + scene = new THREE.Scene(); + scene.name = args.NAME; + scene.background = new THREE.Color("#222"); + //scene.add(new THREE.GridHelper(16, 16)) //future helper section? + this.scenes = { + ...this.scenes, + ...scene, + }; + resetor(0); + } + + reset() { + resetor(1); + } + + async setSceneProperty(args) { + const property = args.PROPERTY; + const value = getAsset(args.VALUE); + + scene[property] = value; + } + getSceneObjects(args) { + const names = []; + if (args.THING === "Objects") { + scene.traverse((obj) => { + if (obj.name) names.push(obj.name); //if it has a name, add to list! + }); + } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); + else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); + else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); + else if (args.THING === "Scene Properties") { + console.log(scene); + return "check console"; + } else if (args.THING === "Other assets") return JSON.stringify(assets); + + return JSON.stringify(names); // if objects + } + } + Scratch.extensions.register(new ThreeScene()); + + class ThreeCameras { + getInfo() { + return { + id: "threeCameras", + name: "Three Cameras", + color1: "#38c59bff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addCamera", + blockType: Scratch.BlockType.COMMAND, + text: "add camera [TYPE] [CAMERA] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "cameraTypes", + }, + }, + }, + { + opcode: "setCamera", + blockType: Scratch.BlockType.COMMAND, + text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "cameraProperties", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "0.1", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "getCamera", + blockType: Scratch.BlockType.REPORTER, + text: "get camera [PROPERTY] of [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "cameraProperties", + }, + }, + }, + "---", + { + opcode: "renderSceneCamera", + blockType: Scratch.BlockType.COMMAND, + text: "set rendering camera to [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + }, + }, + "---", + { + opcode: "cubeCamera", + blockType: Scratch.BlockType.COMMAND, + text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "cubeCamera", + }, + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + "---", + { + opcode: "renderTarget", + blockType: Scratch.BlockType.COMMAND, + text: "set a RenderTarget: [RT] for camera [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + { + opcode: "sizeTarget", + blockType: Scratch.BlockType.COMMAND, + text: "set RenderTarget [RT] size to [W] [H]", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + W: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 480, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 360, + }, + }, + }, + { + opcode: "getTarget", + blockType: Scratch.BlockType.REPORTER, + text: "get RenderTarget: [RT] texture", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + { + opcode: "removeTarget", + blockType: Scratch.BlockType.COMMAND, + text: "remove RenderTarget: [RT]", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + ], + menus: { + cameraTypes: { + acceptReporters: false, + items: [{ + text: "Perspective", + value: "PerspectiveCamera", + }, ], + }, + cameraProperties: { + acceptReporters: false, + items: [{ + text: "Near", + value: "near", + }, + { + text: "Far", + value: "far", + }, + { + text: "FOV", + value: "fov", + }, + { + text: "Focus (nothing...)", + value: "focus", + }, + { + text: "Zoom", + value: "zoom", + }, + ], + }, + }, + }; + } + addCamera(args) { + let v2 = new THREE.Vector2(); + threeRenderer.getSize(v2); + const object = new THREE.PerspectiveCamera(90, v2.x / v2.y); + object.position.z = 3; + + createObject(args.CAMERA, object, args.GROUP); + } + setCamera(args) { + let object = getObject(args.CAMERA); + object[args.PROPERTY] = args.VALUE; + object.updateProjectionMatrix(); + } + getCamera(args) { + let object = getObject(args.CAMERA); + const value = JSON.stringify(object[args.PROPERTY]); + return value; + } + renderSceneCamera(args) { + let object = getObject(args.CAMERA); + if (!object) return; + camera = object; + //reset composer, else it does not update. + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } + + cubeCamera(args) { + // Create cube render target + const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, { + generateMipmaps: true, + }); + // Create cube camera + const cubeCamera = new THREE.CubeCamera(0.1, 500, cubeRenderTarget); + createObject(args.CAMERA, cubeCamera, args.GROUP); + + renderTargets[args.RT] = { + target: cubeRenderTarget, + camera: cubeCamera, + }; + assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture; + } + + renderTarget(args) { + let object = getObject(args.CAMERA); + const renderTarget = new THREE.WebGLRenderTarget(360, 360, { + generateMipmaps: false, + }); + + renderTargets[args.RT] = { + target: renderTarget, + camera: object, + }; + assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; + } + sizeTarget(args) { + renderTargets[args.RT].target.setSize(args.W, args.H); + } + getTarget(args) { + const t = renderTargets[args.RT].target.texture; + console.log(t, renderTargets[args.RT]); + return `renderTargets/${t.uuid}`; + } + removeTarget(args) { + delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; + renderTargets[args.RT].target.dispose(); + delete renderTargets[args.RT]; + } + } + Scratch.extensions.register(new ThreeCameras()); + + class ThreeObjects { + getInfo() { + return { + id: "threeObjects", + name: "Three Objects", + color1: "#38c567ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addObject", + blockType: Scratch.BlockType.COMMAND, + text: "add object [OBJECT3D] [TYPE] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "objectTypes", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "cloneObject", + blockType: Scratch.BlockType.COMMAND, + text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myClone", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "setObject", + blockType: Scratch.BlockType.COMMAND, + text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectProperties", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + { + opcode: "getObject", + blockType: Scratch.BlockType.REPORTER, + text: "get [PROPERTY] of object [OBJECT3D]", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectProperties", + }, + }, + }, + { + opcode: "objectE", + blockType: Scratch.BlockType.BOOLEAN, + text: "is there an object [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "removeObject", + blockType: Scratch.BlockType.COMMAND, + text: "remove object [OBJECT3D] from scene", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: " ↳ Transforms", + }, + { + opcode: "setObjectV3", + extensions: ["colours_motion"], + blockType: Scratch.BlockType.COMMAND, + text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectVector3", + defaultValue: "position", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, + //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, + { + opcode: "getObjectV3", + extensions: ["colours_motion"], + blockType: Scratch.BlockType.REPORTER, + text: "get [PROPERTY] of [OBJECT3D]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectVector3", + defaultValue: "position", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Materials", + }, + { + opcode: "newMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "new material [NAME] [TYPE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "materialTypes", + defaultValue: "MeshStandardMaterial", + }, + }, + }, + { + opcode: "materialE", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.BOOLEAN, + text: "is there a material [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + }, + }, + { + opcode: "removeMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "remove material [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + }, + }, + { + opcode: "setMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [PROPERTY] of [NAME] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "materialProperties", + defaultValue: "color", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "new Color()", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "setBlending", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [NAME] blending to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + }, + }, + }, + { + opcode: "setDepth", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [NAME] depth to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "depthModes", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Geometries", + }, + { + opcode: "newGeometry", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "new geometry [NAME] [TYPE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "geometryTypes", + defaultValue: "BoxGeometry", + }, + }, + }, + { + opcode: "geometryE", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.BOOLEAN, + text: "is there a geometry [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + { + opcode: "removeGeometry", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "remove geometry [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + "---", + { + opcode: "newGeo", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "new empty geometry [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[points]", + }, + }, + }, + { + opcode: "geoPoints", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "set geometry [NAME] vertex points to [POINTS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[points]", + }, + }, + }, + { + opcode: "geoUVs", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "set geometry [NAME] UVs to [POINTS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[UVs]", + }, + }, + }, + "---", + { + opcode: "splines", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "create spline [NAME] from curve [CURVE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySpline", + }, + CURVE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[curve]", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "splineModel", + extensions: ["colours_operators"], + blockType: Scratch.BlockType.COMMAND, + text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySpline", + }, + MODEL: { + type: Scratch.ArgumentType.STRING, + menu: "modelsList", + }, + CURVE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[curve]", + exemptFromNormalization: true, + }, + SPACING: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.BUTTON, + text: "Convert font to JSON", + func: "openConv", + }, + { + blockType: Scratch.BlockType.BUTTON, + text: "Load JSON font file", + func: "loadFont", + }, + { + opcode: "text", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myText", + }, + TEXT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "C-369", + }, + FONT: { + type: Scratch.ArgumentType.STRING, + menu: "fonts", + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + D: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.1, + }, + CS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 6, + }, + }, + }, + ], + menus: { + objectVector3: { + acceptReporters: false, + items: [{ + text: "Positon", + value: "position", + }, + { + text: "Rotation", + value: "rotation", + }, + { + text: "Scale", + value: "scale", + }, + { + text: "Facing Direction (.up)", + value: "up", + }, + ], + }, + objectProperties: { + acceptReporters: false, + items: [{ + text: "Geometry", + value: "geometry", + }, + { + text: "Material", + value: "material", + }, + { + text: "Visible (true/false)", + value: "visible", + }, + ], + }, + objectTypes: { + acceptReporters: false, + items: [{ + text: "Mesh", + value: "Mesh", + }, + { + text: "Sprite", + value: "Sprite", + }, + { + text: "Points", + value: "Points", + }, + { + text: "Line", + value: "Line", + }, + { + text: "Group", + value: "Group", + }, + ], + }, + XYZ: { + acceptReporters: false, + items: [{ + text: "X", + value: "x", + }, + { + text: "Y", + value: "y", + }, + { + text: "Z", + value: "z", + }, + ], + }, + materialProperties: { + acceptReporters: false, + items: [ + "|GENERAL| <-- not a property", + { + text: "Color", + value: "color", + }, + { + text: "Map", + value: "map", + }, + { + text: "Opacity", + value: "opacity", + }, + { + text: "Transparent", + value: "transparent", + }, + { + text: "Alpha Map", + value: "alphaMap", + }, + { + text: "Alpha Test", + value: "alphaTest", + }, + { + text: "Depth Test", + value: "depthTest", + }, + { + text: "Depth Write", + value: "depthWrite", + }, + { + text: "Color Write", + value: "colorWrite", + }, + { + text: "Side", + value: "side", + }, + { + text: "Visible", + value: "visible", + }, + /* + { text: "Blending", value: "blending" }, + { text: "Blend Src", value: "blendSrc" }, + { text: "Blend Dst", value: "blendDst" }, + { text: "Blend Equation", value: "blendEquation" }, + { text: "Blend Src Alpha", value: "blendSrcAlpha" }, + { text: "Blend Dst Alpha", value: "blendDstAlpha" }, + { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ + { + text: "Blend Aplha", + value: "blendAplha", + }, + { + text: "Blend Color", + value: "blendColor", + }, + { + text: "Alpha Hash", + value: "alphaHash", + }, + { + text: "Premultiplied Alpha", + value: "premultipliedAlpha", + }, + + { + text: "Tone Mapped", + value: "toneMapped", + }, + { + text: "Fog", + value: "fog", + }, + { + text: "Flat Shading", + value: "flatShading", + }, + + "|MESH Standard / Physical| <-- not a property", + { + text: "Metalness", + value: "metalness", + }, + { + text: "Metalness Map", + value: "metalnessMap", + }, + { + text: "Roughness", + value: "roughness", + }, + { + text: "Reflectivity", + value: "reflectivity", + }, + { + text: "Roughness Map", + value: "roughnessMap", + }, + { + text: "Emissive", + value: "emissive", + }, + { + text: "Emissive Intensity", + value: "emissiveIntensity", + }, + { + text: "Emissive Map", + value: "emissiveMap", + }, + { + text: "Env Map", + value: "envMap", + }, + { + text: "Env Map Intensity", + value: "envMapIntensity", + }, + { + text: "Env Map Rotation", + value: "envMapRotation", + }, + { + text: "Ior", + value: "ior", + }, + { + text: "Refraction Ratio", + value: "refractionRatio", + }, + { + text: "Clearcoat", + value: "clearcoat", + }, + { + text: "Clearcoat Map", + value: "clearcoatMap", + }, + { + text: "Clearcoat Roughness", + value: "clearcoatRoughness", + }, + { + text: "Clearcoat Roughness Map", + value: "clearcoatRoughnessMap", + }, + { + text: "Dispersion", + value: "dispersion", + }, + { + text: "Sheen", + value: "sheen", + }, + { + text: "Sheen Color", + value: "sheenColor", + }, + { + text: "Sheen Color Map", + value: "sheenColorMap", + }, + { + text: "Sheen Roughness", + value: "sheenRoughness", + }, + { + text: "Sheen Roughness Map", + value: "sheenRoughnessMap", + }, + { + text: "Specular Color", + value: "specularColor", + }, + { + text: "Specular Color Map", + value: "specularColorMap", + }, + { + text: "Specular Intensity", + value: "specularIntensity", + }, + { + text: "Specular Intensity Map", + value: "specularIntensityMap", + }, + { + text: "Transmission", + value: "transmission", + }, + { + text: "Transmission Map", + value: "transmissionMap", + }, + { + text: "Thickness", + value: "thickness", + }, + { + text: "Thickness Map", + value: "thicknessMap", + }, + { + text: "Anisotropy", + value: "anisotropy", + }, + { + text: "Anisotropy Map", + value: "anisotropyMap", + }, + { + text: "Anisotropy Rotation", + value: "anisotropyRotation", + }, + { + text: "Attenuation Distance", + value: "attenuationDistance", + }, + { + text: "Attenuation Color", + value: "attenuationColor", + }, + { + text: "Thickness", + value: "thickness", + }, + { + text: "Iridescence", + value: "iridescence", + }, + { + text: "Iridescence Ior", + value: "iridescenceIOR", + }, + { + text: "Iridescence Map", + value: "iridescenceMap", + }, + { + text: "Iridescence Thickness Range", + value: "iridescenceThicknessRange", + }, + + "|MESH Displacement / Normal / Bump| <-- not a property", + { + text: "Displacement Map", + value: "displacementMap", + }, + { + text: "Displacement Scale", + value: "displacementScale", + }, + { + text: "Displacement Bias", + value: "displacementBias", + }, + { + text: "Bump Map", + value: "bumpMap", + }, + { + text: "Bump Scale", + value: "bumpScale", + }, + { + text: "Normal Map Type", + value: "normalMapType", + }, + + "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", + { + text: "Shininess", + value: "shininess", + }, + + { + text: "Wireframe", + value: "wireframe", + }, + { + text: "Wireframe Linewidth", + value: "wireframeLinewidth", + }, + { + text: "Wireframe Linecap", + value: "wireframeLinecap", + }, + { + text: "Wireframe Linejoin", + value: "wireframeLinejoin", + }, + + "|POINTS| <-- not a property", + { + text: "Size", + value: "size", + }, + { + text: "Size Attenuation", + value: "sizeAttenuation", + }, + + "|LINES| <-- not a property", + { + text: "Scale", + value: "scale", + }, + { + text: "Dash Size", + value: "dashSize", + }, + { + text: "Gap Size", + value: "gapSize", + }, + + "|SPRITES| <-- not a property", + { + text: "Rotation", + value: "rotation", + }, + ], + }, + blendModes: { + acceptReporters: false, + items: [{ + text: "No Blending", + value: "NoBlending", + }, + { + text: "Normal Blending", + value: "NormalBlending", + }, + { + text: "Additive Blending", + value: "AdditiveBlending", + }, + { + text: "Subtractive Blending", + value: "SubtractiveBlending", + }, + { + text: "Multiply Blending", + value: "MultiplyBlending", + }, + { + text: "Custom Blending", + value: "CustomBlending", + }, + ], + }, + depthModes: { + acceptReporters: false, + items: [{ + text: "Never Depth", + value: "NeverDepth", + }, + { + text: "Always Depth", + value: "AlwaysDepth", + }, + { + text: "Equal Depth", + value: "EqualDepth", + }, + { + text: "Less Depth", + value: "LessDepth", + }, + { + text: "Less Equal Depth", + value: "LessEqualDepth", + }, + { + text: "Greater Equal Depth", + value: "GreaterEqualDepth", + }, + { + text: "Greater Depth", + value: "GreaterDepth", + }, + { + text: "Not Equal Depth", + value: "NotEqualDepth", + }, + ], + }, + materialTypes: { + acceptReporters: false, + items: [{ + text: "Mesh Basic Material", + value: "MeshBasicMaterial", + }, + { + text: "Mesh Standard Material", + value: "MeshStandardMaterial", + }, + { + text: "Mesh Physical Material", + value: "MeshPhysicalMaterial", + }, + { + text: "Mesh Lambert Material", + value: "MeshLambertMaterial", + }, + { + text: "Mesh Phong Material", + value: "MeshPhongMaterial", + }, + { + text: "Mesh Depth Material", + value: "MeshDepthMaterial", + }, + { + text: "Mesh Normal Material", + value: "MeshNormalMaterial", + }, + { + text: "Mesh Matcap Material", + value: "MeshMatcapMaterial", + }, + { + text: "Mesh Toon Material", + value: "MeshToonMaterial", + }, + { + text: "Line Basic Material", + value: "LineBasicMaterial", + }, + { + text: "Line Dashed Material", + value: "LineDashedMaterial", + }, + { + text: "Points Material", + value: "PointsMaterial", + }, + { + text: "Sprite Material", + value: "SpriteMaterial", + }, + { + text: "Shadow Material", + value: "ShadowMaterial", + }, + ], + }, + textureModes: { + acceptReporters: false, + items: ["Pixelate", "Blur"], + }, + textureStyles: { + acceptReporters: false, + items: ["Repeat", "Clamp"], + }, + geometryTypes: { + acceptReporters: false, + items: [{ + text: "Box Geometry", + value: "BoxGeometry", + }, + { + text: "Sphere Geometry", + value: "SphereGeometry", + }, + { + text: "Cylinder Geometry", + value: "CylinderGeometry", + }, + { + text: "Plane Geometry", + value: "PlaneGeometry", + }, + { + text: "Circle Geometry", + value: "CircleGeometry", + }, + { + text: "Torus Geometry", + value: "TorusGeometry", + }, + { + text: "Torus Knot Geometry", + value: "TorusKnotGeometry", + }, + ], + }, + modelsList: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".glb")); + if (models.length < 1) return [ + ["Load a model! (GLB Loader category)"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + fonts: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".json")); + if (models.length < 1) return [ + ["Load a font!"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + }, + }; + } + + addObject(args) { + const object = new THREE[args.TYPE](); + + object.castShadow = true; + object.receiveShadow = true; + + createObject(args.OBJECT3D, object, args.GROUP); + } + cloneObject(args) { + let object = getObject(args.OBJECT3D); + const clone = object.clone(true); + clone.name; + createObject(args.NAME, clone, args.GROUP); + } + setObjectV3(args) { + let object = getObject(args.OBJECT3D); + let values = JSON.parse(args.VALUE); + + function degToRad(deg) { + return (deg * Math.PI) / 180; + } + + if (object.rigidBody) { + const x = values[0]; + const y = values[1]; + const z = values[2]; + if (args.PROPERTY === "rotation") { + const euler = new THREE.Euler(degToRad(x), degToRad(y), degToRad(z), "YXZ"); + const quaternion = new THREE.Quaternion(); + quaternion.setFromEuler(euler); + + object.rigidBody.setRotation({ + x: quaternion.x, + y: quaternion.y, + z: quaternion.z, + w: quaternion.w, + }); + } else if (args.PROPERTY === "position") { + object.rigidBody.setTranslation({ + x: x, + y: y, + z: z, + }, + true + ); + } + return; + } + + if (object.isCamera == true && controls) {} + + if (args.PROPERTY === "rotation") { + values = values.map((v) => (v * Math.PI) / 180); + object.rotation.set(0, 0, 0); + } + if (object.isDirectionalLight == true) { + object.pos = new THREE.Vector3(...values); + console.log(true, values, object.pos); + return; + } + object[args.PROPERTY].set(...values); + + if (object.type == "CubeCamera") object.updateCoordinateSystem(); + } + /* + changeObjectV3(args) { + getObject(args.OBJECT3D) + let values = JSON.parse(args.VALUE) + + if (args.PROPERTY === "rotation") { + values = values.map(v => v * Math.PI / 180); + object.rotation.x += values[0] + object.rotation.y += values[1] + object.rotation.z += values[2] + } + else { + object[args.PROPERTY].add(...values); + } + } + changeObjectXV3(args) { + getObject(args.OBJECT3D) + let value = args.VALUE + if (args.PROPERTY === "rotation") value = value * Math.PI / 180 + + object[args.PROPERTY][args.X] += value + } + */ + getObjectV3(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let values = vector3ToString(object[args.PROPERTY]); + if (args.PROPERTY === "rotation") { + const toDeg = Math.PI / 180; + values = [values[0] / toDeg, values[1] / toDeg, values[2] / toDeg]; + } + + return JSON.stringify(values); + } + setObject(args) { + let object = getObject(args.OBJECT3D); + let value = args.VALUE; + if (args.PROPERTY === "material") { + const mat = materials[args.NAME]; + if (mat) value = mat; + else value = undefined; + } else if (args.PROPERTY === "geometry") { + const geo = geometries[args.NAME]; + if (geo) value = geo; + else value = undefined; + } else value = !!value; + + if (value == undefined) return; //invalid geo/mat + object[args.PROPERTY] = value; + } + getObject(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let value; + if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; + else value = object.visible; + + return value; + } + removeObject(args) { + removeObject(args.OBJECT3D); + } + objectE(args) { + return scene.children.map((o) => o.name).includes(args.NAME); + } + + //defines + newMaterial(args) { + if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); + const mat = new THREE[args.TYPE](); + mat.name = args.NAME; + + materials[args.NAME] = mat; + } + async setMaterial(args) { + if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; + const mat = materials[args.NAME]; + + let value = args.VALUE; + + if (args.VALUE == "false") value = false; + + if (args.PROPERTY == "side") { + value = args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide; + } else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)); + else value = getAsset(value); + + console.log("o:", args.VALUE, typeof args.VALUE); + console.log("r:", value, typeof value); + + mat[args.PROPERTY] = await value; //await incase its a texture + mat.needsUpdate = true; + } + setBlending(args) { + const mat = materials[args.NAME]; + mat.blending = THREE[args.VALUE]; + mat.premultipliedAlpha = true; + mat.needsUpdate = true; + } + setDepth(args) { + const mat = materials[args.NAME]; + mat.depthFunc = THREE[args.VALUE]; + mat.needsUpdate = true; + } + removeMaterial(args) { + const mat = materials[args.NAME]; + mat.dispose(); + delete materials[args.NAME]; + } + materialE(args) { + return materials[args.NAME] ? true : false; + } + + newGeometry(args) { + if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); + const geo = new THREE[args.TYPE](); + geo.name = args.NAME; + + geometries[args.NAME] = geo; + } + setGeometry(args) { + const geo = geometries[args.NAME]; + geo[args.PROPERTY] = args.VALUE; + + geo.needsUpdate = true; + } + removeGeometry(args) { + const geo = geometries[args.NAME]; + geo.dispose(); + delete geometries[args.NAME]; + } + geometryE(args) { + return geometries[args.NAME] ? true : false; + } + + newGeo(args) { + const geometry = new THREE.BufferGeometry(); + geometry.name = args.NAME; + geometries[args.NAME] = geometry; + } + async geoPoints(args) { + const geometry = geometries[args.NAME]; + const positions = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v3 of each vertex of each triangle + + geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3)); + geometry.computeVertexNormals(); + + geometry.needsUpdate = true; + } + geoUVs(args) { + const geometry = geometries[args.NAME]; + const UVs = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v2 of each UV of each triangle + + geometry.setAttribute("uv", new THREE.BufferAttribute(new Float32Array(UVs), 2)); + geometry.needsUpdate = true; + } + + splines(args) { + const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); + geometry.name = args.NAME; + + geometries[args.NAME] = geometry; + } + + async splineModel(args) { + const model = await getModel(args.MODEL, args.NAME); + if (!model) return console.warn("Model not found:", args.MODEL); + + const curve = getAsset(args.CURVE); + const spacing = parseFloat(args.SPACING) || 1; + const curveLength = curve.getLength(); + const divisions = Math.floor(curveLength / spacing); + + const geomList = []; + const matList = []; + + for (let i = 0; i <= divisions; i++) { + const t = i / divisions; + const pos = curve.getPointAt(t); + const tangent = curve.getTangentAt(t); + + const temp = model.clone(true); + temp.position.copy(pos); + + const up = new THREE.Vector3(0, 0, 1); + const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()); + temp.quaternion.copy(quat); + + temp.updateMatrixWorld(true); + + temp.traverse((child) => { + if (child.isMesh && child.geometry) { + const geom = child.geometry.clone(); + geom.applyMatrix4(child.matrixWorld); + geomList.push(geom); + matList.push(child.material); //.clone() ? + } + }); + } + + const validGeoms = geomList.filter((g) => { + const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position; + if (!ok) console.warn("geometry skipped:", g); + return ok; + }); + + const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true); + merged.computeBoundingBox(); + merged.computeBoundingSphere(); + + merged.name = args.NAME; + geometries[args.NAME] = merged; + matList.name = args.NAME; + materials[args.NAME] = matList; + } + + async text(args) { + const fontFile = runtime + .getTargetForStage() + .getSounds() + .find((c) => c.name === args.FONT); + if (!fontFile) return; + + const json = new TextDecoder().decode(fontFile.asset.data.buffer); + const fontData = JSON.parse(json); + + const font = fontLoad.parse(fontData); + + const params = { + font: font, + size: JSON.parse(args.S), + height: JSON.parse(args.D), + curveSegments: JSON.parse(args.CS), + bevelEnabled: false, + }; + const geometry = new TextGeometry.TextGeometry(args.TEXT, params); + geometry.computeVertexNormals(); + geometry.center(); // optional, recenters the text + + geometry.name = args.NAME; + + geometries[args.NAME] = geometry; + } + + async loadFont() { + openFileExplorer(".json").then((files) => { + const file = files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + const arrayBuffer = e.target.result; + + // From lily's assets + // // Thank you PenguinMod for providing this code. + + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); + + const buffer = arrayBuffer; + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + alert("Font loaded successfully!"); + } catch (e) { + console.error(e); + alert("Error loading font."); + } + + // End of PenguinMod + }; + + reader.readAsArrayBuffer(file); + }); + } + openConv() { + { + open("https://gero3.github.io/facetype.js/"); + } + } + } + Scratch.extensions.register(new ThreeObjects()); + + class ThreeLights { + getInfo() { + return { + id: "threeLights", + name: "Three Lights", + color1: "#c7a22aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addLight", + blockType: Scratch.BlockType.COMMAND, + text: "add light [NAME] type [TYPE] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myLight", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "lightTypes", + }, + }, + }, + { + opcode: "setLight", + blockType: Scratch.BlockType.COMMAND, + text: "set light [NAME][PROPERTY] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "lightProperties", + defaultValue: "intensity", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myLight", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + exemptFromNormalization: true, + }, + }, + }, + ], + menus: { + lightTypes: { + acceptReporters: false, + items: [{ + text: "Ambient Light", + value: "AmbientLight", + }, + { + text: "Directional Light", + value: "DirectionalLight", + }, + { + text: "Point Light", + value: "PointLight", + }, + { + text: "Hemisphere Light", + value: "HemisphereLight", + }, + { + text: "Spot Light", + value: "SpotLight", + }, + ], + }, + lightProperties: { + acceptReporters: false, + items: [{ + text: "Color", + value: "color", + }, + { + text: "Intensity", + value: "intensity", + }, + { + text: "Cast Shadow?", + value: "castShadow", + }, + { + text: "Ground Color (HemisphereLight)", + value: "groundColor", + }, + { + text: "Map (SpotLight)", + value: "map", + }, + { + text: "Distance (SpotLight)", + value: "distance", + }, + { + text: "Decay (SpotLight)", + value: "decay", + }, + { + text: "Penumbra (SpotLight)", + value: "penumbra", + }, + { + text: "Angle/Size (SpotLight)", + value: "angle", + }, + { + text: "Power (SpotLight)", + value: "power", + }, + { + text: "Target Position (Directional/SpotLight)", + value: "target", + }, + ], + }, + }, + }; + } + + addLight(args) { + const light = new THREE[args.TYPE](0xffffff, 1); + + createObject(args.NAME, light, args.GROUP); + lights[args.NAME] = light; + if (light.type === "AmbientLight" || "HemisphereLight") return; + + light.castShadow = true; + if (light.type === "PointLight") return; + //Directional & Spot Light + light.target.position.set(0, 0, 0); + scene.add(light.target); + + light.pos = new THREE.Vector3(0, 0, 0); + + light.shadow.mapSize.width = 4096; + light.shadow.mapSize.height = 2048; + + if (light.type === "SpotLight") { + light.decay = 0; + light.shadow.camera.near = 500; + light.shadow.camera.far = 4000; + light.shadow.camera.fov = 30; + } + light.shadow.needsUpdate = true; + light.needsUpdate = true; + } + + setLight(args) { + const light = lights[args.NAME]; + if (!args.PROPERTY) return; + if (args.PROPERTY === "target") { + light.target.position.set(...JSON.parse(args.VALUE)); //vector3 + light.target.updateMatrixWorld(); + } else { + light[args.PROPERTY] = getAsset(args.VALUE); + } + light.needsUpdate = true; + + if (light.type === "AmbientLight" || "HemisphereLight") return; + + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; + } + } + Scratch.extensions.register(new ThreeLights()); + + class ThreeUtilities { + getInfo() { + return { + id: "threeUtility", + name: "Three Utilities", + color1: "#3875c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "newVector2", + blockType: Scratch.BlockType.REPORTER, + text: "New Vector [X] [Y]", + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "newVector3", + blockType: Scratch.BlockType.REPORTER, + text: "New Vector [X] [Y] [Z]", + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + }, + Z: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + "---", + { + opcode: "operateV3", + blockType: Scratch.BlockType.REPORTER, + text: "do [V3] [O] [V32]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + O: { + type: Scratch.ArgumentType.STRING, + menu: "operators", + }, + V32: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + { + opcode: "moveVector3", + blockType: Scratch.BlockType.REPORTER, + text: "move [S] steps in vector [V3] in direction [D3]", + arguments: { + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + D3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + { + opcode: "directionTo", + blockType: Scratch.BlockType.REPORTER, + text: "direction from [V3] to [T3]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,3]", + }, + T3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + "---", + { + opcode: "newColor", + blockType: Scratch.BlockType.REPORTER, + text: "New Color [HEX]", + arguments: { + HEX: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#9966ff", + }, + }, + }, + { + opcode: "newFog", + blockType: Scratch.BlockType.REPORTER, + text: "New Fog [COLOR] [NEAR] [FAR]", + arguments: { + COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#9966ff", + exemptFromNormalization: true, + }, + NEAR: { + type: Scratch.ArgumentType.NUMBER, + }, + FAR: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10, + }, + }, + }, + { + opcode: "newTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "textureStyles", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "newCubeTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", + arguments: { + COSTUMEX0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEX1: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEY0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEY1: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEZ0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEZ1: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "textureStyles", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "newEquirectangularTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Equirectangular Texture [COSTUME] [MODE]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + }, + }, + "---", + { + opcode: "curve", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.REPORTER, + text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", + arguments: { + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "curveTypes", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]", + }, + CLOSED: { + type: Scratch.ArgumentType.STRING, + defaultValue: "true", + }, + }, + }, + "---", + { + opcode: "mouseDown", + extensions: ["colours_sensing"], + blockType: Scratch.BlockType.BOOLEAN, + text: "mouse [BUTTON] [action]?", + arguments: { + BUTTON: { + type: Scratch.ArgumentType.STRING, + menu: "mouseButtons", + }, + action: { + type: Scratch.ArgumentType.STRING, + menu: "mouseAction", + }, + }, + }, + { + opcode: "mousePos", + extensions: ["colours_sensing"], + blockType: Scratch.BlockType.REPORTER, + text: "mouse position", + arguments: {}, + }, + "---", + { + opcode: "getItem", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.REPORTER, + text: "get item [ITEM] of [ARRAY]", + arguments: { + ITEM: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + ARRAY: { + type: Scratch.ArgumentType.STRING, + defaultValue: `["myObject", "myLight"]`, + }, + }, + }, + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Raycasting", + }, + { + opcode: "raycast", + blockType: Scratch.BlockType.COMMAND, + text: "Raycast from [V3] in direction [D3]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,3]", + }, + D3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,1]", + }, + }, + }, + { + opcode: "getRaycast", + blockType: Scratch.BlockType.REPORTER, + text: "get raycast [PROPERTY]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "raycastProperties", + }, + }, + }, + ], + menus: { + materialProperties: { + acceptReporters: false, + items: [{ + text: "Color", + value: "color", + }, + { + text: "Map (texture)", + value: "map", + }, + { + text: "Alpha Map (texture)", + value: "alphaMap", + }, + { + text: "Alpha Test (0-1)", + value: "alphaTest", + }, + { + text: "Side (front/back/double)", + value: "side", + }, + { + text: "Bump Map (texture)", + value: "bumpMap", + }, + { + text: "Bump Scale", + value: "bumpScale", + }, + ], + }, + textureModes: { + acceptReporters: false, + items: ["Pixelate", "Blur"], + }, + textureStyles: { + acceptReporters: false, + items: ["Repeat", "Clamp"], + }, + raycastProperties: { + acceptReporters: false, + items: [{ + text: "Intersected Object Names", + value: "name", + }, + { + text: "Number of Objects", + value: "number", + }, + { + text: "Intersected Objects distances", + value: "distance", + }, + ], + }, + mouseButtons: { + acceptReporters: false, + items: ["left", "middle", "right"], + }, + mouseAction: { + acceptReporters: false, + items: ["Down", "Clicked"], + }, + curveTypes: { + acceptReporters: false, + items: ["CatmullRomCurve3"], + }, + operators: { + acceptReporters: false, + items: ["+", "-", "*", "/", "=", "max", "min", "dot", "cross", "distance to", "angle to", "apply euler"], + }, + }, + }; + } + mouseDown(args) { + if (args.action === "Down") return isMouseDown[args.BUTTON]; + if (args.action === "Clicked") { + if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false; + else prevMouse[args.BUTTON] = true; + return true; + } + } + mousePos(event) { + return JSON.stringify(mouseNDC); + } + newVector3(args) { + return JSON.stringify([args.X, args.Y, args.Z]); + } + operateV3(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)); + const v32 = new THREE.Vector3(...JSON.parse(args.V32)); + + let r; + if (args.O == "+") r = v3.add(v32); + else if (args.O == "-") r = v3.sub(v32); + else if (args.O == "*") r = v3.multiply(v32); + else if (args.O == "/") r = v3.divide(v32); + else if (args.O == "=") r = v3.equals(v32); + else if (args.O == "max") r = v3.max(v32); + else if (args.O == "min") r = v3.min(v32); + else if (args.O == "dot") r = v3.dot(v32); + else if (args.O == "cross") r = v3.cross(v32); + else if (args.O == "distance to") r = v3.distanceTo(v32); + else if (args.O == "angle to") r = v3.angleTo(v32); + else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)); + + if (typeof r == "object") return JSON.stringify([r.x, r.y, r.z]); + else return JSON.stringify(r); + } + + newVector2(args) { + return JSON.stringify([args.X, args.Y]); + } + + moveVector3(args) { + const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); + const steps = Number(args.S); + + const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); + + const yaw = THREE.MathUtils.degToRad(yawInputDeg); + const pitch = THREE.MathUtils.degToRad(pitchInputDeg); + const roll = THREE.MathUtils.degToRad(rollInputDeg); + + const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); + + const forwardVector = new THREE.Vector3(0, 0, -1); + const direction = forwardVector.applyEuler(euler).normalize(); + + const newPos = currentPos.add(direction.multiplyScalar(steps)); + return JSON.stringify([newPos.x, newPos.y, newPos.z]); + } + + directionTo(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)); + const toV3 = new THREE.Vector3(...JSON.parse(args.T3)); + + const direction = toV3.clone().sub(v3).normalize(); + // Pitch (X) + const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)); + // Yaw (Y) + const yaw = Math.atan2(direction.x, direction.z); + + // Roll always 0 + return JSON.stringify([180 + THREE.MathUtils.radToDeg(pitch), THREE.MathUtils.radToDeg(yaw), 0]); + } + + newColor(args) { + const color = new THREE.Color(args.HEX); + const uuid = crypto.randomUUID(); + assets.colors[uuid] = color; + return `colors/${uuid}`; + } + newFog(args) { + const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR); + const uuid = crypto.randomUUID(); + assets.fogs[uuid] = fog; + return `fogs/${uuid}`; + } + async newTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newCubeTexture(args) { + const uris = [ + encodeCostume(args.COSTUMEX0), + encodeCostume(args.COSTUMEX1), + encodeCostume(args.COSTUMEY0), + encodeCostume(args.COSTUMEY1), + encodeCostume(args.COSTUMEZ0), + encodeCostume(args.COSTUMEZ1), + ]; + const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); + const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); + + texture.name = "CubeTexture" + args.COSTUMEX0; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newEquirectangularTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + texture.mapping = THREE.EquirectangularReflectionMapping; + + setTexutre(texture, args.MODE); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + + curve(args) { + function parsePoints(input) { + // Match all [x,y,z] groups + const matches = input.match(/\[([^\]]+)\]/g); + if (!matches) return []; + + return matches.map((str) => { + const nums = str + .replace(/[\[\]\s]/g, "") + .split(",") + .map(Number); + return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0); + }); + } + const points = parsePoints(args.POINTS); + const curve = new THREE[args.TYPE](points); + curve.closed = JSON.parse(args.CLOSED); + + const uuid = crypto.randomUUID(); + assets.curves[uuid] = curve; + return `curves/${uuid}`; + } + + getItem(args) { + const items = JSON.parse(args.ARRAY); + const item = items[args.ITEM - 1]; + if (!item) return "0"; + return item; + } + + raycast(args) { + const origin = new THREE.Vector3(...JSON.parse(args.V3)); + // rotation is in degrees => convert to radians first + const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); + + const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder); + const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize(); + + const raycaster = new THREE.Raycaster(); + //const camera = getObject(args.CAMERA) + raycaster.set(origin, direction); + + const intersects = raycaster.intersectObjects(scene.children, true); + + raycastResult = intersects; + } + getRaycast(args) { + if (args.PROPERTY === "number") return raycastResult.length; + if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map((i) => i.distance)); + return JSON.stringify(raycastResult.map((i) => i.object[args.PROPERTY])); + } + } + Scratch.extensions.register(new ThreeUtilities()); + + class ThreeGLB { + getInfo() { + return { + id: "threeGLB", + name: "Three GLB Loader", + color1: "#c53838ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + blockType: Scratch.BlockType.BUTTON, + text: "Load GLB File", + func: "loadModelFile", + }, + { + opcode: "addModel", + blockType: Scratch.BlockType.COMMAND, + text: "add [ITEM] as [NAME] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + ITEM: { + type: Scratch.ArgumentType.STRING, + menu: "modelsList", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + }, + }, + { + opcode: "getModel", + blockType: Scratch.BlockType.REPORTER, + text: "get object [PROPERTY] of [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "modelProperties", + }, + }, + }, + { + opcode: "playAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "play animation [ANAME] of [NAME], [TIMES] times", + arguments: { + TIMES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "pauseAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "set [TOGGLE] animation [ANAME] of [NAME]", + arguments: { + TOGGLE: { + type: Scratch.ArgumentType.NUMBER, + menu: "pauseUn", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "stopAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "stop animation [ANAME] of [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + ], + menus: { + modelProperties: { + acceptReporters: false, + items: [{ + text: "Animations", + value: "animations", + }, ], + }, + pauseUn: { + acceptReporters: true, + items: [{ + text: "Pause", + value: "true", + }, + { + text: "Unpasue", + value: "false", + }, + ], + }, + modelsList: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".glb")); + if (models.length < 1) return [ + ["Load a model!"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + }, + }; + } + + async loadModelFile() { + openFileExplorer(".glb").then((files) => { + const file = files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + const arrayBuffer = e.target.result; + + { + // From lily's assets + + // Thank you PenguinMod for providing this code. + { + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); + + //const res = await Scratch.fetch(args.URL); + //const buffer = await res.arrayBuffer(); + const buffer = arrayBuffer; + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + alert("Model loaded successfully!"); + } catch (e) { + console.error(e); + alert("Error loading model."); + } + } + // End of PenguinMod + } + }; + + reader.readAsArrayBuffer(file); + }); + } + async addModel(args) { + const group = await getModel(args.ITEM, args.NAME); + + createObject(args.NAME, group, args.GROUP); + } + getModel(args) { + if (!models[args.NAME]) return; + return Object.keys(models[args.NAME].actions).toString(); + } + + playAnimation(args) { + const model = models[args.NAME]; + if (!model) { + console.log("no model!"); + return; + } + + const action = model.actions[args.ANAME]; //clones of models dont have a stored actions! + if (!action) { + console.log("no action!"); + return; + } + + args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity); + + action.reset().play(); + } + stopAnimation(args) { + const model = models[args.NAME]; + if (!model) return; + + const action = model.actions[args.ANAME]; + if (action) action.stop(); + } + pauseAnimation(args) { + const model = models[args.NAME]; + if (!model) return; + + const action = model.actions[args.ANAME]; + if (action) action.paused = args.TOGGLE; + } + } + Scratch.extensions.register(new ThreeGLB()); + + class ThreeAddons { + getInfo() { + return { + id: "threeAddons", + name: "Three Addons", + color1: "#c538a2ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + blockType: Scratch.BlockType.LABEL, + text: "Orbit Control", + }, + { + opcode: "OrbitControl", + blockType: Scratch.BlockType.COMMAND, + text: "set addon Orbit Control [STATE]", + arguments: { + STATE: { + type: Scratch.ArgumentType.STRING, + menu: "onoff", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "Post Processing", + }, + { + opcode: "resetComposer", + blockType: Scratch.BlockType.COMMAND, + text: "reset composer", + }, + { + opcode: "bloom", + blockType: Scratch.BlockType.COMMAND, + text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + I: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.5, + }, + T: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.5, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + }, + }, + { + opcode: "godRays", + blockType: Scratch.BlockType.COMMAND, + text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + DEC: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.95, + }, + DENS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + EXP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.1, + }, + WEI: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.4, + }, + RES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + SAMP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 64, + }, + }, + }, + { + opcode: "dots", + blockType: Scratch.BlockType.COMMAND, + text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + A: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: 0, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + }, + }, + { + opcode: "depth", + blockType: Scratch.BlockType.COMMAND, + text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", + arguments: { + FD: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 3, + }, + FL: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.001, + }, + BS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 4, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 240, + }, + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "NORMAL", + }, + }, + }, + "---", + { + opcode: "custom", + blockType: Scratch.BlockType.COMMAND, + text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myShader", + }, + FRA: { + type: Scratch.ArgumentType.STRING, + }, + VER: { + type: Scratch.ArgumentType.STRING, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "NORMAL", + }, + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + ], + menus: { + onoff: { + acceptReporters: true, + items: [{ + text: "enabled", + value: "1", + }, + { + text: "disabled", + value: "0", + }, + ], + }, + blendModes: { + acceptReporters: false, + items: [ + "SKIP", + "SET", + "ADD", + "ALPHA", + "AVERAGE", + "COLOR", + "COLOR_BURN", + "COLOR_DODGE", + "DARKEN", + "DIFFERENCE", + "DIVIDE", + "DST", + "EXCLUSION", + "HARD_LIGHT", + "HARD_MIX", + "HUE", + "INVERT", + "INVERT_RGB", + "LIGHTEN", + "LINEAR_BURN", + "LINEAR_DODGE", + "LINEAR_LIGHT", + "LUMINOSITY", + "MULTIPLY", + "NEGATION", + "NORMAL", + "OVERLAY", + "PIN_LIGHT", + "REFLECT", + "SCREEN", + "SRC", + "SATURATION", + "SOFT_LIGHT", + "SUBTRACT", + "VIVID_LIGHT", + ], + }, + }, + }; + } + + OrbitControl(args) { + if (controls) controls.dispose(); + + console.log("creating...", OrbitControls); + controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); + controls.enableDamping = true; + + controls.enabled = !!args.STATE; + console.log(controls); + } + + resetComposer() { + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } + + bloom(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const bloomEffect = new BloomEffect({ + intensity: args.I, + luminanceThreshold: args.T, // ← correct key + luminanceSmoothing: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + bloomEffect.blendMode.opacity.value = args.OP; + + const pass = new EffectPass(camera, bloomEffect); + + composer.addPass(pass); + } + + godRays(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + let object = getObject(args.NAME); + const sun = object; + + const godRays = new GodRaysEffect(camera, sun, { + resolutionScale: args.RES, + density: args.DENS, // ray density + decay: args.DEC, // fade out + weight: args.WEI, // brightness of rays + exposure: args.EXP, + samples: args.SAMP, + blendFunction: BlendFunction[args.BLEND], + }); + godRays.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, godRays); + composer.addPass(pass); + } + + dots(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dot = new DotScreenEffect({ + angle: args.A, + scale: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + dot.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, dot); + composer.addPass(pass); + } + + depth(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dofEffect = new DepthOfFieldEffect(camera, { + focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) + focalLength: args.FL, // lens focal length in meters + bokehScale: args.BS, // strength/size of the blur circles + height: args.H, // resolution hint (affects quality/perf) + blendFunction: BlendFunction[args.BLEND], + }); + dofEffect.blendMode.opacity.value = args.OP; + + const dofPass = new EffectPass(camera, dofEffect); + composer.addPass(dofPass); + } + + async custom(args) { + function cleanGLSL(glslCode) { + //delete multilines comments + let cleanedCode = glslCode + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/ /g, "\n") + .replace(/\/\/.*$/gm, " ") + .replace(/; /g, ";\n"); + + return cleanedCode; + } + + let fs = cleanGLSL(` + ${args.FRA} + `); + if (!args.FRA.trim()) { + fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`; + } + const vs = cleanGLSL(` + ${args.VER} + `); + console.log(fs); + console.log(vs); + + const effect = new Effect("Custom", fs, { + blendFunction: BlendFunction[args.BLEND], + vertexShader: vs, + uniforms: new Map([ + //uniforms usually in shaders... open to more! + ["time", new THREE.Uniform(0.0)], + [ + "resolution", + new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height)), + ], + ]), + defines: new Map([ + ["USE_TIME", "1"], + ["USE_VERTEX_TRANSFORM", ""], + ]), + }); + + effect.blendMode.opacity.value = args.OP; + + const pass = new EffectPass(camera, effect); + composer.addPass(pass); + + customEffects.push(effect); + } + } + Scratch.extensions.register(new ThreeAddons()); + + class RapierPhysics { + getInfo() { + return { + id: "rapierPhysics", + name: "RAPIER Physics", + color1: "#222222", + color2: "#203024ff", + color3: "#78f07eff", + blocks: [{ + opcode: "createWorld", + blockType: Scratch.BlockType.COMMAND, + text: "create world | gravity:[G]", + arguments: { + G: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,-9.81,0]", + }, + }, + }, + { + opcode: "getWorld", + blockType: Scratch.BlockType.REPORTER, + text: "get world [PROPERTY]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "wProp", + }, + }, + }, + "---", + { + opcode: "objectPhysics", + blockType: Scratch.BlockType.COMMAND, + text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", + arguments: { + state2: { + type: Scratch.ArgumentType.STRING, + menu: "state2", + }, + state: { + type: Scratch.ArgumentType.STRING, + menu: "state", + defaultValue: "true", + }, + type: { + type: Scratch.ArgumentType.STRING, + menu: "objectTypes", + defaultValue: "dynamic", + }, + collider: { + type: Scratch.ArgumentType.STRING, + menu: "colliderTypes", + defaultValue: "cuboid", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + mass: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + density: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + friction: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0.5", + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.LABEL, + text: "- RigidBody", + }, + { + opcode: "setRB", + blockType: Scratch.BlockType.COMMAND, + text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "rigidBodySets", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "getRB", + blockType: Scratch.BlockType.REPORTER, + text: "get rigidbody [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "rigidBodyProperties", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "lockObjectAxis", + blockType: Scratch.BlockType.COMMAND, + text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", + arguments: { + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "lockAxes", + }, + X: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + Y: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + Z: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + }, + }, + "---", + { + opcode: "addForce", + blockType: Scratch.BlockType.COMMAND, + text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,10,0]", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "forces", + defaultValue: "addForce", + }, + SPACE: { + type: Scratch.ArgumentType.STRING, + menu: "spaces", + defaultValue: "world", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "resetForces", + blockType: Scratch.BlockType.COMMAND, + text: "reset [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "resetF", + defaultValue: "resetForces", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "enableCCD", + blockType: Scratch.BlockType.COMMAND, + text: "enable Continuous Collision Detection for [OBJECT] [state]", + arguments: { + state: { + type: Scratch.ArgumentType.STRING, + menu: "state", + defaultValue: "true", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "oPropS", + defaultValue: "physics", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "fixedJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + RA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + RB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + { + opcode: "sphericalJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + }, + }, + { + opcode: "revoluteJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + X: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.LABEL, + text: "- Collider", + }, + { + opcode: "setC", + blockType: Scratch.BlockType.COMMAND, + text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "colliderSets", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "getC", + blockType: Scratch.BlockType.REPORTER, + text: "get collider [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "colliderProperties", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "sensorSingle", + blockType: Scratch.BlockType.BOOLEAN, + text: "is sensor [SENSOR] touching [OBJECT]?", + arguments: { + SENSOR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySensor", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "sensorAll", + blockType: Scratch.BlockType.REPORTER, + text: "objects touching sensor [SENSOR]", + arguments: { + SENSOR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySensor", + }, + }, + }, + ], + menus: { + wProp: { + acceptReporters: false, + items: [{ + text: "Gravity", + value: "gravity", + }, + { + text: "log to console", + value: "log", + }, + ], + }, + tf: { + acceptReporters: true, + items: [{ + text: "false", + value: "false", + }, + { + text: "true", + value: "true", + }, + ], + }, + lockAxes: { + acceptReporters: false, + items: [{ + text: "Translation", + value: "setEnabledTranslations", + }, + { + text: "Rotation", + value: "setEnabledRotations", + }, + ], + }, + rigidBodyProperties: { + acceptReporters: false, + items: [{ + text: "Type", + value: "bodyType", + }, + { + text: "Linear Velocity", + value: "linvel", + }, + { + text: "Angular Velocity", + value: "angvel", + }, + { + text: "Translation (position)", + value: "translation", + }, + { + text: "Rotation (quaternion)", + value: "rotation", + }, + { + text: "Mass", + value: "mass", + }, + //{text: "Center of Mass", value: "centerOfMass"}, + { + text: "Linear Damping", + value: "linearDamping", + }, + { + text: "Angular Damping", + value: "angularDamping", + }, + { + text: "Is Sleeping?", + value: "isSleeping", + }, + //{text: "Can Sleep?", value: "isCanSleep"}, + { + text: "Gravity Scale", + value: "gravityScale", + }, + { + text: "Is Fixed?", + value: "isFixed", + }, + { + text: "Is Dynamic?", + value: "isDynamic", + }, + { + text: "Is Kinematic?", + value: "isKinematic", + }, + //{text: "Sleeping", value: "sleeping"} + ], + }, + rigidBodySets: { + acceptReporters: false, + items: [ + //{text: "Linear Velocity", value: "setLinvel"}, + //{text: "Angular Velocity", value: "setAngvel"}, + //{text: "Mass", value: "setMass"}, + { + text: "Gravity Scale", + value: "setGravityScale", + }, + //{text: "Can Sleep?", value: "setCanSleep"}, + //{text: "Sleeping", value: "sleeping"}, + { + text: "Linear Damping", + value: "setLinearDamping", + }, + { + text: "Angular Damping", + value: "setAngularDamping", + }, + { + text: "Is Fixed?", + value: "isFixed", + }, + { + text: "Is Dynamic?", + value: "isDynamic", + }, + { + text: "Is Kinematic?", + value: "isKinematic", + }, + ], + }, + colliderProperties: { + acceptReporters: false, + items: [ + //{text: "Collider Type", value: "type"}, + { + text: "Is Sensor?", + value: "isSensor", + }, + { + text: "Friction", + value: "friction", + }, + { + text: "Restitution", + value: "restitution", + }, + { + text: "Density", + value: "density", + }, + { + text: "Mass", + value: "mass", + }, + { + text: "Position", + value: "translation", + }, + { + text: "Rotation", + value: "rotation", + }, + //{text: "Area", value: "area"}, + { + text: "Volume", + value: "volume", + }, + { + text: "Collision Groups", + value: "collisionGroups", + }, + //{text: "Collision Mask", value: "collisionMask"}, + //{text: "Is Enabled?", value: "enabled"}, + //{text: "Contact Count", value: "contactCount"}, + //{text: "RigidBody Handle", value: "rigidBody"} + ], + }, + colliderSets: { + acceptReporters: false, + items: [{ + text: "Friction", + value: "setFriction", + }, + { + text: "Restitution", + value: "setRestitution", + }, + { + text: "Density", + value: "setDensity", + }, + { + text: "Is Sensor?", + value: "setSensor", + }, + { + text: "Collision Groups", + value: "setCollisionGroups", + }, + //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool + //{text: "Position Offset", value: "setTranslation"}, + //{text: "Rotation Offset", value: "setRotation"} + ], + }, + state: { + acceptReporters: true, + items: [{ + text: "on", + value: "true", + }, + { + text: "off", + value: "false", + }, + ], + }, + state2: { + acceptReporters: true, + items: [{ + text: "false", + value: "false", + }, + { + text: "true (must be fixed)", + value: "true", + }, + ], + }, + spaces: { + acceptReporters: false, + items: [{ + text: "World", + value: "world", + }, + { + text: "Local", + value: "local", + }, + ], + }, + objectTypes: { + acceptReporters: false, + items: [{ + text: "Dynamic", + value: "dynamic", + }, + { + text: "Fixed", + value: "fixed", + }, + { + text: "Kinematic Position Based", + value: "kinematicPositionBased", + }, + ], + }, + colliderTypes: { + acceptReporters: false, + items: [{ + text: "Box, Rectangle, cuboid", + value: "cuboid", + }, + { + text: "Sphere, ball", + value: "ball", + }, + { + text: "Custom, complex simple shapes, convexHull", + value: "convexHull", + }, + { + text: "Precision, TriMesh", + value: "trimesh", + }, + ], + }, + forces: { + acceptReporters: false, + items: [{ + text: "Force", + value: "addForce", + }, + { + text: "Torque (rotation)", + value: "addTorque", + }, + { + text: "Apply Impulse", + value: "applyImpulse", + }, + { + text: "Apply Torque Impulse (rotation)", + value: "applyTorqueImpulse", + }, + { + text: "Linear Velocity", + value: "setLinvel", + }, + { + text: "Angular Velocity", + value: "setAngvel", + }, + ], + }, + resetF: { + acceptReporters: false, + items: [{ + text: "Forces", + value: "resetForces", + }, + { + text: "Torques", + value: "resetTorques", + }, + ], + }, + }, + }; + } + joint(jointData, bodyA, bodyB) { + physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); + } + + fixedJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + let RA = JSON.parse(args.RA).map(Number); + let RB = JSON.parse(args.RB).map(Number); + + RA = new THREE.Quaternion().setFromEuler( + new THREE.Euler( + THREE.MathUtils.degToRad(RA[0]), + THREE.MathUtils.degToRad(RA[1]), + THREE.MathUtils.degToRad(RA[2]) + ) + ); + RB = new THREE.Quaternion().setFromEuler( + new THREE.Euler( + THREE.MathUtils.degToRad(RB[0]), + THREE.MathUtils.degToRad(RB[1]), + THREE.MathUtils.degToRad(RB[2]) + ) + ); + + const data = RAPIER.JointData.fixed({ + x: VA[0], + y: VA[1], + z: VA[2], + }, + RA, { + x: VB[0], + y: VB[1], + z: VB[2], + }, + RB + ); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + sphericalJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + + const data = RAPIER.JointData.spherical({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + revoluteJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + const x = JSON.parse(args.X).map(Number); + + const data = RAPIER.JointData.revolute({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }, { + x: x[0], + y: x[1], + z: x[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + prismaticJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + const x = JSON.parse(args.X).map(Number); + + const data = RAPIER.JointData.prismatic({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }, { + x: x[0], + y: x[1], + z: x[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + createWorld(args) { + const v3 = JSON.parse(args.G).map(Number); + const gravity = { + x: v3[0], + y: v3[1], + z: v3[2], + }; + physicsWorld = new RAPIER.World(gravity); + + console.log(physicsWorld); + } + + getWorld(args) { + if (args.PROPERTY === "log") { + console.log(physicsWorld); + return "logged"; + } + return JSON.stringify(physicsWorld[args.PROPERTY]); + } + + setRB(args) { + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.rigidBody[args.PROPERTY](value); + } + setC(args) { + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.collider[args.PROPERTY](value); + } + + getRB(args) { + let object = getObject(args.OBJECT); + return JSON.stringify(object.rigidBody[args.PROPERTY]()); + } + getC(args) { + let object = getObject(args.OBJECT); + return JSON.stringify(object.collider[args.PROPERTY]()); + } + + lockObjectAxis(args) { + let object = getObject(args.OBJECT); + const x = !JSON.parse(args.X); + const y = !JSON.parse(args.Y); + const z = !JSON.parse(args.Z); + object.rigidBody[args.PROPERTY](x, y, z, true); //changes is xyz, wake up + } + + objectPhysics(args) { + let object = getObject(args.OBJECT); + object.physics = JSON.parse(args.state); + + if (JSON.parse(args.state)) { + //if already exists delete: + if (object.rigidBody) { + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; + } + /*asing a rigidbody and collider to object and add them to physicsWorld*/ + let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() + .setTranslation(object.position.x, object.position.y, object.position.z) + .setRotation({ + w: object.quaternion._w, + x: object.quaternion._x, + y: object.quaternion._y, + z: object.quaternion._z, + }); + + let colliderDesc; + switch (args.collider) { + case "cuboid": + colliderDesc = createCuboidCollider(object); + break; + case "ball": + colliderDesc = createBallCollider(object); + break; + case "convexHull": + colliderDesc = createConvexHullCollider(object); + break; + case "trimesh": + colliderDesc = TriMesh(object); + break; + } + colliderDesc + .setSensor(JSON.parse(args.state2)) + .setMass(args.mass) + .setDensity(args.density) + .setFriction(args.friction); + + let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc); + let collider = physicsWorld.createCollider(colliderDesc, rigidBody); + + object.rigidBody = rigidBody; + object.collider = collider; + } else { + /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; + } + } + + enableCCD(args) { + let object = getObject(args.OBJECT); + if (object.physics) { + let rigidBody = object.rigidBody; + rigidBody.enableCcd(JSON.parse(args.state)); + } + } + + addForce(args) { + let object = getObject(args.OBJECT); + const vector = JSON.parse(args.VALUE).map(Number); + + let force = new THREE.Vector3(vector[0], vector[1], vector[2]); + if (args.SPACE === "local") { + force.applyQuaternion(object.quaternion); + } + + object.rigidBody[args.PROPERTY](force, true); + } + + resetForces(args) { + rigidBody[args.PROPERTY](true); + } + + sensorSingle(args) { + const sensor = getObject(args.SENSOR); + + let object = getObject(args.OBJECT); + + let touching = false; + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + if (otherCollider === object.collider) touching = true; + }); + + return touching; + } + + sensorAll(args) { + const sensor = getObject(args.SENSOR); + + const touchedObjects = []; + + // loop thruogh every collider touching sensor + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + // find owner of collider + const otherObject = scene.children.find((o) => o.collider === otherCollider); + console.log(otherCollider); + if (otherObject) touchedObjects.push(otherObject.name); + }); + + return JSON.stringify(touchedObjects); + } + } + Scratch.extensions.register(new RapierPhysics()); + + //Thanks to the PointerLock extension of Turbowarp + const mouse = vm.runtime.ioDevices.mouse; + let isLocked = false; + let isPointerLockEnabled = false; + + let rect = threeRenderer.domElement.getBoundingClientRect(); + document.addEventListener("resize", () => { + rect = threeRenderer.domElement.getBoundingClientRect(); + }); + + const postMouseData = (e, isDown) => { + const { + movementX, + movementY + } = e; + const { + width, + height + } = rect; + const x = mouse._clientX + movementX; + const y = mouse._clientY - movementY; + mouse._clientX = x; + mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); + mouse._clientY = y; + mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); + if (typeof isDown === "boolean") { + const data = { + button: e.button, + isDown, + }; + originalPostIOData(data); + } + }; + + const mouseDevice = vm.runtime.ioDevices.mouse; + const originalPostIOData = mouseDevice.postData.bind(mouseDevice); + mouseDevice.postData = (data) => { + if (!isPointerLockEnabled) { + return originalPostIOData(data); + } + }; + + document.addEventListener( + "mousedown", + (e) => { + // @ts-expect-error + if (threeRenderer.domElement.contains(e.target)) { + if (isLocked) { + postMouseData(e, true); + } else if (isPointerLockEnabled) { + threeRenderer.domElement.requestPointerLock(); + } + } + }, + true + ); + document.addEventListener( + "mouseup", + (e) => { + if (isLocked) { + postMouseData(e, false); + // @ts-expect-error + } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { + threeRenderer.domElement.requestPointerLock(); + } + }, + true + ); + document.addEventListener( + "mousemove", + (e) => { + if (isLocked) { + postMouseData(e); + } + }, + true + ); + + document.addEventListener("pointerlockchange", () => { + isLocked = document.pointerLockElement === threeRenderer.domElement; + }); + document.addEventListener("pointerlockerror", (e) => { + console.error("Pointer lock error", e); + }); + + const oldStep = vm.runtime._step; + vm.runtime._step = function(...args) { + const ret = oldStep.call(this, ...args); + if (isPointerLockEnabled) { + const { + width, + height + } = rect; + mouse._clientX = width / 2; + mouse._clientY = height / 2; + mouse._scratchX = 0; + mouse._scratchY = 0; + } + return ret; + }; + + vm.runtime.on("PROJECT_LOADED", () => { + isPointerLockEnabled = false; + if (isLocked) { + document.exitPointerLock(); + } + }); + + class Pointerlock { + getInfo() { + return { + id: "threepointerlockmod", + name: "Pointerlock for Extra 3D", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "setLocked", + blockType: Scratch.BlockType.COMMAND, + text: "set pointer lock [enabled]", + arguments: { + enabled: { + type: Scratch.ArgumentType.STRING, + defaultValue: "true", + menu: "enabled", + }, + }, + }, + { + opcode: "isLocked", + blockType: Scratch.BlockType.BOOLEAN, + text: "pointer locked?", + }, + ], + menus: { + enabled: { + acceptReporters: true, + items: [{ + text: "enabled", + value: "true", + }, + { + text: "disabled", + value: "false", + }, + ], + }, + }, + }; + } + + setLocked(args) { + isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; + if (!isPointerLockEnabled && isLocked) { + document.exitPointerLock(); + } + } + + isLocked() { + return isLocked; + } + } + Scratch.extensions.register(new Pointerlock()); + }); +})(Scratch); diff --git a/threejsD_BACKUP_13852.js b/threejsD_BACKUP_13852.js new file mode 100644 index 0000000..d1db442 --- /dev/null +++ b/threejsD_BACKUP_13852.js @@ -0,0 +1,5029 @@ +/* jshint esversion: 11 */ +// Name: Extra 3D +// ID: threejsExtension +// Description: Use three js inside Turbowarp! A 3D graphics library. +// By: Civero +// License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors + +(function(Scratch) { + "use strict"; + + if (!Scratch.extensions.unsandboxed) { + throw new Error("Three-D extension must run unsandboxed"); + } + + if (Scratch.vm.runtime.isPackaged) { + alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); + return; + } + //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return + + const vm = Scratch.vm; + const runtime = vm.runtime; + const renderer = Scratch.renderer; + const canvas = renderer.canvas; + const Cast = Scratch.Cast; + const menuIconURI = + "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; + + let alerts = false; + console.log("alerts are " + (alerts ? "enabled" : "disabled")); + + let isMouseDown = { + left: false, + middle: false, + right: false, + }; + let prevMouse = { + left: false, + middle: false, + right: false, + }; + + let lastWidth = 0; + let lastHeight = 0; + + let THREE; + let clock; + let running; + let loopId; + //Addons + let GLTFLoader; + let gltf; + let OrbitControls; + let controls; + let BufferGeometryUtils; + let TextGeometry; + let fontLoad; + //Physics + let RAPIER; + let physicsWorld; + + let threeRenderer; + let scene; + let camera; + let eulerOrder = "YXZ"; + + let composer; + let passes = {}; + let customEffects = []; + let renderTargets = {}; + + let materials = {}; + let geometries = {}; + let lights = {}; + let models = {}; + + let assets = { + //should i place materials, geometries; inside too? + textures: {}, + colors: {}, + fogs: {}, + curves: {}, + renderTargets: {}, //not the same as the global one! this one only stores textures + }; + + let raycastResult = []; + + function resetor(level) { + camera = undefined; + composer.reset(); + + passes = {}; + customEffects = []; + renderTargets = {}; + + materials = {}; + geometries = {}; + lights = {}; + models = {}; + + if (level > 0) { + assets = { + textures: {}, + colors: {}, + fogs: {}, + curves: {}, + renderTargets: {}, + }; + } + + updateComposers(); + } + + //utility + function vector3ToString(prop) { + if (!prop) return "0,0,0"; + + const x = + typeof prop.x === "number" ? + prop.x : + typeof prop._x === "number" ? + prop._x : + JSON.stringify(prop).includes("X") ? + prop : + 0; + const y = typeof prop.y === "number" ? prop.y : typeof prop.y === "number" ? prop._y : 0; + const z = typeof prop.z === "number" ? prop.z : typeof prop.z === "number" ? prop.z : 0; + + return [x, y, z]; + } + + //objects + function createObject(name, content, parentName) { + let object = getObject(name, true); + if (object) { + removeObject(name); + alerts ? alert(name + " already exsisted, will replace!") : null; + } + content.name = name; + content.rotation._order = eulerOrder; + parentName === scene.name ? (object = scene) : (object = getObject(parentName)); + content.physics = false; + + object.add(content); + } + + function removeObject(name) { + let object = getObject(name); + if (!object) return; + + scene.remove(object); + + if (object.rigidBody) { + physicsWorld.removeCollider(object.collider, true); + physicsWorld.removeRigidBody(object.rigidBody, true); + object.rigidBody = null; + object.collider = null; + } + if (object.isLight) { + delete lights[name]; + } + } + + function getObject(name, isNew) { + let object = null; + if (!scene) { + alerts ? alert("Can not get " + name + ". Create a scene first!") : null; + return; + } + object = scene.getObjectByName(name); + if (!object && !isNew) { + alerts ? alert(name + " does not exist! Add it to scene") : null; + return; + } + return object; + } + + //materials + function encodeCostume(name) { + if (name.startsWith("data:image/")) return name; + return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); + } + + function setTexutre(texture, mode, style, x, y) { + texture.colorSpace = THREE.SRGBColorSpace; + + if (mode === "Pixelate") { + texture.minFilter = THREE.NearestFilter; + texture.magFilter = THREE.NearestFilter; + } else { + //Blur + texture.minFilter = THREE.NearestMipmapLinearFilter; + texture.magFilter = THREE.NearestMipmapLinearFilter; + } + + if (style === "Repeat") { + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(x, y); + } + + texture.generateMipmaps = true; + } + async function resizeImageToSquare(uri, size = 256) { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + + // clear + draw image scaled to fit canvas + ctx.clearRect(0, 0, size, size); + ctx.drawImage(img, 0, 0, size, size); + + resolve(canvas.toDataURL()); // return normalized Data URI + //delete canvas? + }; + img.src = uri; + }); + } + //light + function updateShadowFrustum(light, focusPos) { + if (light.type !== "DirectionalLight") return; + + // Frustum Size - Increase this value to cover a larger area. + const d = 50; + + // Update Orthographic Shadow Camera Frustum + const shadowCamera = light.shadow.camera; + + // Set the width/height of the frustum + shadowCamera.left = -d; + shadowCamera.right = d; + shadowCamera.top = d; + shadowCamera.bottom = -d; + + // Determine ranges + shadowCamera.near = 0.1; + shadowCamera.far = 500; + + // Position the Light and its Target + light.target.position.copy(focusPos); + const direction = light.position.clone().sub(light.target.position).normalize(); + light.position.copy(focusPos.clone().add(direction.multiplyScalar(100))); + + // Ensure matrices are updated. + light.target.updateMatrixWorld(); + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; + } + //composer + function updateComposers() { + if (!camera || !scene) return; // nothing to do yet + + // always recreate the RenderPass to point to the current scene/camera + passes["Render"] = new RenderPass(scene, camera); + + // ensure composer has a RenderPass as the first pass + const hasRender = composer.passes.some((p) => p && p.scene); + if (!hasRender) composer.addPass(passes["Render"]); + else { + // if composer already has one, replace it so it references current scene/camera + const idx = composer.passes.findIndex((p) => p && p.scene); + composer.passes[idx] = passes["Render"]; + } + } + //utility + function getMouseNDC(event) { + // Use threeRenderer.domElement for correct offset + const rect = threeRenderer.domElement.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + return [x, y]; + } + + function checkCanvasSize() { + const { + width, + height + } = canvas; + if (width !== lastWidth || height !== lastHeight) { + lastWidth = width; + lastHeight = height; + resize(); + } + requestAnimationFrame(checkCanvasSize); //rerun next frame + } + //physics + function computeWorldBoundingBox(mesh) { + // Create a Box3 in world coordinates + const box = new THREE.Box3().setFromObject(mesh); + const size = new THREE.Vector3(); + box.getSize(size); + const center = new THREE.Vector3(); + box.getCenter(center); + return { + size, + center, + }; + } + + function createCuboidCollider(mesh) { + const { + size + } = computeWorldBoundingBox(mesh); + const collider = RAPIER.ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2); + return collider; + } + + function createBallCollider(mesh) { + const { + size + } = computeWorldBoundingBox(mesh); + // radius = 1/2 of the largest verticie + const radius = Math.max(size.x, size.y, size.z) / 2; + const collider = RAPIER.ColliderDesc.ball(radius); + return collider; //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) + } + + function createConvexHullCollider(mesh) { + mesh.updateWorldMatrix(true, false); + + const position = mesh.geometry.attributes.position; + const vertices = []; + const vertex = new THREE.Vector3(); + + // Matrix for scale only + const scaleMatrix = new THREE.Matrix4().makeScale(mesh.scale.x, mesh.scale.y, mesh.scale.z); + + for (let i = 0; i < position.count; i++) { + vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); + vertices.push(vertex.x, vertex.y, vertex.z); + } + + const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); + return collider; + } + + function TriMesh(mesh) { + // Get the positions array (from your geoPoints function) + const positions = mesh.geometry.attributes.position.array; + const numVertices = positions.length / 3; + + // Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] + const indices = Array.from({ + length: numVertices, + }, + (_, i) => i + ); + + const collider = RAPIER.ColliderDesc.trimesh(positions, new Uint32Array(indices)); + + return collider; + } + + function getModel(model, name) { + const file = runtime + .getTargetForStage() + .getSounds() + .find((c) => c.name === model); + if (!file) return; + + return new Promise((resolve, reject) => { + gltf.parse( + file.asset.data.buffer, + "", + (gltf) => { + const root = gltf.scene; + root.traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + const mixer = new THREE.AnimationMixer(root); + const actions = {}; + gltf.animations.forEach((clip) => { + const act = mixer.clipAction(clip); + act.clampWhenFinished = true; + actions[clip.name] = act; + }); + + models[name] = { + root, + mixer, + actions, + }; + resolve(root); + }, + (error) => { + console.error("Error parsing GLB model:", error); + reject(error); + } + ); + }); + } + async function openFileExplorer(format) { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = format; + input.multiple = false; + input.onchange = () => { + resolve(input.files); + input.remove(); + }; + input.click(); + }); + } + + function getMeshesUsingTexture(scene, targetTexture) { + const meshes = []; + + scene.traverse((object) => { + if (object.material) { + const materials = Array.isArray(object.material) ? object.material : [object.material]; + for (const material of materials) { + if (material.map === targetTexture) { + meshes.push(object); + break; + } + } + } + }); + + return meshes; + } + + function getAsset(path) { + if (typeof path == "string") { + //string? + if (path.includes("/")) { + //has the /? + const value = path.split("/"); + console.log(value[0], value[1]); + return assets[value[0]][value[1]]; + } + } + + return JSON.parse(path); //boolean or number + } + + let mouseNDC = [0, 0]; + //loops/init + function stopLoop() { + if (!running) return; + running = false; + + if (loopId) { + cancelAnimationFrame(loopId); + loopId = null; + if (threeRenderer) threeRenderer.clear(); + } + } + async function load() { + if (!THREE) { + // @ts-ignore +<<<<<<< HEAD + THREE = await import("https://esm.sh/three@0.180.0") + window._THREE_ = THREE +======= + THREE = await import("https://esm.sh/three@0.180.0"); +>>>>>>> e4a038b (Update threejsD.js) + //Addons + GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js"); + OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js"); + BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js"); + TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js"); + const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js"); + fontLoad = new FontLoader.FontLoader(); + + const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8"); + const { + EffectComposer, + EffectPass, + RenderPass, + + Effect, + BloomEffect, + GodRaysEffect, + DotScreenEffect, + DepthOfFieldEffect, + + BlendFunction, + } = POSTPROCESSING; + //so i can use them later as global + window.EffectComposer = EffectComposer; + window.EffectPass = EffectPass; + window.RenderPass = RenderPass; + window.Effect = Effect; + window.BloomEffect = BloomEffect; + window.GodRaysEffect = GodRaysEffect; + window.DotScreenEffect = DotScreenEffect; + window.DepthOfFieldEffect = DepthOfFieldEffect; + window.BlendFunction = BlendFunction; + + RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0"); + await RAPIER.init(); + + threeRenderer = new THREE.WebGLRenderer({ + powerPreference: "high-performance", + antialias: false, + stencil: false, + depth: true, + }); + threeRenderer.setPixelRatio(window.devicePixelRatio); + threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors + threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) + //threeRenderer.toneMappingExposure = 1.0 //(test) + + threeRenderer.shadowMap.enabled = true; + threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap; // (optional) + threeRenderer.domElement.style.pointerEvents = "auto"; //will disable turbowarp mouse events, but enable threejs's + + gltf = new GLTFLoader.GLTFLoader(); + clock = new THREE.Clock(); + + // Example: create a composer + composer = new EffectComposer(threeRenderer, { + frameBufferType: THREE.HalfFloatType, + }); + + renderer.addOverlay(threeRenderer.domElement, "manual"); + renderer.addOverlay(canvas, "manual"); + renderer.setBackgroundColor(1, 1, 1, 0); + + resize(); + + window.addEventListener("mousedown", (e) => { + if (e.button === 0) isMouseDown.left = true; + if (e.button === 1) isMouseDown.middle = true; + if (e.button === 2) isMouseDown.right = true; + }); + window.addEventListener("mouseup", (e) => { + if (e.button === 0) isMouseDown.left = false; + prevMouse.left = false; + if (e.button === 1) isMouseDown.middle = false; + prevMouse.middle = false; + if (e.button === 2) isMouseDown.right = false; + prevMouse.right = false; + }); + // prevent contextmenu on right click + threeRenderer.domElement.addEventListener("contextmenu", (e) => e.preventDefault()); + + threeRenderer.domElement.addEventListener("mousemove", (event) => { + mouseNDC = getMouseNDC(event); + }); + + running = false; + load(); + +<<<<<<< HEAD + startRenderLoop() + runtime.on('PROJECT_START', () => startRenderLoop()) + runtime.on('PROJECT_STOP_ALL', () => stopLoop()) + runtime.on('STAGE_SIZE_CHANGED', () => {requestAnimationFrame(() => resize())}) + checkCanvasSize() +======= + startRenderLoop(); + runtime.on("PROJECT_START", () => startRenderLoop()); + runtime.on("PROJECT_STOP_ALL", () => stopLoop()); + runtime.on("STAGE_SIZE_CHANGED", () => { + requestAnimationFrame(() => resize()); + }); + //if (!runtime.isPackaged) checkCanvasSize() //only in editor +>>>>>>> e4a038b (Update threejsD.js) + } + } + + function startRenderLoop() { + if (running) return; + running = true; + + const loop = () => { + if (!running) return; + //RAPIER + if (physicsWorld && scene) { + physicsWorld.step(); + + scene.children.forEach((obj) => { + if (!obj.isMesh || !obj.physics) return; + if (obj.rigidBody) { + obj.position.copy(obj.rigidBody.translation()); + obj.quaternion.copy(obj.rigidBody.rotation()); + } + }); + } + if (scene && camera) { + if (controls) controls.update(); + + const delta = clock.getDelta(); + Object.values(models).forEach((model) => { + if (model) model.mixer.update(delta); + }); + + Object.values(lights).forEach((light) => updateShadowFrustum(light, camera.position)); + + //update custom effects time + customEffects.forEach((e) => { + if (e.uniforms.get("time")) { + e.uniforms.get("time").value += delta; + } + }); + Object.values(renderTargets).forEach((t) => { + if (t.camera.type == "PerspectiveCamera") { + t.camera.aspect = t.target.width / t.target.height; + t.camera.updateProjectionMatrix(); + } + // get meshes using the texture associated with this target + const displayMeshes = getMeshesUsingTexture(scene, t.target.texture); + + displayMeshes.forEach((mesh) => { + mesh.visible = false; + }); + + if (t.camera.type == "PerspectiveCamera") { + threeRenderer.setRenderTarget(t.target); + threeRenderer.clear(true, true, true); + threeRenderer.render(scene, t.camera); + } else { + t.target.clear(threeRenderer); + t.camera.update(threeRenderer, scene); //cubeCamera + } + + displayMeshes.forEach((mesh) => { + mesh.visible = true; + }); + }); + + camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height; + camera.updateProjectionMatrix(); + threeRenderer.setRenderTarget(null); + composer.render(delta); + } + + loopId = requestAnimationFrame(loop); + }; + + loopId = requestAnimationFrame(loop); + } + + function resize() { + const w = canvas.width; + const h = canvas.height; + + threeRenderer.setSize(w, h); + composer.setSize(w, h); + customEffects.forEach((e) => { + if (e.uniforms.get("resolution")) { + e.uniforms.get("resolution").value.set(w, h); + } + }); + + if (camera) { + camera.aspect = w / h; + camera.updateProjectionMatrix(); + } + } + //wait until all packages are loaded + Promise.resolve(load()).then(() => { + class threejsExtension { + getInfo() { + return { + id: "threejsExtension", + name: "Extra 3D", + color1: "#222222", + color2: "#222222", + color3: "#11cc99", + menuIconURI, + blockIconURI: menuIconURI, + + blocks: [{ + blockType: Scratch.BlockType.BUTTON, + text: "Show Docs", + func: "openDocs", + }, + { + blockType: Scratch.BlockType.BUTTON, + text: "Toggle Alerts", + func: "alerts", + }, + ], + menus: {}, + }; + } + openDocs() { + open("https://civ3ro.github.io/extensions/Documentation/"); + } + alerts() { + alerts = !alerts; + alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!"); + } + } + Scratch.extensions.register(new threejsExtension()); + + class ThreeRenderer { + getInfo() { + return { + id: "threeRenderer", + name: "Three Renderer", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "setRendererRatio", + blockType: Scratch.BlockType.COMMAND, + text: "set Pixel Ratio to [VALUE]", + arguments: { + VALUE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + }, + }, + { + opcode: "eulerOrder", + blockType: Scratch.BlockType.COMMAND, + text: "set euler order to [VALUE]", + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "YXZ", + }, + }, + }, + ], + menus: {}, + }; + } + + setRendererRatio(args) { + threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE); + } + eulerOrder(args) { + eulerOrder = args.VALUE; + console.log("euler order set to", eulerOrder); + } + } + Scratch.extensions.register(new ThreeRenderer()); + + class ThreeScene { + constructor() { + this.THREE = THREE; + this.scenes = {}; + } + + getInfo() { + return { + id: "threeScene", + name: "Three Scene", + color1: "#4638c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "newScene", + blockType: Scratch.BlockType.COMMAND, + text: "new Scene [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + }, + }, + + { + opcode: "setSceneProperty", + blockType: Scratch.BlockType.COMMAND, + text: "set Scene [PROPERTY] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "sceneProperties", + defaultValue: "background", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "new Color()", + exemptFromNormalization: true, + }, + }, + }, + "---", + { + opcode: "getSceneObjects", + blockType: Scratch.BlockType.REPORTER, + text: "get Scene [THING]", + arguments: { + THING: { + type: Scratch.ArgumentType.STRING, + menu: "sceneThings", + }, + }, + }, + { + opcode: "reset", + blockType: Scratch.BlockType.COMMAND, + text: "Reset Everything", + }, + ], + menus: { + sceneProperties: { + acceptReporters: false, + items: [{ + text: "Background", + value: "background", + }, + { + text: "Background Blurriness", + value: "backgroundBlurriness", + }, + { + text: "Background Intensity", + value: "backgroundIntensity", + }, + { + text: "Background Rotation", + value: "backgroundRotation", + }, + { + text: "Environment", + value: "environment", + }, + { + text: "Environment Intensity", + value: "environmentIntensity", + }, + { + text: "Environment Rotation", + value: "environmentRotation", + }, + { + text: "Fog", + value: "fog", + }, + ], + }, + sceneThings: { + acceptReporters: false, + items: ["Objects", "Materials", "Geometries", "Lights", "Scene Properties", "Other assets"], + }, + }, + }; + } + + newScene(args) { + scene = new THREE.Scene(); + scene.name = args.NAME; + scene.background = new THREE.Color("#222"); + //scene.add(new THREE.GridHelper(16, 16)) //future helper section? + this.scenes = { + ...this.scenes, + ...scene, + }; + resetor(0); + } + + reset() { + resetor(1); + } + + async setSceneProperty(args) { + const property = args.PROPERTY; + const value = getAsset(args.VALUE); + + scene[property] = value; + } + getSceneObjects(args) { + const names = []; + if (args.THING === "Objects") { + scene.traverse((obj) => { + if (obj.name) names.push(obj.name); //if it has a name, add to list! + }); + } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); + else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); + else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); + else if (args.THING === "Scene Properties") { + console.log(scene); + return "check console"; + } else if (args.THING === "Other assets") return JSON.stringify(assets); + + return JSON.stringify(names); // if objects + } + } + Scratch.extensions.register(new ThreeScene()); + + class ThreeCameras { + getInfo() { + return { + id: "threeCameras", + name: "Three Cameras", + color1: "#38c59bff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addCamera", + blockType: Scratch.BlockType.COMMAND, + text: "add camera [TYPE] [CAMERA] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "cameraTypes", + }, + }, + }, + { + opcode: "setCamera", + blockType: Scratch.BlockType.COMMAND, + text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "cameraProperties", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "0.1", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "getCamera", + blockType: Scratch.BlockType.REPORTER, + text: "get camera [PROPERTY] of [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "cameraProperties", + }, + }, + }, + "---", + { + opcode: "renderSceneCamera", + blockType: Scratch.BlockType.COMMAND, + text: "set rendering camera to [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + }, + }, + "---", + { + opcode: "cubeCamera", + blockType: Scratch.BlockType.COMMAND, + text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "cubeCamera", + }, + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + "---", + { + opcode: "renderTarget", + blockType: Scratch.BlockType.COMMAND, + text: "set a RenderTarget: [RT] for camera [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + { + opcode: "sizeTarget", + blockType: Scratch.BlockType.COMMAND, + text: "set RenderTarget [RT] size to [W] [H]", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + W: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 480, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 360, + }, + }, + }, + { + opcode: "getTarget", + blockType: Scratch.BlockType.REPORTER, + text: "get RenderTarget: [RT] texture", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + { + opcode: "removeTarget", + blockType: Scratch.BlockType.COMMAND, + text: "remove RenderTarget: [RT]", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + ], + menus: { + cameraTypes: { + acceptReporters: false, + items: [{ + text: "Perspective", + value: "PerspectiveCamera", + }, ], + }, + cameraProperties: { + acceptReporters: false, + items: [{ + text: "Near", + value: "near", + }, + { + text: "Far", + value: "far", + }, + { + text: "FOV", + value: "fov", + }, + { + text: "Focus (nothing...)", + value: "focus", + }, + { + text: "Zoom", + value: "zoom", + }, + ], + }, + }, + }; + } + addCamera(args) { + let v2 = new THREE.Vector2(); + threeRenderer.getSize(v2); + const object = new THREE.PerspectiveCamera(90, v2.x / v2.y); + object.position.z = 3; + + createObject(args.CAMERA, object, args.GROUP); + } + setCamera(args) { + let object = getObject(args.CAMERA); + object[args.PROPERTY] = args.VALUE; + object.updateProjectionMatrix(); + } + getCamera(args) { + let object = getObject(args.CAMERA); + const value = JSON.stringify(object[args.PROPERTY]); + return value; + } + renderSceneCamera(args) { + let object = getObject(args.CAMERA); + if (!object) return; + camera = object; + //reset composer, else it does not update. + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } + + cubeCamera(args) { + // Create cube render target + const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, { + generateMipmaps: true, + }); + // Create cube camera + const cubeCamera = new THREE.CubeCamera(0.1, 500, cubeRenderTarget); + createObject(args.CAMERA, cubeCamera, args.GROUP); + + renderTargets[args.RT] = { + target: cubeRenderTarget, + camera: cubeCamera, + }; + assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture; + } + + renderTarget(args) { + let object = getObject(args.CAMERA); + const renderTarget = new THREE.WebGLRenderTarget(360, 360, { + generateMipmaps: false, + }); + + renderTargets[args.RT] = { + target: renderTarget, + camera: object, + }; + assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; + } + sizeTarget(args) { + renderTargets[args.RT].target.setSize(args.W, args.H); + } + getTarget(args) { + const t = renderTargets[args.RT].target.texture; + console.log(t, renderTargets[args.RT]); + return `renderTargets/${t.uuid}`; + } + removeTarget(args) { + delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; + renderTargets[args.RT].target.dispose(); + delete renderTargets[args.RT]; + } + } + Scratch.extensions.register(new ThreeCameras()); + + class ThreeObjects { + getInfo() { + return { + id: "threeObjects", + name: "Three Objects", + color1: "#38c567ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addObject", + blockType: Scratch.BlockType.COMMAND, + text: "add object [OBJECT3D] [TYPE] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "objectTypes", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "cloneObject", + blockType: Scratch.BlockType.COMMAND, + text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myClone", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "setObject", + blockType: Scratch.BlockType.COMMAND, + text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectProperties", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + { + opcode: "getObject", + blockType: Scratch.BlockType.REPORTER, + text: "get [PROPERTY] of object [OBJECT3D]", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectProperties", + }, + }, + }, + { + opcode: "objectE", + blockType: Scratch.BlockType.BOOLEAN, + text: "is there an object [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "removeObject", + blockType: Scratch.BlockType.COMMAND, + text: "remove object [OBJECT3D] from scene", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: " ↳ Transforms", + }, + { + opcode: "setObjectV3", + extensions: ["colours_motion"], + blockType: Scratch.BlockType.COMMAND, + text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectVector3", + defaultValue: "position", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, + //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, + { + opcode: "getObjectV3", + extensions: ["colours_motion"], + blockType: Scratch.BlockType.REPORTER, + text: "get [PROPERTY] of [OBJECT3D]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectVector3", + defaultValue: "position", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Materials", + }, + { + opcode: "newMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "new material [NAME] [TYPE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "materialTypes", + defaultValue: "MeshStandardMaterial", + }, + }, + }, + { + opcode: "materialE", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.BOOLEAN, + text: "is there a material [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + }, + }, + { + opcode: "removeMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "remove material [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + }, + }, + { + opcode: "setMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [PROPERTY] of [NAME] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "materialProperties", + defaultValue: "color", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "new Color()", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "setBlending", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [NAME] blending to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + }, + }, + }, + { + opcode: "setDepth", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [NAME] depth to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "depthModes", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Geometries", + }, + { + opcode: "newGeometry", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "new geometry [NAME] [TYPE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "geometryTypes", + defaultValue: "BoxGeometry", + }, + }, + }, + { + opcode: "geometryE", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.BOOLEAN, + text: "is there a geometry [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + { + opcode: "removeGeometry", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "remove geometry [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + "---", + { + opcode: "newGeo", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "new empty geometry [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[points]", + }, + }, + }, + { + opcode: "geoPoints", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "set geometry [NAME] vertex points to [POINTS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[points]", + }, + }, + }, + { + opcode: "geoUVs", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "set geometry [NAME] UVs to [POINTS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[UVs]", + }, + }, + }, + "---", + { + opcode: "splines", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "create spline [NAME] from curve [CURVE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySpline", + }, + CURVE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[curve]", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "splineModel", + extensions: ["colours_operators"], + blockType: Scratch.BlockType.COMMAND, + text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySpline", + }, + MODEL: { + type: Scratch.ArgumentType.STRING, + menu: "modelsList", + }, + CURVE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[curve]", + exemptFromNormalization: true, + }, + SPACING: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.BUTTON, + text: "Convert font to JSON", + func: "openConv", + }, + { + blockType: Scratch.BlockType.BUTTON, + text: "Load JSON font file", + func: "loadFont", + }, + { + opcode: "text", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myText", + }, + TEXT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "C-369", + }, + FONT: { + type: Scratch.ArgumentType.STRING, + menu: "fonts", + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + D: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.1, + }, + CS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 6, + }, + }, + }, + ], + menus: { + objectVector3: { + acceptReporters: false, + items: [{ + text: "Positon", + value: "position", + }, + { + text: "Rotation", + value: "rotation", + }, + { + text: "Scale", + value: "scale", + }, + { + text: "Facing Direction (.up)", + value: "up", + }, + ], + }, + objectProperties: { + acceptReporters: false, + items: [{ + text: "Geometry", + value: "geometry", + }, + { + text: "Material", + value: "material", + }, + { + text: "Visible (true/false)", + value: "visible", + }, + ], + }, + objectTypes: { + acceptReporters: false, + items: [{ + text: "Mesh", + value: "Mesh", + }, + { + text: "Sprite", + value: "Sprite", + }, + { + text: "Points", + value: "Points", + }, + { + text: "Line", + value: "Line", + }, + { + text: "Group", + value: "Group", + }, + ], + }, + XYZ: { + acceptReporters: false, + items: [{ + text: "X", + value: "x", + }, + { + text: "Y", + value: "y", + }, + { + text: "Z", + value: "z", + }, + ], + }, + materialProperties: { + acceptReporters: false, + items: [ + "|GENERAL| <-- not a property", + { + text: "Color", + value: "color", + }, + { + text: "Map", + value: "map", + }, + { + text: "Opacity", + value: "opacity", + }, + { + text: "Transparent", + value: "transparent", + }, + { + text: "Alpha Map", + value: "alphaMap", + }, + { + text: "Alpha Test", + value: "alphaTest", + }, + { + text: "Depth Test", + value: "depthTest", + }, + { + text: "Depth Write", + value: "depthWrite", + }, + { + text: "Color Write", + value: "colorWrite", + }, + { + text: "Side", + value: "side", + }, + { + text: "Visible", + value: "visible", + }, + /* + { text: "Blending", value: "blending" }, + { text: "Blend Src", value: "blendSrc" }, + { text: "Blend Dst", value: "blendDst" }, + { text: "Blend Equation", value: "blendEquation" }, + { text: "Blend Src Alpha", value: "blendSrcAlpha" }, + { text: "Blend Dst Alpha", value: "blendDstAlpha" }, + { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ + { + text: "Blend Aplha", + value: "blendAplha", + }, + { + text: "Blend Color", + value: "blendColor", + }, + { + text: "Alpha Hash", + value: "alphaHash", + }, + { + text: "Premultiplied Alpha", + value: "premultipliedAlpha", + }, + + { + text: "Tone Mapped", + value: "toneMapped", + }, + { + text: "Fog", + value: "fog", + }, + { + text: "Flat Shading", + value: "flatShading", + }, + + "|MESH Standard / Physical| <-- not a property", + { + text: "Metalness", + value: "metalness", + }, + { + text: "Metalness Map", + value: "metalnessMap", + }, + { + text: "Roughness", + value: "roughness", + }, + { + text: "Reflectivity", + value: "reflectivity", + }, + { + text: "Roughness Map", + value: "roughnessMap", + }, + { + text: "Emissive", + value: "emissive", + }, + { + text: "Emissive Intensity", + value: "emissiveIntensity", + }, + { + text: "Emissive Map", + value: "emissiveMap", + }, + { + text: "Env Map", + value: "envMap", + }, + { + text: "Env Map Intensity", + value: "envMapIntensity", + }, + { + text: "Env Map Rotation", + value: "envMapRotation", + }, + { + text: "Ior", + value: "ior", + }, + { + text: "Refraction Ratio", + value: "refractionRatio", + }, + { + text: "Clearcoat", + value: "clearcoat", + }, + { + text: "Clearcoat Map", + value: "clearcoatMap", + }, + { + text: "Clearcoat Roughness", + value: "clearcoatRoughness", + }, + { + text: "Clearcoat Roughness Map", + value: "clearcoatRoughnessMap", + }, + { + text: "Dispersion", + value: "dispersion", + }, + { + text: "Sheen", + value: "sheen", + }, + { + text: "Sheen Color", + value: "sheenColor", + }, + { + text: "Sheen Color Map", + value: "sheenColorMap", + }, + { + text: "Sheen Roughness", + value: "sheenRoughness", + }, + { + text: "Sheen Roughness Map", + value: "sheenRoughnessMap", + }, + { + text: "Specular Color", + value: "specularColor", + }, + { + text: "Specular Color Map", + value: "specularColorMap", + }, + { + text: "Specular Intensity", + value: "specularIntensity", + }, + { + text: "Specular Intensity Map", + value: "specularIntensityMap", + }, + { + text: "Transmission", + value: "transmission", + }, + { + text: "Transmission Map", + value: "transmissionMap", + }, + { + text: "Thickness", + value: "thickness", + }, + { + text: "Thickness Map", + value: "thicknessMap", + }, + { + text: "Anisotropy", + value: "anisotropy", + }, + { + text: "Anisotropy Map", + value: "anisotropyMap", + }, + { + text: "Anisotropy Rotation", + value: "anisotropyRotation", + }, + { + text: "Attenuation Distance", + value: "attenuationDistance", + }, + { + text: "Attenuation Color", + value: "attenuationColor", + }, + { + text: "Thickness", + value: "thickness", + }, + { + text: "Iridescence", + value: "iridescence", + }, + { + text: "Iridescence Ior", + value: "iridescenceIOR", + }, + { + text: "Iridescence Map", + value: "iridescenceMap", + }, + { + text: "Iridescence Thickness Range", + value: "iridescenceThicknessRange", + }, + + "|MESH Displacement / Normal / Bump| <-- not a property", + { + text: "Displacement Map", + value: "displacementMap", + }, + { + text: "Displacement Scale", + value: "displacementScale", + }, + { + text: "Displacement Bias", + value: "displacementBias", + }, + { + text: "Bump Map", + value: "bumpMap", + }, + { + text: "Bump Scale", + value: "bumpScale", + }, + { + text: "Normal Map Type", + value: "normalMapType", + }, + + "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", + { + text: "Shininess", + value: "shininess", + }, + + { + text: "Wireframe", + value: "wireframe", + }, + { + text: "Wireframe Linewidth", + value: "wireframeLinewidth", + }, + { + text: "Wireframe Linecap", + value: "wireframeLinecap", + }, + { + text: "Wireframe Linejoin", + value: "wireframeLinejoin", + }, + + "|POINTS| <-- not a property", + { + text: "Size", + value: "size", + }, + { + text: "Size Attenuation", + value: "sizeAttenuation", + }, + + "|LINES| <-- not a property", + { + text: "Scale", + value: "scale", + }, + { + text: "Dash Size", + value: "dashSize", + }, + { + text: "Gap Size", + value: "gapSize", + }, + + "|SPRITES| <-- not a property", + { + text: "Rotation", + value: "rotation", + }, + ], + }, + blendModes: { + acceptReporters: false, + items: [{ + text: "No Blending", + value: "NoBlending", + }, + { + text: "Normal Blending", + value: "NormalBlending", + }, + { + text: "Additive Blending", + value: "AdditiveBlending", + }, + { + text: "Subtractive Blending", + value: "SubtractiveBlending", + }, + { + text: "Multiply Blending", + value: "MultiplyBlending", + }, + { + text: "Custom Blending", + value: "CustomBlending", + }, + ], + }, + depthModes: { + acceptReporters: false, + items: [{ + text: "Never Depth", + value: "NeverDepth", + }, + { + text: "Always Depth", + value: "AlwaysDepth", + }, + { + text: "Equal Depth", + value: "EqualDepth", + }, + { + text: "Less Depth", + value: "LessDepth", + }, + { + text: "Less Equal Depth", + value: "LessEqualDepth", + }, + { + text: "Greater Equal Depth", + value: "GreaterEqualDepth", + }, + { + text: "Greater Depth", + value: "GreaterDepth", + }, + { + text: "Not Equal Depth", + value: "NotEqualDepth", + }, + ], + }, + materialTypes: { + acceptReporters: false, + items: [{ + text: "Mesh Basic Material", + value: "MeshBasicMaterial", + }, + { + text: "Mesh Standard Material", + value: "MeshStandardMaterial", + }, + { + text: "Mesh Physical Material", + value: "MeshPhysicalMaterial", + }, + { + text: "Mesh Lambert Material", + value: "MeshLambertMaterial", + }, + { + text: "Mesh Phong Material", + value: "MeshPhongMaterial", + }, + { + text: "Mesh Depth Material", + value: "MeshDepthMaterial", + }, + { + text: "Mesh Normal Material", + value: "MeshNormalMaterial", + }, + { + text: "Mesh Matcap Material", + value: "MeshMatcapMaterial", + }, + { + text: "Mesh Toon Material", + value: "MeshToonMaterial", + }, + { + text: "Line Basic Material", + value: "LineBasicMaterial", + }, + { + text: "Line Dashed Material", + value: "LineDashedMaterial", + }, + { + text: "Points Material", + value: "PointsMaterial", + }, + { + text: "Sprite Material", + value: "SpriteMaterial", + }, + { + text: "Shadow Material", + value: "ShadowMaterial", + }, + ], + }, + textureModes: { + acceptReporters: false, + items: ["Pixelate", "Blur"], + }, + textureStyles: { + acceptReporters: false, + items: ["Repeat", "Clamp"], + }, + geometryTypes: { + acceptReporters: false, + items: [{ + text: "Box Geometry", + value: "BoxGeometry", + }, + { + text: "Sphere Geometry", + value: "SphereGeometry", + }, + { + text: "Cylinder Geometry", + value: "CylinderGeometry", + }, + { + text: "Plane Geometry", + value: "PlaneGeometry", + }, + { + text: "Circle Geometry", + value: "CircleGeometry", + }, + { + text: "Torus Geometry", + value: "TorusGeometry", + }, + { + text: "Torus Knot Geometry", + value: "TorusKnotGeometry", + }, + ], + }, + modelsList: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".glb")); + if (models.length < 1) return [ + ["Load a model! (GLB Loader category)"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + fonts: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".json")); + if (models.length < 1) return [ + ["Load a font!"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + }, + }; + } + + addObject(args) { + const object = new THREE[args.TYPE](); + + object.castShadow = true; + object.receiveShadow = true; + + createObject(args.OBJECT3D, object, args.GROUP); + } + cloneObject(args) { + let object = getObject(args.OBJECT3D); + const clone = object.clone(true); + clone.name; + createObject(args.NAME, clone, args.GROUP); + } + setObjectV3(args) { + let object = getObject(args.OBJECT3D); + let values = JSON.parse(args.VALUE); + + function degToRad(deg) { + return (deg * Math.PI) / 180; + } + + if (object.rigidBody) { + const x = values[0]; + const y = values[1]; + const z = values[2]; + if (args.PROPERTY === "rotation") { + const euler = new THREE.Euler(degToRad(x), degToRad(y), degToRad(z), "YXZ"); + const quaternion = new THREE.Quaternion(); + quaternion.setFromEuler(euler); + + object.rigidBody.setRotation({ + x: quaternion.x, + y: quaternion.y, + z: quaternion.z, + w: quaternion.w, + }); + } else if (args.PROPERTY === "position") { + object.rigidBody.setTranslation({ + x: x, + y: y, + z: z, + }, + true + ); + } + return; + } + + if (object.isCamera == true && controls) {} + + if (args.PROPERTY === "rotation") { + values = values.map((v) => (v * Math.PI) / 180); + object.rotation.set(0, 0, 0); + } + if (object.isDirectionalLight == true) { + object.pos = new THREE.Vector3(...values); + console.log(true, values, object.pos); + return; + } + object[args.PROPERTY].set(...values); + + if (object.type == "CubeCamera") object.updateCoordinateSystem(); + } + /* + changeObjectV3(args) { + getObject(args.OBJECT3D) + let values = JSON.parse(args.VALUE) + + if (args.PROPERTY === "rotation") { + values = values.map(v => v * Math.PI / 180); + object.rotation.x += values[0] + object.rotation.y += values[1] + object.rotation.z += values[2] + } + else { + object[args.PROPERTY].add(...values); + } + } + changeObjectXV3(args) { + getObject(args.OBJECT3D) + let value = args.VALUE + if (args.PROPERTY === "rotation") value = value * Math.PI / 180 + + object[args.PROPERTY][args.X] += value + } + */ + getObjectV3(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let values = vector3ToString(object[args.PROPERTY]); + if (args.PROPERTY === "rotation") { + const toDeg = Math.PI / 180; + values = [values[0] / toDeg, values[1] / toDeg, values[2] / toDeg]; + } + + return JSON.stringify(values); + } + setObject(args) { + let object = getObject(args.OBJECT3D); + let value = args.VALUE; + if (args.PROPERTY === "material") { + const mat = materials[args.NAME]; + if (mat) value = mat; + else value = undefined; + } else if (args.PROPERTY === "geometry") { + const geo = geometries[args.NAME]; + if (geo) value = geo; + else value = undefined; + } else value = !!value; + + if (value == undefined) return; //invalid geo/mat + object[args.PROPERTY] = value; + } + getObject(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let value; + if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; + else value = object.visible; + + return value; + } + removeObject(args) { + removeObject(args.OBJECT3D); + } + objectE(args) { + return scene.children.map((o) => o.name).includes(args.NAME); + } + + //defines + newMaterial(args) { + if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); + const mat = new THREE[args.TYPE](); + mat.name = args.NAME; + + materials[args.NAME] = mat; + } + async setMaterial(args) { + if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; + const mat = materials[args.NAME]; + + let value = args.VALUE; + + if (args.VALUE == "false") value = false; + + if (args.PROPERTY == "side") { + value = args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide; + } else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)); + else value = getAsset(value); + + console.log("o:", args.VALUE, typeof args.VALUE); + console.log("r:", value, typeof value); + + mat[args.PROPERTY] = await value; //await incase its a texture + mat.needsUpdate = true; + } + setBlending(args) { + const mat = materials[args.NAME]; + mat.blending = THREE[args.VALUE]; + mat.premultipliedAlpha = true; + mat.needsUpdate = true; + } + setDepth(args) { + const mat = materials[args.NAME]; + mat.depthFunc = THREE[args.VALUE]; + mat.needsUpdate = true; + } + removeMaterial(args) { + const mat = materials[args.NAME]; + mat.dispose(); + delete materials[args.NAME]; + } + materialE(args) { + return materials[args.NAME] ? true : false; + } + + newGeometry(args) { + if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); + const geo = new THREE[args.TYPE](); + geo.name = args.NAME; + + geometries[args.NAME] = geo; + } + setGeometry(args) { + const geo = geometries[args.NAME]; + geo[args.PROPERTY] = args.VALUE; + + geo.needsUpdate = true; + } + removeGeometry(args) { + const geo = geometries[args.NAME]; + geo.dispose(); + delete geometries[args.NAME]; + } + geometryE(args) { + return geometries[args.NAME] ? true : false; + } + + newGeo(args) { + const geometry = new THREE.BufferGeometry(); + geometry.name = args.NAME; + geometries[args.NAME] = geometry; + } + async geoPoints(args) { + const geometry = geometries[args.NAME]; + const positions = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v3 of each vertex of each triangle + + geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3)); + geometry.computeVertexNormals(); + + geometry.needsUpdate = true; + } + geoUVs(args) { + const geometry = geometries[args.NAME]; + const UVs = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v2 of each UV of each triangle + + geometry.setAttribute("uv", new THREE.BufferAttribute(new Float32Array(UVs), 2)); + geometry.needsUpdate = true; + } + + splines(args) { + const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); + geometry.name = args.NAME; + + geometries[args.NAME] = geometry; + } + + async splineModel(args) { + const model = await getModel(args.MODEL, args.NAME); + if (!model) return console.warn("Model not found:", args.MODEL); + + const curve = getAsset(args.CURVE); + const spacing = parseFloat(args.SPACING) || 1; + const curveLength = curve.getLength(); + const divisions = Math.floor(curveLength / spacing); + + const geomList = []; + const matList = []; + + for (let i = 0; i <= divisions; i++) { + const t = i / divisions; + const pos = curve.getPointAt(t); + const tangent = curve.getTangentAt(t); + + const temp = model.clone(true); + temp.position.copy(pos); + + const up = new THREE.Vector3(0, 0, 1); + const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()); + temp.quaternion.copy(quat); + + temp.updateMatrixWorld(true); + + temp.traverse((child) => { + if (child.isMesh && child.geometry) { + const geom = child.geometry.clone(); + geom.applyMatrix4(child.matrixWorld); + geomList.push(geom); + matList.push(child.material); //.clone() ? + } + }); + } + + const validGeoms = geomList.filter((g) => { + const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position; + if (!ok) console.warn("geometry skipped:", g); + return ok; + }); + + const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true); + merged.computeBoundingBox(); + merged.computeBoundingSphere(); + + merged.name = args.NAME; + geometries[args.NAME] = merged; + matList.name = args.NAME; + materials[args.NAME] = matList; + } + + async text(args) { + const fontFile = runtime + .getTargetForStage() + .getSounds() + .find((c) => c.name === args.FONT); + if (!fontFile) return; + + const json = new TextDecoder().decode(fontFile.asset.data.buffer); + const fontData = JSON.parse(json); + + const font = fontLoad.parse(fontData); + + const params = { + font: font, + size: JSON.parse(args.S), + height: JSON.parse(args.D), + curveSegments: JSON.parse(args.CS), + bevelEnabled: false, + }; + const geometry = new TextGeometry.TextGeometry(args.TEXT, params); + geometry.computeVertexNormals(); + geometry.center(); // optional, recenters the text + + geometry.name = args.NAME; + + geometries[args.NAME] = geometry; + } + + async loadFont() { + openFileExplorer(".json").then((files) => { + const file = files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + const arrayBuffer = e.target.result; + + // From lily's assets + // // Thank you PenguinMod for providing this code. + + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); + + const buffer = arrayBuffer; + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + alert("Font loaded successfully!"); + } catch (e) { + console.error(e); + alert("Error loading font."); + } + + // End of PenguinMod + }; + + reader.readAsArrayBuffer(file); + }); + } + openConv() { + { + open("https://gero3.github.io/facetype.js/"); + } + } + } + Scratch.extensions.register(new ThreeObjects()); + + class ThreeLights { + getInfo() { + return { + id: "threeLights", + name: "Three Lights", + color1: "#c7a22aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addLight", + blockType: Scratch.BlockType.COMMAND, + text: "add light [NAME] type [TYPE] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myLight", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "lightTypes", + }, + }, + }, + { + opcode: "setLight", + blockType: Scratch.BlockType.COMMAND, + text: "set light [NAME][PROPERTY] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "lightProperties", + defaultValue: "intensity", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myLight", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + exemptFromNormalization: true, + }, + }, + }, + ], + menus: { + lightTypes: { + acceptReporters: false, + items: [{ + text: "Ambient Light", + value: "AmbientLight", + }, + { + text: "Directional Light", + value: "DirectionalLight", + }, + { + text: "Point Light", + value: "PointLight", + }, + { + text: "Hemisphere Light", + value: "HemisphereLight", + }, + { + text: "Spot Light", + value: "SpotLight", + }, + ], + }, + lightProperties: { + acceptReporters: false, + items: [{ + text: "Color", + value: "color", + }, + { + text: "Intensity", + value: "intensity", + }, + { + text: "Cast Shadow?", + value: "castShadow", + }, + { + text: "Ground Color (HemisphereLight)", + value: "groundColor", + }, + { + text: "Map (SpotLight)", + value: "map", + }, + { + text: "Distance (SpotLight)", + value: "distance", + }, + { + text: "Decay (SpotLight)", + value: "decay", + }, + { + text: "Penumbra (SpotLight)", + value: "penumbra", + }, + { + text: "Angle/Size (SpotLight)", + value: "angle", + }, + { + text: "Power (SpotLight)", + value: "power", + }, + { + text: "Target Position (Directional/SpotLight)", + value: "target", + }, + ], + }, + }, + }; + } + + addLight(args) { + const light = new THREE[args.TYPE](0xffffff, 1); + + createObject(args.NAME, light, args.GROUP); + lights[args.NAME] = light; + if (light.type === "AmbientLight" || "HemisphereLight") return; + + light.castShadow = true; + if (light.type === "PointLight") return; + //Directional & Spot Light + light.target.position.set(0, 0, 0); + scene.add(light.target); + + light.pos = new THREE.Vector3(0, 0, 0); + + light.shadow.mapSize.width = 4096; + light.shadow.mapSize.height = 2048; + + if (light.type === "SpotLight") { + light.decay = 0; + light.shadow.camera.near = 500; + light.shadow.camera.far = 4000; + light.shadow.camera.fov = 30; + } + light.shadow.needsUpdate = true; + light.needsUpdate = true; + } + + setLight(args) { + const light = lights[args.NAME]; + if (!args.PROPERTY) return; + if (args.PROPERTY === "target") { + light.target.position.set(...JSON.parse(args.VALUE)); //vector3 + light.target.updateMatrixWorld(); + } else { + light[args.PROPERTY] = getAsset(args.VALUE); + } + light.needsUpdate = true; + + if (light.type === "AmbientLight" || "HemisphereLight") return; + + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; + } + } + Scratch.extensions.register(new ThreeLights()); + + class ThreeUtilities { + getInfo() { + return { + id: "threeUtility", + name: "Three Utilities", + color1: "#3875c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "newVector2", + blockType: Scratch.BlockType.REPORTER, + text: "New Vector [X] [Y]", + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "newVector3", + blockType: Scratch.BlockType.REPORTER, + text: "New Vector [X] [Y] [Z]", + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + }, + Z: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + "---", + { + opcode: "operateV3", + blockType: Scratch.BlockType.REPORTER, + text: "do [V3] [O] [V32]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + O: { + type: Scratch.ArgumentType.STRING, + menu: "operators", + }, + V32: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + { + opcode: "moveVector3", + blockType: Scratch.BlockType.REPORTER, + text: "move [S] steps in vector [V3] in direction [D3]", + arguments: { + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + D3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + { + opcode: "directionTo", + blockType: Scratch.BlockType.REPORTER, + text: "direction from [V3] to [T3]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,3]", + }, + T3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + "---", + { + opcode: "newColor", + blockType: Scratch.BlockType.REPORTER, + text: "New Color [HEX]", + arguments: { + HEX: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#9966ff", + }, + }, + }, + { + opcode: "newFog", + blockType: Scratch.BlockType.REPORTER, + text: "New Fog [COLOR] [NEAR] [FAR]", + arguments: { + COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#9966ff", + exemptFromNormalization: true, + }, + NEAR: { + type: Scratch.ArgumentType.NUMBER, + }, + FAR: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10, + }, + }, + }, + { + opcode: "newTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "textureStyles", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "newCubeTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", + arguments: { + COSTUMEX0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEX1: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEY0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEY1: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEZ0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEZ1: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "textureStyles", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "newEquirectangularTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Equirectangular Texture [COSTUME] [MODE]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + }, + }, + "---", + { + opcode: "curve", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.REPORTER, + text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", + arguments: { + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "curveTypes", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]", + }, + CLOSED: { + type: Scratch.ArgumentType.STRING, + defaultValue: "true", + }, + }, + }, + "---", + { + opcode: "mouseDown", + extensions: ["colours_sensing"], + blockType: Scratch.BlockType.BOOLEAN, + text: "mouse [BUTTON] [action]?", + arguments: { + BUTTON: { + type: Scratch.ArgumentType.STRING, + menu: "mouseButtons", + }, + action: { + type: Scratch.ArgumentType.STRING, + menu: "mouseAction", + }, + }, + }, + { + opcode: "mousePos", + extensions: ["colours_sensing"], + blockType: Scratch.BlockType.REPORTER, + text: "mouse position", + arguments: {}, + }, + "---", + { + opcode: "getItem", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.REPORTER, + text: "get item [ITEM] of [ARRAY]", + arguments: { + ITEM: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + ARRAY: { + type: Scratch.ArgumentType.STRING, + defaultValue: `["myObject", "myLight"]`, + }, + }, + }, + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Raycasting", + }, + { + opcode: "raycast", + blockType: Scratch.BlockType.COMMAND, + text: "Raycast from [V3] in direction [D3]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,3]", + }, + D3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,1]", + }, + }, + }, + { + opcode: "getRaycast", + blockType: Scratch.BlockType.REPORTER, + text: "get raycast [PROPERTY]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "raycastProperties", + }, + }, + }, + ], + menus: { + materialProperties: { + acceptReporters: false, + items: [{ + text: "Color", + value: "color", + }, + { + text: "Map (texture)", + value: "map", + }, + { + text: "Alpha Map (texture)", + value: "alphaMap", + }, + { + text: "Alpha Test (0-1)", + value: "alphaTest", + }, + { + text: "Side (front/back/double)", + value: "side", + }, + { + text: "Bump Map (texture)", + value: "bumpMap", + }, + { + text: "Bump Scale", + value: "bumpScale", + }, + ], + }, + textureModes: { + acceptReporters: false, + items: ["Pixelate", "Blur"], + }, + textureStyles: { + acceptReporters: false, + items: ["Repeat", "Clamp"], + }, + raycastProperties: { + acceptReporters: false, + items: [{ + text: "Intersected Object Names", + value: "name", + }, + { + text: "Number of Objects", + value: "number", + }, + { + text: "Intersected Objects distances", + value: "distance", + }, + ], + }, + mouseButtons: { + acceptReporters: false, + items: ["left", "middle", "right"], + }, + mouseAction: { + acceptReporters: false, + items: ["Down", "Clicked"], + }, + curveTypes: { + acceptReporters: false, + items: ["CatmullRomCurve3"], + }, + operators: { + acceptReporters: false, + items: ["+", "-", "*", "/", "=", "max", "min", "dot", "cross", "distance to", "angle to", "apply euler"], + }, + }, + }; + } + mouseDown(args) { + if (args.action === "Down") return isMouseDown[args.BUTTON]; + if (args.action === "Clicked") { + if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false; + else prevMouse[args.BUTTON] = true; + return true; + } + } + mousePos(event) { + return JSON.stringify(mouseNDC); + } + newVector3(args) { + return JSON.stringify([args.X, args.Y, args.Z]); + } + operateV3(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)); + const v32 = new THREE.Vector3(...JSON.parse(args.V32)); + + let r; + if (args.O == "+") r = v3.add(v32); + else if (args.O == "-") r = v3.sub(v32); + else if (args.O == "*") r = v3.multiply(v32); + else if (args.O == "/") r = v3.divide(v32); + else if (args.O == "=") r = v3.equals(v32); + else if (args.O == "max") r = v3.max(v32); + else if (args.O == "min") r = v3.min(v32); + else if (args.O == "dot") r = v3.dot(v32); + else if (args.O == "cross") r = v3.cross(v32); + else if (args.O == "distance to") r = v3.distanceTo(v32); + else if (args.O == "angle to") r = v3.angleTo(v32); + else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)); + + if (typeof r == "object") return JSON.stringify([r.x, r.y, r.z]); + else return JSON.stringify(r); + } + + newVector2(args) { + return JSON.stringify([args.X, args.Y]); + } + + moveVector3(args) { + const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); + const steps = Number(args.S); + + const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); + + const yaw = THREE.MathUtils.degToRad(yawInputDeg); + const pitch = THREE.MathUtils.degToRad(pitchInputDeg); + const roll = THREE.MathUtils.degToRad(rollInputDeg); + + const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); + + const forwardVector = new THREE.Vector3(0, 0, -1); + const direction = forwardVector.applyEuler(euler).normalize(); + + const newPos = currentPos.add(direction.multiplyScalar(steps)); + return JSON.stringify([newPos.x, newPos.y, newPos.z]); + } + + directionTo(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)); + const toV3 = new THREE.Vector3(...JSON.parse(args.T3)); + + const direction = toV3.clone().sub(v3).normalize(); + // Pitch (X) + const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)); + // Yaw (Y) + const yaw = Math.atan2(direction.x, direction.z); + + // Roll always 0 + return JSON.stringify([180 + THREE.MathUtils.radToDeg(pitch), THREE.MathUtils.radToDeg(yaw), 0]); + } + + newColor(args) { + const color = new THREE.Color(args.HEX); + const uuid = crypto.randomUUID(); + assets.colors[uuid] = color; + return `colors/${uuid}`; + } + newFog(args) { + const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR); + const uuid = crypto.randomUUID(); + assets.fogs[uuid] = fog; + return `fogs/${uuid}`; + } + async newTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newCubeTexture(args) { + const uris = [ + encodeCostume(args.COSTUMEX0), + encodeCostume(args.COSTUMEX1), + encodeCostume(args.COSTUMEY0), + encodeCostume(args.COSTUMEY1), + encodeCostume(args.COSTUMEZ0), + encodeCostume(args.COSTUMEZ1), + ]; + const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); + const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); + + texture.name = "CubeTexture" + args.COSTUMEX0; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newEquirectangularTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + texture.mapping = THREE.EquirectangularReflectionMapping; + + setTexutre(texture, args.MODE); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + + curve(args) { + function parsePoints(input) { + // Match all [x,y,z] groups + const matches = input.match(/\[([^\]]+)\]/g); + if (!matches) return []; + + return matches.map((str) => { + const nums = str + .replace(/[\[\]\s]/g, "") + .split(",") + .map(Number); + return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0); + }); + } + const points = parsePoints(args.POINTS); + const curve = new THREE[args.TYPE](points); + curve.closed = JSON.parse(args.CLOSED); + + const uuid = crypto.randomUUID(); + assets.curves[uuid] = curve; + return `curves/${uuid}`; + } + + getItem(args) { + const items = JSON.parse(args.ARRAY); + const item = items[args.ITEM - 1]; + if (!item) return "0"; + return item; + } + + raycast(args) { + const origin = new THREE.Vector3(...JSON.parse(args.V3)); + // rotation is in degrees => convert to radians first + const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); + + const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder); + const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize(); + + const raycaster = new THREE.Raycaster(); + //const camera = getObject(args.CAMERA) + raycaster.set(origin, direction); + + const intersects = raycaster.intersectObjects(scene.children, true); + + raycastResult = intersects; + } + getRaycast(args) { + if (args.PROPERTY === "number") return raycastResult.length; + if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map((i) => i.distance)); + return JSON.stringify(raycastResult.map((i) => i.object[args.PROPERTY])); + } + } + Scratch.extensions.register(new ThreeUtilities()); + + class ThreeGLB { + getInfo() { + return { + id: "threeGLB", + name: "Three GLB Loader", + color1: "#c53838ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + blockType: Scratch.BlockType.BUTTON, + text: "Load GLB File", + func: "loadModelFile", + }, + { + opcode: "addModel", + blockType: Scratch.BlockType.COMMAND, + text: "add [ITEM] as [NAME] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + ITEM: { + type: Scratch.ArgumentType.STRING, + menu: "modelsList", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + }, + }, + { + opcode: "getModel", + blockType: Scratch.BlockType.REPORTER, + text: "get object [PROPERTY] of [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "modelProperties", + }, + }, + }, + { + opcode: "playAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "play animation [ANAME] of [NAME], [TIMES] times", + arguments: { + TIMES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "pauseAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "set [TOGGLE] animation [ANAME] of [NAME]", + arguments: { + TOGGLE: { + type: Scratch.ArgumentType.NUMBER, + menu: "pauseUn", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "stopAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "stop animation [ANAME] of [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + ], + menus: { + modelProperties: { + acceptReporters: false, + items: [{ + text: "Animations", + value: "animations", + }, ], + }, + pauseUn: { + acceptReporters: true, + items: [{ + text: "Pause", + value: "true", + }, + { + text: "Unpasue", + value: "false", + }, + ], + }, + modelsList: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".glb")); + if (models.length < 1) return [ + ["Load a model!"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + }, + }; + } + + async loadModelFile() { + openFileExplorer(".glb").then((files) => { + const file = files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + const arrayBuffer = e.target.result; + + { + // From lily's assets + + // Thank you PenguinMod for providing this code. + { + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); + + //const res = await Scratch.fetch(args.URL); + //const buffer = await res.arrayBuffer(); + const buffer = arrayBuffer; + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + alert("Model loaded successfully!"); + } catch (e) { + console.error(e); + alert("Error loading model."); + } + } + // End of PenguinMod + } + }; + + reader.readAsArrayBuffer(file); + }); + } + async addModel(args) { + const group = await getModel(args.ITEM, args.NAME); + + createObject(args.NAME, group, args.GROUP); + } + getModel(args) { + if (!models[args.NAME]) return; + return Object.keys(models[args.NAME].actions).toString(); + } + + playAnimation(args) { + const model = models[args.NAME]; + if (!model) { + console.log("no model!"); + return; + } + + const action = model.actions[args.ANAME]; //clones of models dont have a stored actions! + if (!action) { + console.log("no action!"); + return; + } + + args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity); + + action.reset().play(); + } + stopAnimation(args) { + const model = models[args.NAME]; + if (!model) return; + + const action = model.actions[args.ANAME]; + if (action) action.stop(); + } + pauseAnimation(args) { + const model = models[args.NAME]; + if (!model) return; + + const action = model.actions[args.ANAME]; + if (action) action.paused = args.TOGGLE; + } + } + Scratch.extensions.register(new ThreeGLB()); + + class ThreeAddons { + getInfo() { + return { + id: "threeAddons", + name: "Three Addons", + color1: "#c538a2ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + blockType: Scratch.BlockType.LABEL, + text: "Orbit Control", + }, + { + opcode: "OrbitControl", + blockType: Scratch.BlockType.COMMAND, + text: "set addon Orbit Control [STATE]", + arguments: { + STATE: { + type: Scratch.ArgumentType.STRING, + menu: "onoff", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "Post Processing", + }, + { + opcode: "resetComposer", + blockType: Scratch.BlockType.COMMAND, + text: "reset composer", + }, + { + opcode: "bloom", + blockType: Scratch.BlockType.COMMAND, + text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + I: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.5, + }, + T: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.5, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + }, + }, + { + opcode: "godRays", + blockType: Scratch.BlockType.COMMAND, + text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + DEC: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.95, + }, + DENS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + EXP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.1, + }, + WEI: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.4, + }, + RES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + SAMP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 64, + }, + }, + }, + { + opcode: "dots", + blockType: Scratch.BlockType.COMMAND, + text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + A: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: 0, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + }, + }, + { + opcode: "depth", + blockType: Scratch.BlockType.COMMAND, + text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", + arguments: { + FD: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 3, + }, + FL: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.001, + }, + BS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 4, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 240, + }, + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "NORMAL", + }, + }, + }, + "---", + { + opcode: "custom", + blockType: Scratch.BlockType.COMMAND, + text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myShader", + }, + FRA: { + type: Scratch.ArgumentType.STRING, + }, + VER: { + type: Scratch.ArgumentType.STRING, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "NORMAL", + }, + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + ], + menus: { + onoff: { + acceptReporters: true, + items: [{ + text: "enabled", + value: "1", + }, + { + text: "disabled", + value: "0", + }, + ], + }, + blendModes: { + acceptReporters: false, + items: [ + "SKIP", + "SET", + "ADD", + "ALPHA", + "AVERAGE", + "COLOR", + "COLOR_BURN", + "COLOR_DODGE", + "DARKEN", + "DIFFERENCE", + "DIVIDE", + "DST", + "EXCLUSION", + "HARD_LIGHT", + "HARD_MIX", + "HUE", + "INVERT", + "INVERT_RGB", + "LIGHTEN", + "LINEAR_BURN", + "LINEAR_DODGE", + "LINEAR_LIGHT", + "LUMINOSITY", + "MULTIPLY", + "NEGATION", + "NORMAL", + "OVERLAY", + "PIN_LIGHT", + "REFLECT", + "SCREEN", + "SRC", + "SATURATION", + "SOFT_LIGHT", + "SUBTRACT", + "VIVID_LIGHT", + ], + }, + }, + }; + } + + OrbitControl(args) { + if (controls) controls.dispose(); + + console.log("creating...", OrbitControls); + controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); + controls.enableDamping = true; + + controls.enabled = !!args.STATE; + console.log(controls); + } + + resetComposer() { + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } + + bloom(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const bloomEffect = new BloomEffect({ + intensity: args.I, + luminanceThreshold: args.T, // ← correct key + luminanceSmoothing: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + bloomEffect.blendMode.opacity.value = args.OP; + + const pass = new EffectPass(camera, bloomEffect); + + composer.addPass(pass); + } + + godRays(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + let object = getObject(args.NAME); + const sun = object; + + const godRays = new GodRaysEffect(camera, sun, { + resolutionScale: args.RES, + density: args.DENS, // ray density + decay: args.DEC, // fade out + weight: args.WEI, // brightness of rays + exposure: args.EXP, + samples: args.SAMP, + blendFunction: BlendFunction[args.BLEND], + }); + godRays.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, godRays); + composer.addPass(pass); + } + + dots(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dot = new DotScreenEffect({ + angle: args.A, + scale: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + dot.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, dot); + composer.addPass(pass); + } + + depth(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dofEffect = new DepthOfFieldEffect(camera, { + focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) + focalLength: args.FL, // lens focal length in meters + bokehScale: args.BS, // strength/size of the blur circles + height: args.H, // resolution hint (affects quality/perf) + blendFunction: BlendFunction[args.BLEND], + }); + dofEffect.blendMode.opacity.value = args.OP; + + const dofPass = new EffectPass(camera, dofEffect); + composer.addPass(dofPass); + } + + async custom(args) { + function cleanGLSL(glslCode) { + //delete multilines comments + let cleanedCode = glslCode + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/ /g, "\n") + .replace(/\/\/.*$/gm, " ") + .replace(/; /g, ";\n"); + + return cleanedCode; + } + + let fs = cleanGLSL(` + ${args.FRA} + `); + if (!args.FRA.trim()) { + fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`; + } + const vs = cleanGLSL(` + ${args.VER} + `); + console.log(fs); + console.log(vs); + + const effect = new Effect("Custom", fs, { + blendFunction: BlendFunction[args.BLEND], + vertexShader: vs, + uniforms: new Map([ + //uniforms usually in shaders... open to more! + ["time", new THREE.Uniform(0.0)], + [ + "resolution", + new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height)), + ], + ]), + defines: new Map([ + ["USE_TIME", "1"], + ["USE_VERTEX_TRANSFORM", ""], + ]), + }); + + effect.blendMode.opacity.value = args.OP; + + const pass = new EffectPass(camera, effect); + composer.addPass(pass); + + customEffects.push(effect); + } + } + Scratch.extensions.register(new ThreeAddons()); + + class RapierPhysics { + getInfo() { + return { + id: "rapierPhysics", + name: "RAPIER Physics", + color1: "#222222", + color2: "#203024ff", + color3: "#78f07eff", + blocks: [{ + opcode: "createWorld", + blockType: Scratch.BlockType.COMMAND, + text: "create world | gravity:[G]", + arguments: { + G: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,-9.81,0]", + }, + }, + }, + { + opcode: "getWorld", + blockType: Scratch.BlockType.REPORTER, + text: "get world [PROPERTY]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "wProp", + }, + }, + }, + "---", + { + opcode: "objectPhysics", + blockType: Scratch.BlockType.COMMAND, + text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", + arguments: { + state2: { + type: Scratch.ArgumentType.STRING, + menu: "state2", + }, + state: { + type: Scratch.ArgumentType.STRING, + menu: "state", + defaultValue: "true", + }, + type: { + type: Scratch.ArgumentType.STRING, + menu: "objectTypes", + defaultValue: "dynamic", + }, + collider: { + type: Scratch.ArgumentType.STRING, + menu: "colliderTypes", + defaultValue: "cuboid", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + mass: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + density: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + friction: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0.5", + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.LABEL, + text: "- RigidBody", + }, + { + opcode: "setRB", + blockType: Scratch.BlockType.COMMAND, + text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "rigidBodySets", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "getRB", + blockType: Scratch.BlockType.REPORTER, + text: "get rigidbody [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "rigidBodyProperties", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "lockObjectAxis", + blockType: Scratch.BlockType.COMMAND, + text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", + arguments: { + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "lockAxes", + }, + X: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + Y: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + Z: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + }, + }, + "---", + { + opcode: "addForce", + blockType: Scratch.BlockType.COMMAND, + text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,10,0]", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "forces", + defaultValue: "addForce", + }, + SPACE: { + type: Scratch.ArgumentType.STRING, + menu: "spaces", + defaultValue: "world", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "resetForces", + blockType: Scratch.BlockType.COMMAND, + text: "reset [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "resetF", + defaultValue: "resetForces", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "enableCCD", + blockType: Scratch.BlockType.COMMAND, + text: "enable Continuous Collision Detection for [OBJECT] [state]", + arguments: { + state: { + type: Scratch.ArgumentType.STRING, + menu: "state", + defaultValue: "true", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "oPropS", + defaultValue: "physics", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "fixedJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + RA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + RB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + { + opcode: "sphericalJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + }, + }, + { + opcode: "revoluteJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + X: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.LABEL, + text: "- Collider", + }, + { + opcode: "setC", + blockType: Scratch.BlockType.COMMAND, + text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "colliderSets", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "getC", + blockType: Scratch.BlockType.REPORTER, + text: "get collider [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "colliderProperties", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "sensorSingle", + blockType: Scratch.BlockType.BOOLEAN, + text: "is sensor [SENSOR] touching [OBJECT]?", + arguments: { + SENSOR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySensor", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "sensorAll", + blockType: Scratch.BlockType.REPORTER, + text: "objects touching sensor [SENSOR]", + arguments: { + SENSOR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySensor", + }, + }, + }, + ], + menus: { + wProp: { + acceptReporters: false, + items: [{ + text: "Gravity", + value: "gravity", + }, + { + text: "log to console", + value: "log", + }, + ], + }, + tf: { + acceptReporters: true, + items: [{ + text: "false", + value: "false", + }, + { + text: "true", + value: "true", + }, + ], + }, + lockAxes: { + acceptReporters: false, + items: [{ + text: "Translation", + value: "setEnabledTranslations", + }, + { + text: "Rotation", + value: "setEnabledRotations", + }, + ], + }, + rigidBodyProperties: { + acceptReporters: false, + items: [{ + text: "Type", + value: "bodyType", + }, + { + text: "Linear Velocity", + value: "linvel", + }, + { + text: "Angular Velocity", + value: "angvel", + }, + { + text: "Translation (position)", + value: "translation", + }, + { + text: "Rotation (quaternion)", + value: "rotation", + }, + { + text: "Mass", + value: "mass", + }, + //{text: "Center of Mass", value: "centerOfMass"}, + { + text: "Linear Damping", + value: "linearDamping", + }, + { + text: "Angular Damping", + value: "angularDamping", + }, + { + text: "Is Sleeping?", + value: "isSleeping", + }, + //{text: "Can Sleep?", value: "isCanSleep"}, + { + text: "Gravity Scale", + value: "gravityScale", + }, + { + text: "Is Fixed?", + value: "isFixed", + }, + { + text: "Is Dynamic?", + value: "isDynamic", + }, + { + text: "Is Kinematic?", + value: "isKinematic", + }, + //{text: "Sleeping", value: "sleeping"} + ], + }, + rigidBodySets: { + acceptReporters: false, + items: [ + //{text: "Linear Velocity", value: "setLinvel"}, + //{text: "Angular Velocity", value: "setAngvel"}, + //{text: "Mass", value: "setMass"}, + { + text: "Gravity Scale", + value: "setGravityScale", + }, + //{text: "Can Sleep?", value: "setCanSleep"}, + //{text: "Sleeping", value: "sleeping"}, + { + text: "Linear Damping", + value: "setLinearDamping", + }, + { + text: "Angular Damping", + value: "setAngularDamping", + }, + { + text: "Is Fixed?", + value: "isFixed", + }, + { + text: "Is Dynamic?", + value: "isDynamic", + }, + { + text: "Is Kinematic?", + value: "isKinematic", + }, + ], + }, + colliderProperties: { + acceptReporters: false, + items: [ + //{text: "Collider Type", value: "type"}, + { + text: "Is Sensor?", + value: "isSensor", + }, + { + text: "Friction", + value: "friction", + }, + { + text: "Restitution", + value: "restitution", + }, + { + text: "Density", + value: "density", + }, + { + text: "Mass", + value: "mass", + }, + { + text: "Position", + value: "translation", + }, + { + text: "Rotation", + value: "rotation", + }, + //{text: "Area", value: "area"}, + { + text: "Volume", + value: "volume", + }, + { + text: "Collision Groups", + value: "collisionGroups", + }, + //{text: "Collision Mask", value: "collisionMask"}, + //{text: "Is Enabled?", value: "enabled"}, + //{text: "Contact Count", value: "contactCount"}, + //{text: "RigidBody Handle", value: "rigidBody"} + ], + }, + colliderSets: { + acceptReporters: false, + items: [{ + text: "Friction", + value: "setFriction", + }, + { + text: "Restitution", + value: "setRestitution", + }, + { + text: "Density", + value: "setDensity", + }, + { + text: "Is Sensor?", + value: "setSensor", + }, + { + text: "Collision Groups", + value: "setCollisionGroups", + }, + //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool + //{text: "Position Offset", value: "setTranslation"}, + //{text: "Rotation Offset", value: "setRotation"} + ], + }, + state: { + acceptReporters: true, + items: [{ + text: "on", + value: "true", + }, + { + text: "off", + value: "false", + }, + ], + }, + state2: { + acceptReporters: true, + items: [{ + text: "false", + value: "false", + }, + { + text: "true (must be fixed)", + value: "true", + }, + ], + }, + spaces: { + acceptReporters: false, + items: [{ + text: "World", + value: "world", + }, + { + text: "Local", + value: "local", + }, + ], + }, + objectTypes: { + acceptReporters: false, + items: [{ + text: "Dynamic", + value: "dynamic", + }, + { + text: "Fixed", + value: "fixed", + }, + { + text: "Kinematic Position Based", + value: "kinematicPositionBased", + }, + ], + }, + colliderTypes: { + acceptReporters: false, + items: [{ + text: "Box, Rectangle, cuboid", + value: "cuboid", + }, + { + text: "Sphere, ball", + value: "ball", + }, + { + text: "Custom, complex simple shapes, convexHull", + value: "convexHull", + }, + { + text: "Precision, TriMesh", + value: "trimesh", + }, + ], + }, + forces: { + acceptReporters: false, + items: [{ + text: "Force", + value: "addForce", + }, + { + text: "Torque (rotation)", + value: "addTorque", + }, + { + text: "Apply Impulse", + value: "applyImpulse", + }, + { + text: "Apply Torque Impulse (rotation)", + value: "applyTorqueImpulse", + }, + { + text: "Linear Velocity", + value: "setLinvel", + }, + { + text: "Angular Velocity", + value: "setAngvel", + }, + ], + }, + resetF: { + acceptReporters: false, + items: [{ + text: "Forces", + value: "resetForces", + }, + { + text: "Torques", + value: "resetTorques", + }, + ], + }, + }, + }; + } + joint(jointData, bodyA, bodyB) { + physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); + } + + fixedJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + let RA = JSON.parse(args.RA).map(Number); + let RB = JSON.parse(args.RB).map(Number); + + RA = new THREE.Quaternion().setFromEuler( + new THREE.Euler( + THREE.MathUtils.degToRad(RA[0]), + THREE.MathUtils.degToRad(RA[1]), + THREE.MathUtils.degToRad(RA[2]) + ) + ); + RB = new THREE.Quaternion().setFromEuler( + new THREE.Euler( + THREE.MathUtils.degToRad(RB[0]), + THREE.MathUtils.degToRad(RB[1]), + THREE.MathUtils.degToRad(RB[2]) + ) + ); + + const data = RAPIER.JointData.fixed({ + x: VA[0], + y: VA[1], + z: VA[2], + }, + RA, { + x: VB[0], + y: VB[1], + z: VB[2], + }, + RB + ); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + sphericalJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + + const data = RAPIER.JointData.spherical({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + revoluteJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + const x = JSON.parse(args.X).map(Number); + + const data = RAPIER.JointData.revolute({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }, { + x: x[0], + y: x[1], + z: x[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + prismaticJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + const x = JSON.parse(args.X).map(Number); + + const data = RAPIER.JointData.prismatic({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }, { + x: x[0], + y: x[1], + z: x[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + createWorld(args) { + const v3 = JSON.parse(args.G).map(Number); + const gravity = { + x: v3[0], + y: v3[1], + z: v3[2], + }; + physicsWorld = new RAPIER.World(gravity); + + console.log(physicsWorld); + } + + getWorld(args) { + if (args.PROPERTY === "log") { + console.log(physicsWorld); + return "logged"; + } + return JSON.stringify(physicsWorld[args.PROPERTY]); + } + + setRB(args) { + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.rigidBody[args.PROPERTY](value); + } + setC(args) { + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.collider[args.PROPERTY](value); + } + + getRB(args) { + let object = getObject(args.OBJECT); + return JSON.stringify(object.rigidBody[args.PROPERTY]()); + } + getC(args) { + let object = getObject(args.OBJECT); + return JSON.stringify(object.collider[args.PROPERTY]()); + } + + lockObjectAxis(args) { + let object = getObject(args.OBJECT); + const x = !JSON.parse(args.X); + const y = !JSON.parse(args.Y); + const z = !JSON.parse(args.Z); + object.rigidBody[args.PROPERTY](x, y, z, true); //changes is xyz, wake up + } + + objectPhysics(args) { + let object = getObject(args.OBJECT); + object.physics = JSON.parse(args.state); + + if (JSON.parse(args.state)) { + //if already exists delete: + if (object.rigidBody) { + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; + } + /*asing a rigidbody and collider to object and add them to physicsWorld*/ + let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() + .setTranslation(object.position.x, object.position.y, object.position.z) + .setRotation({ + w: object.quaternion._w, + x: object.quaternion._x, + y: object.quaternion._y, + z: object.quaternion._z, + }); + + let colliderDesc; + switch (args.collider) { + case "cuboid": + colliderDesc = createCuboidCollider(object); + break; + case "ball": + colliderDesc = createBallCollider(object); + break; + case "convexHull": + colliderDesc = createConvexHullCollider(object); + break; + case "trimesh": + colliderDesc = TriMesh(object); + break; + } + colliderDesc + .setSensor(JSON.parse(args.state2)) + .setMass(args.mass) + .setDensity(args.density) + .setFriction(args.friction); + + let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc); + let collider = physicsWorld.createCollider(colliderDesc, rigidBody); + + object.rigidBody = rigidBody; + object.collider = collider; + } else { + /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; + } + } + + enableCCD(args) { + let object = getObject(args.OBJECT); + if (object.physics) { + let rigidBody = object.rigidBody; + rigidBody.enableCcd(JSON.parse(args.state)); + } + } + + addForce(args) { + let object = getObject(args.OBJECT); + const vector = JSON.parse(args.VALUE).map(Number); + + let force = new THREE.Vector3(vector[0], vector[1], vector[2]); + if (args.SPACE === "local") { + force.applyQuaternion(object.quaternion); + } + + object.rigidBody[args.PROPERTY](force, true); + } + + resetForces(args) { + rigidBody[args.PROPERTY](true); + } + + sensorSingle(args) { + const sensor = getObject(args.SENSOR); + + let object = getObject(args.OBJECT); + + let touching = false; + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + if (otherCollider === object.collider) touching = true; + }); + + return touching; + } + + sensorAll(args) { + const sensor = getObject(args.SENSOR); + + const touchedObjects = []; + + // loop thruogh every collider touching sensor + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + // find owner of collider + const otherObject = scene.children.find((o) => o.collider === otherCollider); + console.log(otherCollider); + if (otherObject) touchedObjects.push(otherObject.name); + }); + + return JSON.stringify(touchedObjects); + } + } + Scratch.extensions.register(new RapierPhysics()); + + //Thanks to the PointerLock extension of Turbowarp + const mouse = vm.runtime.ioDevices.mouse; + let isLocked = false; + let isPointerLockEnabled = false; + + let rect = threeRenderer.domElement.getBoundingClientRect(); + document.addEventListener("resize", () => { + rect = threeRenderer.domElement.getBoundingClientRect(); + }); + + const postMouseData = (e, isDown) => { + const { + movementX, + movementY + } = e; + const { + width, + height + } = rect; + const x = mouse._clientX + movementX; + const y = mouse._clientY - movementY; + mouse._clientX = x; + mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); + mouse._clientY = y; + mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); + if (typeof isDown === "boolean") { + const data = { + button: e.button, + isDown, + }; + originalPostIOData(data); + } + }; + + const mouseDevice = vm.runtime.ioDevices.mouse; + const originalPostIOData = mouseDevice.postData.bind(mouseDevice); + mouseDevice.postData = (data) => { + if (!isPointerLockEnabled) { + return originalPostIOData(data); + } + }; + + document.addEventListener( + "mousedown", + (e) => { + // @ts-expect-error + if (threeRenderer.domElement.contains(e.target)) { + if (isLocked) { + postMouseData(e, true); + } else if (isPointerLockEnabled) { + threeRenderer.domElement.requestPointerLock(); + } + } + }, + true + ); + document.addEventListener( + "mouseup", + (e) => { + if (isLocked) { + postMouseData(e, false); + // @ts-expect-error + } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { + threeRenderer.domElement.requestPointerLock(); + } + }, + true + ); + document.addEventListener( + "mousemove", + (e) => { + if (isLocked) { + postMouseData(e); + } + }, + true + ); + + document.addEventListener("pointerlockchange", () => { + isLocked = document.pointerLockElement === threeRenderer.domElement; + }); + document.addEventListener("pointerlockerror", (e) => { + console.error("Pointer lock error", e); + }); + + const oldStep = vm.runtime._step; + vm.runtime._step = function(...args) { + const ret = oldStep.call(this, ...args); + if (isPointerLockEnabled) { + const { + width, + height + } = rect; + mouse._clientX = width / 2; + mouse._clientY = height / 2; + mouse._scratchX = 0; + mouse._scratchY = 0; + } + return ret; + }; + + vm.runtime.on("PROJECT_LOADED", () => { + isPointerLockEnabled = false; + if (isLocked) { + document.exitPointerLock(); + } + }); + + class Pointerlock { + getInfo() { + return { + id: "threepointerlockmod", + name: "Pointerlock for Extra 3D", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "setLocked", + blockType: Scratch.BlockType.COMMAND, + text: "set pointer lock [enabled]", + arguments: { + enabled: { + type: Scratch.ArgumentType.STRING, + defaultValue: "true", + menu: "enabled", + }, + }, + }, + { + opcode: "isLocked", + blockType: Scratch.BlockType.BOOLEAN, + text: "pointer locked?", + }, + ], + menus: { + enabled: { + acceptReporters: true, + items: [{ + text: "enabled", + value: "true", + }, + { + text: "disabled", + value: "false", + }, + ], + }, + }, + }; + } + + setLocked(args) { + isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; + if (!isPointerLockEnabled && isLocked) { + document.exitPointerLock(); + } + } + + isLocked() { + return isLocked; + } + } + Scratch.extensions.register(new Pointerlock()); + }); +})(Scratch); diff --git a/threejsD_BASE_13852.js b/threejsD_BASE_13852.js new file mode 100644 index 0000000..ced1928 --- /dev/null +++ b/threejsD_BASE_13852.js @@ -0,0 +1,2413 @@ +// Name: Extra 3D +// ID: threejsExtension +// Description: Use three js inside Turbowarp! A 3D graphics library. +// By: Civero +// License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors + +(function (Scratch) { + "use strict"; + + if (!Scratch.extensions.unsandboxed) { + throw new Error("Three-D extension must run unsandboxed"); + } + + if (Scratch.vm.runtime.isPackaged) {alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); return} + //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return + + const vm = Scratch.vm; + const runtime = vm.runtime + const renderer = Scratch.renderer; + const canvas = renderer.canvas + const Cast = Scratch.Cast; + const menuIconURI = "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; + + let alerts = false + console.log("alerts are "+ (alerts ? "enabled" : "disabled")) + + let isMouseDown = { left: false, middle: false, right: false } + let prevMouse = { left: false, middle: false, right: false } + + let lastWidth = 0 + let lastHeight = 0 + + let THREE + let clock + let running + let loopId + //Addons + let GLTFLoader + let gltf + let OrbitControls + let controls + let BufferGeometryUtils + let TextGeometry + let fontLoad + //Physics + let RAPIER + let physicsWorld + + let threeRenderer + let scene + let camera + let eulerOrder = "YXZ" + + let composer + let passes = {} + let customEffects = [] + let renderTargets = {} + + let materials = {} + let geometries = {} + let lights = {} + let models = {} + + let assets = { //should i place materials, geometries; inside too? + textures: {}, + colors: {}, + fogs: {}, + curves: {}, + renderTargets: {}, //not the same as the global one! this one only stores textures + } + + let raycastResult = [] + + function resetor(level) { + camera = undefined + composer.reset() + + passes = {} + customEffects = [] + renderTargets = {} + + materials = {} + geometries = {} + lights = {} + models = {} + + if (level > 0) { + assets = { + textures: {}, + colors: {}, + fogs: {}, + curves: {}, + renderTargets: {}, + } + } + + updateComposers() + } + +//utility + function vector3ToString(prop) { + if (!prop) return "0,0,0"; + + const x = (typeof(prop.x) === "number") ? prop.x : (typeof(prop._x) === "number") ? prop._x : (JSON.stringify(prop).includes("X")) ? prop : 0 + const y = (typeof(prop.y) === "number") ? prop.y : (typeof(prop.y) === "number") ? prop._y : 0 + const z = (typeof(prop.z) === "number") ? prop.z : (typeof(prop.z) === "number") ? prop.z : 0 + + return [x, y, z] + } + +//objects + function createObject(name, content, parentName) { + let object = getObject(name, true) + if (object) { + removeObject(name) + alerts ? alert(name + " already exsisted, will replace!") : null + } + content.name = name + content.rotation._order = eulerOrder + parentName === scene.name ? object = scene : object = getObject(parentName) + content.physics = false + + object.add(content) + } + function removeObject(name) { + let object = getObject(name) + if (!object) return + + scene.remove(object) + + if (object.rigidBody) { + physicsWorld.removeCollider(object.collider, true) + physicsWorld.removeRigidBody(object.rigidBody, true) + object.rigidBody = null + object.collider = null + } + if (object.isLight) { + delete(lights[name]) + } + } + function getObject(name, isNew) { + let object = null + if (!scene) { + alerts ? alert("Can not get " + name + ". Create a scene first!") : null; return;} + object = scene.getObjectByName(name) + if (!object && !isNew) {alerts ? alert(name + " does not exist! Add it to scene"):null; return;} + return object + } + +//materials + function encodeCostume (name) { + if (name.startsWith("data:image/")) return name + return Scratch.vm.editingTarget.sprite.costumes.find(c => c.name === name).asset.encodeDataURI() + } + function setTexutre (texture, mode, style, x, y) { + texture.colorSpace = THREE.SRGBColorSpace + + if (mode === "Pixelate") { + texture.minFilter = THREE.NearestFilter; + texture.magFilter = THREE.NearestFilter; + } else { //Blur + texture.minFilter = THREE.NearestMipmapLinearFilter + texture.magFilter = THREE.NearestMipmapLinearFilter + } + + if (style === "Repeat") { + texture.wrapS = THREE.RepeatWrapping + texture.wrapT = THREE.RepeatWrapping + texture.repeat.set(x, y) + } + + texture.generateMipmaps = true; + } + async function resizeImageToSquare(uri, size = 256) { + return new Promise((resolve) => { + const img = new Image() + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + const ctx = canvas.getContext('2d') + + // clear + draw image scaled to fit canvas + ctx.clearRect(0, 0, size, size) + ctx.drawImage(img, 0, 0, size, size) + + resolve(canvas.toDataURL()) // return normalized Data URI + //delete canvas? + }; + img.src = uri + }); +} +//light +function updateShadowFrustum(light, focusPos) { + if (light.type !== "DirectionalLight") return + + // Frustum Size - Increase this value to cover a larger area. + const d = 50; + + // Update Orthographic Shadow Camera Frustum + const shadowCamera = light.shadow.camera; + + // Set the width/height of the frustum + shadowCamera.left = -d; + shadowCamera.right = d; + shadowCamera.top = d; + shadowCamera.bottom = -d; + + // Determine ranges + shadowCamera.near = 0.1 + shadowCamera.far = 500 + + // Position the Light and its Target + light.target.position.copy(focusPos); + const direction = light.position.clone().sub(light.target.position).normalize(); + light.position.copy(focusPos.clone().add(direction.multiplyScalar(100))); + + // Ensure matrices are updated. + light.target.updateMatrixWorld(); + light.shadow.camera.updateProjectionMatrix() + light.shadow.needsUpdate = true; +} +//composer +function updateComposers() { + if (!camera || !scene) return; // nothing to do yet + + // always recreate the RenderPass to point to the current scene/camera + passes["Render"] = new RenderPass(scene, camera); + + // ensure composer has a RenderPass as the first pass + const hasRender = composer.passes.some(p => p && p.scene); + if (!hasRender) composer.addPass(passes["Render"]); + else { + // if composer already has one, replace it so it references current scene/camera + const idx = composer.passes.findIndex(p => p && p.scene); + composer.passes[idx] = passes["Render"]; + } +} +//utility +function getMouseNDC(event) { + // Use threeRenderer.domElement for correct offset + const rect = threeRenderer.domElement.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + return [x, y]; +} +function checkCanvasSize() { + const { width, height } = canvas + if (width !== lastWidth || height !== lastHeight) { + lastWidth = width + lastHeight = height + resize() + } + requestAnimationFrame(checkCanvasSize) //rerun next frame +} +//physics +function computeWorldBoundingBox(mesh) { + // Create a Box3 in world coordinates + const box = new THREE.Box3().setFromObject(mesh); + const size = new THREE.Vector3(); + box.getSize(size); + const center = new THREE.Vector3(); + box.getCenter(center); + return { size, center }; +} +function createCuboidCollider(mesh) { + const { size } = computeWorldBoundingBox(mesh); + const collider = RAPIER.ColliderDesc.cuboid( + size.x / 2, + size.y / 2, + size.z / 2 + ) + return collider; +} +function createBallCollider(mesh) { + const { size } = computeWorldBoundingBox(mesh); + // radius = 1/2 of the largest verticie + const radius = Math.max(size.x, size.y, size.z) / 2; + const collider = RAPIER.ColliderDesc.ball(radius) + return collider //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) +} +function createConvexHullCollider(mesh) { + mesh.updateWorldMatrix(true, false); + + const position = mesh.geometry.attributes.position; + const vertices = []; + const vertex = new THREE.Vector3(); + + // Matrix for scale only + const scaleMatrix = new THREE.Matrix4().makeScale( + mesh.scale.x, + mesh.scale.y, + mesh.scale.z + ); + + for (let i = 0; i < position.count; i++) { + vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); + vertices.push(vertex.x, vertex.y, vertex.z); + } + + const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); + return collider; +} +function TriMesh(mesh) { + // Get the positions array (from your geoPoints function) +const positions = mesh.geometry.attributes.position.array; +const numVertices = positions.length / 3; + +// Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] +const indices = Array.from({ length: numVertices }, (_, i) => i); + +const collider = RAPIER.ColliderDesc.trimesh( + positions, + new Uint32Array(indices) +); + +return collider +} +function getModel(model, name) { + const file = runtime.getTargetForStage().getSounds().find(c => c.name === model) + if (!file) return + +return new Promise((resolve, reject) => { + gltf.parse( + file.asset.data.buffer, + "", + gltf => { + const root = gltf.scene + root.traverse(child => { + if (child.isMesh) { + child.castShadow = true + child.receiveShadow = true + } + }); + + const mixer = new THREE.AnimationMixer(root) + const actions = {} + gltf.animations.forEach(clip => { + const act = mixer.clipAction(clip) + act.clampWhenFinished = true + actions[clip.name] = act + }); + + models[name] = { root, mixer, actions } + resolve(root) + }, + error => { + console.error("Error parsing GLB model:", error) + reject(error) + } + )}) +} +async function openFileExplorer(format) { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file" + input.accept = format + input.multiple = false + input.onchange = () => { + resolve(input.files) + input.remove() + }; + input.click(); + }) +} +function getMeshesUsingTexture(scene, targetTexture) { + const meshes = [] + + scene.traverse(object => { + if (object.material) { + const materials = Array.isArray(object.material) ? object.material : [object.material] + for (const material of materials) { + if (material.map === targetTexture) { + meshes.push(object) + break + } + } + } + }) + + return meshes +} +function getAsset(path) { + if (typeof(path) == "string") { //string? + if (path.includes("/")) { //has the /? + const value = path.split("/") + console.log(value[0], value[1]) + return assets[value[0]][value[1]] + } + } + + return JSON.parse(path) //boolean or number +} + +let mouseNDC = [0, 0] +//loops/init +function stopLoop() { + if (!running) return + running = false + + if (loopId) { + cancelAnimationFrame(loopId) + loopId = null + if (threeRenderer) threeRenderer.clear(); + } +} +async function load() { + if (!THREE) { + + // @ts-ignore + THREE = await import("https://esm.sh/three@0.180.0") + //Addons + GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js") + OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js") + BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js") + TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js") + const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js") + fontLoad = new FontLoader.FontLoader() + + const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8") + const { + EffectComposer, + EffectPass, + RenderPass, + + Effect, + BloomEffect, + GodRaysEffect, + DotScreenEffect, + DepthOfFieldEffect, + + BlendFunction + } = POSTPROCESSING + //so i can use them later as global + window.EffectComposer = EffectComposer; + window.EffectPass = EffectPass; + window.RenderPass = RenderPass; + window.Effect = Effect; + window.BloomEffect = BloomEffect; + window.GodRaysEffect = GodRaysEffect; + window.DotScreenEffect = DotScreenEffect; + window.DepthOfFieldEffect = DepthOfFieldEffect; + window.BlendFunction = BlendFunction; + + RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0") + await RAPIER.init() + + threeRenderer = new THREE.WebGLRenderer({ + powerPreference: "high-performance", + antialias: false, + stencil: false, + depth: true + }) + threeRenderer.setPixelRatio(window.devicePixelRatio) + threeRenderer.outputColorSpace = THREE.SRGBColorSpace // correct colors + threeRenderer.toneMapping = THREE.ACESFilmicToneMapping // HDR look (test) + //threeRenderer.toneMappingExposure = 1.0 //(test) + + threeRenderer.shadowMap.enabled = true + threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap // (optional) + threeRenderer.domElement.style.pointerEvents = 'auto' //will disable turbowarp mouse events, but enable threejs's + + gltf = new GLTFLoader.GLTFLoader() + clock = new THREE.Clock() + + // Example: create a composer + composer = new EffectComposer(threeRenderer, {frameBufferType: THREE.HalfFloatType}) + + renderer.addOverlay( threeRenderer.domElement, "manual" ) + renderer.addOverlay(canvas, "manual") + renderer.setBackgroundColor(1, 1, 1, 0) + + resize() + + window.addEventListener("mousedown", e => { + if (e.button === 0) isMouseDown.left = true + if (e.button === 1) isMouseDown.middle = true + if (e.button === 2) isMouseDown.right = true + }) + window.addEventListener("mouseup", e => { + if (e.button === 0) isMouseDown.left = false; prevMouse.left = false + if (e.button === 1) isMouseDown.middle = false; prevMouse.middle = false + if (e.button === 2) isMouseDown.right = false; prevMouse.right = false + }) + // prevent contextmenu on right click + threeRenderer.domElement.addEventListener("contextmenu", e => e.preventDefault()); + + threeRenderer.domElement.addEventListener('mousemove', (event) => { + mouseNDC = getMouseNDC(event); + }) + + running = false + load() + + startRenderLoop() + runtime.on('PROJECT_START', () => startRenderLoop()) + runtime.on('PROJECT_STOP_ALL', () => stopLoop()) + runtime.on('STAGE_SIZE_CHANGED', () => {requestAnimationFrame(() => resize())}) + //if (!runtime.isPackaged) checkCanvasSize() //only in editor + } + } +function startRenderLoop() { + if (running) return + running = true + + const loop = () => { + if (!running) return + //RAPIER + if (physicsWorld && scene) { + physicsWorld.step() + + scene.children.forEach(obj => { + if (!(obj.isMesh) || !(obj.physics)) return + if (obj.rigidBody) { + obj.position.copy(obj.rigidBody.translation()) + obj.quaternion.copy(obj.rigidBody.rotation()) + } + }) + + } + if (scene && camera) { + if (controls) controls.update() + + const delta = clock.getDelta() + Object.values(models).forEach( model => { if (model) model.mixer.update(delta) } ) + + Object.values(lights).forEach(light => updateShadowFrustum(light, camera.position)) + + //update custom effects time + customEffects.forEach(e => { + if (e.uniforms.get('time')) { + e.uniforms.get('time').value += delta + } + }) + Object.values(renderTargets).forEach(t => { + if ( t.camera.type == "PerspectiveCamera") { + t.camera.aspect = t.target.width / t.target.height + t.camera.updateProjectionMatrix() + } + // get meshes using the texture associated with this target + const displayMeshes = getMeshesUsingTexture(scene, t.target.texture) + + displayMeshes.forEach(mesh => { + mesh.visible = false + }) + + if (t.camera.type == "PerspectiveCamera") { + threeRenderer.setRenderTarget(t.target) + threeRenderer.clear(true, true, true) + threeRenderer.render(scene, t.camera) + } else { + t.target.clear(threeRenderer) + t.camera.update( threeRenderer, scene ) //cubeCamera + } + + displayMeshes.forEach(mesh => { + mesh.visible = true + }) + }) + + camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height + camera.updateProjectionMatrix() + threeRenderer.setRenderTarget(null) + composer.render(delta) + } + + loopId = requestAnimationFrame(loop) + } + + loopId = requestAnimationFrame(loop) +} + +function resize() { + const w = canvas.width + const h = canvas.height + + threeRenderer.setSize(w, h) + composer.setSize(w, h) + customEffects.forEach(e => { + if (e.uniforms.get('resolution')) { + e.uniforms.get('resolution').value.set(w,h) + } + }) + + if (camera) { + camera.aspect = w / h + camera.updateProjectionMatrix() +} +} +//wait until all packages are loaded +Promise.resolve(load()).then(() => { + + class threejsExtension { + getInfo() { + return { + id: "threejsExtension", + name: "Extra 3D", + color1: "#222222", + color2: "#222222", + color3: "#11cc99", + menuIconURI, + blockIconURI: menuIconURI, + + blocks: [ + {blockType: Scratch.BlockType.BUTTON, text: "Show Docs", func: "openDocs"}, + {blockType: Scratch.BlockType.BUTTON, text: "Toggle Alerts", func: "alerts"}, + ], + menus: {} + }} + openDocs(){ + open("https://civ3ro.github.io/extensions/Documentation/") + } + alerts() {alerts = !alerts; alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!")} + } + Scratch.extensions.register(new threejsExtension()) + + class ThreeRenderer { + getInfo() { + return { + id: "threeRenderer", + name: "Three Renderer", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "setRendererRatio", blockType: Scratch.BlockType.COMMAND, text: "set Pixel Ratio to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"}}}, + {opcode: "eulerOrder", blockType: Scratch.BlockType.COMMAND, text: "set euler order to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "YXZ"}}}, + ], + menus: {} + }} + + setRendererRatio(args) { + threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE) + } + eulerOrder(args) { + eulerOrder = args.VALUE + console.log("euler order set to", eulerOrder) + } + + } + Scratch.extensions.register(new ThreeRenderer()) + + class ThreeScene { + constructor() { + this.THREE = THREE; + this.scenes = {}; + } + + getInfo() { + return { + id: "threeScene", + name: "Three Scene", + color1: "#4638c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "newScene", blockType: Scratch.BlockType.COMMAND, text: "new Scene [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}}}, + + {opcode: "setSceneProperty", blockType: Scratch.BlockType.COMMAND, text: "set Scene [PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "sceneProperties", defaultValue: "background"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, + "---", + {opcode: "getSceneObjects", blockType: Scratch.BlockType.REPORTER, text: "get Scene [THING]", arguments:{THING: {type: Scratch.ArgumentType.STRING, menu: "sceneThings"}}}, + {opcode: "reset", blockType: Scratch.BlockType.COMMAND, text: "Reset Everything"} + ], + menus: { + sceneProperties: {acceptReporters: false, items: [ + {text: "Background", value: "background"},{text: "Background Blurriness", value: "backgroundBlurriness"},{text: "Background Intensity", value: "backgroundIntensity"},{text: "Background Rotation", value: "backgroundRotation"}, + {text: "Environment", value: "environment"},{text: "Environment Intensity", value: "environmentIntensity"},{text: "Environment Rotation", value: "environmentRotation"},{text: "Fog", value: "fog"}, + ]}, + sceneThings: {acceptReporters: false, items: ["Objects", "Materials", "Geometries","Lights","Scene Properties","Other assets"]}, + + } + }} + + newScene(args) { + scene = new THREE.Scene(); + scene.name = args.NAME + scene.background = new THREE.Color("#222") + //scene.add(new THREE.GridHelper(16, 16)) //future helper section? + this.scenes = {...this.scenes, {scene}}; + resetor(0) + } + + reset() { + resetor(1) + } + + async setSceneProperty(args) { + const property = args.PROPERTY; + const value = getAsset(args.VALUE); + + scene[property] = value; + } + getSceneObjects(args){ + const names = []; + if (args.THING === "Objects") { + scene.traverse(obj => { + if (obj.name) names.push(obj.name); //if it has a name, add to list! + }); + } + else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)) + else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)) + else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)) + else if (args.THING === "Scene Properties") {console.log(scene); return "check console"} + else if (args.THING === "Other assets") return JSON.stringify(assets) + + return JSON.stringify(names); // if objects + } + + } + Scratch.extensions.register(new ThreeScene()) + + class ThreeCameras { + getInfo() { + return { + id: "threeCameras", + name: "Three Cameras", + color1: "#38c59bff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "addCamera", blockType: Scratch.BlockType.COMMAND, text: "add camera [TYPE] [CAMERA] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "cameraTypes"}}}, + {opcode: "setCamera", blockType: Scratch.BlockType.COMMAND, text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "0.1", exemptFromNormalization: true}}}, + {opcode: "getCamera", blockType: Scratch.BlockType.REPORTER, text: "get camera [PROPERTY] of [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}}}, + "---", + {opcode: "renderSceneCamera", blockType: Scratch.BlockType.COMMAND, text: "set rendering camera to [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}}}, + "---", + {opcode: "cubeCamera", blockType: Scratch.BlockType.COMMAND, text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "cubeCamera"}, GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, + "---", + {opcode: "renderTarget", blockType: Scratch.BlockType.COMMAND, text: "set a RenderTarget: [RT] for camera [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, + {opcode: "sizeTarget", blockType: Scratch.BlockType.COMMAND, text: "set RenderTarget [RT] size to [W] [H]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, W: {type: Scratch.ArgumentType.NUMBER, defaultValue: 480}, H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 360},} }, + {opcode: "getTarget", blockType: Scratch.BlockType.REPORTER, text: "get RenderTarget: [RT] texture", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, + {opcode: "removeTarget", blockType: Scratch.BlockType.COMMAND, text: "remove RenderTarget: [RT]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, + ], + menus: { + cameraTypes: {acceptReporters: false, items: [ + {text: "Perspective", value: "PerspectiveCamera"}, + ]}, + cameraProperties: {acceptReporters: false, items: [ + {text: "Near", value: "near"},{text: "Far", value: "far"},{text: "FOV", value: "fov"},{text: "Focus (nothing...)", value: "focus"},{text: "Zoom", value: "zoom"}, + ]}, + } + }} + addCamera(args) { + let v2 = new THREE.Vector2() + threeRenderer.getSize(v2) + const object = new THREE.PerspectiveCamera(90, v2.x / v2.y ) + object.position.z = 3 + + createObject(args.CAMERA, object, args.GROUP) + } + setCamera(args) { + let object = getObject(args.CAMERA) + object[args.PROPERTY] = args.VALUE + object.updateProjectionMatrix() + } + getCamera(args) { + let object = getObject(args.CAMERA) + const value = JSON.stringify(object[args.PROPERTY]) + return value + } + renderSceneCamera(args) { + let object = getObject(args.CAMERA) + if (!object) return + camera = object + //reset composer, else it does not update. + composer.passes = [] + passes = {} + customEffects = [] + updateComposers() + } + + cubeCamera(args) { + // Create cube render target + const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true } ) + // Create cube camera + const cubeCamera = new THREE.CubeCamera( 0.1, 500, cubeRenderTarget ) + createObject(args.CAMERA, cubeCamera, args.GROUP) + + renderTargets[args.RT] = {target: cubeRenderTarget, camera: cubeCamera} + assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture + } + + renderTarget(args) { + let object = getObject(args.CAMERA) + const renderTarget = new THREE.WebGLRenderTarget( + 360, + 360, + { + generateMipmaps: false + } + ) + + renderTargets[args.RT] = {target: renderTarget, camera: object} + assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture + } + sizeTarget(args) { + renderTargets[args.RT].target.setSize(args.W, args.H) + } + getTarget(args) { + const t = renderTargets[args.RT].target.texture + console.log(t, renderTargets[args.RT]) + return `renderTargets/${t.uuid}` + } + removeTarget(args) { + delete(assets.renderTargets[renderTargets[args.RT].target.texture.uuid]) + renderTargets[args.RT].target.dispose() + delete(renderTargets[args.RT]) + } + } + Scratch.extensions.register(new ThreeCameras()) + + class ThreeObjects { + getInfo() { + return { + id: "threeObjects", + name: "Three Objects", + color1: "#38c567ff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "addObject", blockType: Scratch.BlockType.COMMAND, text: "add object [OBJECT3D] [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},TYPE: {type: Scratch.ArgumentType.STRING, menu: "objectTypes"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + {opcode: "cloneObject", blockType: Scratch.BlockType.COMMAND, text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myClone"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "setObject", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, + {opcode: "getObject", blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of object [OBJECT3D]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}}}, + {opcode: "objectE", blockType: Scratch.BlockType.BOOLEAN, text: "is there an object [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "removeObject", blockType: Scratch.BlockType.COMMAND, text: "remove object [OBJECT3D] from scene", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + + {blockType: Scratch.BlockType.LABEL, text: " ↳ Transforms"}, + {opcode: "setObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, + //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, + //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, + {opcode: "getObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of [OBJECT3D]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + + {blockType: Scratch.BlockType.LABEL, text: "↳ Materials"}, + {opcode: "newMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "new material [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "materialTypes", defaultValue: "MeshStandardMaterial"}}}, + {opcode: "materialE",extensions: ["colours_looks"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a material [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, + {opcode: "removeMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "remove material [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, + {opcode: "setMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [PROPERTY] of [NAME] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "materialProperties", defaultValue: "color"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, + {opcode: "setBlending",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] blending to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "blendModes"}}}, + {opcode: "setDepth",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] depth to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "depthModes"}}}, + + {blockType: Scratch.BlockType.LABEL, text: "↳ Geometries"}, + {opcode: "newGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new geometry [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "geometryTypes", defaultValue: "BoxGeometry"}}}, + {opcode: "geometryE",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a geometry [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, + {opcode: "removeGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "remove geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, + "---", + {opcode: "newGeo",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new empty geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, + {opcode: "geoPoints",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] vertex points to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, + {opcode: "geoUVs",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] UVs to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[UVs]"}}}, + "---", + {opcode: "splines", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create spline [NAME] from curve [CURVE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}}}, + {opcode: "splineModel", extensions: ["colours_operators"], blockType: Scratch.BlockType.COMMAND, text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, MODEL: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}, SPACING: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, + "---", + {blockType: Scratch.BlockType.BUTTON, text: "Convert font to JSON", func: "openConv"}, + {blockType: Scratch.BlockType.BUTTON, text: "Load JSON font file", func: "loadFont"}, + {opcode: "text", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myText"}, TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "C-369"}, FONT: {type: Scratch.ArgumentType.STRING, menu: "fonts"}, S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, D: {type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1}, CS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 6}}}, + ], + menus: { + objectVector3: {acceptReporters: false, items: [ + {text: "Positon", value: "position"},{text: "Rotation", value: "rotation"},{text: "Scale", value: "scale"},{text: "Facing Direction (.up)", value: "up"} + ]}, + objectProperties: {acceptReporters: false, items: [ + {text: "Geometry", value: "geometry"},{text: "Material", value: "material"},{text: "Visible (true/false)", value: "visible"}, + ]}, + objectTypes: { acceptReporters: false, items: [ + { text: "Mesh", value: "Mesh" }, { text: "Sprite", value: "Sprite" }, { text: "Points", value: "Points" }, { text: "Line", value: "Line" }, { text: "Group", value: "Group" } + ]}, + XYZ: {acceptReporters: false, items: [{text: "X", value: "x"},{text: "Y", value: "y"},{text: "Z", value: "z"}]}, + materialProperties: {acceptReporters: false, items: [ + "|GENERAL| <-- not a property", + { text: "Color", value: "color" }, + { text: "Map", value: "map" }, + { text: "Opacity", value: "opacity" }, + { text: "Transparent", value: "transparent" }, + { text: "Alpha Map", value: "alphaMap" }, + { text: "Alpha Test", value: "alphaTest" }, + { text: "Depth Test", value: "depthTest" }, + { text: "Depth Write", value: "depthWrite" }, + { text: "Color Write", value: "colorWrite" }, + { text: "Side", value: "side" }, + { text: "Visible", value: "visible" },/* + { text: "Blending", value: "blending" }, + { text: "Blend Src", value: "blendSrc" }, + { text: "Blend Dst", value: "blendDst" }, + { text: "Blend Equation", value: "blendEquation" }, + { text: "Blend Src Alpha", value: "blendSrcAlpha" }, + { text: "Blend Dst Alpha", value: "blendDstAlpha" }, + { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ + { text: "Blend Aplha", value: "blendAplha" }, + { text: "Blend Color", value: "blendColor" }, + { text: "Alpha Hash", value: "alphaHash" }, + { text: "Premultiplied Alpha", value: "premultipliedAlpha" }, + + { text: "Tone Mapped", value: "toneMapped" }, + { text: "Fog", value: "fog" }, + { text: "Flat Shading", value: "flatShading" }, + + "|MESH Standard / Physical| <-- not a property", + { text: "Metalness", value: "metalness" }, + { text: "Metalness Map", value: "metalnessMap" }, + { text: "Roughness", value: "roughness" }, + { text: "Reflectivity", value: "reflectivity" }, + { text: "Roughness Map", value: "roughnessMap" }, + { text: "Emissive", value: "emissive" }, + { text: "Emissive Intensity", value: "emissiveIntensity" }, + { text: "Emissive Map", value: "emissiveMap" }, + { text: "Env Map", value: "envMap" }, + { text: "Env Map Intensity", value: "envMapIntensity" }, + { text: "Env Map Rotation", value: "envMapRotation" }, + { text: "Ior", value: "ior" }, + { text: "Refraction Ratio", value: "refractionRatio" }, + { text: "Clearcoat", value: "clearcoat" }, + { text: "Clearcoat Map", value: "clearcoatMap" }, + { text: "Clearcoat Roughness", value: "clearcoatRoughness" }, + { text: "Clearcoat Roughness Map", value: "clearcoatRoughnessMap" }, + { text: "Dispersion", value: "dispersion" }, + { text: "Sheen", value: "sheen" }, + { text: "Sheen Color", value: "sheenColor" }, + { text: "Sheen Color Map", value: "sheenColorMap" }, + { text: "Sheen Roughness", value: "sheenRoughness" }, + { text: "Sheen Roughness Map", value: "sheenRoughnessMap" }, + { text: "Specular Color", value: "specularColor" }, + { text: "Specular Color Map", value: "specularColorMap" }, + { text: "Specular Intensity", value: "specularIntensity" }, + { text: "Specular Intensity Map", value: "specularIntensityMap" }, + { text: "Transmission", value: "transmission" }, + { text: "Transmission Map", value: "transmissionMap" }, + { text: "Thickness", value: "thickness" }, + { text: "Thickness Map", value: "thicknessMap" }, + { text: "Anisotropy", value: "anisotropy" }, + { text: "Anisotropy Map", value: "anisotropyMap" }, + { text: "Anisotropy Rotation", value: "anisotropyRotation" }, + { text: "Attenuation Distance", value: "attenuationDistance" }, + { text: "Attenuation Color", value: "attenuationColor" }, + { text: "Thickness", value: "thickness" }, + { text: "Iridescence", value: "iridescence" }, + { text: "Iridescence Ior", value: "iridescenceIOR" }, + { text: "Iridescence Map", value: "iridescenceMap" }, + { text: "Iridescence Thickness Range", value: "iridescenceThicknessRange" }, + + "|MESH Displacement / Normal / Bump| <-- not a property", + { text: "Displacement Map", value: "displacementMap" }, + { text: "Displacement Scale", value: "displacementScale" }, + { text: "Displacement Bias", value: "displacementBias" }, + { text: "Bump Map", value: "bumpMap" }, + { text: "Bump Scale", value: "bumpScale" }, + { text: "Normal Map Type", value: "normalMapType" }, + + "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", + { text: "Shininess", value: "shininess" }, + + { text: "Wireframe", value: "wireframe" }, + { text: "Wireframe Linewidth", value: "wireframeLinewidth" }, + { text: "Wireframe Linecap", value: "wireframeLinecap" }, + { text: "Wireframe Linejoin", value: "wireframeLinejoin" }, + + "|POINTS| <-- not a property", + { text: "Size", value: "size" }, + { text: "Size Attenuation", value: "sizeAttenuation" }, + + "|LINES| <-- not a property", + { text: "Scale", value: "scale" }, + { text: "Dash Size", value: "dashSize" }, + { text: "Gap Size", value: "gapSize" }, + + "|SPRITES| <-- not a property", + { text: "Rotation", value: "rotation" } +]}, + blendModes: {acceptReporters: false, items: [ + { text: "No Blending", value: "NoBlending" },{ text: "Normal Blending", value: "NormalBlending" },{ text: "Additive Blending", value: "AdditiveBlending" },{ text: "Subtractive Blending", value: "SubtractiveBlending" },{ text: "Multiply Blending", value: "MultiplyBlending" },{ text: "Custom Blending", value: "CustomBlending" } + ]}, + depthModes: {acceptReporters: false, items: [ + { text: "Never Depth", value: "NeverDepth" },{ text: "Always Depth", value: "AlwaysDepth" },{ text: "Equal Depth", value: "EqualDepth" },{ text: "Less Depth", value: "LessDepth" },{ text: "Less Equal Depth", value: "LessEqualDepth" },{ text: "Greater Equal Depth", value: "GreaterEqualDepth" },{ text: "Greater Depth", value: "GreaterDepth" },{ text: "Not Equal Depth", value: "NotEqualDepth" } + ]}, + materialTypes:{acceptReporters: false, items: [ + {text:"Mesh Basic Material",value:"MeshBasicMaterial"},{text:"Mesh Standard Material",value:"MeshStandardMaterial"},{text:"Mesh Physical Material",value:"MeshPhysicalMaterial"},{text:"Mesh Lambert Material",value:"MeshLambertMaterial"},{text:"Mesh Phong Material",value:"MeshPhongMaterial"},{text:"Mesh Depth Material",value:"MeshDepthMaterial"},{text:"Mesh Normal Material",value:"MeshNormalMaterial"},{text:"Mesh Matcap Material",value:"MeshMatcapMaterial"},{text:"Mesh Toon Material",value:"MeshToonMaterial"},{text:"Line Basic Material",value:"LineBasicMaterial"},{text:"Line Dashed Material",value:"LineDashedMaterial"},{text:"Points Material",value:"PointsMaterial"},{text:"Sprite Material",value:"SpriteMaterial"},{text:"Shadow Material",value:"ShadowMaterial"} + ]}, + textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, + textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, + geometryTypes: {acceptReporters: false, items: [ + {text: "Box Geometry", value: "BoxGeometry"},{text: "Sphere Geometry", value: "SphereGeometry"},{text: "Cylinder Geometry", value: "CylinderGeometry"},{text: "Plane Geometry", value: "PlaneGeometry"},{text: "Circle Geometry", value: "CircleGeometry"},{text: "Torus Geometry", value: "TorusGeometry"},{text: "Torus Knot Geometry", value: "TorusKnotGeometry"}, + ]}, + modelsList: {acceptReporters: false, items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) + if (models.length < 1) return [["Load a model! (GLB Loader category)"]] + + // @ts-ignore + return models.map( m => [m.name] ) + }}, + fonts: {acceptReporters: false, items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.json')) + if (models.length < 1) return [["Load a font!"]] + + // @ts-ignore + return models.map( m => [m.name] ) + }}, + + } + }} + + addObject(args) { + const object = new THREE[args.TYPE](); + + object.castShadow = true + object.receiveShadow = true + + createObject(args.OBJECT3D, object, args.GROUP) + } + cloneObject(args) { + let object = getObject(args.OBJECT3D) + const clone = object.clone(true) + clone.name + createObject(args.NAME, clone, args.GROUP) + } + setObjectV3(args) { + let object = getObject(args.OBJECT3D) + let values = JSON.parse(args.VALUE) + + function degToRad(deg) { + return deg * Math.PI / 180; + } + + + if (object.rigidBody) { + const x = values[0] + const y = values[1] + const z = values[2] + if (args.PROPERTY === "rotation") { + const euler = new THREE.Euler( + degToRad(x), + degToRad(y), + degToRad(z), + 'YXZ' + ) + const quaternion = new THREE.Quaternion() + quaternion.setFromEuler(euler) + + object.rigidBody.setRotation({ + x: quaternion.x, + y: quaternion.y, + z: quaternion.z, + w: quaternion.w + }); + } else if (args.PROPERTY === "position") { + object.rigidBody.setTranslation({ x: x, y: y, z: z }, true) + } + return + } + + if (object.isCamera == true && controls) { + + } + + if (args.PROPERTY === "rotation") { + values = values.map(v => v * Math.PI / 180); + object.rotation.set(0,0,0) + } + if (object.isDirectionalLight == true) {object.pos = new THREE.Vector3(...values); console.log(true, values, object.pos); return} + object[args.PROPERTY].set(...values); + + if (object.type == "CubeCamera") object.updateCoordinateSystem() + } + /* + changeObjectV3(args) { + getObject(args.OBJECT3D) + let values = JSON.parse(args.VALUE) + + if (args.PROPERTY === "rotation") { + values = values.map(v => v * Math.PI / 180); + object.rotation.x += values[0] + object.rotation.y += values[1] + object.rotation.z += values[2] + } + else { + object[args.PROPERTY].add(...values); + } + } + changeObjectXV3(args) { + getObject(args.OBJECT3D) + let value = args.VALUE + if (args.PROPERTY === "rotation") value = value * Math.PI / 180 + + object[args.PROPERTY][args.X] += value + } + */ + getObjectV3(args) { + let object = getObject(args.OBJECT3D) + if (!object) return + let values = vector3ToString(object[args.PROPERTY]) + if (args.PROPERTY === "rotation") { + const toDeg = Math.PI/180 + values = [values[0]/toDeg,values[1]/toDeg,values[2]/toDeg,] + } + + return JSON.stringify(values) + } + setObject(args){ + let object = getObject(args.OBJECT3D) + let value = args.VALUE + if (args.PROPERTY === "material") {const mat = materials[args.NAME]; if (mat) value = mat; else value = undefined} + else if (args.PROPERTY === "geometry") {const geo = geometries[args.NAME]; if (geo) value = geo; else value = undefined} + else value = !!value + + if (value == undefined) return //invalid geo/mat + object[args.PROPERTY] = value + } + getObject(args){ + let object = getObject(args.OBJECT3D) + if (!object) return + let value + if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; + else value = object.visible; + + return value + } + removeObject(args) { + removeObject(args.OBJECT3D) + } + objectE(args) { + return scene.children.map(o => o.name).includes(args.NAME) + } + +//defines + newMaterial(args) { + if (materials[args.NAME] && alerts) alert ("material already exists! will replace...") + const mat = new THREE[args.TYPE](); + mat.name = args.NAME; + + materials[args.NAME] = mat; + } + async setMaterial(args) { + if (typeof(args.VALUE) == "string" && args.VALUE.at(0) == "|") return + const mat = materials[args.NAME] + + let value = args.VALUE + + if (args.VALUE == "false") value = false + + if (args.PROPERTY == "side") {value = (args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide)} + else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)) + else value = getAsset(value) + + + console.log("o:", args.VALUE, typeof(args.VALUE)) + console.log("r:", value, typeof(value)) + + mat[args.PROPERTY] = await (value) //await incase its a texture + mat.needsUpdate = true + } + setBlending(args) { + const mat = materials[args.NAME] + mat.blending = THREE[args.VALUE] + mat.premultipliedAlpha = true + mat.needsUpdate = true + } + setDepth(args) { + const mat = materials[args.NAME] + mat.depthFunc = THREE[args.VALUE] + mat.needsUpdate = true + } + removeMaterial(args){ + const mat = materials[args.NAME] + mat.dispose() + delete(materials[args.NAME]) + } + materialE(args) { + return materials[args.NAME] ? true : false + } + + newGeometry(args) { + if (geometries[args.NAME] && alerts) alert ("geometry already exists! will replace...") + const geo = new THREE[args.TYPE]() + geo.name = args.NAME + + geometries[args.NAME] = geo + } + setGeometry(args) { + const geo = geometries[args.NAME] + geo[args.PROPERTY] = (args.VALUE) + + geo.needsUpdate = true; + } + removeGeometry(args){ + const geo = geometries[args.NAME] + geo.dispose() + delete(geometries[args.NAME]) + } + geometryE(args) { + return geometries[args.NAME] ? true : false + } + + newGeo(args) { + const geometry = new THREE.BufferGeometry() + geometry.name = args.NAME + geometries[args.NAME] = geometry + } + async geoPoints(args) { + const geometry = geometries[args.NAME] + const positions = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v3 of each vertex of each triangle + + geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3)) + geometry.computeVertexNormals() + + geometry.needsUpdate = true + } + geoUVs(args) { + const geometry = geometries[args.NAME] + const UVs = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v2 of each UV of each triangle + + geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(UVs), 2)) + geometry.needsUpdate = true + } + + splines(args) { + const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)) + geometry.name = args.NAME + + geometries[args.NAME] = geometry + } + + async splineModel(args) { + const model = await getModel(args.MODEL, args.NAME) + if (!model) return console.warn("Model not found:", args.MODEL) + + const curve = getAsset(args.CURVE) + const spacing = parseFloat(args.SPACING) || 1 + const curveLength = curve.getLength() + const divisions = Math.floor(curveLength / spacing) + + const geomList = [] + const matList = [] + + for (let i = 0; i <= divisions; i++) { + const t = i / divisions + const pos = curve.getPointAt(t) + const tangent = curve.getTangentAt(t) + + const temp = model.clone(true) + temp.position.copy(pos) + + const up = new THREE.Vector3(0, 0, 1) + const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()) + temp.quaternion.copy(quat) + + temp.updateMatrixWorld(true) + + temp.traverse(child => { + if (child.isMesh && child.geometry) { + const geom = child.geometry.clone() + geom.applyMatrix4(child.matrixWorld) + geomList.push(geom) + matList.push(child.material) //.clone() ? + } + }) + } + + const validGeoms = geomList.filter(g => { + const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position + if (!ok) console.warn("geometry skipped:", g) + return ok + }) + + const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true) + merged.computeBoundingBox() + merged.computeBoundingSphere() + + merged.name = args.NAME + geometries[args.NAME] = merged + matList.name = args.NAME + materials[args.NAME] = matList + } + + async text(args) { + const fontFile = runtime.getTargetForStage().getSounds().find(c => c.name === args.FONT) + if (!fontFile) return + + const json = new TextDecoder().decode(fontFile.asset.data.buffer) + const fontData = JSON.parse(json) + + const font = fontLoad.parse(fontData) + + const params = {font: font, size: JSON.parse(args.S), height: JSON.parse(args.D), curveSegments: JSON.parse(args.CS), bevelEnabled: false} + const geometry = new TextGeometry.TextGeometry(args.TEXT, params) + geometry.computeVertexNormals() + geometry.center() // optional, recenters the text + + + geometry.name = args.NAME + + geometries[args.NAME] = geometry + } + + async loadFont() { + openFileExplorer(".json").then(files => { + const file = files[0] + const reader = new FileReader() + + reader.onload = async (e) => { + const arrayBuffer = e.target.result + + // From lily's assets + // // Thank you PenguinMod for providing this code. + + const targetId = runtime.getTargetForStage().id //util.target.id not working! + const assetName = Cast.toString(file.name) + + const buffer = arrayBuffer + + const storage = runtime.storage + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ) + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ) + alert("Font loaded successfully!") + } catch (e) { + console.error(e) + alert("Error loading font.") + } + + // End of PenguinMod + } + + reader.readAsArrayBuffer(file); + }) + } + openConv() {{open("https://gero3.github.io/facetype.js/")}} + + } + Scratch.extensions.register(new ThreeObjects()) + + class ThreeLights { + getInfo() { + return { + id: "threeLights", + name: "Three Lights", + color1: "#c7a22aff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "addLight", blockType: Scratch.BlockType.COMMAND, text: "add light [NAME] type [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "lightTypes"}}}, + {opcode: "setLight", blockType: Scratch.BlockType.COMMAND, text: "set light [NAME][PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lightProperties", defaultValue: "intensity"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1", exemptFromNormalization: true}}}, + ], + menus: { + lightTypes: {acceptReporters: false, items: [ + {text: "Ambient Light", value: "AmbientLight"},{text: "Directional Light", value: "DirectionalLight"},{text: "Point Light", value: "PointLight"},{text: "Hemisphere Light", value: "HemisphereLight"},{text: "Spot Light", value: "SpotLight"}, + ]}, + lightProperties: {acceptReporters: false, items: [ + {text: "Color", value: "color"},{text: "Intensity", value: "intensity"},{text: "Cast Shadow?", value: "castShadow"}, + {text: "Ground Color (HemisphereLight)", value: "groundColor"}, + {text: "Map (SpotLight)", value: "map"},{text: "Distance (SpotLight)", value: "distance"},{text: "Decay (SpotLight)", value: "decay"},{text: "Penumbra (SpotLight)", value: "penumbra"},{text: "Angle/Size (SpotLight)", value: "angle"},{text: "Power (SpotLight)", value: "power"}, + {text: "Target Position (Directional/SpotLight)", value: "target"}, + ]}, + } + }} + + addLight(args) { + const light = new THREE[args.TYPE](0xffffff, 1) + + createObject(args.NAME, light, args.GROUP) + lights[args.NAME] = light + if (light.type === "AmbientLight" || "HemisphereLight") return + + light.castShadow = true + if (light.type === "PointLight") return + //Directional & Spot Light + light.target.position.set(0, 0, 0) + scene.add(light.target) + + light.pos = new THREE.Vector3(0,0,0) + + light.shadow.mapSize.width = 4096 + light.shadow.mapSize.height = 2048 + + if (light.type === "SpotLight") { + light.decay = 0 + light.shadow.camera.near = 500; + light.shadow.camera.far = 4000; + light.shadow.camera.fov = 30; + } + light.shadow.needsUpdate = true + light.needsUpdate = true + } + + setLight(args) { + const light = lights[args.NAME] + if (!args.PROPERTY) return + if (args.PROPERTY === "target") { + light.target.position.set(...JSON.parse(args.VALUE)) //vector3 + light.target.updateMatrixWorld(); + } + else { + light[args.PROPERTY] = getAsset(args.VALUE) + } + light.needsUpdate = true + + if (light.type === "AmbientLight" || "HemisphereLight") return + + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true + } + + } + Scratch.extensions.register(new ThreeLights()) + + class ThreeUtilities { + getInfo() { + return { + id: "threeUtility", + name: "Three Utilities", + color1: "#3875c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "newVector2", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}}}, + {opcode: "newVector3", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y] [Z]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}, Z: {type: Scratch.ArgumentType.NUMBER}}}, + "---", + {opcode: "operateV3", blockType: Scratch.BlockType.REPORTER, text: "do [V3] [O] [V32]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, O: {type: Scratch.ArgumentType.STRING, menu: "operators"}, V32: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, + {opcode: "moveVector3", blockType: Scratch.BlockType.REPORTER, text: "move [S] steps in vector [V3] in direction [D3]", arguments: {S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, + {opcode: "directionTo", blockType: Scratch.BlockType.REPORTER, text: "direction from [V3] to [T3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, T3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, + "---", + {opcode: "newColor", blockType: Scratch.BlockType.REPORTER, text: "New Color [HEX]", arguments: {HEX: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff"}}}, + {opcode: "newFog", blockType: Scratch.BlockType.REPORTER, text: "New Fog [COLOR] [NEAR] [FAR]", arguments: {COLOR: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff", exemptFromNormalization: true}, NEAR: {type: Scratch.ArgumentType.NUMBER}, FAR: {type: Scratch.ArgumentType.NUMBER, defaultValue: 10}}}, + {opcode: "newTexture", blockType: Scratch.BlockType.REPORTER, text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, + {opcode: "newCubeTexture", blockType: Scratch.BlockType.REPORTER, text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", arguments: {"COSTUMEX0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEX1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ1": {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, + {opcode: "newEquirectangularTexture", blockType: Scratch.BlockType.REPORTER, text: "New Equirectangular Texture [COSTUME] [MODE]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"}}}, + "---", + {opcode: "curve", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", arguments: {TYPE: {type: Scratch.ArgumentType.STRING, menu: "curveTypes"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]"}, CLOSED: {type: Scratch.ArgumentType.STRING, defaultValue: "true"}}}, + "---", + {opcode:"mouseDown",extensions: ["colours_sensing"], blockType: Scratch.BlockType.BOOLEAN, text: "mouse [BUTTON] [action]?", arguments: {BUTTON: {type: Scratch.ArgumentType.STRING, menu: "mouseButtons"},action: {type: Scratch.ArgumentType.STRING, menu: "mouseAction"}}}, + {opcode: "mousePos",extensions: ["colours_sensing"], blockType: Scratch.BlockType.REPORTER, text: "mouse position", arguments: {}}, + "---", + {opcode: "getItem",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "get item [ITEM] of [ARRAY]", arguments: {ITEM: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}, ARRAY: {type: Scratch.ArgumentType.STRING, defaultValue: `["myObject", "myLight"]`}}}, + {blockType: Scratch.BlockType.LABEL, text: "↳ Raycasting"}, + {opcode: "raycast", blockType: Scratch.BlockType.COMMAND, text: "Raycast from [V3] in direction [D3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,1]"}}}, + {opcode: "getRaycast", blockType: Scratch.BlockType.REPORTER, text: "get raycast [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "raycastProperties"}}}, + + ], + menus: { + materialProperties: {acceptReporters: false, items: [ + {text: "Color", value: "color"},{text: "Map (texture)", value: "map"},{text: "Alpha Map (texture)", value: "alphaMap"},{text: "Alpha Test (0-1)", value: "alphaTest"},{text: "Side (front/back/double)", value: "side"},{text: "Bump Map (texture)", value: "bumpMap"},{text: "Bump Scale", value: "bumpScale"}, + ]}, + textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, + textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, + raycastProperties: {acceptReporters: false, items: [ + {text: "Intersected Object Names", value: "name"},{text: "Number of Objects", value: "number"},{text: "Intersected Objects distances", value: "distance"}, + ]}, + mouseButtons: {acceptReporters: false, items: ["left","middle","right"]}, + mouseAction: {acceptReporters: false, items: ["Down","Clicked"]}, + curveTypes: {acceptReporters: false, items: ["CatmullRomCurve3"]}, + operators: {acceptReporters: false, items: [ + "+","-","*","/","=","max","min","dot","cross","distance to","angle to","apply euler", + ]} + } + }} + mouseDown(args) { + if (args.action === "Down") return isMouseDown[args.BUTTON] + if (args.action === "Clicked") { + if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false + else prevMouse[args.BUTTON] = true; return true + } + } + mousePos(event) { + return JSON.stringify(mouseNDC) + } + newVector3(args) { + return JSON.stringify([args.X, args.Y, args.Z]) + } + operateV3(args){ + const v3 = new THREE.Vector3(...JSON.parse(args.V3)) + const v32 = new THREE.Vector3(...JSON.parse(args.V32)) + + let r + if (args.O == "+") r = v3.add(v32) + else if (args.O == "-") r = v3.sub(v32) + else if (args.O == "*") r = v3.multiply(v32) + else if (args.O == "/") r = v3.divide(v32) + else if (args.O == "=") r = v3.equals(v32) + else if (args.O == "max") r = v3.max(v32) + else if (args.O == "min") r = v3.min(v32) + else if (args.O == "dot") r = v3.dot(v32) + else if (args.O == "cross") r = v3.cross(v32) + else if (args.O == "distance to") r = v3.distanceTo(v32) + else if (args.O == "angle to") r = v3.angleTo(v32) + else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)) + + if (typeof(r) == "object") return JSON.stringify([r.x, r.y, r.z]) + else return JSON.stringify(r) + } + + newVector2(args) { + return JSON.stringify([args.X, args.Y]) + } + + moveVector3(args) { + const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); + const steps = Number(args.S); + + const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); + + const yaw = THREE.MathUtils.degToRad(yawInputDeg); + const pitch = THREE.MathUtils.degToRad(pitchInputDeg); + const roll = THREE.MathUtils.degToRad(rollInputDeg); + + const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); + + const forwardVector = new THREE.Vector3(0, 0, -1); + const direction = forwardVector.applyEuler(euler).normalize(); + + const newPos = currentPos.add(direction.multiplyScalar(steps)); + return JSON.stringify([newPos.x, newPos.y, newPos.z]); + } + + directionTo(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)) + const toV3 = new THREE.Vector3(...JSON.parse(args.T3)) + + const direction = toV3.clone().sub(v3).normalize(); + // Pitch (X) + const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x*direction.x + direction.z*direction.z)); + // Yaw (Y) + const yaw = Math.atan2(direction.x, direction.z); + + // Roll always 0 + return JSON.stringify([180+THREE.MathUtils.radToDeg(pitch),THREE.MathUtils.radToDeg(yaw),0]) + } + + newColor(args) { + const color = new THREE.Color(args.HEX) + const uuid = crypto.randomUUID() + assets.colors[uuid] = color + return `colors/${uuid}` + } + newFog(args) { + const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR) + const uuid = crypto.randomUUID() + assets.fogs[uuid] = fog + return `fogs/${uuid}` + } + async newTexture(args) { + const textureURI = encodeCostume(args.COSTUME) + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) + assets.textures[texture.uuid] = texture + return `textures/${texture.uuid}` + } + async newCubeTexture(args) { + const uris = [encodeCostume(args.COSTUMEX0),encodeCostume(args.COSTUMEX1), encodeCostume(args.COSTUMEY0),encodeCostume(args.COSTUMEY1), encodeCostume(args.COSTUMEZ0),encodeCostume(args.COSTUMEZ1)] + const normalized = await Promise.all(uris.map(uri => resizeImageToSquare(uri, 256))); + const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); + + texture.name = "CubeTexture" + args.COSTUMEX0; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) + assets.textures[texture.uuid] = texture + return `textures/${texture.uuid}` + } + async newEquirectangularTexture(args) { + const textureURI = encodeCostume(args.COSTUME) + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME + texture.mapping = THREE.EquirectangularReflectionMapping + + setTexutre(texture, args.MODE) + assets.textures[texture.uuid] = texture + return `textures/${texture.uuid}` + } + + curve(args) { + function parsePoints(input) { + // Match all [x,y,z] groups + const matches = input.match(/\[([^\]]+)\]/g) + if (!matches) return [] + + return matches.map(str => { + const nums = str + .replace(/[\[\]\s]/g, '') + .split(',') + .map(Number) + return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0) + }) + } + const points = parsePoints(args.POINTS) + const curve = new THREE[args.TYPE](points) + curve.closed = JSON.parse(args.CLOSED) + + const uuid = crypto.randomUUID() + assets.curves[uuid] = curve + return `curves/${uuid}` + } + + getItem(args) { + const items = JSON.parse(args.ARRAY) + const item = items[args.ITEM - 1] + if (!item) return "0" + return item + } + + raycast(args) { + const origin = new THREE.Vector3(...JSON.parse(args.V3)) + // rotation is in degrees => convert to radians first + const rot = JSON.parse(args.D3).map(v => v * Math.PI / 180) + + const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder) + const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize() + + const raycaster = new THREE.Raycaster() + //const camera = getObject(args.CAMERA) + raycaster.set( origin, direction ); + + const intersects = raycaster.intersectObjects( scene.children, true ) + + raycastResult = intersects + } + getRaycast(args) { + if (args.PROPERTY === "number") return raycastResult.length + if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map(i => i.distance)) + return JSON.stringify(raycastResult.map(i => i.object[args.PROPERTY])) + } + + } + Scratch.extensions.register(new ThreeUtilities()) + + class ThreeGLB { + getInfo() { + return { + id: "threeGLB", + name: "Three GLB Loader", + color1: "#c53838ff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {blockType: Scratch.BlockType.BUTTON, text: "Load GLB File", func: "loadModelFile"}, + {opcode: "addModel", blockType: Scratch.BlockType.COMMAND, text: "add [ITEM] as [NAME] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},ITEM: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}}}, + {opcode: "getModel", blockType: Scratch.BlockType.REPORTER, text: "get object [PROPERTY] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "modelProperties"}}}, + {opcode: "playAnimation", blockType: Scratch.BlockType.COMMAND, text: "play animation [ANAME] of [NAME], [TIMES] times", arguments: {TIMES: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, + {opcode: "pauseAnimation", blockType: Scratch.BlockType.COMMAND, text: "set [TOGGLE] animation [ANAME] of [NAME]", arguments: {TOGGLE: {type: Scratch.ArgumentType.NUMBER, menu: "pauseUn"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, + {opcode: "stopAnimation", blockType: Scratch.BlockType.COMMAND, text: "stop animation [ANAME] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, + + ], + menus: { + modelProperties: {acceptReporters: false, items: [ + {text: "Animations", value: "animations"}, + ]}, + pauseUn: {acceptReporters: true, items: [{text: "Pause", value: "true"},{text: "Unpasue", value: "false"},]}, + modelsList: {acceptReporters: false, items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) + if (models.length < 1) return [["Load a model!"]] + + // @ts-ignore + return models.map( m => [m.name] ) + }}, + } + }} + + async loadModelFile() { + + openFileExplorer(".glb").then(files => { + const file = files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + const arrayBuffer = e.target.result; + + { // From lily's assets + + // Thank you PenguinMod for providing this code. + { + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); + + //const res = await Scratch.fetch(args.URL); + //const buffer = await res.arrayBuffer(); + const buffer = arrayBuffer + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + alert("Model loaded successfully!"); + } catch (e) { + console.error(e); + alert("Error loading model."); + } + } + // End of PenguinMod + } + }; + + reader.readAsArrayBuffer(file); + }) + + } + async addModel(args) { + const group = await getModel(args.ITEM, args.NAME) + + createObject(args.NAME, group, args.GROUP) + } + getModel(args){ + if (!models[args.NAME]) return; + return Object.keys(models[args.NAME].actions).toString() + } + + playAnimation(args) { + const model = models[args.NAME] + if (!model) {console.log("no model!"); return} + + const action = model.actions[args.ANAME] //clones of models dont have a stored actions! + if (!action) { + console.log("no action!") + return + } + + args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity) + + action.reset() + .play() + } + stopAnimation(args) { + const model = models[args.NAME]; + if (!model) return; + + const action = model.actions[args.ANAME]; + if (action) action.stop(); + } + pauseAnimation(args) { + const model = models[args.NAME]; + if (!model) return; + + const action = model.actions[args.ANAME]; + if (action) action.paused = args.TOGGLE + } + + } + Scratch.extensions.register(new ThreeGLB()) + + class ThreeAddons { + getInfo() { + return { + id: "threeAddons", + name: "Three Addons", + color1: "#c538a2ff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {blockType: Scratch.BlockType.LABEL, text: "Orbit Control"}, + {opcode: "OrbitControl", blockType: Scratch.BlockType.COMMAND, text: "set addon Orbit Control [STATE]", arguments: {STATE: {type: Scratch.ArgumentType.STRING, menu: "onoff"},}}, + + {blockType: Scratch.BlockType.LABEL, text: "Post Processing"}, + {opcode: "resetComposer", blockType: Scratch.BlockType.COMMAND, text: "reset composer"}, + {opcode: "bloom", blockType: Scratch.BlockType.COMMAND, text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},I: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, T:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, + {opcode: "godRays", blockType: Scratch.BlockType.COMMAND, text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}, DEC:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.95}, DENS:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},EXP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1},WEI:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.4},RES:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},SAMP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 64},}}, + {opcode: "dots", blockType: Scratch.BlockType.COMMAND, text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, A: {type: Scratch.ArgumentType.ANGLE, defaultValue: 0},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, + {opcode: "depth", blockType: Scratch.BlockType.COMMAND, text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", arguments: {FD: {type: Scratch.ArgumentType.NUMBER, defaultValue: (3)},FL: {type: Scratch.ArgumentType.NUMBER, defaultValue: (0.001)},BS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 4},H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 240},OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}}}, + "---", + {opcode: "custom", blockType: Scratch.BlockType.COMMAND, text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myShader"}, FRA: {type: Scratch.ArgumentType.STRING}, VER: {type: Scratch.ArgumentType.STRING}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}, OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, + ], + menus: { + onoff: {acceptReporters: true, items: [{text: "enabled", value: "1"},{text: "disabled", value: "0"},]}, + blendModes: {acceptReporters: false, items: [ + "SKIP","SET","ADD","ALPHA","AVERAGE","COLOR","COLOR_BURN","COLOR_DODGE", + "DARKEN","DIFFERENCE","DIVIDE","DST","EXCLUSION","HARD_LIGHT","HARD_MIX", + "HUE","INVERT","INVERT_RGB","LIGHTEN","LINEAR_BURN","LINEAR_DODGE", + "LINEAR_LIGHT","LUMINOSITY","MULTIPLY","NEGATION","NORMAL","OVERLAY", + "PIN_LIGHT","REFLECT","SCREEN","SRC","SATURATION","SOFT_LIGHT","SUBTRACT", + "VIVID_LIGHT" + ]}, + } + }} + + OrbitControl(args) { + if (controls) controls.dispose() + + console.log("creating...", OrbitControls) + controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); + controls.enableDamping = true + + controls.enabled = !!args.STATE + console.log(controls) + } + + resetComposer() { + composer.passes = [] + passes = {} + customEffects = [] + updateComposers() + } + + bloom(args) { + if (!camera || !scene) {if (alerts) alert("set a camera!"); return} + const bloomEffect = new BloomEffect({ + intensity: args.I, + luminanceThreshold: args.T, // ← correct key + luminanceSmoothing: args.S, + blendFunction: BlendFunction[args.BLEND], + }) + bloomEffect.blendMode.opacity.value = args.OP + + const pass = new EffectPass(camera, bloomEffect) + + composer.addPass(pass) + } + + godRays(args) { + if (!camera || !scene) {if (alerts) alert("set a camera!"); return} + let object = getObject(args.NAME) + const sun = object + + const godRays = new GodRaysEffect(camera, sun, { + resolutionScale: args.RES, + density: args.DENS, // ray density + decay: args.DEC, // fade out + weight: args.WEI, // brightness of rays + exposure: args.EXP, + samples: args.SAMP, + blendFunction: BlendFunction[args.BLEND], + }) + godRays.blendMode.opacity.value = args.OP + const pass = new EffectPass(camera, godRays) + composer.addPass(pass) + } + + dots(args) { + if (!camera || !scene) {if (alerts) alert("set a camera!"); return} + const dot = new DotScreenEffect({ + angle: args.A, + scale: args.S, + blendFunction: BlendFunction[args.BLEND], + }) + dot.blendMode.opacity.value = args.OP + const pass = new EffectPass(camera, dot) + composer.addPass(pass) + } + + depth(args) { + if (!camera || !scene) {if (alerts) alert("set a camera!"); return} + const dofEffect = new DepthOfFieldEffect(camera, { + focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) + focalLength: args.FL, // lens focal length in meters + bokehScale: args.BS, // strength/size of the blur circles + height: args.H, // resolution hint (affects quality/perf) + blendFunction: BlendFunction[args.BLEND], + }) + dofEffect.blendMode.opacity.value = args.OP + + const dofPass = new EffectPass(camera, dofEffect) + composer.addPass(dofPass) + } + + async custom(args) { + function cleanGLSL(glslCode) { + //delete multilines comments + let cleanedCode = glslCode.replace(/\/\*[\s\S]*?\*\//g, ' ') + .replace(/ /g, '\n') + .replace(/\/\/.*$/gm, ' ') + .replace(/; /g, ';\n') + + return cleanedCode; + } + + let fs = cleanGLSL(` + ${args.FRA} + `) + if (!args.FRA.trim()) {fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`} + const vs = cleanGLSL(` + ${args.VER} + `) + console.log(fs) + console.log(vs) + + const effect = new Effect( + "Custom", + fs, + { + blendFunction: BlendFunction[args.BLEND], + vertexShader: vs, + uniforms: new Map([ //uniforms usually in shaders... open to more! + ['time', new THREE.Uniform(0.0)], + ['resolution', new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height))] + ]), + defines: new Map([['USE_TIME', '1'], ['USE_VERTEX_TRANSFORM', '']]), + } + ); + + effect.blendMode.opacity.value = args.OP + + const pass = new EffectPass(camera, effect); + composer.addPass(pass); + + customEffects.push(effect); + } + + } + Scratch.extensions.register(new ThreeAddons()) + + class RapierPhysics { + getInfo() { + return { + id: "rapierPhysics", + name: "RAPIER Physics", + color1: "#222222", + color2: "#203024ff", + color3: "#78f07eff", + blocks: [ + {opcode: "createWorld", blockType: Scratch.BlockType.COMMAND, text: "create world | gravity:[G]", arguments: {G: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,-9.81,0]"}}}, + {opcode: "getWorld", blockType: Scratch.BlockType.REPORTER, text: "get world [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "wProp"}}}, + "---", + {opcode: "objectPhysics", blockType: Scratch.BlockType.COMMAND, text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", arguments: {state2: {type: Scratch.ArgumentType.STRING, menu: "state2"},state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"}, type: {type: Scratch.ArgumentType.STRING, menu: "objectTypes", defaultValue: "dynamic"}, collider: {type: Scratch.ArgumentType.STRING, menu: "colliderTypes", defaultValue: "cuboid"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},mass: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},density: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},friction: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0.5"}}}, + "---", + {blockType: Scratch.BlockType.LABEL, text: "- RigidBody"}, + {opcode: "setRB", blockType: Scratch.BlockType.COMMAND, text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodySets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, + {opcode: "getRB", blockType: Scratch.BlockType.REPORTER, text: "get rigidbody [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodyProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "lockObjectAxis", blockType: Scratch.BlockType.COMMAND, text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", arguments: {OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lockAxes"}, X: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Y: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Z: {type: Scratch.ArgumentType.STRING, menu: "tf"}}}, + "---", + {opcode: "addForce", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,10,0]"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "forces", defaultValue: "addForce"},SPACE: {type: Scratch.ArgumentType.STRING, menu: "spaces", defaultValue: "world"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + {opcode: "resetForces", blockType: Scratch.BlockType.COMMAND, text: "reset [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "resetF", defaultValue: "resetForces"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "enableCCD", blockType: Scratch.BlockType.COMMAND, text: "enable Continuous Collision Detection for [OBJECT] [state]", arguments: {state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "oPropS", defaultValue: "physics"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "fixedJoint", blockType: Scratch.BlockType.COMMAND, text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},RA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},RB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},}}, + {opcode: "sphericalJoint", blockType: Scratch.BlockType.COMMAND, text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},}}, + {opcode: "revoluteJoint", blockType: Scratch.BlockType.COMMAND, text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},X: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"},}}, + "---", + {blockType: Scratch.BlockType.LABEL, text: "- Collider"}, + {opcode: "setC", blockType: Scratch.BlockType.COMMAND, text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderSets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, + {opcode: "getC", blockType: Scratch.BlockType.REPORTER, text: "get collider [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "sensorSingle", blockType: Scratch.BlockType.BOOLEAN, text: "is sensor [SENSOR] touching [OBJECT]?", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + {opcode: "sensorAll", blockType: Scratch.BlockType.REPORTER, text: "objects touching sensor [SENSOR]", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}}} + ], + menus: { + wProp: {acceptReporters: false, items: [ + {text: "Gravity", value: "gravity"}, {text: "log to console", value: "log"} + ]}, + tf: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true", value: "true"}]}, + lockAxes: {acceptReporters: false, items: [ + {text: "Translation", value: "setEnabledTranslations"}, {text: "Rotation", value: "setEnabledRotations"} + ]}, + rigidBodyProperties: {acceptReporters: false, items: [ + {text: "Type", value: "bodyType"}, + {text: "Linear Velocity", value: "linvel"}, + {text: "Angular Velocity", value: "angvel"}, + {text: "Translation (position)", value: "translation"}, + {text: "Rotation (quaternion)", value: "rotation"}, + {text: "Mass", value: "mass"}, + //{text: "Center of Mass", value: "centerOfMass"}, + {text: "Linear Damping", value: "linearDamping"}, + {text: "Angular Damping", value: "angularDamping"}, + {text: "Is Sleeping?", value: "isSleeping"}, + //{text: "Can Sleep?", value: "isCanSleep"}, + {text: "Gravity Scale", value: "gravityScale"}, + {text: "Is Fixed?", value: "isFixed"}, + {text: "Is Dynamic?", value: "isDynamic"}, + {text: "Is Kinematic?", value: "isKinematic"}, + //{text: "Sleeping", value: "sleeping"} + ]}, + rigidBodySets: {acceptReporters: false, items: [ + //{text: "Linear Velocity", value: "setLinvel"}, + //{text: "Angular Velocity", value: "setAngvel"}, + //{text: "Mass", value: "setMass"}, + {text: "Gravity Scale", value: "setGravityScale"}, + //{text: "Can Sleep?", value: "setCanSleep"}, + //{text: "Sleeping", value: "sleeping"}, + {text: "Linear Damping", value: "setLinearDamping"}, + {text: "Angular Damping", value: "setAngularDamping"}, + {text: "Is Fixed?", value: "isFixed"}, + {text: "Is Dynamic?", value: "isDynamic"}, + {text: "Is Kinematic?", value: "isKinematic"} + ]}, + colliderProperties: {acceptReporters: false, items: [ + //{text: "Collider Type", value: "type"}, + {text: "Is Sensor?", value: "isSensor"}, + {text: "Friction", value: "friction"}, + {text: "Restitution", value: "restitution"}, + {text: "Density", value: "density"}, + {text: "Mass", value: "mass"}, + {text: "Position", value: "translation"}, + {text: "Rotation", value: "rotation"}, + //{text: "Area", value: "area"}, + {text: "Volume", value: "volume"}, + {text: "Collision Groups", value: "collisionGroups"}, + //{text: "Collision Mask", value: "collisionMask"}, + //{text: "Is Enabled?", value: "enabled"}, + //{text: "Contact Count", value: "contactCount"}, + //{text: "RigidBody Handle", value: "rigidBody"} + ]}, + colliderSets: {acceptReporters: false, items: [ + {text: "Friction", value: "setFriction"}, + {text: "Restitution", value: "setRestitution"}, + {text: "Density", value: "setDensity"}, + {text: "Is Sensor?", value: "setSensor"}, + {text: "Collision Groups", value: "setCollisionGroups"}, + //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool + //{text: "Position Offset", value: "setTranslation"}, + //{text: "Rotation Offset", value: "setRotation"} + ]}, + state: {acceptReporters: true, items: [{text: "on", value: "true"},{text: "off", value: "false"}]}, + state2: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true (must be fixed)", value: "true"}]}, + spaces: {acceptReporters: false, items: [{text: "World", value: "world"},{text: "Local", value: "local"}]}, + objectTypes: {acceptReporters: false, items: [{text: "Dynamic", value: "dynamic"},{text: "Fixed", value: "fixed"},{text: "Kinematic Position Based",value: "kinematicPositionBased"}]}, + colliderTypes: {acceptReporters: false, items: [{text: "Box, Rectangle, cuboid", value: "cuboid"},{text: "Sphere, ball", value: "ball"},{text: "Custom, complex simple shapes, convexHull", value: "convexHull"},{text:"Precision, TriMesh",value:"trimesh"}]}, + forces: {acceptReporters: false, items: [{text: "Force", value: "addForce"},{text: "Torque (rotation)", value: "addTorque"},{text: "Apply Impulse", value: "applyImpulse"},{text: "Apply Torque Impulse (rotation)", value: "applyTorqueImpulse"},{text: "Linear Velocity", value: "setLinvel"},{text: "Angular Velocity", value: "setAngvel"},]}, + resetF: {acceptReporters: false, items: [{text:"Forces", value: "resetForces"},{text:"Torques", value: "resetTorques"},]} + } + } + } + joint(jointData, bodyA, bodyB) { + physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true) + } + + fixedJoint(args) { + const VA = JSON.parse(args.VA).map(Number) + const VB = JSON.parse(args.VB).map(Number) + let RA = JSON.parse(args.RA).map(Number) + let RB = JSON.parse(args.RB).map(Number) + + RA = new THREE.Quaternion().setFromEuler( + new THREE.Euler( + THREE.MathUtils.degToRad(RA[0]), + THREE.MathUtils.degToRad(RA[1]), + THREE.MathUtils.degToRad(RA[2]) + ) + ) + RB = new THREE.Quaternion().setFromEuler( + new THREE.Euler( + THREE.MathUtils.degToRad(RB[0]), + THREE.MathUtils.degToRad(RB[1]), + THREE.MathUtils.degToRad(RB[2]) + ) + ) + + const data = RAPIER.JointData.fixed( + { x: VA[0], y: VA[1], z: VA[2] }, RA, + { x: VB[0], y: VB[1], z: VB[2] }, RB + ) + const objectA = getObject(args.ObjA) + let object = getObject(args.ObjB) + this.joint(data, objectA, object) + } + + sphericalJoint(args) { + const VA = JSON.parse(args.VA).map(Number) + const VB = JSON.parse(args.VB).map(Number) + + const data = RAPIER.JointData.spherical( + { x: VA[0], y: VA[1], z: VA[2] }, + { x: VB[0], y: VB[1], z: VB[2] } + ) + const objectA = getObject(args.ObjA) + let object = getObject(args.ObjB) + this.joint(data, objectA, object) + } + + revoluteJoint(args) { + const VA = JSON.parse(args.VA).map(Number) + const VB = JSON.parse(args.VB).map(Number) + const x = JSON.parse(args.X).map(Number) + + const data = RAPIER.JointData.revolute( + { x: VA[0], y: VA[1], z: VA[2] }, + { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, + ) + const objectA = getObject(args.ObjA) + let object = getObject(args.ObjB) + this.joint(data, objectA, object) + } + + prismaticJoint(args) { + const VA = JSON.parse(args.VA).map(Number) + const VB = JSON.parse(args.VB).map(Number) + const x = JSON.parse(args.X).map(Number) + + const data = RAPIER.JointData.prismatic( + { x: VA[0], y: VA[1], z: VA[2] }, + { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, + ) + const objectA = getObject(args.ObjA) + let object = getObject(args.ObjB) + this.joint(data, objectA, object) + } + + createWorld(args) { + const v3 = JSON.parse(args.G).map(Number) + const gravity = { x: v3[0], y: v3[1], z: v3[2]} + physicsWorld = new RAPIER.World(gravity) + + console.log(physicsWorld) + } + + getWorld(args) { + if (args.PROPERTY === "log") {console.log(physicsWorld); return "logged"} + return JSON.stringify(physicsWorld[args.PROPERTY]) + } + + setRB(args) { + let value = args.VALUE + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE + let object = getObject(args.OBJECT) + object.rigidBody[args.PROPERTY](value) + } + setC(args) { + let value = args.VALUE + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE + let object = getObject(args.OBJECT) + object.collider[args.PROPERTY](value) + } + + getRB(args) { + let object = getObject(args.OBJECT) + return JSON.stringify(object.rigidBody[args.PROPERTY]()) + } + getC(args) { + let object = getObject(args.OBJECT) + return JSON.stringify(object.collider[args.PROPERTY]()) + } + + lockObjectAxis(args) { + let object = getObject(args.OBJECT) + const x = !JSON.parse(args.X) + const y = !JSON.parse(args.Y) + const z = !JSON.parse(args.Z) + object.rigidBody[args.PROPERTY](x,y,z,true) //changes is xyz, wake up + } + + objectPhysics(args) { + let object = getObject(args.OBJECT) + object.physics = JSON.parse(args.state) + + if (JSON.parse(args.state)) { + //if already exists delete: + if (object.rigidBody) { + physicsWorld.removeRigidBody(object.rigidBody) + object.rigidBody = null + object.collider = null + } + /*asing a rigidbody and collider to object and add them to physicsWorld*/ + let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() + .setTranslation(object.position.x, object.position.y, object.position.z) + .setRotation({w: object.quaternion._w, x: object.quaternion._x, y: object.quaternion._y, z: object.quaternion._z}) + + let colliderDesc + switch(args.collider) { + case "cuboid": colliderDesc = createCuboidCollider(object,); break + case "ball": colliderDesc = createBallCollider(object); break + case "convexHull": colliderDesc = createConvexHullCollider(object); break + case "trimesh": colliderDesc = TriMesh(object); break + } + colliderDesc.setSensor(JSON.parse(args.state2)).setMass(args.mass).setDensity(args.density).setFriction(args.friction) + + let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc) + let collider = physicsWorld.createCollider(colliderDesc, rigidBody) + + object.rigidBody = rigidBody + object.collider = collider + } else { + /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ + physicsWorld.removeRigidBody(object.rigidBody) + object.rigidBody = null + object.collider = null + } + + } + + enableCCD(args) { + let object = getObject(args.OBJECT) + if (object.physics) { + let rigidBody = object.rigidBody + rigidBody.enableCcd(JSON.parse(args.state)) + } + } + + addForce(args) { + let object = getObject(args.OBJECT) + const vector = JSON.parse(args.VALUE).map(Number) + + let force = new THREE.Vector3(vector[0],vector[1],vector[2]) + if (args.SPACE === "local") { + force.applyQuaternion(object.quaternion); + } + + object.rigidBody[args.PROPERTY](force,true) + } + + resetForces(args) { + rigidBody[args.PROPERTY](true) + } + + sensorSingle(args) { + const sensor = getObject(args.SENSOR) + + let object = getObject(args.OBJECT) + + let touching = false + physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { + if (otherCollider === object.collider) touching = true + }) + + return touching + } + + sensorAll(args) { + const sensor = getObject(args.SENSOR) + + const touchedObjects = [] + + // loop thruogh every collider touching sensor + physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { + // find owner of collider + const otherObject = scene.children.find(o => o.collider === otherCollider) + console.log(otherCollider) + if (otherObject) touchedObjects.push(otherObject.name) + }) + + return JSON.stringify(touchedObjects) + } + + } + Scratch.extensions.register(new RapierPhysics()) + + //Thanks to the PointerLock extension of Turbowarp + const mouse = vm.runtime.ioDevices.mouse; + let isLocked = false; + let isPointerLockEnabled = false; + + let rect = threeRenderer.domElement.getBoundingClientRect(); + document.addEventListener("resize", () => { + rect = threeRenderer.domElement.getBoundingClientRect(); + }); + + const postMouseData = (e, isDown) => { + const { movementX, movementY } = e; + const { width, height } = rect; + const x = mouse._clientX + movementX; + const y = mouse._clientY - movementY; + mouse._clientX = x; + mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); + mouse._clientY = y; + mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); + if (typeof isDown === "boolean") { + const data = { + button: e.button, + isDown, + }; + originalPostIOData(data); + } + }; + + const mouseDevice = vm.runtime.ioDevices.mouse; + const originalPostIOData = mouseDevice.postData.bind(mouseDevice); + mouseDevice.postData = (data) => { + if (!isPointerLockEnabled) { + return originalPostIOData(data); + } + }; + + document.addEventListener( + "mousedown", + (e) => { + // @ts-expect-error + if (threeRenderer.domElement.contains(e.target)) { + if (isLocked) { + postMouseData(e, true); + } else if (isPointerLockEnabled) { + threeRenderer.domElement.requestPointerLock(); + } + } + }, + true + ); + document.addEventListener( + "mouseup", + (e) => { + if (isLocked) { + postMouseData(e, false); + // @ts-expect-error + } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { + threeRenderer.domElement.requestPointerLock(); + } + }, + true + ); + document.addEventListener( + "mousemove", + (e) => { + if (isLocked) { + postMouseData(e); + } + }, + true + ); + + document.addEventListener("pointerlockchange", () => { + isLocked = document.pointerLockElement === threeRenderer.domElement; + }); + document.addEventListener("pointerlockerror", (e) => { + console.error("Pointer lock error", e); + }); + + const oldStep = vm.runtime._step; + vm.runtime._step = function (...args) { + const ret = oldStep.call(this, ...args); + if (isPointerLockEnabled) { + const { width, height } = rect; + mouse._clientX = width / 2; + mouse._clientY = height / 2; + mouse._scratchX = 0; + mouse._scratchY = 0; + } + return ret; + }; + + vm.runtime.on("PROJECT_LOADED", () => { + isPointerLockEnabled = false; + if (isLocked) { + document.exitPointerLock(); + } + }); + + class Pointerlock { + getInfo() { + return { + id: "threepointerlockmod", + name: "Pointerlock for Extra 3D", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "setLocked", blockType: Scratch.BlockType.COMMAND, text: "set pointer lock [enabled]", arguments: { enabled: { type: Scratch.ArgumentType.STRING, defaultValue: "true", menu: "enabled"}},}, + {opcode: "isLocked", blockType: Scratch.BlockType.BOOLEAN, text: "pointer locked?",}, + ], + menus: { + enabled: {acceptReporters: true, items: [ + {text: "enabled", value: "true"},{text: "disabled", value: "false"}, + ]} + }, + } + } + + setLocked(args) { + isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; + if (!isPointerLockEnabled && isLocked) { + document.exitPointerLock(); + } + } + + isLocked() { + return isLocked; + } + } +Scratch.extensions.register(new Pointerlock()) + + }) + + + + +})(Scratch); diff --git a/threejsD_LOCAL_13852.js b/threejsD_LOCAL_13852.js new file mode 100644 index 0000000..27ba380 --- /dev/null +++ b/threejsD_LOCAL_13852.js @@ -0,0 +1,2414 @@ +// Name: Extra 3D +// ID: threejsExtension +// Description: Use three js inside Turbowarp! A 3D graphics library. +// By: Civero +// License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors + +(function (Scratch) { + "use strict"; + + if (!Scratch.extensions.unsandboxed) { + throw new Error("Three-D extension must run unsandboxed"); + } + + if (Scratch.vm.runtime.isPackaged) {alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); return} + //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return + + const vm = Scratch.vm; + const runtime = vm.runtime + const renderer = Scratch.renderer; + const canvas = renderer.canvas + const Cast = Scratch.Cast; + const menuIconURI = "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; + + let alerts = false + console.log("alerts are "+ (alerts ? "enabled" : "disabled")) + + let isMouseDown = { left: false, middle: false, right: false } + let prevMouse = { left: false, middle: false, right: false } + + let lastWidth = 0 + let lastHeight = 0 + + let THREE + let clock + let running + let loopId + //Addons + let GLTFLoader + let gltf + let OrbitControls + let controls + let BufferGeometryUtils + let TextGeometry + let fontLoad + //Physics + let RAPIER + let physicsWorld + + let threeRenderer + let scene + let camera + let eulerOrder = "YXZ" + + let composer + let passes = {} + let customEffects = [] + let renderTargets = {} + + let materials = {} + let geometries = {} + let lights = {} + let models = {} + + let assets = { //should i place materials, geometries; inside too? + textures: {}, + colors: {}, + fogs: {}, + curves: {}, + renderTargets: {}, //not the same as the global one! this one only stores textures + } + + let raycastResult = [] + + function resetor(level) { + camera = undefined + composer.reset() + + passes = {} + customEffects = [] + renderTargets = {} + + materials = {} + geometries = {} + lights = {} + models = {} + + if (level > 0) { + assets = { + textures: {}, + colors: {}, + fogs: {}, + curves: {}, + renderTargets: {}, + } + } + + updateComposers() + } + +//utility + function vector3ToString(prop) { + if (!prop) return "0,0,0"; + + const x = (typeof(prop.x) === "number") ? prop.x : (typeof(prop._x) === "number") ? prop._x : (JSON.stringify(prop).includes("X")) ? prop : 0 + const y = (typeof(prop.y) === "number") ? prop.y : (typeof(prop.y) === "number") ? prop._y : 0 + const z = (typeof(prop.z) === "number") ? prop.z : (typeof(prop.z) === "number") ? prop.z : 0 + + return [x, y, z] + } + +//objects + function createObject(name, content, parentName) { + let object = getObject(name, true) + if (object) { + removeObject(name) + alerts ? alert(name + " already exsisted, will replace!") : null + } + content.name = name + content.rotation._order = eulerOrder + parentName === scene.name ? object = scene : object = getObject(parentName) + content.physics = false + + object.add(content) + } + function removeObject(name) { + let object = getObject(name) + if (!object) return + + scene.remove(object) + + if (object.rigidBody) { + physicsWorld.removeCollider(object.collider, true) + physicsWorld.removeRigidBody(object.rigidBody, true) + object.rigidBody = null + object.collider = null + } + if (object.isLight) { + delete(lights[name]) + } + } + function getObject(name, isNew) { + let object = null + if (!scene) { + alerts ? alert("Can not get " + name + ". Create a scene first!") : null; return;} + object = scene.getObjectByName(name) + if (!object && !isNew) {alerts ? alert(name + " does not exist! Add it to scene"):null; return;} + return object + } + +//materials + function encodeCostume (name) { + if (name.startsWith("data:image/")) return name + return Scratch.vm.editingTarget.sprite.costumes.find(c => c.name === name).asset.encodeDataURI() + } + function setTexutre (texture, mode, style, x, y) { + texture.colorSpace = THREE.SRGBColorSpace + + if (mode === "Pixelate") { + texture.minFilter = THREE.NearestFilter; + texture.magFilter = THREE.NearestFilter; + } else { //Blur + texture.minFilter = THREE.NearestMipmapLinearFilter + texture.magFilter = THREE.NearestMipmapLinearFilter + } + + if (style === "Repeat") { + texture.wrapS = THREE.RepeatWrapping + texture.wrapT = THREE.RepeatWrapping + texture.repeat.set(x, y) + } + + texture.generateMipmaps = true; + } + async function resizeImageToSquare(uri, size = 256) { + return new Promise((resolve) => { + const img = new Image() + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + const ctx = canvas.getContext('2d') + + // clear + draw image scaled to fit canvas + ctx.clearRect(0, 0, size, size) + ctx.drawImage(img, 0, 0, size, size) + + resolve(canvas.toDataURL()) // return normalized Data URI + //delete canvas? + }; + img.src = uri + }); +} +//light +function updateShadowFrustum(light, focusPos) { + if (light.type !== "DirectionalLight") return + + // Frustum Size - Increase this value to cover a larger area. + const d = 50; + + // Update Orthographic Shadow Camera Frustum + const shadowCamera = light.shadow.camera; + + // Set the width/height of the frustum + shadowCamera.left = -d; + shadowCamera.right = d; + shadowCamera.top = d; + shadowCamera.bottom = -d; + + // Determine ranges + shadowCamera.near = 0.1 + shadowCamera.far = 500 + + // Position the Light and its Target + light.target.position.copy(focusPos); + const direction = light.position.clone().sub(light.target.position).normalize(); + light.position.copy(focusPos.clone().add(direction.multiplyScalar(100))); + + // Ensure matrices are updated. + light.target.updateMatrixWorld(); + light.shadow.camera.updateProjectionMatrix() + light.shadow.needsUpdate = true; +} +//composer +function updateComposers() { + if (!camera || !scene) return; // nothing to do yet + + // always recreate the RenderPass to point to the current scene/camera + passes["Render"] = new RenderPass(scene, camera); + + // ensure composer has a RenderPass as the first pass + const hasRender = composer.passes.some(p => p && p.scene); + if (!hasRender) composer.addPass(passes["Render"]); + else { + // if composer already has one, replace it so it references current scene/camera + const idx = composer.passes.findIndex(p => p && p.scene); + composer.passes[idx] = passes["Render"]; + } +} +//utility +function getMouseNDC(event) { + // Use threeRenderer.domElement for correct offset + const rect = threeRenderer.domElement.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + return [x, y]; +} +function checkCanvasSize() { + const { width, height } = canvas + if (width !== lastWidth || height !== lastHeight) { + lastWidth = width + lastHeight = height + resize() + } + requestAnimationFrame(checkCanvasSize) //rerun next frame +} +//physics +function computeWorldBoundingBox(mesh) { + // Create a Box3 in world coordinates + const box = new THREE.Box3().setFromObject(mesh); + const size = new THREE.Vector3(); + box.getSize(size); + const center = new THREE.Vector3(); + box.getCenter(center); + return { size, center }; +} +function createCuboidCollider(mesh) { + const { size } = computeWorldBoundingBox(mesh); + const collider = RAPIER.ColliderDesc.cuboid( + size.x / 2, + size.y / 2, + size.z / 2 + ) + return collider; +} +function createBallCollider(mesh) { + const { size } = computeWorldBoundingBox(mesh); + // radius = 1/2 of the largest verticie + const radius = Math.max(size.x, size.y, size.z) / 2; + const collider = RAPIER.ColliderDesc.ball(radius) + return collider //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) +} +function createConvexHullCollider(mesh) { + mesh.updateWorldMatrix(true, false); + + const position = mesh.geometry.attributes.position; + const vertices = []; + const vertex = new THREE.Vector3(); + + // Matrix for scale only + const scaleMatrix = new THREE.Matrix4().makeScale( + mesh.scale.x, + mesh.scale.y, + mesh.scale.z + ); + + for (let i = 0; i < position.count; i++) { + vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); + vertices.push(vertex.x, vertex.y, vertex.z); + } + + const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); + return collider; +} +function TriMesh(mesh) { + // Get the positions array (from your geoPoints function) +const positions = mesh.geometry.attributes.position.array; +const numVertices = positions.length / 3; + +// Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] +const indices = Array.from({ length: numVertices }, (_, i) => i); + +const collider = RAPIER.ColliderDesc.trimesh( + positions, + new Uint32Array(indices) +); + +return collider +} +function getModel(model, name) { + const file = runtime.getTargetForStage().getSounds().find(c => c.name === model) + if (!file) return + +return new Promise((resolve, reject) => { + gltf.parse( + file.asset.data.buffer, + "", + gltf => { + const root = gltf.scene + root.traverse(child => { + if (child.isMesh) { + child.castShadow = true + child.receiveShadow = true + } + }); + + const mixer = new THREE.AnimationMixer(root) + const actions = {} + gltf.animations.forEach(clip => { + const act = mixer.clipAction(clip) + act.clampWhenFinished = true + actions[clip.name] = act + }); + + models[name] = { root, mixer, actions } + resolve(root) + }, + error => { + console.error("Error parsing GLB model:", error) + reject(error) + } + )}) +} +async function openFileExplorer(format) { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file" + input.accept = format + input.multiple = false + input.onchange = () => { + resolve(input.files) + input.remove() + }; + input.click(); + }) +} +function getMeshesUsingTexture(scene, targetTexture) { + const meshes = [] + + scene.traverse(object => { + if (object.material) { + const materials = Array.isArray(object.material) ? object.material : [object.material] + for (const material of materials) { + if (material.map === targetTexture) { + meshes.push(object) + break + } + } + } + }) + + return meshes +} +function getAsset(path) { + if (typeof(path) == "string") { //string? + if (path.includes("/")) { //has the /? + const value = path.split("/") + console.log(value[0], value[1]) + return assets[value[0]][value[1]] + } + } + + return JSON.parse(path) //boolean or number +} + +let mouseNDC = [0, 0] +//loops/init +function stopLoop() { + if (!running) return + running = false + + if (loopId) { + cancelAnimationFrame(loopId) + loopId = null + if (threeRenderer) threeRenderer.clear(); + } +} +async function load() { + if (!THREE) { + + // @ts-ignore + THREE = await import("https://esm.sh/three@0.180.0") + window._THREE_ = THREE + //Addons + GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js") + OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js") + BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js") + TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js") + const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js") + fontLoad = new FontLoader.FontLoader() + + const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8") + const { + EffectComposer, + EffectPass, + RenderPass, + + Effect, + BloomEffect, + GodRaysEffect, + DotScreenEffect, + DepthOfFieldEffect, + + BlendFunction + } = POSTPROCESSING + //so i can use them later as global + window.EffectComposer = EffectComposer; + window.EffectPass = EffectPass; + window.RenderPass = RenderPass; + window.Effect = Effect; + window.BloomEffect = BloomEffect; + window.GodRaysEffect = GodRaysEffect; + window.DotScreenEffect = DotScreenEffect; + window.DepthOfFieldEffect = DepthOfFieldEffect; + window.BlendFunction = BlendFunction; + + RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0") + await RAPIER.init() + + threeRenderer = new THREE.WebGLRenderer({ + powerPreference: "high-performance", + antialias: false, + stencil: false, + depth: true + }) + threeRenderer.setPixelRatio(window.devicePixelRatio) + threeRenderer.outputColorSpace = THREE.SRGBColorSpace // correct colors + threeRenderer.toneMapping = THREE.ACESFilmicToneMapping // HDR look (test) + //threeRenderer.toneMappingExposure = 1.0 //(test) + + threeRenderer.shadowMap.enabled = true + threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap // (optional) + threeRenderer.domElement.style.pointerEvents = 'auto' //will disable turbowarp mouse events, but enable threejs's + + gltf = new GLTFLoader.GLTFLoader() + clock = new THREE.Clock() + + // Example: create a composer + composer = new EffectComposer(threeRenderer, {frameBufferType: THREE.HalfFloatType}) + + renderer.addOverlay( threeRenderer.domElement, "manual" ) + renderer.addOverlay(canvas, "manual") + renderer.setBackgroundColor(1, 1, 1, 0) + + resize() + + window.addEventListener("mousedown", e => { + if (e.button === 0) isMouseDown.left = true + if (e.button === 1) isMouseDown.middle = true + if (e.button === 2) isMouseDown.right = true + }) + window.addEventListener("mouseup", e => { + if (e.button === 0) isMouseDown.left = false; prevMouse.left = false + if (e.button === 1) isMouseDown.middle = false; prevMouse.middle = false + if (e.button === 2) isMouseDown.right = false; prevMouse.right = false + }) + // prevent contextmenu on right click + threeRenderer.domElement.addEventListener("contextmenu", e => e.preventDefault()); + + threeRenderer.domElement.addEventListener('mousemove', (event) => { + mouseNDC = getMouseNDC(event); + }) + + running = false + load() + + startRenderLoop() + runtime.on('PROJECT_START', () => startRenderLoop()) + runtime.on('PROJECT_STOP_ALL', () => stopLoop()) + runtime.on('STAGE_SIZE_CHANGED', () => {requestAnimationFrame(() => resize())}) + checkCanvasSize() + } + } +function startRenderLoop() { + if (running) return + running = true + + const loop = () => { + if (!running) return + //RAPIER + if (physicsWorld && scene) { + physicsWorld.step() + + scene.children.forEach(obj => { + if (!(obj.isMesh) || !(obj.physics)) return + if (obj.rigidBody) { + obj.position.copy(obj.rigidBody.translation()) + obj.quaternion.copy(obj.rigidBody.rotation()) + } + }) + + } + if (scene && camera) { + if (controls) controls.update() + + const delta = clock.getDelta() + Object.values(models).forEach( model => { if (model) model.mixer.update(delta) } ) + + Object.values(lights).forEach(light => updateShadowFrustum(light, camera.position)) + + //update custom effects time + customEffects.forEach(e => { + if (e.uniforms.get('time')) { + e.uniforms.get('time').value += delta + } + }) + Object.values(renderTargets).forEach(t => { + if ( t.camera.type == "PerspectiveCamera") { + t.camera.aspect = t.target.width / t.target.height + t.camera.updateProjectionMatrix() + } + // get meshes using the texture associated with this target + const displayMeshes = getMeshesUsingTexture(scene, t.target.texture) + + displayMeshes.forEach(mesh => { + mesh.visible = false + }) + + if (t.camera.type == "PerspectiveCamera") { + threeRenderer.setRenderTarget(t.target) + threeRenderer.clear(true, true, true) + threeRenderer.render(scene, t.camera) + } else { + t.target.clear(threeRenderer) + t.camera.update( threeRenderer, scene ) //cubeCamera + } + + displayMeshes.forEach(mesh => { + mesh.visible = true + }) + }) + + camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height + camera.updateProjectionMatrix() + threeRenderer.setRenderTarget(null) + composer.render(delta) + } + + loopId = requestAnimationFrame(loop) + } + + loopId = requestAnimationFrame(loop) +} + +function resize() { + const w = canvas.width + const h = canvas.height + + threeRenderer.setSize(w, h) + composer.setSize(w, h) + customEffects.forEach(e => { + if (e.uniforms.get('resolution')) { + e.uniforms.get('resolution').value.set(w,h) + } + }) + + if (camera) { + camera.aspect = w / h + camera.updateProjectionMatrix() +} +} +//wait until all packages are loaded +Promise.resolve(load()).then(() => { + + class threejsExtension { + getInfo() { + return { + id: "threejsExtension", + name: "Extra 3D", + color1: "#222222", + color2: "#222222", + color3: "#11cc99", + menuIconURI, + blockIconURI: menuIconURI, + + blocks: [ + {blockType: Scratch.BlockType.BUTTON, text: "Show Docs", func: "openDocs"}, + {blockType: Scratch.BlockType.BUTTON, text: "Toggle Alerts", func: "alerts"}, + ], + menus: {} + }} + openDocs(){ + open("https://civ3ro.github.io/extensions/Documentation/") + } + alerts() {alerts = !alerts; alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!")} + } + Scratch.extensions.register(new threejsExtension()) + + class ThreeRenderer { + getInfo() { + return { + id: "threeRenderer", + name: "Three Renderer", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "setRendererRatio", blockType: Scratch.BlockType.COMMAND, text: "set Pixel Ratio to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"}}}, + {opcode: "eulerOrder", blockType: Scratch.BlockType.COMMAND, text: "set euler order to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "YXZ"}}}, + ], + menus: {} + }} + + setRendererRatio(args) { + threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE) + } + eulerOrder(args) { + eulerOrder = args.VALUE + console.log("euler order set to", eulerOrder) + } + + } + Scratch.extensions.register(new ThreeRenderer()) + + class ThreeScene { + constructor() { + this.THREE = THREE; + this.scenes = {}; + } + + getInfo() { + return { + id: "threeScene", + name: "Three Scene", + color1: "#4638c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "newScene", blockType: Scratch.BlockType.COMMAND, text: "new Scene [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}}}, + + {opcode: "setSceneProperty", blockType: Scratch.BlockType.COMMAND, text: "set Scene [PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "sceneProperties", defaultValue: "background"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, + "---", + {opcode: "getSceneObjects", blockType: Scratch.BlockType.REPORTER, text: "get Scene [THING]", arguments:{THING: {type: Scratch.ArgumentType.STRING, menu: "sceneThings"}}}, + {opcode: "reset", blockType: Scratch.BlockType.COMMAND, text: "Reset Everything"} + ], + menus: { + sceneProperties: {acceptReporters: false, items: [ + {text: "Background", value: "background"},{text: "Background Blurriness", value: "backgroundBlurriness"},{text: "Background Intensity", value: "backgroundIntensity"},{text: "Background Rotation", value: "backgroundRotation"}, + {text: "Environment", value: "environment"},{text: "Environment Intensity", value: "environmentIntensity"},{text: "Environment Rotation", value: "environmentRotation"},{text: "Fog", value: "fog"}, + ]}, + sceneThings: {acceptReporters: false, items: ["Objects", "Materials", "Geometries","Lights","Scene Properties","Other assets"]}, + + } + }} + + newScene(args) { + scene = new THREE.Scene(); + scene.name = args.NAME + scene.background = new THREE.Color("#222") + //scene.add(new THREE.GridHelper(16, 16)) //future helper section? + this.scenes = {...this.scenes, {scene}}; + resetor(0) + } + + reset() { + resetor(1) + } + + async setSceneProperty(args) { + const property = args.PROPERTY; + const value = getAsset(args.VALUE); + + scene[property] = value; + } + getSceneObjects(args){ + const names = []; + if (args.THING === "Objects") { + scene.traverse(obj => { + if (obj.name) names.push(obj.name); //if it has a name, add to list! + }); + } + else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)) + else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)) + else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)) + else if (args.THING === "Scene Properties") {console.log(scene); return "check console"} + else if (args.THING === "Other assets") return JSON.stringify(assets) + + return JSON.stringify(names); // if objects + } + + } + Scratch.extensions.register(new ThreeScene()) + + class ThreeCameras { + getInfo() { + return { + id: "threeCameras", + name: "Three Cameras", + color1: "#38c59bff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "addCamera", blockType: Scratch.BlockType.COMMAND, text: "add camera [TYPE] [CAMERA] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "cameraTypes"}}}, + {opcode: "setCamera", blockType: Scratch.BlockType.COMMAND, text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "0.1", exemptFromNormalization: true}}}, + {opcode: "getCamera", blockType: Scratch.BlockType.REPORTER, text: "get camera [PROPERTY] of [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}}}, + "---", + {opcode: "renderSceneCamera", blockType: Scratch.BlockType.COMMAND, text: "set rendering camera to [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}}}, + "---", + {opcode: "cubeCamera", blockType: Scratch.BlockType.COMMAND, text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "cubeCamera"}, GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, + "---", + {opcode: "renderTarget", blockType: Scratch.BlockType.COMMAND, text: "set a RenderTarget: [RT] for camera [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, + {opcode: "sizeTarget", blockType: Scratch.BlockType.COMMAND, text: "set RenderTarget [RT] size to [W] [H]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, W: {type: Scratch.ArgumentType.NUMBER, defaultValue: 480}, H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 360},} }, + {opcode: "getTarget", blockType: Scratch.BlockType.REPORTER, text: "get RenderTarget: [RT] texture", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, + {opcode: "removeTarget", blockType: Scratch.BlockType.COMMAND, text: "remove RenderTarget: [RT]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, + ], + menus: { + cameraTypes: {acceptReporters: false, items: [ + {text: "Perspective", value: "PerspectiveCamera"}, + ]}, + cameraProperties: {acceptReporters: false, items: [ + {text: "Near", value: "near"},{text: "Far", value: "far"},{text: "FOV", value: "fov"},{text: "Focus (nothing...)", value: "focus"},{text: "Zoom", value: "zoom"}, + ]}, + } + }} + addCamera(args) { + let v2 = new THREE.Vector2() + threeRenderer.getSize(v2) + const object = new THREE.PerspectiveCamera(90, v2.x / v2.y ) + object.position.z = 3 + + createObject(args.CAMERA, object, args.GROUP) + } + setCamera(args) { + let object = getObject(args.CAMERA) + object[args.PROPERTY] = args.VALUE + object.updateProjectionMatrix() + } + getCamera(args) { + let object = getObject(args.CAMERA) + const value = JSON.stringify(object[args.PROPERTY]) + return value + } + renderSceneCamera(args) { + let object = getObject(args.CAMERA) + if (!object) return + camera = object + //reset composer, else it does not update. + composer.passes = [] + passes = {} + customEffects = [] + updateComposers() + } + + cubeCamera(args) { + // Create cube render target + const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true } ) + // Create cube camera + const cubeCamera = new THREE.CubeCamera( 0.1, 500, cubeRenderTarget ) + createObject(args.CAMERA, cubeCamera, args.GROUP) + + renderTargets[args.RT] = {target: cubeRenderTarget, camera: cubeCamera} + assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture + } + + renderTarget(args) { + let object = getObject(args.CAMERA) + const renderTarget = new THREE.WebGLRenderTarget( + 360, + 360, + { + generateMipmaps: false + } + ) + + renderTargets[args.RT] = {target: renderTarget, camera: object} + assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture + } + sizeTarget(args) { + renderTargets[args.RT].target.setSize(args.W, args.H) + } + getTarget(args) { + const t = renderTargets[args.RT].target.texture + console.log(t, renderTargets[args.RT]) + return `renderTargets/${t.uuid}` + } + removeTarget(args) { + delete(assets.renderTargets[renderTargets[args.RT].target.texture.uuid]) + renderTargets[args.RT].target.dispose() + delete(renderTargets[args.RT]) + } + } + Scratch.extensions.register(new ThreeCameras()) + + class ThreeObjects { + getInfo() { + return { + id: "threeObjects", + name: "Three Objects", + color1: "#38c567ff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "addObject", blockType: Scratch.BlockType.COMMAND, text: "add object [OBJECT3D] [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},TYPE: {type: Scratch.ArgumentType.STRING, menu: "objectTypes"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + {opcode: "cloneObject", blockType: Scratch.BlockType.COMMAND, text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myClone"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "setObject", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, + {opcode: "getObject", blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of object [OBJECT3D]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}}}, + {opcode: "objectE", blockType: Scratch.BlockType.BOOLEAN, text: "is there an object [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "removeObject", blockType: Scratch.BlockType.COMMAND, text: "remove object [OBJECT3D] from scene", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + + {blockType: Scratch.BlockType.LABEL, text: " ↳ Transforms"}, + {opcode: "setObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, + //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, + //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, + {opcode: "getObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of [OBJECT3D]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + + {blockType: Scratch.BlockType.LABEL, text: "↳ Materials"}, + {opcode: "newMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "new material [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "materialTypes", defaultValue: "MeshStandardMaterial"}}}, + {opcode: "materialE",extensions: ["colours_looks"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a material [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, + {opcode: "removeMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "remove material [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, + {opcode: "setMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [PROPERTY] of [NAME] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "materialProperties", defaultValue: "color"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, + {opcode: "setBlending",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] blending to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "blendModes"}}}, + {opcode: "setDepth",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] depth to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "depthModes"}}}, + + {blockType: Scratch.BlockType.LABEL, text: "↳ Geometries"}, + {opcode: "newGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new geometry [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "geometryTypes", defaultValue: "BoxGeometry"}}}, + {opcode: "geometryE",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a geometry [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, + {opcode: "removeGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "remove geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, + "---", + {opcode: "newGeo",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new empty geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, + {opcode: "geoPoints",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] vertex points to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, + {opcode: "geoUVs",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] UVs to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[UVs]"}}}, + "---", + {opcode: "splines", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create spline [NAME] from curve [CURVE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}}}, + {opcode: "splineModel", extensions: ["colours_operators"], blockType: Scratch.BlockType.COMMAND, text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, MODEL: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}, SPACING: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, + "---", + {blockType: Scratch.BlockType.BUTTON, text: "Convert font to JSON", func: "openConv"}, + {blockType: Scratch.BlockType.BUTTON, text: "Load JSON font file", func: "loadFont"}, + {opcode: "text", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myText"}, TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "C-369"}, FONT: {type: Scratch.ArgumentType.STRING, menu: "fonts"}, S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, D: {type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1}, CS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 6}}}, + ], + menus: { + objectVector3: {acceptReporters: false, items: [ + {text: "Positon", value: "position"},{text: "Rotation", value: "rotation"},{text: "Scale", value: "scale"},{text: "Facing Direction (.up)", value: "up"} + ]}, + objectProperties: {acceptReporters: false, items: [ + {text: "Geometry", value: "geometry"},{text: "Material", value: "material"},{text: "Visible (true/false)", value: "visible"}, + ]}, + objectTypes: { acceptReporters: false, items: [ + { text: "Mesh", value: "Mesh" }, { text: "Sprite", value: "Sprite" }, { text: "Points", value: "Points" }, { text: "Line", value: "Line" }, { text: "Group", value: "Group" } + ]}, + XYZ: {acceptReporters: false, items: [{text: "X", value: "x"},{text: "Y", value: "y"},{text: "Z", value: "z"}]}, + materialProperties: {acceptReporters: false, items: [ + "|GENERAL| <-- not a property", + { text: "Color", value: "color" }, + { text: "Map", value: "map" }, + { text: "Opacity", value: "opacity" }, + { text: "Transparent", value: "transparent" }, + { text: "Alpha Map", value: "alphaMap" }, + { text: "Alpha Test", value: "alphaTest" }, + { text: "Depth Test", value: "depthTest" }, + { text: "Depth Write", value: "depthWrite" }, + { text: "Color Write", value: "colorWrite" }, + { text: "Side", value: "side" }, + { text: "Visible", value: "visible" },/* + { text: "Blending", value: "blending" }, + { text: "Blend Src", value: "blendSrc" }, + { text: "Blend Dst", value: "blendDst" }, + { text: "Blend Equation", value: "blendEquation" }, + { text: "Blend Src Alpha", value: "blendSrcAlpha" }, + { text: "Blend Dst Alpha", value: "blendDstAlpha" }, + { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ + { text: "Blend Aplha", value: "blendAplha" }, + { text: "Blend Color", value: "blendColor" }, + { text: "Alpha Hash", value: "alphaHash" }, + { text: "Premultiplied Alpha", value: "premultipliedAlpha" }, + + { text: "Tone Mapped", value: "toneMapped" }, + { text: "Fog", value: "fog" }, + { text: "Flat Shading", value: "flatShading" }, + + "|MESH Standard / Physical| <-- not a property", + { text: "Metalness", value: "metalness" }, + { text: "Metalness Map", value: "metalnessMap" }, + { text: "Roughness", value: "roughness" }, + { text: "Reflectivity", value: "reflectivity" }, + { text: "Roughness Map", value: "roughnessMap" }, + { text: "Emissive", value: "emissive" }, + { text: "Emissive Intensity", value: "emissiveIntensity" }, + { text: "Emissive Map", value: "emissiveMap" }, + { text: "Env Map", value: "envMap" }, + { text: "Env Map Intensity", value: "envMapIntensity" }, + { text: "Env Map Rotation", value: "envMapRotation" }, + { text: "Ior", value: "ior" }, + { text: "Refraction Ratio", value: "refractionRatio" }, + { text: "Clearcoat", value: "clearcoat" }, + { text: "Clearcoat Map", value: "clearcoatMap" }, + { text: "Clearcoat Roughness", value: "clearcoatRoughness" }, + { text: "Clearcoat Roughness Map", value: "clearcoatRoughnessMap" }, + { text: "Dispersion", value: "dispersion" }, + { text: "Sheen", value: "sheen" }, + { text: "Sheen Color", value: "sheenColor" }, + { text: "Sheen Color Map", value: "sheenColorMap" }, + { text: "Sheen Roughness", value: "sheenRoughness" }, + { text: "Sheen Roughness Map", value: "sheenRoughnessMap" }, + { text: "Specular Color", value: "specularColor" }, + { text: "Specular Color Map", value: "specularColorMap" }, + { text: "Specular Intensity", value: "specularIntensity" }, + { text: "Specular Intensity Map", value: "specularIntensityMap" }, + { text: "Transmission", value: "transmission" }, + { text: "Transmission Map", value: "transmissionMap" }, + { text: "Thickness", value: "thickness" }, + { text: "Thickness Map", value: "thicknessMap" }, + { text: "Anisotropy", value: "anisotropy" }, + { text: "Anisotropy Map", value: "anisotropyMap" }, + { text: "Anisotropy Rotation", value: "anisotropyRotation" }, + { text: "Attenuation Distance", value: "attenuationDistance" }, + { text: "Attenuation Color", value: "attenuationColor" }, + { text: "Thickness", value: "thickness" }, + { text: "Iridescence", value: "iridescence" }, + { text: "Iridescence Ior", value: "iridescenceIOR" }, + { text: "Iridescence Map", value: "iridescenceMap" }, + { text: "Iridescence Thickness Range", value: "iridescenceThicknessRange" }, + + "|MESH Displacement / Normal / Bump| <-- not a property", + { text: "Displacement Map", value: "displacementMap" }, + { text: "Displacement Scale", value: "displacementScale" }, + { text: "Displacement Bias", value: "displacementBias" }, + { text: "Bump Map", value: "bumpMap" }, + { text: "Bump Scale", value: "bumpScale" }, + { text: "Normal Map Type", value: "normalMapType" }, + + "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", + { text: "Shininess", value: "shininess" }, + + { text: "Wireframe", value: "wireframe" }, + { text: "Wireframe Linewidth", value: "wireframeLinewidth" }, + { text: "Wireframe Linecap", value: "wireframeLinecap" }, + { text: "Wireframe Linejoin", value: "wireframeLinejoin" }, + + "|POINTS| <-- not a property", + { text: "Size", value: "size" }, + { text: "Size Attenuation", value: "sizeAttenuation" }, + + "|LINES| <-- not a property", + { text: "Scale", value: "scale" }, + { text: "Dash Size", value: "dashSize" }, + { text: "Gap Size", value: "gapSize" }, + + "|SPRITES| <-- not a property", + { text: "Rotation", value: "rotation" } +]}, + blendModes: {acceptReporters: false, items: [ + { text: "No Blending", value: "NoBlending" },{ text: "Normal Blending", value: "NormalBlending" },{ text: "Additive Blending", value: "AdditiveBlending" },{ text: "Subtractive Blending", value: "SubtractiveBlending" },{ text: "Multiply Blending", value: "MultiplyBlending" },{ text: "Custom Blending", value: "CustomBlending" } + ]}, + depthModes: {acceptReporters: false, items: [ + { text: "Never Depth", value: "NeverDepth" },{ text: "Always Depth", value: "AlwaysDepth" },{ text: "Equal Depth", value: "EqualDepth" },{ text: "Less Depth", value: "LessDepth" },{ text: "Less Equal Depth", value: "LessEqualDepth" },{ text: "Greater Equal Depth", value: "GreaterEqualDepth" },{ text: "Greater Depth", value: "GreaterDepth" },{ text: "Not Equal Depth", value: "NotEqualDepth" } + ]}, + materialTypes:{acceptReporters: false, items: [ + {text:"Mesh Basic Material",value:"MeshBasicMaterial"},{text:"Mesh Standard Material",value:"MeshStandardMaterial"},{text:"Mesh Physical Material",value:"MeshPhysicalMaterial"},{text:"Mesh Lambert Material",value:"MeshLambertMaterial"},{text:"Mesh Phong Material",value:"MeshPhongMaterial"},{text:"Mesh Depth Material",value:"MeshDepthMaterial"},{text:"Mesh Normal Material",value:"MeshNormalMaterial"},{text:"Mesh Matcap Material",value:"MeshMatcapMaterial"},{text:"Mesh Toon Material",value:"MeshToonMaterial"},{text:"Line Basic Material",value:"LineBasicMaterial"},{text:"Line Dashed Material",value:"LineDashedMaterial"},{text:"Points Material",value:"PointsMaterial"},{text:"Sprite Material",value:"SpriteMaterial"},{text:"Shadow Material",value:"ShadowMaterial"} + ]}, + textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, + textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, + geometryTypes: {acceptReporters: false, items: [ + {text: "Box Geometry", value: "BoxGeometry"},{text: "Sphere Geometry", value: "SphereGeometry"},{text: "Cylinder Geometry", value: "CylinderGeometry"},{text: "Plane Geometry", value: "PlaneGeometry"},{text: "Circle Geometry", value: "CircleGeometry"},{text: "Torus Geometry", value: "TorusGeometry"},{text: "Torus Knot Geometry", value: "TorusKnotGeometry"}, + ]}, + modelsList: {acceptReporters: false, items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) + if (models.length < 1) return [["Load a model! (GLB Loader category)"]] + + // @ts-ignore + return models.map( m => [m.name] ) + }}, + fonts: {acceptReporters: false, items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.json')) + if (models.length < 1) return [["Load a font!"]] + + // @ts-ignore + return models.map( m => [m.name] ) + }}, + + } + }} + + addObject(args) { + const object = new THREE[args.TYPE](); + + object.castShadow = true + object.receiveShadow = true + + createObject(args.OBJECT3D, object, args.GROUP) + } + cloneObject(args) { + let object = getObject(args.OBJECT3D) + const clone = object.clone(true) + clone.name + createObject(args.NAME, clone, args.GROUP) + } + setObjectV3(args) { + let object = getObject(args.OBJECT3D) + let values = JSON.parse(args.VALUE) + + function degToRad(deg) { + return deg * Math.PI / 180; + } + + + if (object.rigidBody) { + const x = values[0] + const y = values[1] + const z = values[2] + if (args.PROPERTY === "rotation") { + const euler = new THREE.Euler( + degToRad(x), + degToRad(y), + degToRad(z), + 'YXZ' + ) + const quaternion = new THREE.Quaternion() + quaternion.setFromEuler(euler) + + object.rigidBody.setRotation({ + x: quaternion.x, + y: quaternion.y, + z: quaternion.z, + w: quaternion.w + }); + } else if (args.PROPERTY === "position") { + object.rigidBody.setTranslation({ x: x, y: y, z: z }, true) + } + return + } + + if (object.isCamera == true && controls) { + + } + + if (args.PROPERTY === "rotation") { + values = values.map(v => v * Math.PI / 180); + object.rotation.set(0,0,0) + } + if (object.isDirectionalLight == true) {object.pos = new THREE.Vector3(...values); console.log(true, values, object.pos); return} + object[args.PROPERTY].set(...values); + + if (object.type == "CubeCamera") object.updateCoordinateSystem() + } + /* + changeObjectV3(args) { + getObject(args.OBJECT3D) + let values = JSON.parse(args.VALUE) + + if (args.PROPERTY === "rotation") { + values = values.map(v => v * Math.PI / 180); + object.rotation.x += values[0] + object.rotation.y += values[1] + object.rotation.z += values[2] + } + else { + object[args.PROPERTY].add(...values); + } + } + changeObjectXV3(args) { + getObject(args.OBJECT3D) + let value = args.VALUE + if (args.PROPERTY === "rotation") value = value * Math.PI / 180 + + object[args.PROPERTY][args.X] += value + } + */ + getObjectV3(args) { + let object = getObject(args.OBJECT3D) + if (!object) return + let values = vector3ToString(object[args.PROPERTY]) + if (args.PROPERTY === "rotation") { + const toDeg = Math.PI/180 + values = [values[0]/toDeg,values[1]/toDeg,values[2]/toDeg,] + } + + return JSON.stringify(values) + } + setObject(args){ + let object = getObject(args.OBJECT3D) + let value = args.VALUE + if (args.PROPERTY === "material") {const mat = materials[args.NAME]; if (mat) value = mat; else value = undefined} + else if (args.PROPERTY === "geometry") {const geo = geometries[args.NAME]; if (geo) value = geo; else value = undefined} + else value = !!value + + if (value == undefined) return //invalid geo/mat + object[args.PROPERTY] = value + } + getObject(args){ + let object = getObject(args.OBJECT3D) + if (!object) return + let value + if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; + else value = object.visible; + + return value + } + removeObject(args) { + removeObject(args.OBJECT3D) + } + objectE(args) { + return scene.children.map(o => o.name).includes(args.NAME) + } + +//defines + newMaterial(args) { + if (materials[args.NAME] && alerts) alert ("material already exists! will replace...") + const mat = new THREE[args.TYPE](); + mat.name = args.NAME; + + materials[args.NAME] = mat; + } + async setMaterial(args) { + if (typeof(args.VALUE) == "string" && args.VALUE.at(0) == "|") return + const mat = materials[args.NAME] + + let value = args.VALUE + + if (args.VALUE == "false") value = false + + if (args.PROPERTY == "side") {value = (args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide)} + else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)) + else value = getAsset(value) + + + console.log("o:", args.VALUE, typeof(args.VALUE)) + console.log("r:", value, typeof(value)) + + mat[args.PROPERTY] = await (value) //await incase its a texture + mat.needsUpdate = true + } + setBlending(args) { + const mat = materials[args.NAME] + mat.blending = THREE[args.VALUE] + mat.premultipliedAlpha = true + mat.needsUpdate = true + } + setDepth(args) { + const mat = materials[args.NAME] + mat.depthFunc = THREE[args.VALUE] + mat.needsUpdate = true + } + removeMaterial(args){ + const mat = materials[args.NAME] + mat.dispose() + delete(materials[args.NAME]) + } + materialE(args) { + return materials[args.NAME] ? true : false + } + + newGeometry(args) { + if (geometries[args.NAME] && alerts) alert ("geometry already exists! will replace...") + const geo = new THREE[args.TYPE]() + geo.name = args.NAME + + geometries[args.NAME] = geo + } + setGeometry(args) { + const geo = geometries[args.NAME] + geo[args.PROPERTY] = (args.VALUE) + + geo.needsUpdate = true; + } + removeGeometry(args){ + const geo = geometries[args.NAME] + geo.dispose() + delete(geometries[args.NAME]) + } + geometryE(args) { + return geometries[args.NAME] ? true : false + } + + newGeo(args) { + const geometry = new THREE.BufferGeometry() + geometry.name = args.NAME + geometries[args.NAME] = geometry + } + async geoPoints(args) { + const geometry = geometries[args.NAME] + const positions = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v3 of each vertex of each triangle + + geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3)) + geometry.computeVertexNormals() + + geometry.needsUpdate = true + } + geoUVs(args) { + const geometry = geometries[args.NAME] + const UVs = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v2 of each UV of each triangle + + geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(UVs), 2)) + geometry.needsUpdate = true + } + + splines(args) { + const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)) + geometry.name = args.NAME + + geometries[args.NAME] = geometry + } + + async splineModel(args) { + const model = await getModel(args.MODEL, args.NAME) + if (!model) return console.warn("Model not found:", args.MODEL) + + const curve = getAsset(args.CURVE) + const spacing = parseFloat(args.SPACING) || 1 + const curveLength = curve.getLength() + const divisions = Math.floor(curveLength / spacing) + + const geomList = [] + const matList = [] + + for (let i = 0; i <= divisions; i++) { + const t = i / divisions + const pos = curve.getPointAt(t) + const tangent = curve.getTangentAt(t) + + const temp = model.clone(true) + temp.position.copy(pos) + + const up = new THREE.Vector3(0, 0, 1) + const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()) + temp.quaternion.copy(quat) + + temp.updateMatrixWorld(true) + + temp.traverse(child => { + if (child.isMesh && child.geometry) { + const geom = child.geometry.clone() + geom.applyMatrix4(child.matrixWorld) + geomList.push(geom) + matList.push(child.material) //.clone() ? + } + }) + } + + const validGeoms = geomList.filter(g => { + const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position + if (!ok) console.warn("geometry skipped:", g) + return ok + }) + + const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true) + merged.computeBoundingBox() + merged.computeBoundingSphere() + + merged.name = args.NAME + geometries[args.NAME] = merged + matList.name = args.NAME + materials[args.NAME] = matList + } + + async text(args) { + const fontFile = runtime.getTargetForStage().getSounds().find(c => c.name === args.FONT) + if (!fontFile) return + + const json = new TextDecoder().decode(fontFile.asset.data.buffer) + const fontData = JSON.parse(json) + + const font = fontLoad.parse(fontData) + + const params = {font: font, size: JSON.parse(args.S), height: JSON.parse(args.D), curveSegments: JSON.parse(args.CS), bevelEnabled: false} + const geometry = new TextGeometry.TextGeometry(args.TEXT, params) + geometry.computeVertexNormals() + geometry.center() // optional, recenters the text + + + geometry.name = args.NAME + + geometries[args.NAME] = geometry + } + + async loadFont() { + openFileExplorer(".json").then(files => { + const file = files[0] + const reader = new FileReader() + + reader.onload = async (e) => { + const arrayBuffer = e.target.result + + // From lily's assets + // // Thank you PenguinMod for providing this code. + + const targetId = runtime.getTargetForStage().id //util.target.id not working! + const assetName = Cast.toString(file.name) + + const buffer = arrayBuffer + + const storage = runtime.storage + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ) + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ) + alert("Font loaded successfully!") + } catch (e) { + console.error(e) + alert("Error loading font.") + } + + // End of PenguinMod + } + + reader.readAsArrayBuffer(file); + }) + } + openConv() {{open("https://gero3.github.io/facetype.js/")}} + + } + Scratch.extensions.register(new ThreeObjects()) + + class ThreeLights { + getInfo() { + return { + id: "threeLights", + name: "Three Lights", + color1: "#c7a22aff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "addLight", blockType: Scratch.BlockType.COMMAND, text: "add light [NAME] type [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "lightTypes"}}}, + {opcode: "setLight", blockType: Scratch.BlockType.COMMAND, text: "set light [NAME][PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lightProperties", defaultValue: "intensity"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1", exemptFromNormalization: true}}}, + ], + menus: { + lightTypes: {acceptReporters: false, items: [ + {text: "Ambient Light", value: "AmbientLight"},{text: "Directional Light", value: "DirectionalLight"},{text: "Point Light", value: "PointLight"},{text: "Hemisphere Light", value: "HemisphereLight"},{text: "Spot Light", value: "SpotLight"}, + ]}, + lightProperties: {acceptReporters: false, items: [ + {text: "Color", value: "color"},{text: "Intensity", value: "intensity"},{text: "Cast Shadow?", value: "castShadow"}, + {text: "Ground Color (HemisphereLight)", value: "groundColor"}, + {text: "Map (SpotLight)", value: "map"},{text: "Distance (SpotLight)", value: "distance"},{text: "Decay (SpotLight)", value: "decay"},{text: "Penumbra (SpotLight)", value: "penumbra"},{text: "Angle/Size (SpotLight)", value: "angle"},{text: "Power (SpotLight)", value: "power"}, + {text: "Target Position (Directional/SpotLight)", value: "target"}, + ]}, + } + }} + + addLight(args) { + const light = new THREE[args.TYPE](0xffffff, 1) + + createObject(args.NAME, light, args.GROUP) + lights[args.NAME] = light + if (light.type === "AmbientLight" || "HemisphereLight") return + + light.castShadow = true + if (light.type === "PointLight") return + //Directional & Spot Light + light.target.position.set(0, 0, 0) + scene.add(light.target) + + light.pos = new THREE.Vector3(0,0,0) + + light.shadow.mapSize.width = 4096 + light.shadow.mapSize.height = 2048 + + if (light.type === "SpotLight") { + light.decay = 0 + light.shadow.camera.near = 500; + light.shadow.camera.far = 4000; + light.shadow.camera.fov = 30; + } + light.shadow.needsUpdate = true + light.needsUpdate = true + } + + setLight(args) { + const light = lights[args.NAME] + if (!args.PROPERTY) return + if (args.PROPERTY === "target") { + light.target.position.set(...JSON.parse(args.VALUE)) //vector3 + light.target.updateMatrixWorld(); + } + else { + light[args.PROPERTY] = getAsset(args.VALUE) + } + light.needsUpdate = true + + if (light.type === "AmbientLight" || "HemisphereLight") return + + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true + } + + } + Scratch.extensions.register(new ThreeLights()) + + class ThreeUtilities { + getInfo() { + return { + id: "threeUtility", + name: "Three Utilities", + color1: "#3875c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "newVector2", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}}}, + {opcode: "newVector3", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y] [Z]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}, Z: {type: Scratch.ArgumentType.NUMBER}}}, + "---", + {opcode: "operateV3", blockType: Scratch.BlockType.REPORTER, text: "do [V3] [O] [V32]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, O: {type: Scratch.ArgumentType.STRING, menu: "operators"}, V32: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, + {opcode: "moveVector3", blockType: Scratch.BlockType.REPORTER, text: "move [S] steps in vector [V3] in direction [D3]", arguments: {S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, + {opcode: "directionTo", blockType: Scratch.BlockType.REPORTER, text: "direction from [V3] to [T3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, T3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, + "---", + {opcode: "newColor", blockType: Scratch.BlockType.REPORTER, text: "New Color [HEX]", arguments: {HEX: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff"}}}, + {opcode: "newFog", blockType: Scratch.BlockType.REPORTER, text: "New Fog [COLOR] [NEAR] [FAR]", arguments: {COLOR: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff", exemptFromNormalization: true}, NEAR: {type: Scratch.ArgumentType.NUMBER}, FAR: {type: Scratch.ArgumentType.NUMBER, defaultValue: 10}}}, + {opcode: "newTexture", blockType: Scratch.BlockType.REPORTER, text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, + {opcode: "newCubeTexture", blockType: Scratch.BlockType.REPORTER, text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", arguments: {"COSTUMEX0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEX1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ1": {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, + {opcode: "newEquirectangularTexture", blockType: Scratch.BlockType.REPORTER, text: "New Equirectangular Texture [COSTUME] [MODE]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"}}}, + "---", + {opcode: "curve", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", arguments: {TYPE: {type: Scratch.ArgumentType.STRING, menu: "curveTypes"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]"}, CLOSED: {type: Scratch.ArgumentType.STRING, defaultValue: "true"}}}, + "---", + {opcode:"mouseDown",extensions: ["colours_sensing"], blockType: Scratch.BlockType.BOOLEAN, text: "mouse [BUTTON] [action]?", arguments: {BUTTON: {type: Scratch.ArgumentType.STRING, menu: "mouseButtons"},action: {type: Scratch.ArgumentType.STRING, menu: "mouseAction"}}}, + {opcode: "mousePos",extensions: ["colours_sensing"], blockType: Scratch.BlockType.REPORTER, text: "mouse position", arguments: {}}, + "---", + {opcode: "getItem",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "get item [ITEM] of [ARRAY]", arguments: {ITEM: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}, ARRAY: {type: Scratch.ArgumentType.STRING, defaultValue: `["myObject", "myLight"]`}}}, + {blockType: Scratch.BlockType.LABEL, text: "↳ Raycasting"}, + {opcode: "raycast", blockType: Scratch.BlockType.COMMAND, text: "Raycast from [V3] in direction [D3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,1]"}}}, + {opcode: "getRaycast", blockType: Scratch.BlockType.REPORTER, text: "get raycast [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "raycastProperties"}}}, + + ], + menus: { + materialProperties: {acceptReporters: false, items: [ + {text: "Color", value: "color"},{text: "Map (texture)", value: "map"},{text: "Alpha Map (texture)", value: "alphaMap"},{text: "Alpha Test (0-1)", value: "alphaTest"},{text: "Side (front/back/double)", value: "side"},{text: "Bump Map (texture)", value: "bumpMap"},{text: "Bump Scale", value: "bumpScale"}, + ]}, + textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, + textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, + raycastProperties: {acceptReporters: false, items: [ + {text: "Intersected Object Names", value: "name"},{text: "Number of Objects", value: "number"},{text: "Intersected Objects distances", value: "distance"}, + ]}, + mouseButtons: {acceptReporters: false, items: ["left","middle","right"]}, + mouseAction: {acceptReporters: false, items: ["Down","Clicked"]}, + curveTypes: {acceptReporters: false, items: ["CatmullRomCurve3"]}, + operators: {acceptReporters: false, items: [ + "+","-","*","/","=","max","min","dot","cross","distance to","angle to","apply euler", + ]} + } + }} + mouseDown(args) { + if (args.action === "Down") return isMouseDown[args.BUTTON] + if (args.action === "Clicked") { + if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false + else prevMouse[args.BUTTON] = true; return true + } + } + mousePos(event) { + return JSON.stringify(mouseNDC) + } + newVector3(args) { + return JSON.stringify([args.X, args.Y, args.Z]) + } + operateV3(args){ + const v3 = new THREE.Vector3(...JSON.parse(args.V3)) + const v32 = new THREE.Vector3(...JSON.parse(args.V32)) + + let r + if (args.O == "+") r = v3.add(v32) + else if (args.O == "-") r = v3.sub(v32) + else if (args.O == "*") r = v3.multiply(v32) + else if (args.O == "/") r = v3.divide(v32) + else if (args.O == "=") r = v3.equals(v32) + else if (args.O == "max") r = v3.max(v32) + else if (args.O == "min") r = v3.min(v32) + else if (args.O == "dot") r = v3.dot(v32) + else if (args.O == "cross") r = v3.cross(v32) + else if (args.O == "distance to") r = v3.distanceTo(v32) + else if (args.O == "angle to") r = v3.angleTo(v32) + else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)) + + if (typeof(r) == "object") return JSON.stringify([r.x, r.y, r.z]) + else return JSON.stringify(r) + } + + newVector2(args) { + return JSON.stringify([args.X, args.Y]) + } + + moveVector3(args) { + const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); + const steps = Number(args.S); + + const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); + + const yaw = THREE.MathUtils.degToRad(yawInputDeg); + const pitch = THREE.MathUtils.degToRad(pitchInputDeg); + const roll = THREE.MathUtils.degToRad(rollInputDeg); + + const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); + + const forwardVector = new THREE.Vector3(0, 0, -1); + const direction = forwardVector.applyEuler(euler).normalize(); + + const newPos = currentPos.add(direction.multiplyScalar(steps)); + return JSON.stringify([newPos.x, newPos.y, newPos.z]); + } + + directionTo(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)) + const toV3 = new THREE.Vector3(...JSON.parse(args.T3)) + + const direction = toV3.clone().sub(v3).normalize(); + // Pitch (X) + const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x*direction.x + direction.z*direction.z)); + // Yaw (Y) + const yaw = Math.atan2(direction.x, direction.z); + + // Roll always 0 + return JSON.stringify([180+THREE.MathUtils.radToDeg(pitch),THREE.MathUtils.radToDeg(yaw),0]) + } + + newColor(args) { + const color = new THREE.Color(args.HEX) + const uuid = crypto.randomUUID() + assets.colors[uuid] = color + return `colors/${uuid}` + } + newFog(args) { + const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR) + const uuid = crypto.randomUUID() + assets.fogs[uuid] = fog + return `fogs/${uuid}` + } + async newTexture(args) { + const textureURI = encodeCostume(args.COSTUME) + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) + assets.textures[texture.uuid] = texture + return `textures/${texture.uuid}` + } + async newCubeTexture(args) { + const uris = [encodeCostume(args.COSTUMEX0),encodeCostume(args.COSTUMEX1), encodeCostume(args.COSTUMEY0),encodeCostume(args.COSTUMEY1), encodeCostume(args.COSTUMEZ0),encodeCostume(args.COSTUMEZ1)] + const normalized = await Promise.all(uris.map(uri => resizeImageToSquare(uri, 256))); + const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); + + texture.name = "CubeTexture" + args.COSTUMEX0; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) + assets.textures[texture.uuid] = texture + return `textures/${texture.uuid}` + } + async newEquirectangularTexture(args) { + const textureURI = encodeCostume(args.COSTUME) + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME + texture.mapping = THREE.EquirectangularReflectionMapping + + setTexutre(texture, args.MODE) + assets.textures[texture.uuid] = texture + return `textures/${texture.uuid}` + } + + curve(args) { + function parsePoints(input) { + // Match all [x,y,z] groups + const matches = input.match(/\[([^\]]+)\]/g) + if (!matches) return [] + + return matches.map(str => { + const nums = str + .replace(/[\[\]\s]/g, '') + .split(',') + .map(Number) + return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0) + }) + } + const points = parsePoints(args.POINTS) + const curve = new THREE[args.TYPE](points) + curve.closed = JSON.parse(args.CLOSED) + + const uuid = crypto.randomUUID() + assets.curves[uuid] = curve + return `curves/${uuid}` + } + + getItem(args) { + const items = JSON.parse(args.ARRAY) + const item = items[args.ITEM - 1] + if (!item) return "0" + return item + } + + raycast(args) { + const origin = new THREE.Vector3(...JSON.parse(args.V3)) + // rotation is in degrees => convert to radians first + const rot = JSON.parse(args.D3).map(v => v * Math.PI / 180) + + const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder) + const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize() + + const raycaster = new THREE.Raycaster() + //const camera = getObject(args.CAMERA) + raycaster.set( origin, direction ); + + const intersects = raycaster.intersectObjects( scene.children, true ) + + raycastResult = intersects + } + getRaycast(args) { + if (args.PROPERTY === "number") return raycastResult.length + if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map(i => i.distance)) + return JSON.stringify(raycastResult.map(i => i.object[args.PROPERTY])) + } + + } + Scratch.extensions.register(new ThreeUtilities()) + + class ThreeGLB { + getInfo() { + return { + id: "threeGLB", + name: "Three GLB Loader", + color1: "#c53838ff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {blockType: Scratch.BlockType.BUTTON, text: "Load GLB File", func: "loadModelFile"}, + {opcode: "addModel", blockType: Scratch.BlockType.COMMAND, text: "add [ITEM] as [NAME] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},ITEM: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}}}, + {opcode: "getModel", blockType: Scratch.BlockType.REPORTER, text: "get object [PROPERTY] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "modelProperties"}}}, + {opcode: "playAnimation", blockType: Scratch.BlockType.COMMAND, text: "play animation [ANAME] of [NAME], [TIMES] times", arguments: {TIMES: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, + {opcode: "pauseAnimation", blockType: Scratch.BlockType.COMMAND, text: "set [TOGGLE] animation [ANAME] of [NAME]", arguments: {TOGGLE: {type: Scratch.ArgumentType.NUMBER, menu: "pauseUn"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, + {opcode: "stopAnimation", blockType: Scratch.BlockType.COMMAND, text: "stop animation [ANAME] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, + + ], + menus: { + modelProperties: {acceptReporters: false, items: [ + {text: "Animations", value: "animations"}, + ]}, + pauseUn: {acceptReporters: true, items: [{text: "Pause", value: "true"},{text: "Unpasue", value: "false"},]}, + modelsList: {acceptReporters: false, items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) + if (models.length < 1) return [["Load a model!"]] + + // @ts-ignore + return models.map( m => [m.name] ) + }}, + } + }} + + async loadModelFile() { + + openFileExplorer(".glb").then(files => { + const file = files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + const arrayBuffer = e.target.result; + + { // From lily's assets + + // Thank you PenguinMod for providing this code. + { + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); + + //const res = await Scratch.fetch(args.URL); + //const buffer = await res.arrayBuffer(); + const buffer = arrayBuffer + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + alert("Model loaded successfully!"); + } catch (e) { + console.error(e); + alert("Error loading model."); + } + } + // End of PenguinMod + } + }; + + reader.readAsArrayBuffer(file); + }) + + } + async addModel(args) { + const group = await getModel(args.ITEM, args.NAME) + + createObject(args.NAME, group, args.GROUP) + } + getModel(args){ + if (!models[args.NAME]) return; + return Object.keys(models[args.NAME].actions).toString() + } + + playAnimation(args) { + const model = models[args.NAME] + if (!model) {console.log("no model!"); return} + + const action = model.actions[args.ANAME] //clones of models dont have a stored actions! + if (!action) { + console.log("no action!") + return + } + + args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity) + + action.reset() + .play() + } + stopAnimation(args) { + const model = models[args.NAME]; + if (!model) return; + + const action = model.actions[args.ANAME]; + if (action) action.stop(); + } + pauseAnimation(args) { + const model = models[args.NAME]; + if (!model) return; + + const action = model.actions[args.ANAME]; + if (action) action.paused = args.TOGGLE + } + + } + Scratch.extensions.register(new ThreeGLB()) + + class ThreeAddons { + getInfo() { + return { + id: "threeAddons", + name: "Three Addons", + color1: "#c538a2ff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {blockType: Scratch.BlockType.LABEL, text: "Orbit Control"}, + {opcode: "OrbitControl", blockType: Scratch.BlockType.COMMAND, text: "set addon Orbit Control [STATE]", arguments: {STATE: {type: Scratch.ArgumentType.STRING, menu: "onoff"},}}, + + {blockType: Scratch.BlockType.LABEL, text: "Post Processing"}, + {opcode: "resetComposer", blockType: Scratch.BlockType.COMMAND, text: "reset composer"}, + {opcode: "bloom", blockType: Scratch.BlockType.COMMAND, text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},I: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, T:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, + {opcode: "godRays", blockType: Scratch.BlockType.COMMAND, text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}, DEC:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.95}, DENS:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},EXP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1},WEI:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.4},RES:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},SAMP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 64},}}, + {opcode: "dots", blockType: Scratch.BlockType.COMMAND, text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, A: {type: Scratch.ArgumentType.ANGLE, defaultValue: 0},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, + {opcode: "depth", blockType: Scratch.BlockType.COMMAND, text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", arguments: {FD: {type: Scratch.ArgumentType.NUMBER, defaultValue: (3)},FL: {type: Scratch.ArgumentType.NUMBER, defaultValue: (0.001)},BS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 4},H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 240},OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}}}, + "---", + {opcode: "custom", blockType: Scratch.BlockType.COMMAND, text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myShader"}, FRA: {type: Scratch.ArgumentType.STRING}, VER: {type: Scratch.ArgumentType.STRING}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}, OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, + ], + menus: { + onoff: {acceptReporters: true, items: [{text: "enabled", value: "1"},{text: "disabled", value: "0"},]}, + blendModes: {acceptReporters: false, items: [ + "SKIP","SET","ADD","ALPHA","AVERAGE","COLOR","COLOR_BURN","COLOR_DODGE", + "DARKEN","DIFFERENCE","DIVIDE","DST","EXCLUSION","HARD_LIGHT","HARD_MIX", + "HUE","INVERT","INVERT_RGB","LIGHTEN","LINEAR_BURN","LINEAR_DODGE", + "LINEAR_LIGHT","LUMINOSITY","MULTIPLY","NEGATION","NORMAL","OVERLAY", + "PIN_LIGHT","REFLECT","SCREEN","SRC","SATURATION","SOFT_LIGHT","SUBTRACT", + "VIVID_LIGHT" + ]}, + } + }} + + OrbitControl(args) { + if (controls) controls.dispose() + + console.log("creating...", OrbitControls) + controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); + controls.enableDamping = true + + controls.enabled = !!args.STATE + console.log(controls) + } + + resetComposer() { + composer.passes = [] + passes = {} + customEffects = [] + updateComposers() + } + + bloom(args) { + if (!camera || !scene) {if (alerts) alert("set a camera!"); return} + const bloomEffect = new BloomEffect({ + intensity: args.I, + luminanceThreshold: args.T, // ← correct key + luminanceSmoothing: args.S, + blendFunction: BlendFunction[args.BLEND], + }) + bloomEffect.blendMode.opacity.value = args.OP + + const pass = new EffectPass(camera, bloomEffect) + + composer.addPass(pass) + } + + godRays(args) { + if (!camera || !scene) {if (alerts) alert("set a camera!"); return} + let object = getObject(args.NAME) + const sun = object + + const godRays = new GodRaysEffect(camera, sun, { + resolutionScale: args.RES, + density: args.DENS, // ray density + decay: args.DEC, // fade out + weight: args.WEI, // brightness of rays + exposure: args.EXP, + samples: args.SAMP, + blendFunction: BlendFunction[args.BLEND], + }) + godRays.blendMode.opacity.value = args.OP + const pass = new EffectPass(camera, godRays) + composer.addPass(pass) + } + + dots(args) { + if (!camera || !scene) {if (alerts) alert("set a camera!"); return} + const dot = new DotScreenEffect({ + angle: args.A, + scale: args.S, + blendFunction: BlendFunction[args.BLEND], + }) + dot.blendMode.opacity.value = args.OP + const pass = new EffectPass(camera, dot) + composer.addPass(pass) + } + + depth(args) { + if (!camera || !scene) {if (alerts) alert("set a camera!"); return} + const dofEffect = new DepthOfFieldEffect(camera, { + focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) + focalLength: args.FL, // lens focal length in meters + bokehScale: args.BS, // strength/size of the blur circles + height: args.H, // resolution hint (affects quality/perf) + blendFunction: BlendFunction[args.BLEND], + }) + dofEffect.blendMode.opacity.value = args.OP + + const dofPass = new EffectPass(camera, dofEffect) + composer.addPass(dofPass) + } + + async custom(args) { + function cleanGLSL(glslCode) { + //delete multilines comments + let cleanedCode = glslCode.replace(/\/\*[\s\S]*?\*\//g, ' ') + .replace(/ /g, '\n') + .replace(/\/\/.*$/gm, ' ') + .replace(/; /g, ';\n') + + return cleanedCode; + } + + let fs = cleanGLSL(` + ${args.FRA} + `) + if (!args.FRA.trim()) {fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`} + const vs = cleanGLSL(` + ${args.VER} + `) + console.log(fs) + console.log(vs) + + const effect = new Effect( + "Custom", + fs, + { + blendFunction: BlendFunction[args.BLEND], + vertexShader: vs, + uniforms: new Map([ //uniforms usually in shaders... open to more! + ['time', new THREE.Uniform(0.0)], + ['resolution', new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height))] + ]), + defines: new Map([['USE_TIME', '1'], ['USE_VERTEX_TRANSFORM', '']]), + } + ); + + effect.blendMode.opacity.value = args.OP + + const pass = new EffectPass(camera, effect); + composer.addPass(pass); + + customEffects.push(effect); + } + + } + Scratch.extensions.register(new ThreeAddons()) + + class RapierPhysics { + getInfo() { + return { + id: "rapierPhysics", + name: "RAPIER Physics", + color1: "#222222", + color2: "#203024ff", + color3: "#78f07eff", + blocks: [ + {opcode: "createWorld", blockType: Scratch.BlockType.COMMAND, text: "create world | gravity:[G]", arguments: {G: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,-9.81,0]"}}}, + {opcode: "getWorld", blockType: Scratch.BlockType.REPORTER, text: "get world [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "wProp"}}}, + "---", + {opcode: "objectPhysics", blockType: Scratch.BlockType.COMMAND, text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", arguments: {state2: {type: Scratch.ArgumentType.STRING, menu: "state2"},state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"}, type: {type: Scratch.ArgumentType.STRING, menu: "objectTypes", defaultValue: "dynamic"}, collider: {type: Scratch.ArgumentType.STRING, menu: "colliderTypes", defaultValue: "cuboid"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},mass: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},density: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},friction: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0.5"}}}, + "---", + {blockType: Scratch.BlockType.LABEL, text: "- RigidBody"}, + {opcode: "setRB", blockType: Scratch.BlockType.COMMAND, text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodySets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, + {opcode: "getRB", blockType: Scratch.BlockType.REPORTER, text: "get rigidbody [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodyProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "lockObjectAxis", blockType: Scratch.BlockType.COMMAND, text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", arguments: {OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lockAxes"}, X: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Y: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Z: {type: Scratch.ArgumentType.STRING, menu: "tf"}}}, + "---", + {opcode: "addForce", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,10,0]"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "forces", defaultValue: "addForce"},SPACE: {type: Scratch.ArgumentType.STRING, menu: "spaces", defaultValue: "world"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + {opcode: "resetForces", blockType: Scratch.BlockType.COMMAND, text: "reset [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "resetF", defaultValue: "resetForces"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "enableCCD", blockType: Scratch.BlockType.COMMAND, text: "enable Continuous Collision Detection for [OBJECT] [state]", arguments: {state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "oPropS", defaultValue: "physics"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "fixedJoint", blockType: Scratch.BlockType.COMMAND, text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},RA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},RB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},}}, + {opcode: "sphericalJoint", blockType: Scratch.BlockType.COMMAND, text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},}}, + {opcode: "revoluteJoint", blockType: Scratch.BlockType.COMMAND, text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},X: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"},}}, + "---", + {blockType: Scratch.BlockType.LABEL, text: "- Collider"}, + {opcode: "setC", blockType: Scratch.BlockType.COMMAND, text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderSets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, + {opcode: "getC", blockType: Scratch.BlockType.REPORTER, text: "get collider [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + "---", + {opcode: "sensorSingle", blockType: Scratch.BlockType.BOOLEAN, text: "is sensor [SENSOR] touching [OBJECT]?", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, + {opcode: "sensorAll", blockType: Scratch.BlockType.REPORTER, text: "objects touching sensor [SENSOR]", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}}} + ], + menus: { + wProp: {acceptReporters: false, items: [ + {text: "Gravity", value: "gravity"}, {text: "log to console", value: "log"} + ]}, + tf: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true", value: "true"}]}, + lockAxes: {acceptReporters: false, items: [ + {text: "Translation", value: "setEnabledTranslations"}, {text: "Rotation", value: "setEnabledRotations"} + ]}, + rigidBodyProperties: {acceptReporters: false, items: [ + {text: "Type", value: "bodyType"}, + {text: "Linear Velocity", value: "linvel"}, + {text: "Angular Velocity", value: "angvel"}, + {text: "Translation (position)", value: "translation"}, + {text: "Rotation (quaternion)", value: "rotation"}, + {text: "Mass", value: "mass"}, + //{text: "Center of Mass", value: "centerOfMass"}, + {text: "Linear Damping", value: "linearDamping"}, + {text: "Angular Damping", value: "angularDamping"}, + {text: "Is Sleeping?", value: "isSleeping"}, + //{text: "Can Sleep?", value: "isCanSleep"}, + {text: "Gravity Scale", value: "gravityScale"}, + {text: "Is Fixed?", value: "isFixed"}, + {text: "Is Dynamic?", value: "isDynamic"}, + {text: "Is Kinematic?", value: "isKinematic"}, + //{text: "Sleeping", value: "sleeping"} + ]}, + rigidBodySets: {acceptReporters: false, items: [ + //{text: "Linear Velocity", value: "setLinvel"}, + //{text: "Angular Velocity", value: "setAngvel"}, + //{text: "Mass", value: "setMass"}, + {text: "Gravity Scale", value: "setGravityScale"}, + //{text: "Can Sleep?", value: "setCanSleep"}, + //{text: "Sleeping", value: "sleeping"}, + {text: "Linear Damping", value: "setLinearDamping"}, + {text: "Angular Damping", value: "setAngularDamping"}, + {text: "Is Fixed?", value: "isFixed"}, + {text: "Is Dynamic?", value: "isDynamic"}, + {text: "Is Kinematic?", value: "isKinematic"} + ]}, + colliderProperties: {acceptReporters: false, items: [ + //{text: "Collider Type", value: "type"}, + {text: "Is Sensor?", value: "isSensor"}, + {text: "Friction", value: "friction"}, + {text: "Restitution", value: "restitution"}, + {text: "Density", value: "density"}, + {text: "Mass", value: "mass"}, + {text: "Position", value: "translation"}, + {text: "Rotation", value: "rotation"}, + //{text: "Area", value: "area"}, + {text: "Volume", value: "volume"}, + {text: "Collision Groups", value: "collisionGroups"}, + //{text: "Collision Mask", value: "collisionMask"}, + //{text: "Is Enabled?", value: "enabled"}, + //{text: "Contact Count", value: "contactCount"}, + //{text: "RigidBody Handle", value: "rigidBody"} + ]}, + colliderSets: {acceptReporters: false, items: [ + {text: "Friction", value: "setFriction"}, + {text: "Restitution", value: "setRestitution"}, + {text: "Density", value: "setDensity"}, + {text: "Is Sensor?", value: "setSensor"}, + {text: "Collision Groups", value: "setCollisionGroups"}, + //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool + //{text: "Position Offset", value: "setTranslation"}, + //{text: "Rotation Offset", value: "setRotation"} + ]}, + state: {acceptReporters: true, items: [{text: "on", value: "true"},{text: "off", value: "false"}]}, + state2: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true (must be fixed)", value: "true"}]}, + spaces: {acceptReporters: false, items: [{text: "World", value: "world"},{text: "Local", value: "local"}]}, + objectTypes: {acceptReporters: false, items: [{text: "Dynamic", value: "dynamic"},{text: "Fixed", value: "fixed"},{text: "Kinematic Position Based",value: "kinematicPositionBased"}]}, + colliderTypes: {acceptReporters: false, items: [{text: "Box, Rectangle, cuboid", value: "cuboid"},{text: "Sphere, ball", value: "ball"},{text: "Custom, complex simple shapes, convexHull", value: "convexHull"},{text:"Precision, TriMesh",value:"trimesh"}]}, + forces: {acceptReporters: false, items: [{text: "Force", value: "addForce"},{text: "Torque (rotation)", value: "addTorque"},{text: "Apply Impulse", value: "applyImpulse"},{text: "Apply Torque Impulse (rotation)", value: "applyTorqueImpulse"},{text: "Linear Velocity", value: "setLinvel"},{text: "Angular Velocity", value: "setAngvel"},]}, + resetF: {acceptReporters: false, items: [{text:"Forces", value: "resetForces"},{text:"Torques", value: "resetTorques"},]} + } + } + } + joint(jointData, bodyA, bodyB) { + physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true) + } + + fixedJoint(args) { + const VA = JSON.parse(args.VA).map(Number) + const VB = JSON.parse(args.VB).map(Number) + let RA = JSON.parse(args.RA).map(Number) + let RB = JSON.parse(args.RB).map(Number) + + RA = new THREE.Quaternion().setFromEuler( + new THREE.Euler( + THREE.MathUtils.degToRad(RA[0]), + THREE.MathUtils.degToRad(RA[1]), + THREE.MathUtils.degToRad(RA[2]) + ) + ) + RB = new THREE.Quaternion().setFromEuler( + new THREE.Euler( + THREE.MathUtils.degToRad(RB[0]), + THREE.MathUtils.degToRad(RB[1]), + THREE.MathUtils.degToRad(RB[2]) + ) + ) + + const data = RAPIER.JointData.fixed( + { x: VA[0], y: VA[1], z: VA[2] }, RA, + { x: VB[0], y: VB[1], z: VB[2] }, RB + ) + const objectA = getObject(args.ObjA) + let object = getObject(args.ObjB) + this.joint(data, objectA, object) + } + + sphericalJoint(args) { + const VA = JSON.parse(args.VA).map(Number) + const VB = JSON.parse(args.VB).map(Number) + + const data = RAPIER.JointData.spherical( + { x: VA[0], y: VA[1], z: VA[2] }, + { x: VB[0], y: VB[1], z: VB[2] } + ) + const objectA = getObject(args.ObjA) + let object = getObject(args.ObjB) + this.joint(data, objectA, object) + } + + revoluteJoint(args) { + const VA = JSON.parse(args.VA).map(Number) + const VB = JSON.parse(args.VB).map(Number) + const x = JSON.parse(args.X).map(Number) + + const data = RAPIER.JointData.revolute( + { x: VA[0], y: VA[1], z: VA[2] }, + { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, + ) + const objectA = getObject(args.ObjA) + let object = getObject(args.ObjB) + this.joint(data, objectA, object) + } + + prismaticJoint(args) { + const VA = JSON.parse(args.VA).map(Number) + const VB = JSON.parse(args.VB).map(Number) + const x = JSON.parse(args.X).map(Number) + + const data = RAPIER.JointData.prismatic( + { x: VA[0], y: VA[1], z: VA[2] }, + { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, + ) + const objectA = getObject(args.ObjA) + let object = getObject(args.ObjB) + this.joint(data, objectA, object) + } + + createWorld(args) { + const v3 = JSON.parse(args.G).map(Number) + const gravity = { x: v3[0], y: v3[1], z: v3[2]} + physicsWorld = new RAPIER.World(gravity) + + console.log(physicsWorld) + } + + getWorld(args) { + if (args.PROPERTY === "log") {console.log(physicsWorld); return "logged"} + return JSON.stringify(physicsWorld[args.PROPERTY]) + } + + setRB(args) { + let value = args.VALUE + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE + let object = getObject(args.OBJECT) + object.rigidBody[args.PROPERTY](value) + } + setC(args) { + let value = args.VALUE + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE + let object = getObject(args.OBJECT) + object.collider[args.PROPERTY](value) + } + + getRB(args) { + let object = getObject(args.OBJECT) + return JSON.stringify(object.rigidBody[args.PROPERTY]()) + } + getC(args) { + let object = getObject(args.OBJECT) + return JSON.stringify(object.collider[args.PROPERTY]()) + } + + lockObjectAxis(args) { + let object = getObject(args.OBJECT) + const x = !JSON.parse(args.X) + const y = !JSON.parse(args.Y) + const z = !JSON.parse(args.Z) + object.rigidBody[args.PROPERTY](x,y,z,true) //changes is xyz, wake up + } + + objectPhysics(args) { + let object = getObject(args.OBJECT) + object.physics = JSON.parse(args.state) + + if (JSON.parse(args.state)) { + //if already exists delete: + if (object.rigidBody) { + physicsWorld.removeRigidBody(object.rigidBody) + object.rigidBody = null + object.collider = null + } + /*asing a rigidbody and collider to object and add them to physicsWorld*/ + let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() + .setTranslation(object.position.x, object.position.y, object.position.z) + .setRotation({w: object.quaternion._w, x: object.quaternion._x, y: object.quaternion._y, z: object.quaternion._z}) + + let colliderDesc + switch(args.collider) { + case "cuboid": colliderDesc = createCuboidCollider(object,); break + case "ball": colliderDesc = createBallCollider(object); break + case "convexHull": colliderDesc = createConvexHullCollider(object); break + case "trimesh": colliderDesc = TriMesh(object); break + } + colliderDesc.setSensor(JSON.parse(args.state2)).setMass(args.mass).setDensity(args.density).setFriction(args.friction) + + let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc) + let collider = physicsWorld.createCollider(colliderDesc, rigidBody) + + object.rigidBody = rigidBody + object.collider = collider + } else { + /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ + physicsWorld.removeRigidBody(object.rigidBody) + object.rigidBody = null + object.collider = null + } + + } + + enableCCD(args) { + let object = getObject(args.OBJECT) + if (object.physics) { + let rigidBody = object.rigidBody + rigidBody.enableCcd(JSON.parse(args.state)) + } + } + + addForce(args) { + let object = getObject(args.OBJECT) + const vector = JSON.parse(args.VALUE).map(Number) + + let force = new THREE.Vector3(vector[0],vector[1],vector[2]) + if (args.SPACE === "local") { + force.applyQuaternion(object.quaternion); + } + + object.rigidBody[args.PROPERTY](force,true) + } + + resetForces(args) { + rigidBody[args.PROPERTY](true) + } + + sensorSingle(args) { + const sensor = getObject(args.SENSOR) + + let object = getObject(args.OBJECT) + + let touching = false + physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { + if (otherCollider === object.collider) touching = true + }) + + return touching + } + + sensorAll(args) { + const sensor = getObject(args.SENSOR) + + const touchedObjects = [] + + // loop thruogh every collider touching sensor + physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { + // find owner of collider + const otherObject = scene.children.find(o => o.collider === otherCollider) + console.log(otherCollider) + if (otherObject) touchedObjects.push(otherObject.name) + }) + + return JSON.stringify(touchedObjects) + } + + } + Scratch.extensions.register(new RapierPhysics()) + + //Thanks to the PointerLock extension of Turbowarp + const mouse = vm.runtime.ioDevices.mouse; + let isLocked = false; + let isPointerLockEnabled = false; + + let rect = threeRenderer.domElement.getBoundingClientRect(); + document.addEventListener("resize", () => { + rect = threeRenderer.domElement.getBoundingClientRect(); + }); + + const postMouseData = (e, isDown) => { + const { movementX, movementY } = e; + const { width, height } = rect; + const x = mouse._clientX + movementX; + const y = mouse._clientY - movementY; + mouse._clientX = x; + mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); + mouse._clientY = y; + mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); + if (typeof isDown === "boolean") { + const data = { + button: e.button, + isDown, + }; + originalPostIOData(data); + } + }; + + const mouseDevice = vm.runtime.ioDevices.mouse; + const originalPostIOData = mouseDevice.postData.bind(mouseDevice); + mouseDevice.postData = (data) => { + if (!isPointerLockEnabled) { + return originalPostIOData(data); + } + }; + + document.addEventListener( + "mousedown", + (e) => { + // @ts-expect-error + if (threeRenderer.domElement.contains(e.target)) { + if (isLocked) { + postMouseData(e, true); + } else if (isPointerLockEnabled) { + threeRenderer.domElement.requestPointerLock(); + } + } + }, + true + ); + document.addEventListener( + "mouseup", + (e) => { + if (isLocked) { + postMouseData(e, false); + // @ts-expect-error + } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { + threeRenderer.domElement.requestPointerLock(); + } + }, + true + ); + document.addEventListener( + "mousemove", + (e) => { + if (isLocked) { + postMouseData(e); + } + }, + true + ); + + document.addEventListener("pointerlockchange", () => { + isLocked = document.pointerLockElement === threeRenderer.domElement; + }); + document.addEventListener("pointerlockerror", (e) => { + console.error("Pointer lock error", e); + }); + + const oldStep = vm.runtime._step; + vm.runtime._step = function (...args) { + const ret = oldStep.call(this, ...args); + if (isPointerLockEnabled) { + const { width, height } = rect; + mouse._clientX = width / 2; + mouse._clientY = height / 2; + mouse._scratchX = 0; + mouse._scratchY = 0; + } + return ret; + }; + + vm.runtime.on("PROJECT_LOADED", () => { + isPointerLockEnabled = false; + if (isLocked) { + document.exitPointerLock(); + } + }); + + class Pointerlock { + getInfo() { + return { + id: "threepointerlockmod", + name: "Pointerlock for Extra 3D", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [ + {opcode: "setLocked", blockType: Scratch.BlockType.COMMAND, text: "set pointer lock [enabled]", arguments: { enabled: { type: Scratch.ArgumentType.STRING, defaultValue: "true", menu: "enabled"}},}, + {opcode: "isLocked", blockType: Scratch.BlockType.BOOLEAN, text: "pointer locked?",}, + ], + menus: { + enabled: {acceptReporters: true, items: [ + {text: "enabled", value: "true"},{text: "disabled", value: "false"}, + ]} + }, + } + } + + setLocked(args) { + isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; + if (!isPointerLockEnabled && isLocked) { + document.exitPointerLock(); + } + } + + isLocked() { + return isLocked; + } + } +Scratch.extensions.register(new Pointerlock()) + + }) + + + + +})(Scratch); diff --git a/threejsD_REMOTE_13852.js b/threejsD_REMOTE_13852.js new file mode 100644 index 0000000..447584f --- /dev/null +++ b/threejsD_REMOTE_13852.js @@ -0,0 +1,5016 @@ +/* jshint esversion: 11 */ +// Name: Extra 3D +// ID: threejsExtension +// Description: Use three js inside Turbowarp! A 3D graphics library. +// By: Civero +// License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors + +(function(Scratch) { + "use strict"; + + if (!Scratch.extensions.unsandboxed) { + throw new Error("Three-D extension must run unsandboxed"); + } + + if (Scratch.vm.runtime.isPackaged) { + alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); + return; + } + //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return + + const vm = Scratch.vm; + const runtime = vm.runtime; + const renderer = Scratch.renderer; + const canvas = renderer.canvas; + const Cast = Scratch.Cast; + const menuIconURI = + "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; + + let alerts = false; + console.log("alerts are " + (alerts ? "enabled" : "disabled")); + + let isMouseDown = { + left: false, + middle: false, + right: false, + }; + let prevMouse = { + left: false, + middle: false, + right: false, + }; + + let lastWidth = 0; + let lastHeight = 0; + + let THREE; + let clock; + let running; + let loopId; + //Addons + let GLTFLoader; + let gltf; + let OrbitControls; + let controls; + let BufferGeometryUtils; + let TextGeometry; + let fontLoad; + //Physics + let RAPIER; + let physicsWorld; + + let threeRenderer; + let scene; + let camera; + let eulerOrder = "YXZ"; + + let composer; + let passes = {}; + let customEffects = []; + let renderTargets = {}; + + let materials = {}; + let geometries = {}; + let lights = {}; + let models = {}; + + let assets = { + //should i place materials, geometries; inside too? + textures: {}, + colors: {}, + fogs: {}, + curves: {}, + renderTargets: {}, //not the same as the global one! this one only stores textures + }; + + let raycastResult = []; + + function resetor(level) { + camera = undefined; + composer.reset(); + + passes = {}; + customEffects = []; + renderTargets = {}; + + materials = {}; + geometries = {}; + lights = {}; + models = {}; + + if (level > 0) { + assets = { + textures: {}, + colors: {}, + fogs: {}, + curves: {}, + renderTargets: {}, + }; + } + + updateComposers(); + } + + //utility + function vector3ToString(prop) { + if (!prop) return "0,0,0"; + + const x = + typeof prop.x === "number" ? + prop.x : + typeof prop._x === "number" ? + prop._x : + JSON.stringify(prop).includes("X") ? + prop : + 0; + const y = typeof prop.y === "number" ? prop.y : typeof prop.y === "number" ? prop._y : 0; + const z = typeof prop.z === "number" ? prop.z : typeof prop.z === "number" ? prop.z : 0; + + return [x, y, z]; + } + + //objects + function createObject(name, content, parentName) { + let object = getObject(name, true); + if (object) { + removeObject(name); + alerts ? alert(name + " already exsisted, will replace!") : null; + } + content.name = name; + content.rotation._order = eulerOrder; + parentName === scene.name ? (object = scene) : (object = getObject(parentName)); + content.physics = false; + + object.add(content); + } + + function removeObject(name) { + let object = getObject(name); + if (!object) return; + + scene.remove(object); + + if (object.rigidBody) { + physicsWorld.removeCollider(object.collider, true); + physicsWorld.removeRigidBody(object.rigidBody, true); + object.rigidBody = null; + object.collider = null; + } + if (object.isLight) { + delete lights[name]; + } + } + + function getObject(name, isNew) { + let object = null; + if (!scene) { + alerts ? alert("Can not get " + name + ". Create a scene first!") : null; + return; + } + object = scene.getObjectByName(name); + if (!object && !isNew) { + alerts ? alert(name + " does not exist! Add it to scene") : null; + return; + } + return object; + } + + //materials + function encodeCostume(name) { + if (name.startsWith("data:image/")) return name; + return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); + } + + function setTexutre(texture, mode, style, x, y) { + texture.colorSpace = THREE.SRGBColorSpace; + + if (mode === "Pixelate") { + texture.minFilter = THREE.NearestFilter; + texture.magFilter = THREE.NearestFilter; + } else { + //Blur + texture.minFilter = THREE.NearestMipmapLinearFilter; + texture.magFilter = THREE.NearestMipmapLinearFilter; + } + + if (style === "Repeat") { + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(x, y); + } + + texture.generateMipmaps = true; + } + async function resizeImageToSquare(uri, size = 256) { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext("2d"); + + // clear + draw image scaled to fit canvas + ctx.clearRect(0, 0, size, size); + ctx.drawImage(img, 0, 0, size, size); + + resolve(canvas.toDataURL()); // return normalized Data URI + //delete canvas? + }; + img.src = uri; + }); + } + //light + function updateShadowFrustum(light, focusPos) { + if (light.type !== "DirectionalLight") return; + + // Frustum Size - Increase this value to cover a larger area. + const d = 50; + + // Update Orthographic Shadow Camera Frustum + const shadowCamera = light.shadow.camera; + + // Set the width/height of the frustum + shadowCamera.left = -d; + shadowCamera.right = d; + shadowCamera.top = d; + shadowCamera.bottom = -d; + + // Determine ranges + shadowCamera.near = 0.1; + shadowCamera.far = 500; + + // Position the Light and its Target + light.target.position.copy(focusPos); + const direction = light.position.clone().sub(light.target.position).normalize(); + light.position.copy(focusPos.clone().add(direction.multiplyScalar(100))); + + // Ensure matrices are updated. + light.target.updateMatrixWorld(); + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; + } + //composer + function updateComposers() { + if (!camera || !scene) return; // nothing to do yet + + // always recreate the RenderPass to point to the current scene/camera + passes["Render"] = new RenderPass(scene, camera); + + // ensure composer has a RenderPass as the first pass + const hasRender = composer.passes.some((p) => p && p.scene); + if (!hasRender) composer.addPass(passes["Render"]); + else { + // if composer already has one, replace it so it references current scene/camera + const idx = composer.passes.findIndex((p) => p && p.scene); + composer.passes[idx] = passes["Render"]; + } + } + //utility + function getMouseNDC(event) { + // Use threeRenderer.domElement for correct offset + const rect = threeRenderer.domElement.getBoundingClientRect(); + const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + return [x, y]; + } + + function checkCanvasSize() { + const { + width, + height + } = canvas; + if (width !== lastWidth || height !== lastHeight) { + lastWidth = width; + lastHeight = height; + resize(); + } + requestAnimationFrame(checkCanvasSize); //rerun next frame + } + //physics + function computeWorldBoundingBox(mesh) { + // Create a Box3 in world coordinates + const box = new THREE.Box3().setFromObject(mesh); + const size = new THREE.Vector3(); + box.getSize(size); + const center = new THREE.Vector3(); + box.getCenter(center); + return { + size, + center, + }; + } + + function createCuboidCollider(mesh) { + const { + size + } = computeWorldBoundingBox(mesh); + const collider = RAPIER.ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2); + return collider; + } + + function createBallCollider(mesh) { + const { + size + } = computeWorldBoundingBox(mesh); + // radius = 1/2 of the largest verticie + const radius = Math.max(size.x, size.y, size.z) / 2; + const collider = RAPIER.ColliderDesc.ball(radius); + return collider; //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) + } + + function createConvexHullCollider(mesh) { + mesh.updateWorldMatrix(true, false); + + const position = mesh.geometry.attributes.position; + const vertices = []; + const vertex = new THREE.Vector3(); + + // Matrix for scale only + const scaleMatrix = new THREE.Matrix4().makeScale(mesh.scale.x, mesh.scale.y, mesh.scale.z); + + for (let i = 0; i < position.count; i++) { + vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); + vertices.push(vertex.x, vertex.y, vertex.z); + } + + const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); + return collider; + } + + function TriMesh(mesh) { + // Get the positions array (from your geoPoints function) + const positions = mesh.geometry.attributes.position.array; + const numVertices = positions.length / 3; + + // Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] + const indices = Array.from({ + length: numVertices, + }, + (_, i) => i + ); + + const collider = RAPIER.ColliderDesc.trimesh(positions, new Uint32Array(indices)); + + return collider; + } + + function getModel(model, name) { + const file = runtime + .getTargetForStage() + .getSounds() + .find((c) => c.name === model); + if (!file) return; + + return new Promise((resolve, reject) => { + gltf.parse( + file.asset.data.buffer, + "", + (gltf) => { + const root = gltf.scene; + root.traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + const mixer = new THREE.AnimationMixer(root); + const actions = {}; + gltf.animations.forEach((clip) => { + const act = mixer.clipAction(clip); + act.clampWhenFinished = true; + actions[clip.name] = act; + }); + + models[name] = { + root, + mixer, + actions, + }; + resolve(root); + }, + (error) => { + console.error("Error parsing GLB model:", error); + reject(error); + } + ); + }); + } + async function openFileExplorer(format) { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = format; + input.multiple = false; + input.onchange = () => { + resolve(input.files); + input.remove(); + }; + input.click(); + }); + } + + function getMeshesUsingTexture(scene, targetTexture) { + const meshes = []; + + scene.traverse((object) => { + if (object.material) { + const materials = Array.isArray(object.material) ? object.material : [object.material]; + for (const material of materials) { + if (material.map === targetTexture) { + meshes.push(object); + break; + } + } + } + }); + + return meshes; + } + + function getAsset(path) { + if (typeof path == "string") { + //string? + if (path.includes("/")) { + //has the /? + const value = path.split("/"); + console.log(value[0], value[1]); + return assets[value[0]][value[1]]; + } + } + + return JSON.parse(path); //boolean or number + } + + let mouseNDC = [0, 0]; + //loops/init + function stopLoop() { + if (!running) return; + running = false; + + if (loopId) { + cancelAnimationFrame(loopId); + loopId = null; + if (threeRenderer) threeRenderer.clear(); + } + } + async function load() { + if (!THREE) { + // @ts-ignore + THREE = await import("https://esm.sh/three@0.180.0"); + //Addons + GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js"); + OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js"); + BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js"); + TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js"); + const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js"); + fontLoad = new FontLoader.FontLoader(); + + const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8"); + const { + EffectComposer, + EffectPass, + RenderPass, + + Effect, + BloomEffect, + GodRaysEffect, + DotScreenEffect, + DepthOfFieldEffect, + + BlendFunction, + } = POSTPROCESSING; + //so i can use them later as global + window.EffectComposer = EffectComposer; + window.EffectPass = EffectPass; + window.RenderPass = RenderPass; + window.Effect = Effect; + window.BloomEffect = BloomEffect; + window.GodRaysEffect = GodRaysEffect; + window.DotScreenEffect = DotScreenEffect; + window.DepthOfFieldEffect = DepthOfFieldEffect; + window.BlendFunction = BlendFunction; + + RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0"); + await RAPIER.init(); + + threeRenderer = new THREE.WebGLRenderer({ + powerPreference: "high-performance", + antialias: false, + stencil: false, + depth: true, + }); + threeRenderer.setPixelRatio(window.devicePixelRatio); + threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors + threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) + //threeRenderer.toneMappingExposure = 1.0 //(test) + + threeRenderer.shadowMap.enabled = true; + threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap; // (optional) + threeRenderer.domElement.style.pointerEvents = "auto"; //will disable turbowarp mouse events, but enable threejs's + + gltf = new GLTFLoader.GLTFLoader(); + clock = new THREE.Clock(); + + // Example: create a composer + composer = new EffectComposer(threeRenderer, { + frameBufferType: THREE.HalfFloatType, + }); + + renderer.addOverlay(threeRenderer.domElement, "manual"); + renderer.addOverlay(canvas, "manual"); + renderer.setBackgroundColor(1, 1, 1, 0); + + resize(); + + window.addEventListener("mousedown", (e) => { + if (e.button === 0) isMouseDown.left = true; + if (e.button === 1) isMouseDown.middle = true; + if (e.button === 2) isMouseDown.right = true; + }); + window.addEventListener("mouseup", (e) => { + if (e.button === 0) isMouseDown.left = false; + prevMouse.left = false; + if (e.button === 1) isMouseDown.middle = false; + prevMouse.middle = false; + if (e.button === 2) isMouseDown.right = false; + prevMouse.right = false; + }); + // prevent contextmenu on right click + threeRenderer.domElement.addEventListener("contextmenu", (e) => e.preventDefault()); + + threeRenderer.domElement.addEventListener("mousemove", (event) => { + mouseNDC = getMouseNDC(event); + }); + + running = false; + load(); + + startRenderLoop(); + runtime.on("PROJECT_START", () => startRenderLoop()); + runtime.on("PROJECT_STOP_ALL", () => stopLoop()); + runtime.on("STAGE_SIZE_CHANGED", () => { + requestAnimationFrame(() => resize()); + }); + //if (!runtime.isPackaged) checkCanvasSize() //only in editor + } + } + + function startRenderLoop() { + if (running) return; + running = true; + + const loop = () => { + if (!running) return; + //RAPIER + if (physicsWorld && scene) { + physicsWorld.step(); + + scene.children.forEach((obj) => { + if (!obj.isMesh || !obj.physics) return; + if (obj.rigidBody) { + obj.position.copy(obj.rigidBody.translation()); + obj.quaternion.copy(obj.rigidBody.rotation()); + } + }); + } + if (scene && camera) { + if (controls) controls.update(); + + const delta = clock.getDelta(); + Object.values(models).forEach((model) => { + if (model) model.mixer.update(delta); + }); + + Object.values(lights).forEach((light) => updateShadowFrustum(light, camera.position)); + + //update custom effects time + customEffects.forEach((e) => { + if (e.uniforms.get("time")) { + e.uniforms.get("time").value += delta; + } + }); + Object.values(renderTargets).forEach((t) => { + if (t.camera.type == "PerspectiveCamera") { + t.camera.aspect = t.target.width / t.target.height; + t.camera.updateProjectionMatrix(); + } + // get meshes using the texture associated with this target + const displayMeshes = getMeshesUsingTexture(scene, t.target.texture); + + displayMeshes.forEach((mesh) => { + mesh.visible = false; + }); + + if (t.camera.type == "PerspectiveCamera") { + threeRenderer.setRenderTarget(t.target); + threeRenderer.clear(true, true, true); + threeRenderer.render(scene, t.camera); + } else { + t.target.clear(threeRenderer); + t.camera.update(threeRenderer, scene); //cubeCamera + } + + displayMeshes.forEach((mesh) => { + mesh.visible = true; + }); + }); + + camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height; + camera.updateProjectionMatrix(); + threeRenderer.setRenderTarget(null); + composer.render(delta); + } + + loopId = requestAnimationFrame(loop); + }; + + loopId = requestAnimationFrame(loop); + } + + function resize() { + const w = canvas.width; + const h = canvas.height; + + threeRenderer.setSize(w, h); + composer.setSize(w, h); + customEffects.forEach((e) => { + if (e.uniforms.get("resolution")) { + e.uniforms.get("resolution").value.set(w, h); + } + }); + + if (camera) { + camera.aspect = w / h; + camera.updateProjectionMatrix(); + } + } + //wait until all packages are loaded + Promise.resolve(load()).then(() => { + class threejsExtension { + getInfo() { + return { + id: "threejsExtension", + name: "Extra 3D", + color1: "#222222", + color2: "#222222", + color3: "#11cc99", + menuIconURI, + blockIconURI: menuIconURI, + + blocks: [{ + blockType: Scratch.BlockType.BUTTON, + text: "Show Docs", + func: "openDocs", + }, + { + blockType: Scratch.BlockType.BUTTON, + text: "Toggle Alerts", + func: "alerts", + }, + ], + menus: {}, + }; + } + openDocs() { + open("https://civ3ro.github.io/extensions/Documentation/"); + } + alerts() { + alerts = !alerts; + alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!"); + } + } + Scratch.extensions.register(new threejsExtension()); + + class ThreeRenderer { + getInfo() { + return { + id: "threeRenderer", + name: "Three Renderer", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "setRendererRatio", + blockType: Scratch.BlockType.COMMAND, + text: "set Pixel Ratio to [VALUE]", + arguments: { + VALUE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + }, + }, + { + opcode: "eulerOrder", + blockType: Scratch.BlockType.COMMAND, + text: "set euler order to [VALUE]", + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "YXZ", + }, + }, + }, + ], + menus: {}, + }; + } + + setRendererRatio(args) { + threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE); + } + eulerOrder(args) { + eulerOrder = args.VALUE; + console.log("euler order set to", eulerOrder); + } + } + Scratch.extensions.register(new ThreeRenderer()); + + class ThreeScene { + constructor() { + this.THREE = THREE; + this.scenes = {}; + } + + getInfo() { + return { + id: "threeScene", + name: "Three Scene", + color1: "#4638c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "newScene", + blockType: Scratch.BlockType.COMMAND, + text: "new Scene [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + }, + }, + + { + opcode: "setSceneProperty", + blockType: Scratch.BlockType.COMMAND, + text: "set Scene [PROPERTY] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "sceneProperties", + defaultValue: "background", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "new Color()", + exemptFromNormalization: true, + }, + }, + }, + "---", + { + opcode: "getSceneObjects", + blockType: Scratch.BlockType.REPORTER, + text: "get Scene [THING]", + arguments: { + THING: { + type: Scratch.ArgumentType.STRING, + menu: "sceneThings", + }, + }, + }, + { + opcode: "reset", + blockType: Scratch.BlockType.COMMAND, + text: "Reset Everything", + }, + ], + menus: { + sceneProperties: { + acceptReporters: false, + items: [{ + text: "Background", + value: "background", + }, + { + text: "Background Blurriness", + value: "backgroundBlurriness", + }, + { + text: "Background Intensity", + value: "backgroundIntensity", + }, + { + text: "Background Rotation", + value: "backgroundRotation", + }, + { + text: "Environment", + value: "environment", + }, + { + text: "Environment Intensity", + value: "environmentIntensity", + }, + { + text: "Environment Rotation", + value: "environmentRotation", + }, + { + text: "Fog", + value: "fog", + }, + ], + }, + sceneThings: { + acceptReporters: false, + items: ["Objects", "Materials", "Geometries", "Lights", "Scene Properties", "Other assets"], + }, + }, + }; + } + + newScene(args) { + scene = new THREE.Scene(); + scene.name = args.NAME; + scene.background = new THREE.Color("#222"); + //scene.add(new THREE.GridHelper(16, 16)) //future helper section? + this.scenes = { + ...this.scenes, + ...scene, + }; + resetor(0); + } + + reset() { + resetor(1); + } + + async setSceneProperty(args) { + const property = args.PROPERTY; + const value = getAsset(args.VALUE); + + scene[property] = value; + } + getSceneObjects(args) { + const names = []; + if (args.THING === "Objects") { + scene.traverse((obj) => { + if (obj.name) names.push(obj.name); //if it has a name, add to list! + }); + } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); + else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); + else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); + else if (args.THING === "Scene Properties") { + console.log(scene); + return "check console"; + } else if (args.THING === "Other assets") return JSON.stringify(assets); + + return JSON.stringify(names); // if objects + } + } + Scratch.extensions.register(new ThreeScene()); + + class ThreeCameras { + getInfo() { + return { + id: "threeCameras", + name: "Three Cameras", + color1: "#38c59bff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addCamera", + blockType: Scratch.BlockType.COMMAND, + text: "add camera [TYPE] [CAMERA] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "cameraTypes", + }, + }, + }, + { + opcode: "setCamera", + blockType: Scratch.BlockType.COMMAND, + text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "cameraProperties", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "0.1", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "getCamera", + blockType: Scratch.BlockType.REPORTER, + text: "get camera [PROPERTY] of [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "cameraProperties", + }, + }, + }, + "---", + { + opcode: "renderSceneCamera", + blockType: Scratch.BlockType.COMMAND, + text: "set rendering camera to [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + }, + }, + "---", + { + opcode: "cubeCamera", + blockType: Scratch.BlockType.COMMAND, + text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "cubeCamera", + }, + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + "---", + { + opcode: "renderTarget", + blockType: Scratch.BlockType.COMMAND, + text: "set a RenderTarget: [RT] for camera [CAMERA]", + arguments: { + CAMERA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myCamera", + }, + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + { + opcode: "sizeTarget", + blockType: Scratch.BlockType.COMMAND, + text: "set RenderTarget [RT] size to [W] [H]", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + W: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 480, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 360, + }, + }, + }, + { + opcode: "getTarget", + blockType: Scratch.BlockType.REPORTER, + text: "get RenderTarget: [RT] texture", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + { + opcode: "removeTarget", + blockType: Scratch.BlockType.COMMAND, + text: "remove RenderTarget: [RT]", + arguments: { + RT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myTarget", + }, + }, + }, + ], + menus: { + cameraTypes: { + acceptReporters: false, + items: [{ + text: "Perspective", + value: "PerspectiveCamera", + }, ], + }, + cameraProperties: { + acceptReporters: false, + items: [{ + text: "Near", + value: "near", + }, + { + text: "Far", + value: "far", + }, + { + text: "FOV", + value: "fov", + }, + { + text: "Focus (nothing...)", + value: "focus", + }, + { + text: "Zoom", + value: "zoom", + }, + ], + }, + }, + }; + } + addCamera(args) { + let v2 = new THREE.Vector2(); + threeRenderer.getSize(v2); + const object = new THREE.PerspectiveCamera(90, v2.x / v2.y); + object.position.z = 3; + + createObject(args.CAMERA, object, args.GROUP); + } + setCamera(args) { + let object = getObject(args.CAMERA); + object[args.PROPERTY] = args.VALUE; + object.updateProjectionMatrix(); + } + getCamera(args) { + let object = getObject(args.CAMERA); + const value = JSON.stringify(object[args.PROPERTY]); + return value; + } + renderSceneCamera(args) { + let object = getObject(args.CAMERA); + if (!object) return; + camera = object; + //reset composer, else it does not update. + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } + + cubeCamera(args) { + // Create cube render target + const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, { + generateMipmaps: true, + }); + // Create cube camera + const cubeCamera = new THREE.CubeCamera(0.1, 500, cubeRenderTarget); + createObject(args.CAMERA, cubeCamera, args.GROUP); + + renderTargets[args.RT] = { + target: cubeRenderTarget, + camera: cubeCamera, + }; + assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture; + } + + renderTarget(args) { + let object = getObject(args.CAMERA); + const renderTarget = new THREE.WebGLRenderTarget(360, 360, { + generateMipmaps: false, + }); + + renderTargets[args.RT] = { + target: renderTarget, + camera: object, + }; + assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; + } + sizeTarget(args) { + renderTargets[args.RT].target.setSize(args.W, args.H); + } + getTarget(args) { + const t = renderTargets[args.RT].target.texture; + console.log(t, renderTargets[args.RT]); + return `renderTargets/${t.uuid}`; + } + removeTarget(args) { + delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; + renderTargets[args.RT].target.dispose(); + delete renderTargets[args.RT]; + } + } + Scratch.extensions.register(new ThreeCameras()); + + class ThreeObjects { + getInfo() { + return { + id: "threeObjects", + name: "Three Objects", + color1: "#38c567ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addObject", + blockType: Scratch.BlockType.COMMAND, + text: "add object [OBJECT3D] [TYPE] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "objectTypes", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "cloneObject", + blockType: Scratch.BlockType.COMMAND, + text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myClone", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "setObject", + blockType: Scratch.BlockType.COMMAND, + text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectProperties", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + { + opcode: "getObject", + blockType: Scratch.BlockType.REPORTER, + text: "get [PROPERTY] of object [OBJECT3D]", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectProperties", + }, + }, + }, + { + opcode: "objectE", + blockType: Scratch.BlockType.BOOLEAN, + text: "is there an object [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "removeObject", + blockType: Scratch.BlockType.COMMAND, + text: "remove object [OBJECT3D] from scene", + arguments: { + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: " ↳ Transforms", + }, + { + opcode: "setObjectV3", + extensions: ["colours_motion"], + blockType: Scratch.BlockType.COMMAND, + text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectVector3", + defaultValue: "position", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, + //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, + { + opcode: "getObjectV3", + extensions: ["colours_motion"], + blockType: Scratch.BlockType.REPORTER, + text: "get [PROPERTY] of [OBJECT3D]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "objectVector3", + defaultValue: "position", + }, + OBJECT3D: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Materials", + }, + { + opcode: "newMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "new material [NAME] [TYPE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "materialTypes", + defaultValue: "MeshStandardMaterial", + }, + }, + }, + { + opcode: "materialE", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.BOOLEAN, + text: "is there a material [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + }, + }, + { + opcode: "removeMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "remove material [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + }, + }, + { + opcode: "setMaterial", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [PROPERTY] of [NAME] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "materialProperties", + defaultValue: "color", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "new Color()", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "setBlending", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [NAME] blending to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + }, + }, + }, + { + opcode: "setDepth", + extensions: ["colours_looks"], + blockType: Scratch.BlockType.COMMAND, + text: "set material [NAME] depth to [VALUE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myMaterial", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + menu: "depthModes", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Geometries", + }, + { + opcode: "newGeometry", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "new geometry [NAME] [TYPE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "geometryTypes", + defaultValue: "BoxGeometry", + }, + }, + }, + { + opcode: "geometryE", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.BOOLEAN, + text: "is there a geometry [NAME]?", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + { + opcode: "removeGeometry", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "remove geometry [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + }, + }, + "---", + { + opcode: "newGeo", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "new empty geometry [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[points]", + }, + }, + }, + { + opcode: "geoPoints", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "set geometry [NAME] vertex points to [POINTS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[points]", + }, + }, + }, + { + opcode: "geoUVs", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "set geometry [NAME] UVs to [POINTS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myGeometry", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[UVs]", + }, + }, + }, + "---", + { + opcode: "splines", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "create spline [NAME] from curve [CURVE]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySpline", + }, + CURVE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[curve]", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "splineModel", + extensions: ["colours_operators"], + blockType: Scratch.BlockType.COMMAND, + text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySpline", + }, + MODEL: { + type: Scratch.ArgumentType.STRING, + menu: "modelsList", + }, + CURVE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[curve]", + exemptFromNormalization: true, + }, + SPACING: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.BUTTON, + text: "Convert font to JSON", + func: "openConv", + }, + { + blockType: Scratch.BlockType.BUTTON, + text: "Load JSON font file", + func: "loadFont", + }, + { + opcode: "text", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.COMMAND, + text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myText", + }, + TEXT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "C-369", + }, + FONT: { + type: Scratch.ArgumentType.STRING, + menu: "fonts", + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + D: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.1, + }, + CS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 6, + }, + }, + }, + ], + menus: { + objectVector3: { + acceptReporters: false, + items: [{ + text: "Positon", + value: "position", + }, + { + text: "Rotation", + value: "rotation", + }, + { + text: "Scale", + value: "scale", + }, + { + text: "Facing Direction (.up)", + value: "up", + }, + ], + }, + objectProperties: { + acceptReporters: false, + items: [{ + text: "Geometry", + value: "geometry", + }, + { + text: "Material", + value: "material", + }, + { + text: "Visible (true/false)", + value: "visible", + }, + ], + }, + objectTypes: { + acceptReporters: false, + items: [{ + text: "Mesh", + value: "Mesh", + }, + { + text: "Sprite", + value: "Sprite", + }, + { + text: "Points", + value: "Points", + }, + { + text: "Line", + value: "Line", + }, + { + text: "Group", + value: "Group", + }, + ], + }, + XYZ: { + acceptReporters: false, + items: [{ + text: "X", + value: "x", + }, + { + text: "Y", + value: "y", + }, + { + text: "Z", + value: "z", + }, + ], + }, + materialProperties: { + acceptReporters: false, + items: [ + "|GENERAL| <-- not a property", + { + text: "Color", + value: "color", + }, + { + text: "Map", + value: "map", + }, + { + text: "Opacity", + value: "opacity", + }, + { + text: "Transparent", + value: "transparent", + }, + { + text: "Alpha Map", + value: "alphaMap", + }, + { + text: "Alpha Test", + value: "alphaTest", + }, + { + text: "Depth Test", + value: "depthTest", + }, + { + text: "Depth Write", + value: "depthWrite", + }, + { + text: "Color Write", + value: "colorWrite", + }, + { + text: "Side", + value: "side", + }, + { + text: "Visible", + value: "visible", + }, + /* + { text: "Blending", value: "blending" }, + { text: "Blend Src", value: "blendSrc" }, + { text: "Blend Dst", value: "blendDst" }, + { text: "Blend Equation", value: "blendEquation" }, + { text: "Blend Src Alpha", value: "blendSrcAlpha" }, + { text: "Blend Dst Alpha", value: "blendDstAlpha" }, + { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ + { + text: "Blend Aplha", + value: "blendAplha", + }, + { + text: "Blend Color", + value: "blendColor", + }, + { + text: "Alpha Hash", + value: "alphaHash", + }, + { + text: "Premultiplied Alpha", + value: "premultipliedAlpha", + }, + + { + text: "Tone Mapped", + value: "toneMapped", + }, + { + text: "Fog", + value: "fog", + }, + { + text: "Flat Shading", + value: "flatShading", + }, + + "|MESH Standard / Physical| <-- not a property", + { + text: "Metalness", + value: "metalness", + }, + { + text: "Metalness Map", + value: "metalnessMap", + }, + { + text: "Roughness", + value: "roughness", + }, + { + text: "Reflectivity", + value: "reflectivity", + }, + { + text: "Roughness Map", + value: "roughnessMap", + }, + { + text: "Emissive", + value: "emissive", + }, + { + text: "Emissive Intensity", + value: "emissiveIntensity", + }, + { + text: "Emissive Map", + value: "emissiveMap", + }, + { + text: "Env Map", + value: "envMap", + }, + { + text: "Env Map Intensity", + value: "envMapIntensity", + }, + { + text: "Env Map Rotation", + value: "envMapRotation", + }, + { + text: "Ior", + value: "ior", + }, + { + text: "Refraction Ratio", + value: "refractionRatio", + }, + { + text: "Clearcoat", + value: "clearcoat", + }, + { + text: "Clearcoat Map", + value: "clearcoatMap", + }, + { + text: "Clearcoat Roughness", + value: "clearcoatRoughness", + }, + { + text: "Clearcoat Roughness Map", + value: "clearcoatRoughnessMap", + }, + { + text: "Dispersion", + value: "dispersion", + }, + { + text: "Sheen", + value: "sheen", + }, + { + text: "Sheen Color", + value: "sheenColor", + }, + { + text: "Sheen Color Map", + value: "sheenColorMap", + }, + { + text: "Sheen Roughness", + value: "sheenRoughness", + }, + { + text: "Sheen Roughness Map", + value: "sheenRoughnessMap", + }, + { + text: "Specular Color", + value: "specularColor", + }, + { + text: "Specular Color Map", + value: "specularColorMap", + }, + { + text: "Specular Intensity", + value: "specularIntensity", + }, + { + text: "Specular Intensity Map", + value: "specularIntensityMap", + }, + { + text: "Transmission", + value: "transmission", + }, + { + text: "Transmission Map", + value: "transmissionMap", + }, + { + text: "Thickness", + value: "thickness", + }, + { + text: "Thickness Map", + value: "thicknessMap", + }, + { + text: "Anisotropy", + value: "anisotropy", + }, + { + text: "Anisotropy Map", + value: "anisotropyMap", + }, + { + text: "Anisotropy Rotation", + value: "anisotropyRotation", + }, + { + text: "Attenuation Distance", + value: "attenuationDistance", + }, + { + text: "Attenuation Color", + value: "attenuationColor", + }, + { + text: "Thickness", + value: "thickness", + }, + { + text: "Iridescence", + value: "iridescence", + }, + { + text: "Iridescence Ior", + value: "iridescenceIOR", + }, + { + text: "Iridescence Map", + value: "iridescenceMap", + }, + { + text: "Iridescence Thickness Range", + value: "iridescenceThicknessRange", + }, + + "|MESH Displacement / Normal / Bump| <-- not a property", + { + text: "Displacement Map", + value: "displacementMap", + }, + { + text: "Displacement Scale", + value: "displacementScale", + }, + { + text: "Displacement Bias", + value: "displacementBias", + }, + { + text: "Bump Map", + value: "bumpMap", + }, + { + text: "Bump Scale", + value: "bumpScale", + }, + { + text: "Normal Map Type", + value: "normalMapType", + }, + + "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", + { + text: "Shininess", + value: "shininess", + }, + + { + text: "Wireframe", + value: "wireframe", + }, + { + text: "Wireframe Linewidth", + value: "wireframeLinewidth", + }, + { + text: "Wireframe Linecap", + value: "wireframeLinecap", + }, + { + text: "Wireframe Linejoin", + value: "wireframeLinejoin", + }, + + "|POINTS| <-- not a property", + { + text: "Size", + value: "size", + }, + { + text: "Size Attenuation", + value: "sizeAttenuation", + }, + + "|LINES| <-- not a property", + { + text: "Scale", + value: "scale", + }, + { + text: "Dash Size", + value: "dashSize", + }, + { + text: "Gap Size", + value: "gapSize", + }, + + "|SPRITES| <-- not a property", + { + text: "Rotation", + value: "rotation", + }, + ], + }, + blendModes: { + acceptReporters: false, + items: [{ + text: "No Blending", + value: "NoBlending", + }, + { + text: "Normal Blending", + value: "NormalBlending", + }, + { + text: "Additive Blending", + value: "AdditiveBlending", + }, + { + text: "Subtractive Blending", + value: "SubtractiveBlending", + }, + { + text: "Multiply Blending", + value: "MultiplyBlending", + }, + { + text: "Custom Blending", + value: "CustomBlending", + }, + ], + }, + depthModes: { + acceptReporters: false, + items: [{ + text: "Never Depth", + value: "NeverDepth", + }, + { + text: "Always Depth", + value: "AlwaysDepth", + }, + { + text: "Equal Depth", + value: "EqualDepth", + }, + { + text: "Less Depth", + value: "LessDepth", + }, + { + text: "Less Equal Depth", + value: "LessEqualDepth", + }, + { + text: "Greater Equal Depth", + value: "GreaterEqualDepth", + }, + { + text: "Greater Depth", + value: "GreaterDepth", + }, + { + text: "Not Equal Depth", + value: "NotEqualDepth", + }, + ], + }, + materialTypes: { + acceptReporters: false, + items: [{ + text: "Mesh Basic Material", + value: "MeshBasicMaterial", + }, + { + text: "Mesh Standard Material", + value: "MeshStandardMaterial", + }, + { + text: "Mesh Physical Material", + value: "MeshPhysicalMaterial", + }, + { + text: "Mesh Lambert Material", + value: "MeshLambertMaterial", + }, + { + text: "Mesh Phong Material", + value: "MeshPhongMaterial", + }, + { + text: "Mesh Depth Material", + value: "MeshDepthMaterial", + }, + { + text: "Mesh Normal Material", + value: "MeshNormalMaterial", + }, + { + text: "Mesh Matcap Material", + value: "MeshMatcapMaterial", + }, + { + text: "Mesh Toon Material", + value: "MeshToonMaterial", + }, + { + text: "Line Basic Material", + value: "LineBasicMaterial", + }, + { + text: "Line Dashed Material", + value: "LineDashedMaterial", + }, + { + text: "Points Material", + value: "PointsMaterial", + }, + { + text: "Sprite Material", + value: "SpriteMaterial", + }, + { + text: "Shadow Material", + value: "ShadowMaterial", + }, + ], + }, + textureModes: { + acceptReporters: false, + items: ["Pixelate", "Blur"], + }, + textureStyles: { + acceptReporters: false, + items: ["Repeat", "Clamp"], + }, + geometryTypes: { + acceptReporters: false, + items: [{ + text: "Box Geometry", + value: "BoxGeometry", + }, + { + text: "Sphere Geometry", + value: "SphereGeometry", + }, + { + text: "Cylinder Geometry", + value: "CylinderGeometry", + }, + { + text: "Plane Geometry", + value: "PlaneGeometry", + }, + { + text: "Circle Geometry", + value: "CircleGeometry", + }, + { + text: "Torus Geometry", + value: "TorusGeometry", + }, + { + text: "Torus Knot Geometry", + value: "TorusKnotGeometry", + }, + ], + }, + modelsList: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".glb")); + if (models.length < 1) return [ + ["Load a model! (GLB Loader category)"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + fonts: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".json")); + if (models.length < 1) return [ + ["Load a font!"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + }, + }; + } + + addObject(args) { + const object = new THREE[args.TYPE](); + + object.castShadow = true; + object.receiveShadow = true; + + createObject(args.OBJECT3D, object, args.GROUP); + } + cloneObject(args) { + let object = getObject(args.OBJECT3D); + const clone = object.clone(true); + clone.name; + createObject(args.NAME, clone, args.GROUP); + } + setObjectV3(args) { + let object = getObject(args.OBJECT3D); + let values = JSON.parse(args.VALUE); + + function degToRad(deg) { + return (deg * Math.PI) / 180; + } + + if (object.rigidBody) { + const x = values[0]; + const y = values[1]; + const z = values[2]; + if (args.PROPERTY === "rotation") { + const euler = new THREE.Euler(degToRad(x), degToRad(y), degToRad(z), "YXZ"); + const quaternion = new THREE.Quaternion(); + quaternion.setFromEuler(euler); + + object.rigidBody.setRotation({ + x: quaternion.x, + y: quaternion.y, + z: quaternion.z, + w: quaternion.w, + }); + } else if (args.PROPERTY === "position") { + object.rigidBody.setTranslation({ + x: x, + y: y, + z: z, + }, + true + ); + } + return; + } + + if (object.isCamera == true && controls) {} + + if (args.PROPERTY === "rotation") { + values = values.map((v) => (v * Math.PI) / 180); + object.rotation.set(0, 0, 0); + } + if (object.isDirectionalLight == true) { + object.pos = new THREE.Vector3(...values); + console.log(true, values, object.pos); + return; + } + object[args.PROPERTY].set(...values); + + if (object.type == "CubeCamera") object.updateCoordinateSystem(); + } + /* + changeObjectV3(args) { + getObject(args.OBJECT3D) + let values = JSON.parse(args.VALUE) + + if (args.PROPERTY === "rotation") { + values = values.map(v => v * Math.PI / 180); + object.rotation.x += values[0] + object.rotation.y += values[1] + object.rotation.z += values[2] + } + else { + object[args.PROPERTY].add(...values); + } + } + changeObjectXV3(args) { + getObject(args.OBJECT3D) + let value = args.VALUE + if (args.PROPERTY === "rotation") value = value * Math.PI / 180 + + object[args.PROPERTY][args.X] += value + } + */ + getObjectV3(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let values = vector3ToString(object[args.PROPERTY]); + if (args.PROPERTY === "rotation") { + const toDeg = Math.PI / 180; + values = [values[0] / toDeg, values[1] / toDeg, values[2] / toDeg]; + } + + return JSON.stringify(values); + } + setObject(args) { + let object = getObject(args.OBJECT3D); + let value = args.VALUE; + if (args.PROPERTY === "material") { + const mat = materials[args.NAME]; + if (mat) value = mat; + else value = undefined; + } else if (args.PROPERTY === "geometry") { + const geo = geometries[args.NAME]; + if (geo) value = geo; + else value = undefined; + } else value = !!value; + + if (value == undefined) return; //invalid geo/mat + object[args.PROPERTY] = value; + } + getObject(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let value; + if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; + else value = object.visible; + + return value; + } + removeObject(args) { + removeObject(args.OBJECT3D); + } + objectE(args) { + return scene.children.map((o) => o.name).includes(args.NAME); + } + + //defines + newMaterial(args) { + if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); + const mat = new THREE[args.TYPE](); + mat.name = args.NAME; + + materials[args.NAME] = mat; + } + async setMaterial(args) { + if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; + const mat = materials[args.NAME]; + + let value = args.VALUE; + + if (args.VALUE == "false") value = false; + + if (args.PROPERTY == "side") { + value = args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide; + } else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)); + else value = getAsset(value); + + console.log("o:", args.VALUE, typeof args.VALUE); + console.log("r:", value, typeof value); + + mat[args.PROPERTY] = await value; //await incase its a texture + mat.needsUpdate = true; + } + setBlending(args) { + const mat = materials[args.NAME]; + mat.blending = THREE[args.VALUE]; + mat.premultipliedAlpha = true; + mat.needsUpdate = true; + } + setDepth(args) { + const mat = materials[args.NAME]; + mat.depthFunc = THREE[args.VALUE]; + mat.needsUpdate = true; + } + removeMaterial(args) { + const mat = materials[args.NAME]; + mat.dispose(); + delete materials[args.NAME]; + } + materialE(args) { + return materials[args.NAME] ? true : false; + } + + newGeometry(args) { + if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); + const geo = new THREE[args.TYPE](); + geo.name = args.NAME; + + geometries[args.NAME] = geo; + } + setGeometry(args) { + const geo = geometries[args.NAME]; + geo[args.PROPERTY] = args.VALUE; + + geo.needsUpdate = true; + } + removeGeometry(args) { + const geo = geometries[args.NAME]; + geo.dispose(); + delete geometries[args.NAME]; + } + geometryE(args) { + return geometries[args.NAME] ? true : false; + } + + newGeo(args) { + const geometry = new THREE.BufferGeometry(); + geometry.name = args.NAME; + geometries[args.NAME] = geometry; + } + async geoPoints(args) { + const geometry = geometries[args.NAME]; + const positions = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v3 of each vertex of each triangle + + geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3)); + geometry.computeVertexNormals(); + + geometry.needsUpdate = true; + } + geoUVs(args) { + const geometry = geometries[args.NAME]; + const UVs = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v2 of each UV of each triangle + + geometry.setAttribute("uv", new THREE.BufferAttribute(new Float32Array(UVs), 2)); + geometry.needsUpdate = true; + } + + splines(args) { + const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); + geometry.name = args.NAME; + + geometries[args.NAME] = geometry; + } + + async splineModel(args) { + const model = await getModel(args.MODEL, args.NAME); + if (!model) return console.warn("Model not found:", args.MODEL); + + const curve = getAsset(args.CURVE); + const spacing = parseFloat(args.SPACING) || 1; + const curveLength = curve.getLength(); + const divisions = Math.floor(curveLength / spacing); + + const geomList = []; + const matList = []; + + for (let i = 0; i <= divisions; i++) { + const t = i / divisions; + const pos = curve.getPointAt(t); + const tangent = curve.getTangentAt(t); + + const temp = model.clone(true); + temp.position.copy(pos); + + const up = new THREE.Vector3(0, 0, 1); + const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()); + temp.quaternion.copy(quat); + + temp.updateMatrixWorld(true); + + temp.traverse((child) => { + if (child.isMesh && child.geometry) { + const geom = child.geometry.clone(); + geom.applyMatrix4(child.matrixWorld); + geomList.push(geom); + matList.push(child.material); //.clone() ? + } + }); + } + + const validGeoms = geomList.filter((g) => { + const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position; + if (!ok) console.warn("geometry skipped:", g); + return ok; + }); + + const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true); + merged.computeBoundingBox(); + merged.computeBoundingSphere(); + + merged.name = args.NAME; + geometries[args.NAME] = merged; + matList.name = args.NAME; + materials[args.NAME] = matList; + } + + async text(args) { + const fontFile = runtime + .getTargetForStage() + .getSounds() + .find((c) => c.name === args.FONT); + if (!fontFile) return; + + const json = new TextDecoder().decode(fontFile.asset.data.buffer); + const fontData = JSON.parse(json); + + const font = fontLoad.parse(fontData); + + const params = { + font: font, + size: JSON.parse(args.S), + height: JSON.parse(args.D), + curveSegments: JSON.parse(args.CS), + bevelEnabled: false, + }; + const geometry = new TextGeometry.TextGeometry(args.TEXT, params); + geometry.computeVertexNormals(); + geometry.center(); // optional, recenters the text + + geometry.name = args.NAME; + + geometries[args.NAME] = geometry; + } + + async loadFont() { + openFileExplorer(".json").then((files) => { + const file = files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + const arrayBuffer = e.target.result; + + // From lily's assets + // // Thank you PenguinMod for providing this code. + + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); + + const buffer = arrayBuffer; + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + alert("Font loaded successfully!"); + } catch (e) { + console.error(e); + alert("Error loading font."); + } + + // End of PenguinMod + }; + + reader.readAsArrayBuffer(file); + }); + } + openConv() { + { + open("https://gero3.github.io/facetype.js/"); + } + } + } + Scratch.extensions.register(new ThreeObjects()); + + class ThreeLights { + getInfo() { + return { + id: "threeLights", + name: "Three Lights", + color1: "#c7a22aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "addLight", + blockType: Scratch.BlockType.COMMAND, + text: "add light [NAME] type [TYPE] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myLight", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "lightTypes", + }, + }, + }, + { + opcode: "setLight", + blockType: Scratch.BlockType.COMMAND, + text: "set light [NAME][PROPERTY] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "lightProperties", + defaultValue: "intensity", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myLight", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + exemptFromNormalization: true, + }, + }, + }, + ], + menus: { + lightTypes: { + acceptReporters: false, + items: [{ + text: "Ambient Light", + value: "AmbientLight", + }, + { + text: "Directional Light", + value: "DirectionalLight", + }, + { + text: "Point Light", + value: "PointLight", + }, + { + text: "Hemisphere Light", + value: "HemisphereLight", + }, + { + text: "Spot Light", + value: "SpotLight", + }, + ], + }, + lightProperties: { + acceptReporters: false, + items: [{ + text: "Color", + value: "color", + }, + { + text: "Intensity", + value: "intensity", + }, + { + text: "Cast Shadow?", + value: "castShadow", + }, + { + text: "Ground Color (HemisphereLight)", + value: "groundColor", + }, + { + text: "Map (SpotLight)", + value: "map", + }, + { + text: "Distance (SpotLight)", + value: "distance", + }, + { + text: "Decay (SpotLight)", + value: "decay", + }, + { + text: "Penumbra (SpotLight)", + value: "penumbra", + }, + { + text: "Angle/Size (SpotLight)", + value: "angle", + }, + { + text: "Power (SpotLight)", + value: "power", + }, + { + text: "Target Position (Directional/SpotLight)", + value: "target", + }, + ], + }, + }, + }; + } + + addLight(args) { + const light = new THREE[args.TYPE](0xffffff, 1); + + createObject(args.NAME, light, args.GROUP); + lights[args.NAME] = light; + if (light.type === "AmbientLight" || "HemisphereLight") return; + + light.castShadow = true; + if (light.type === "PointLight") return; + //Directional & Spot Light + light.target.position.set(0, 0, 0); + scene.add(light.target); + + light.pos = new THREE.Vector3(0, 0, 0); + + light.shadow.mapSize.width = 4096; + light.shadow.mapSize.height = 2048; + + if (light.type === "SpotLight") { + light.decay = 0; + light.shadow.camera.near = 500; + light.shadow.camera.far = 4000; + light.shadow.camera.fov = 30; + } + light.shadow.needsUpdate = true; + light.needsUpdate = true; + } + + setLight(args) { + const light = lights[args.NAME]; + if (!args.PROPERTY) return; + if (args.PROPERTY === "target") { + light.target.position.set(...JSON.parse(args.VALUE)); //vector3 + light.target.updateMatrixWorld(); + } else { + light[args.PROPERTY] = getAsset(args.VALUE); + } + light.needsUpdate = true; + + if (light.type === "AmbientLight" || "HemisphereLight") return; + + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; + } + } + Scratch.extensions.register(new ThreeLights()); + + class ThreeUtilities { + getInfo() { + return { + id: "threeUtility", + name: "Three Utilities", + color1: "#3875c5ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "newVector2", + blockType: Scratch.BlockType.REPORTER, + text: "New Vector [X] [Y]", + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + { + opcode: "newVector3", + blockType: Scratch.BlockType.REPORTER, + text: "New Vector [X] [Y] [Z]", + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + }, + Z: { + type: Scratch.ArgumentType.NUMBER, + }, + }, + }, + "---", + { + opcode: "operateV3", + blockType: Scratch.BlockType.REPORTER, + text: "do [V3] [O] [V32]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + O: { + type: Scratch.ArgumentType.STRING, + menu: "operators", + }, + V32: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + { + opcode: "moveVector3", + blockType: Scratch.BlockType.REPORTER, + text: "move [S] steps in vector [V3] in direction [D3]", + arguments: { + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + D3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + { + opcode: "directionTo", + blockType: Scratch.BlockType.REPORTER, + text: "direction from [V3] to [T3]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,3]", + }, + T3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + "---", + { + opcode: "newColor", + blockType: Scratch.BlockType.REPORTER, + text: "New Color [HEX]", + arguments: { + HEX: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#9966ff", + }, + }, + }, + { + opcode: "newFog", + blockType: Scratch.BlockType.REPORTER, + text: "New Fog [COLOR] [NEAR] [FAR]", + arguments: { + COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#9966ff", + exemptFromNormalization: true, + }, + NEAR: { + type: Scratch.ArgumentType.NUMBER, + }, + FAR: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10, + }, + }, + }, + { + opcode: "newTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "textureStyles", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "newCubeTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", + arguments: { + COSTUMEX0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEX1: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEY0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEY1: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEZ0: { + type: Scratch.ArgumentType.COSTUME, + }, + COSTUMEZ1: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + STYLE: { + type: Scratch.ArgumentType.STRING, + menu: "textureStyles", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + { + opcode: "newEquirectangularTexture", + blockType: Scratch.BlockType.REPORTER, + text: "New Equirectangular Texture [COSTUME] [MODE]", + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME, + }, + MODE: { + type: Scratch.ArgumentType.STRING, + menu: "textureModes", + }, + }, + }, + "---", + { + opcode: "curve", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.REPORTER, + text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", + arguments: { + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "curveTypes", + }, + POINTS: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]", + }, + CLOSED: { + type: Scratch.ArgumentType.STRING, + defaultValue: "true", + }, + }, + }, + "---", + { + opcode: "mouseDown", + extensions: ["colours_sensing"], + blockType: Scratch.BlockType.BOOLEAN, + text: "mouse [BUTTON] [action]?", + arguments: { + BUTTON: { + type: Scratch.ArgumentType.STRING, + menu: "mouseButtons", + }, + action: { + type: Scratch.ArgumentType.STRING, + menu: "mouseAction", + }, + }, + }, + { + opcode: "mousePos", + extensions: ["colours_sensing"], + blockType: Scratch.BlockType.REPORTER, + text: "mouse position", + arguments: {}, + }, + "---", + { + opcode: "getItem", + extensions: ["colours_data_lists"], + blockType: Scratch.BlockType.REPORTER, + text: "get item [ITEM] of [ARRAY]", + arguments: { + ITEM: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + ARRAY: { + type: Scratch.ArgumentType.STRING, + defaultValue: `["myObject", "myLight"]`, + }, + }, + }, + { + blockType: Scratch.BlockType.LABEL, + text: "↳ Raycasting", + }, + { + opcode: "raycast", + blockType: Scratch.BlockType.COMMAND, + text: "Raycast from [V3] in direction [D3]", + arguments: { + V3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,3]", + }, + D3: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,1]", + }, + }, + }, + { + opcode: "getRaycast", + blockType: Scratch.BlockType.REPORTER, + text: "get raycast [PROPERTY]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "raycastProperties", + }, + }, + }, + ], + menus: { + materialProperties: { + acceptReporters: false, + items: [{ + text: "Color", + value: "color", + }, + { + text: "Map (texture)", + value: "map", + }, + { + text: "Alpha Map (texture)", + value: "alphaMap", + }, + { + text: "Alpha Test (0-1)", + value: "alphaTest", + }, + { + text: "Side (front/back/double)", + value: "side", + }, + { + text: "Bump Map (texture)", + value: "bumpMap", + }, + { + text: "Bump Scale", + value: "bumpScale", + }, + ], + }, + textureModes: { + acceptReporters: false, + items: ["Pixelate", "Blur"], + }, + textureStyles: { + acceptReporters: false, + items: ["Repeat", "Clamp"], + }, + raycastProperties: { + acceptReporters: false, + items: [{ + text: "Intersected Object Names", + value: "name", + }, + { + text: "Number of Objects", + value: "number", + }, + { + text: "Intersected Objects distances", + value: "distance", + }, + ], + }, + mouseButtons: { + acceptReporters: false, + items: ["left", "middle", "right"], + }, + mouseAction: { + acceptReporters: false, + items: ["Down", "Clicked"], + }, + curveTypes: { + acceptReporters: false, + items: ["CatmullRomCurve3"], + }, + operators: { + acceptReporters: false, + items: ["+", "-", "*", "/", "=", "max", "min", "dot", "cross", "distance to", "angle to", "apply euler"], + }, + }, + }; + } + mouseDown(args) { + if (args.action === "Down") return isMouseDown[args.BUTTON]; + if (args.action === "Clicked") { + if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false; + else prevMouse[args.BUTTON] = true; + return true; + } + } + mousePos(event) { + return JSON.stringify(mouseNDC); + } + newVector3(args) { + return JSON.stringify([args.X, args.Y, args.Z]); + } + operateV3(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)); + const v32 = new THREE.Vector3(...JSON.parse(args.V32)); + + let r; + if (args.O == "+") r = v3.add(v32); + else if (args.O == "-") r = v3.sub(v32); + else if (args.O == "*") r = v3.multiply(v32); + else if (args.O == "/") r = v3.divide(v32); + else if (args.O == "=") r = v3.equals(v32); + else if (args.O == "max") r = v3.max(v32); + else if (args.O == "min") r = v3.min(v32); + else if (args.O == "dot") r = v3.dot(v32); + else if (args.O == "cross") r = v3.cross(v32); + else if (args.O == "distance to") r = v3.distanceTo(v32); + else if (args.O == "angle to") r = v3.angleTo(v32); + else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)); + + if (typeof r == "object") return JSON.stringify([r.x, r.y, r.z]); + else return JSON.stringify(r); + } + + newVector2(args) { + return JSON.stringify([args.X, args.Y]); + } + + moveVector3(args) { + const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); + const steps = Number(args.S); + + const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); + + const yaw = THREE.MathUtils.degToRad(yawInputDeg); + const pitch = THREE.MathUtils.degToRad(pitchInputDeg); + const roll = THREE.MathUtils.degToRad(rollInputDeg); + + const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); + + const forwardVector = new THREE.Vector3(0, 0, -1); + const direction = forwardVector.applyEuler(euler).normalize(); + + const newPos = currentPos.add(direction.multiplyScalar(steps)); + return JSON.stringify([newPos.x, newPos.y, newPos.z]); + } + + directionTo(args) { + const v3 = new THREE.Vector3(...JSON.parse(args.V3)); + const toV3 = new THREE.Vector3(...JSON.parse(args.T3)); + + const direction = toV3.clone().sub(v3).normalize(); + // Pitch (X) + const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)); + // Yaw (Y) + const yaw = Math.atan2(direction.x, direction.z); + + // Roll always 0 + return JSON.stringify([180 + THREE.MathUtils.radToDeg(pitch), THREE.MathUtils.radToDeg(yaw), 0]); + } + + newColor(args) { + const color = new THREE.Color(args.HEX); + const uuid = crypto.randomUUID(); + assets.colors[uuid] = color; + return `colors/${uuid}`; + } + newFog(args) { + const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR); + const uuid = crypto.randomUUID(); + assets.fogs[uuid] = fog; + return `fogs/${uuid}`; + } + async newTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newCubeTexture(args) { + const uris = [ + encodeCostume(args.COSTUMEX0), + encodeCostume(args.COSTUMEX1), + encodeCostume(args.COSTUMEY0), + encodeCostume(args.COSTUMEY1), + encodeCostume(args.COSTUMEZ0), + encodeCostume(args.COSTUMEZ1), + ]; + const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); + const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); + + texture.name = "CubeTexture" + args.COSTUMEX0; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newEquirectangularTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + texture.mapping = THREE.EquirectangularReflectionMapping; + + setTexutre(texture, args.MODE); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + + curve(args) { + function parsePoints(input) { + // Match all [x,y,z] groups + const matches = input.match(/\[([^\]]+)\]/g); + if (!matches) return []; + + return matches.map((str) => { + const nums = str + .replace(/[\[\]\s]/g, "") + .split(",") + .map(Number); + return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0); + }); + } + const points = parsePoints(args.POINTS); + const curve = new THREE[args.TYPE](points); + curve.closed = JSON.parse(args.CLOSED); + + const uuid = crypto.randomUUID(); + assets.curves[uuid] = curve; + return `curves/${uuid}`; + } + + getItem(args) { + const items = JSON.parse(args.ARRAY); + const item = items[args.ITEM - 1]; + if (!item) return "0"; + return item; + } + + raycast(args) { + const origin = new THREE.Vector3(...JSON.parse(args.V3)); + // rotation is in degrees => convert to radians first + const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); + + const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder); + const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize(); + + const raycaster = new THREE.Raycaster(); + //const camera = getObject(args.CAMERA) + raycaster.set(origin, direction); + + const intersects = raycaster.intersectObjects(scene.children, true); + + raycastResult = intersects; + } + getRaycast(args) { + if (args.PROPERTY === "number") return raycastResult.length; + if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map((i) => i.distance)); + return JSON.stringify(raycastResult.map((i) => i.object[args.PROPERTY])); + } + } + Scratch.extensions.register(new ThreeUtilities()); + + class ThreeGLB { + getInfo() { + return { + id: "threeGLB", + name: "Three GLB Loader", + color1: "#c53838ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + blockType: Scratch.BlockType.BUTTON, + text: "Load GLB File", + func: "loadModelFile", + }, + { + opcode: "addModel", + blockType: Scratch.BlockType.COMMAND, + text: "add [ITEM] as [NAME] to [GROUP]", + arguments: { + GROUP: { + type: Scratch.ArgumentType.STRING, + defaultValue: "scene", + }, + ITEM: { + type: Scratch.ArgumentType.STRING, + menu: "modelsList", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + }, + }, + { + opcode: "getModel", + blockType: Scratch.BlockType.REPORTER, + text: "get object [PROPERTY] of [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "modelProperties", + }, + }, + }, + { + opcode: "playAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "play animation [ANAME] of [NAME], [TIMES] times", + arguments: { + TIMES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "pauseAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "set [TOGGLE] animation [ANAME] of [NAME]", + arguments: { + TOGGLE: { + type: Scratch.ArgumentType.NUMBER, + menu: "pauseUn", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + { + opcode: "stopAnimation", + blockType: Scratch.BlockType.COMMAND, + text: "stop animation [ANAME] of [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myModel", + }, + ANAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "walk", + exemptFromNormalization: true, + }, + }, + }, + ], + menus: { + modelProperties: { + acceptReporters: false, + items: [{ + text: "Animations", + value: "animations", + }, ], + }, + pauseUn: { + acceptReporters: true, + items: [{ + text: "Pause", + value: "true", + }, + { + text: "Unpasue", + value: "false", + }, + ], + }, + modelsList: { + acceptReporters: false, + items: () => { + const stage = runtime.getTargetForStage(); + if (!stage) return ["(loading...)"]; + + // @ts-ignore + const models = Scratch.vm.runtime + .getTargetForStage() + .getSounds() + .filter((e) => e.name && e.name.endsWith(".glb")); + if (models.length < 1) return [ + ["Load a model!"] + ]; + + // @ts-ignore + return models.map((m) => [m.name]); + }, + }, + }, + }; + } + + async loadModelFile() { + openFileExplorer(".glb").then((files) => { + const file = files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + const arrayBuffer = e.target.result; + + { + // From lily's assets + + // Thank you PenguinMod for providing this code. + { + const targetId = runtime.getTargetForStage().id; //util.target.id not working! + const assetName = Cast.toString(file.name); + + //const res = await Scratch.fetch(args.URL); + //const buffer = await res.arrayBuffer(); + const buffer = arrayBuffer; + + const storage = runtime.storage; + const asset = storage.createAsset( + storage.AssetType.Sound, + storage.DataFormat.MP3, + // @ts-ignore + new Uint8Array(buffer), + null, + true + ); + + try { + await vm.addSound( + // @ts-ignore + { + asset, + md5: asset.assetId + "." + asset.dataFormat, + name: assetName, + }, + targetId + ); + alert("Model loaded successfully!"); + } catch (e) { + console.error(e); + alert("Error loading model."); + } + } + // End of PenguinMod + } + }; + + reader.readAsArrayBuffer(file); + }); + } + async addModel(args) { + const group = await getModel(args.ITEM, args.NAME); + + createObject(args.NAME, group, args.GROUP); + } + getModel(args) { + if (!models[args.NAME]) return; + return Object.keys(models[args.NAME].actions).toString(); + } + + playAnimation(args) { + const model = models[args.NAME]; + if (!model) { + console.log("no model!"); + return; + } + + const action = model.actions[args.ANAME]; //clones of models dont have a stored actions! + if (!action) { + console.log("no action!"); + return; + } + + args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity); + + action.reset().play(); + } + stopAnimation(args) { + const model = models[args.NAME]; + if (!model) return; + + const action = model.actions[args.ANAME]; + if (action) action.stop(); + } + pauseAnimation(args) { + const model = models[args.NAME]; + if (!model) return; + + const action = model.actions[args.ANAME]; + if (action) action.paused = args.TOGGLE; + } + } + Scratch.extensions.register(new ThreeGLB()); + + class ThreeAddons { + getInfo() { + return { + id: "threeAddons", + name: "Three Addons", + color1: "#c538a2ff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + blockType: Scratch.BlockType.LABEL, + text: "Orbit Control", + }, + { + opcode: "OrbitControl", + blockType: Scratch.BlockType.COMMAND, + text: "set addon Orbit Control [STATE]", + arguments: { + STATE: { + type: Scratch.ArgumentType.STRING, + menu: "onoff", + }, + }, + }, + + { + blockType: Scratch.BlockType.LABEL, + text: "Post Processing", + }, + { + opcode: "resetComposer", + blockType: Scratch.BlockType.COMMAND, + text: "reset composer", + }, + { + opcode: "bloom", + blockType: Scratch.BlockType.COMMAND, + text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + I: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.5, + }, + T: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.5, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + }, + }, + { + opcode: "godRays", + blockType: Scratch.BlockType.COMMAND, + text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + DEC: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.95, + }, + DENS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + EXP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.1, + }, + WEI: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.4, + }, + RES: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + SAMP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 64, + }, + }, + }, + { + opcode: "dots", + blockType: Scratch.BlockType.COMMAND, + text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", + arguments: { + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + S: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + A: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: 0, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "SCREEN", + }, + }, + }, + { + opcode: "depth", + blockType: Scratch.BlockType.COMMAND, + text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", + arguments: { + FD: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 3, + }, + FL: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0.001, + }, + BS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 4, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 240, + }, + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "NORMAL", + }, + }, + }, + "---", + { + opcode: "custom", + blockType: Scratch.BlockType.COMMAND, + text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myShader", + }, + FRA: { + type: Scratch.ArgumentType.STRING, + }, + VER: { + type: Scratch.ArgumentType.STRING, + }, + BLEND: { + type: Scratch.ArgumentType.STRING, + menu: "blendModes", + defaultValue: "NORMAL", + }, + OP: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + }, + }, + ], + menus: { + onoff: { + acceptReporters: true, + items: [{ + text: "enabled", + value: "1", + }, + { + text: "disabled", + value: "0", + }, + ], + }, + blendModes: { + acceptReporters: false, + items: [ + "SKIP", + "SET", + "ADD", + "ALPHA", + "AVERAGE", + "COLOR", + "COLOR_BURN", + "COLOR_DODGE", + "DARKEN", + "DIFFERENCE", + "DIVIDE", + "DST", + "EXCLUSION", + "HARD_LIGHT", + "HARD_MIX", + "HUE", + "INVERT", + "INVERT_RGB", + "LIGHTEN", + "LINEAR_BURN", + "LINEAR_DODGE", + "LINEAR_LIGHT", + "LUMINOSITY", + "MULTIPLY", + "NEGATION", + "NORMAL", + "OVERLAY", + "PIN_LIGHT", + "REFLECT", + "SCREEN", + "SRC", + "SATURATION", + "SOFT_LIGHT", + "SUBTRACT", + "VIVID_LIGHT", + ], + }, + }, + }; + } + + OrbitControl(args) { + if (controls) controls.dispose(); + + console.log("creating...", OrbitControls); + controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); + controls.enableDamping = true; + + controls.enabled = !!args.STATE; + console.log(controls); + } + + resetComposer() { + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } + + bloom(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const bloomEffect = new BloomEffect({ + intensity: args.I, + luminanceThreshold: args.T, // ← correct key + luminanceSmoothing: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + bloomEffect.blendMode.opacity.value = args.OP; + + const pass = new EffectPass(camera, bloomEffect); + + composer.addPass(pass); + } + + godRays(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + let object = getObject(args.NAME); + const sun = object; + + const godRays = new GodRaysEffect(camera, sun, { + resolutionScale: args.RES, + density: args.DENS, // ray density + decay: args.DEC, // fade out + weight: args.WEI, // brightness of rays + exposure: args.EXP, + samples: args.SAMP, + blendFunction: BlendFunction[args.BLEND], + }); + godRays.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, godRays); + composer.addPass(pass); + } + + dots(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dot = new DotScreenEffect({ + angle: args.A, + scale: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + dot.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, dot); + composer.addPass(pass); + } + + depth(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dofEffect = new DepthOfFieldEffect(camera, { + focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) + focalLength: args.FL, // lens focal length in meters + bokehScale: args.BS, // strength/size of the blur circles + height: args.H, // resolution hint (affects quality/perf) + blendFunction: BlendFunction[args.BLEND], + }); + dofEffect.blendMode.opacity.value = args.OP; + + const dofPass = new EffectPass(camera, dofEffect); + composer.addPass(dofPass); + } + + async custom(args) { + function cleanGLSL(glslCode) { + //delete multilines comments + let cleanedCode = glslCode + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/ /g, "\n") + .replace(/\/\/.*$/gm, " ") + .replace(/; /g, ";\n"); + + return cleanedCode; + } + + let fs = cleanGLSL(` + ${args.FRA} + `); + if (!args.FRA.trim()) { + fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`; + } + const vs = cleanGLSL(` + ${args.VER} + `); + console.log(fs); + console.log(vs); + + const effect = new Effect("Custom", fs, { + blendFunction: BlendFunction[args.BLEND], + vertexShader: vs, + uniforms: new Map([ + //uniforms usually in shaders... open to more! + ["time", new THREE.Uniform(0.0)], + [ + "resolution", + new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height)), + ], + ]), + defines: new Map([ + ["USE_TIME", "1"], + ["USE_VERTEX_TRANSFORM", ""], + ]), + }); + + effect.blendMode.opacity.value = args.OP; + + const pass = new EffectPass(camera, effect); + composer.addPass(pass); + + customEffects.push(effect); + } + } + Scratch.extensions.register(new ThreeAddons()); + + class RapierPhysics { + getInfo() { + return { + id: "rapierPhysics", + name: "RAPIER Physics", + color1: "#222222", + color2: "#203024ff", + color3: "#78f07eff", + blocks: [{ + opcode: "createWorld", + blockType: Scratch.BlockType.COMMAND, + text: "create world | gravity:[G]", + arguments: { + G: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,-9.81,0]", + }, + }, + }, + { + opcode: "getWorld", + blockType: Scratch.BlockType.REPORTER, + text: "get world [PROPERTY]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "wProp", + }, + }, + }, + "---", + { + opcode: "objectPhysics", + blockType: Scratch.BlockType.COMMAND, + text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", + arguments: { + state2: { + type: Scratch.ArgumentType.STRING, + menu: "state2", + }, + state: { + type: Scratch.ArgumentType.STRING, + menu: "state", + defaultValue: "true", + }, + type: { + type: Scratch.ArgumentType.STRING, + menu: "objectTypes", + defaultValue: "dynamic", + }, + collider: { + type: Scratch.ArgumentType.STRING, + menu: "colliderTypes", + defaultValue: "cuboid", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + mass: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + density: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "1", + }, + friction: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: "0.5", + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.LABEL, + text: "- RigidBody", + }, + { + opcode: "setRB", + blockType: Scratch.BlockType.COMMAND, + text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "rigidBodySets", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "getRB", + blockType: Scratch.BlockType.REPORTER, + text: "get rigidbody [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "rigidBodyProperties", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "lockObjectAxis", + blockType: Scratch.BlockType.COMMAND, + text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", + arguments: { + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "lockAxes", + }, + X: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + Y: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + Z: { + type: Scratch.ArgumentType.STRING, + menu: "tf", + }, + }, + }, + "---", + { + opcode: "addForce", + blockType: Scratch.BlockType.COMMAND, + text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,10,0]", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "forces", + defaultValue: "addForce", + }, + SPACE: { + type: Scratch.ArgumentType.STRING, + menu: "spaces", + defaultValue: "world", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "resetForces", + blockType: Scratch.BlockType.COMMAND, + text: "reset [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "resetF", + defaultValue: "resetForces", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "enableCCD", + blockType: Scratch.BlockType.COMMAND, + text: "enable Continuous Collision Detection for [OBJECT] [state]", + arguments: { + state: { + type: Scratch.ArgumentType.STRING, + menu: "state", + defaultValue: "true", + }, + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "oPropS", + defaultValue: "physics", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "fixedJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + RA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + RB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + }, + }, + { + opcode: "sphericalJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + }, + }, + { + opcode: "revoluteJoint", + blockType: Scratch.BlockType.COMMAND, + text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", + arguments: { + ObjA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + ObjB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObjectB", + }, + VA: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,0,0]", + }, + VB: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[0,1,0]", + }, + X: { + type: Scratch.ArgumentType.STRING, + defaultValue: "[1,0,0]", + }, + }, + }, + "---", + { + blockType: Scratch.BlockType.LABEL, + text: "- Collider", + }, + { + opcode: "setC", + blockType: Scratch.BlockType.COMMAND, + text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "colliderSets", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1", + }, + }, + }, + { + opcode: "getC", + blockType: Scratch.BlockType.REPORTER, + text: "get collider [PROPERTY] of [OBJECT]", + arguments: { + PROPERTY: { + type: Scratch.ArgumentType.STRING, + menu: "colliderProperties", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + "---", + { + opcode: "sensorSingle", + blockType: Scratch.BlockType.BOOLEAN, + text: "is sensor [SENSOR] touching [OBJECT]?", + arguments: { + SENSOR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySensor", + }, + OBJECT: { + type: Scratch.ArgumentType.STRING, + defaultValue: "myObject", + }, + }, + }, + { + opcode: "sensorAll", + blockType: Scratch.BlockType.REPORTER, + text: "objects touching sensor [SENSOR]", + arguments: { + SENSOR: { + type: Scratch.ArgumentType.STRING, + defaultValue: "mySensor", + }, + }, + }, + ], + menus: { + wProp: { + acceptReporters: false, + items: [{ + text: "Gravity", + value: "gravity", + }, + { + text: "log to console", + value: "log", + }, + ], + }, + tf: { + acceptReporters: true, + items: [{ + text: "false", + value: "false", + }, + { + text: "true", + value: "true", + }, + ], + }, + lockAxes: { + acceptReporters: false, + items: [{ + text: "Translation", + value: "setEnabledTranslations", + }, + { + text: "Rotation", + value: "setEnabledRotations", + }, + ], + }, + rigidBodyProperties: { + acceptReporters: false, + items: [{ + text: "Type", + value: "bodyType", + }, + { + text: "Linear Velocity", + value: "linvel", + }, + { + text: "Angular Velocity", + value: "angvel", + }, + { + text: "Translation (position)", + value: "translation", + }, + { + text: "Rotation (quaternion)", + value: "rotation", + }, + { + text: "Mass", + value: "mass", + }, + //{text: "Center of Mass", value: "centerOfMass"}, + { + text: "Linear Damping", + value: "linearDamping", + }, + { + text: "Angular Damping", + value: "angularDamping", + }, + { + text: "Is Sleeping?", + value: "isSleeping", + }, + //{text: "Can Sleep?", value: "isCanSleep"}, + { + text: "Gravity Scale", + value: "gravityScale", + }, + { + text: "Is Fixed?", + value: "isFixed", + }, + { + text: "Is Dynamic?", + value: "isDynamic", + }, + { + text: "Is Kinematic?", + value: "isKinematic", + }, + //{text: "Sleeping", value: "sleeping"} + ], + }, + rigidBodySets: { + acceptReporters: false, + items: [ + //{text: "Linear Velocity", value: "setLinvel"}, + //{text: "Angular Velocity", value: "setAngvel"}, + //{text: "Mass", value: "setMass"}, + { + text: "Gravity Scale", + value: "setGravityScale", + }, + //{text: "Can Sleep?", value: "setCanSleep"}, + //{text: "Sleeping", value: "sleeping"}, + { + text: "Linear Damping", + value: "setLinearDamping", + }, + { + text: "Angular Damping", + value: "setAngularDamping", + }, + { + text: "Is Fixed?", + value: "isFixed", + }, + { + text: "Is Dynamic?", + value: "isDynamic", + }, + { + text: "Is Kinematic?", + value: "isKinematic", + }, + ], + }, + colliderProperties: { + acceptReporters: false, + items: [ + //{text: "Collider Type", value: "type"}, + { + text: "Is Sensor?", + value: "isSensor", + }, + { + text: "Friction", + value: "friction", + }, + { + text: "Restitution", + value: "restitution", + }, + { + text: "Density", + value: "density", + }, + { + text: "Mass", + value: "mass", + }, + { + text: "Position", + value: "translation", + }, + { + text: "Rotation", + value: "rotation", + }, + //{text: "Area", value: "area"}, + { + text: "Volume", + value: "volume", + }, + { + text: "Collision Groups", + value: "collisionGroups", + }, + //{text: "Collision Mask", value: "collisionMask"}, + //{text: "Is Enabled?", value: "enabled"}, + //{text: "Contact Count", value: "contactCount"}, + //{text: "RigidBody Handle", value: "rigidBody"} + ], + }, + colliderSets: { + acceptReporters: false, + items: [{ + text: "Friction", + value: "setFriction", + }, + { + text: "Restitution", + value: "setRestitution", + }, + { + text: "Density", + value: "setDensity", + }, + { + text: "Is Sensor?", + value: "setSensor", + }, + { + text: "Collision Groups", + value: "setCollisionGroups", + }, + //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool + //{text: "Position Offset", value: "setTranslation"}, + //{text: "Rotation Offset", value: "setRotation"} + ], + }, + state: { + acceptReporters: true, + items: [{ + text: "on", + value: "true", + }, + { + text: "off", + value: "false", + }, + ], + }, + state2: { + acceptReporters: true, + items: [{ + text: "false", + value: "false", + }, + { + text: "true (must be fixed)", + value: "true", + }, + ], + }, + spaces: { + acceptReporters: false, + items: [{ + text: "World", + value: "world", + }, + { + text: "Local", + value: "local", + }, + ], + }, + objectTypes: { + acceptReporters: false, + items: [{ + text: "Dynamic", + value: "dynamic", + }, + { + text: "Fixed", + value: "fixed", + }, + { + text: "Kinematic Position Based", + value: "kinematicPositionBased", + }, + ], + }, + colliderTypes: { + acceptReporters: false, + items: [{ + text: "Box, Rectangle, cuboid", + value: "cuboid", + }, + { + text: "Sphere, ball", + value: "ball", + }, + { + text: "Custom, complex simple shapes, convexHull", + value: "convexHull", + }, + { + text: "Precision, TriMesh", + value: "trimesh", + }, + ], + }, + forces: { + acceptReporters: false, + items: [{ + text: "Force", + value: "addForce", + }, + { + text: "Torque (rotation)", + value: "addTorque", + }, + { + text: "Apply Impulse", + value: "applyImpulse", + }, + { + text: "Apply Torque Impulse (rotation)", + value: "applyTorqueImpulse", + }, + { + text: "Linear Velocity", + value: "setLinvel", + }, + { + text: "Angular Velocity", + value: "setAngvel", + }, + ], + }, + resetF: { + acceptReporters: false, + items: [{ + text: "Forces", + value: "resetForces", + }, + { + text: "Torques", + value: "resetTorques", + }, + ], + }, + }, + }; + } + joint(jointData, bodyA, bodyB) { + physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); + } + + fixedJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + let RA = JSON.parse(args.RA).map(Number); + let RB = JSON.parse(args.RB).map(Number); + + RA = new THREE.Quaternion().setFromEuler( + new THREE.Euler( + THREE.MathUtils.degToRad(RA[0]), + THREE.MathUtils.degToRad(RA[1]), + THREE.MathUtils.degToRad(RA[2]) + ) + ); + RB = new THREE.Quaternion().setFromEuler( + new THREE.Euler( + THREE.MathUtils.degToRad(RB[0]), + THREE.MathUtils.degToRad(RB[1]), + THREE.MathUtils.degToRad(RB[2]) + ) + ); + + const data = RAPIER.JointData.fixed({ + x: VA[0], + y: VA[1], + z: VA[2], + }, + RA, { + x: VB[0], + y: VB[1], + z: VB[2], + }, + RB + ); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + sphericalJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + + const data = RAPIER.JointData.spherical({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + revoluteJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + const x = JSON.parse(args.X).map(Number); + + const data = RAPIER.JointData.revolute({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }, { + x: x[0], + y: x[1], + z: x[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + prismaticJoint(args) { + const VA = JSON.parse(args.VA).map(Number); + const VB = JSON.parse(args.VB).map(Number); + const x = JSON.parse(args.X).map(Number); + + const data = RAPIER.JointData.prismatic({ + x: VA[0], + y: VA[1], + z: VA[2], + }, { + x: VB[0], + y: VB[1], + z: VB[2], + }, { + x: x[0], + y: x[1], + z: x[2], + }); + const objectA = getObject(args.ObjA); + let object = getObject(args.ObjB); + this.joint(data, objectA, object); + } + + createWorld(args) { + const v3 = JSON.parse(args.G).map(Number); + const gravity = { + x: v3[0], + y: v3[1], + z: v3[2], + }; + physicsWorld = new RAPIER.World(gravity); + + console.log(physicsWorld); + } + + getWorld(args) { + if (args.PROPERTY === "log") { + console.log(physicsWorld); + return "logged"; + } + return JSON.stringify(physicsWorld[args.PROPERTY]); + } + + setRB(args) { + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.rigidBody[args.PROPERTY](value); + } + setC(args) { + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.collider[args.PROPERTY](value); + } + + getRB(args) { + let object = getObject(args.OBJECT); + return JSON.stringify(object.rigidBody[args.PROPERTY]()); + } + getC(args) { + let object = getObject(args.OBJECT); + return JSON.stringify(object.collider[args.PROPERTY]()); + } + + lockObjectAxis(args) { + let object = getObject(args.OBJECT); + const x = !JSON.parse(args.X); + const y = !JSON.parse(args.Y); + const z = !JSON.parse(args.Z); + object.rigidBody[args.PROPERTY](x, y, z, true); //changes is xyz, wake up + } + + objectPhysics(args) { + let object = getObject(args.OBJECT); + object.physics = JSON.parse(args.state); + + if (JSON.parse(args.state)) { + //if already exists delete: + if (object.rigidBody) { + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; + } + /*asing a rigidbody and collider to object and add them to physicsWorld*/ + let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() + .setTranslation(object.position.x, object.position.y, object.position.z) + .setRotation({ + w: object.quaternion._w, + x: object.quaternion._x, + y: object.quaternion._y, + z: object.quaternion._z, + }); + + let colliderDesc; + switch (args.collider) { + case "cuboid": + colliderDesc = createCuboidCollider(object); + break; + case "ball": + colliderDesc = createBallCollider(object); + break; + case "convexHull": + colliderDesc = createConvexHullCollider(object); + break; + case "trimesh": + colliderDesc = TriMesh(object); + break; + } + colliderDesc + .setSensor(JSON.parse(args.state2)) + .setMass(args.mass) + .setDensity(args.density) + .setFriction(args.friction); + + let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc); + let collider = physicsWorld.createCollider(colliderDesc, rigidBody); + + object.rigidBody = rigidBody; + object.collider = collider; + } else { + /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; + } + } + + enableCCD(args) { + let object = getObject(args.OBJECT); + if (object.physics) { + let rigidBody = object.rigidBody; + rigidBody.enableCcd(JSON.parse(args.state)); + } + } + + addForce(args) { + let object = getObject(args.OBJECT); + const vector = JSON.parse(args.VALUE).map(Number); + + let force = new THREE.Vector3(vector[0], vector[1], vector[2]); + if (args.SPACE === "local") { + force.applyQuaternion(object.quaternion); + } + + object.rigidBody[args.PROPERTY](force, true); + } + + resetForces(args) { + rigidBody[args.PROPERTY](true); + } + + sensorSingle(args) { + const sensor = getObject(args.SENSOR); + + let object = getObject(args.OBJECT); + + let touching = false; + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + if (otherCollider === object.collider) touching = true; + }); + + return touching; + } + + sensorAll(args) { + const sensor = getObject(args.SENSOR); + + const touchedObjects = []; + + // loop thruogh every collider touching sensor + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + // find owner of collider + const otherObject = scene.children.find((o) => o.collider === otherCollider); + console.log(otherCollider); + if (otherObject) touchedObjects.push(otherObject.name); + }); + + return JSON.stringify(touchedObjects); + } + } + Scratch.extensions.register(new RapierPhysics()); + + //Thanks to the PointerLock extension of Turbowarp + const mouse = vm.runtime.ioDevices.mouse; + let isLocked = false; + let isPointerLockEnabled = false; + + let rect = threeRenderer.domElement.getBoundingClientRect(); + document.addEventListener("resize", () => { + rect = threeRenderer.domElement.getBoundingClientRect(); + }); + + const postMouseData = (e, isDown) => { + const { + movementX, + movementY + } = e; + const { + width, + height + } = rect; + const x = mouse._clientX + movementX; + const y = mouse._clientY - movementY; + mouse._clientX = x; + mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); + mouse._clientY = y; + mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); + if (typeof isDown === "boolean") { + const data = { + button: e.button, + isDown, + }; + originalPostIOData(data); + } + }; + + const mouseDevice = vm.runtime.ioDevices.mouse; + const originalPostIOData = mouseDevice.postData.bind(mouseDevice); + mouseDevice.postData = (data) => { + if (!isPointerLockEnabled) { + return originalPostIOData(data); + } + }; + + document.addEventListener( + "mousedown", + (e) => { + // @ts-expect-error + if (threeRenderer.domElement.contains(e.target)) { + if (isLocked) { + postMouseData(e, true); + } else if (isPointerLockEnabled) { + threeRenderer.domElement.requestPointerLock(); + } + } + }, + true + ); + document.addEventListener( + "mouseup", + (e) => { + if (isLocked) { + postMouseData(e, false); + // @ts-expect-error + } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { + threeRenderer.domElement.requestPointerLock(); + } + }, + true + ); + document.addEventListener( + "mousemove", + (e) => { + if (isLocked) { + postMouseData(e); + } + }, + true + ); + + document.addEventListener("pointerlockchange", () => { + isLocked = document.pointerLockElement === threeRenderer.domElement; + }); + document.addEventListener("pointerlockerror", (e) => { + console.error("Pointer lock error", e); + }); + + const oldStep = vm.runtime._step; + vm.runtime._step = function(...args) { + const ret = oldStep.call(this, ...args); + if (isPointerLockEnabled) { + const { + width, + height + } = rect; + mouse._clientX = width / 2; + mouse._clientY = height / 2; + mouse._scratchX = 0; + mouse._scratchY = 0; + } + return ret; + }; + + vm.runtime.on("PROJECT_LOADED", () => { + isPointerLockEnabled = false; + if (isLocked) { + document.exitPointerLock(); + } + }); + + class Pointerlock { + getInfo() { + return { + id: "threepointerlockmod", + name: "Pointerlock for Extra 3D", + color1: "#8a8a8aff", + color2: "#222222", + color3: "#222222", + + blocks: [{ + opcode: "setLocked", + blockType: Scratch.BlockType.COMMAND, + text: "set pointer lock [enabled]", + arguments: { + enabled: { + type: Scratch.ArgumentType.STRING, + defaultValue: "true", + menu: "enabled", + }, + }, + }, + { + opcode: "isLocked", + blockType: Scratch.BlockType.BOOLEAN, + text: "pointer locked?", + }, + ], + menus: { + enabled: { + acceptReporters: true, + items: [{ + text: "enabled", + value: "true", + }, + { + text: "disabled", + value: "false", + }, + ], + }, + }, + }; + } + + setLocked(args) { + isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; + if (!isPointerLockEnabled && isLocked) { + document.exitPointerLock(); + } + } + + isLocked() { + return isLocked; + } + } + Scratch.extensions.register(new Pointerlock()); + }); +})(Scratch); From a7b3332b9bbdb9e2e396d06d7269e0d5f79b29e9 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 Date: Tue, 16 Dec 2025 17:00:26 -0600 Subject: [PATCH 14/32] merge --- threejsD.js.orig | 5029 -------------------------------------- threejsD_BACKUP_13852.js | 5029 -------------------------------------- threejsD_BASE_13852.js | 2413 ------------------ threejsD_LOCAL_13852.js | 2414 ------------------ threejsD_REMOTE_13852.js | 5016 ------------------------------------- 5 files changed, 19901 deletions(-) delete mode 100644 threejsD.js.orig delete mode 100644 threejsD_BACKUP_13852.js delete mode 100644 threejsD_BASE_13852.js delete mode 100644 threejsD_LOCAL_13852.js delete mode 100644 threejsD_REMOTE_13852.js diff --git a/threejsD.js.orig b/threejsD.js.orig deleted file mode 100644 index d1db442..0000000 --- a/threejsD.js.orig +++ /dev/null @@ -1,5029 +0,0 @@ -/* jshint esversion: 11 */ -// Name: Extra 3D -// ID: threejsExtension -// Description: Use three js inside Turbowarp! A 3D graphics library. -// By: Civero -// License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors - -(function(Scratch) { - "use strict"; - - if (!Scratch.extensions.unsandboxed) { - throw new Error("Three-D extension must run unsandboxed"); - } - - if (Scratch.vm.runtime.isPackaged) { - alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); - return; - } - //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return - - const vm = Scratch.vm; - const runtime = vm.runtime; - const renderer = Scratch.renderer; - const canvas = renderer.canvas; - const Cast = Scratch.Cast; - const menuIconURI = - "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; - - let alerts = false; - console.log("alerts are " + (alerts ? "enabled" : "disabled")); - - let isMouseDown = { - left: false, - middle: false, - right: false, - }; - let prevMouse = { - left: false, - middle: false, - right: false, - }; - - let lastWidth = 0; - let lastHeight = 0; - - let THREE; - let clock; - let running; - let loopId; - //Addons - let GLTFLoader; - let gltf; - let OrbitControls; - let controls; - let BufferGeometryUtils; - let TextGeometry; - let fontLoad; - //Physics - let RAPIER; - let physicsWorld; - - let threeRenderer; - let scene; - let camera; - let eulerOrder = "YXZ"; - - let composer; - let passes = {}; - let customEffects = []; - let renderTargets = {}; - - let materials = {}; - let geometries = {}; - let lights = {}; - let models = {}; - - let assets = { - //should i place materials, geometries; inside too? - textures: {}, - colors: {}, - fogs: {}, - curves: {}, - renderTargets: {}, //not the same as the global one! this one only stores textures - }; - - let raycastResult = []; - - function resetor(level) { - camera = undefined; - composer.reset(); - - passes = {}; - customEffects = []; - renderTargets = {}; - - materials = {}; - geometries = {}; - lights = {}; - models = {}; - - if (level > 0) { - assets = { - textures: {}, - colors: {}, - fogs: {}, - curves: {}, - renderTargets: {}, - }; - } - - updateComposers(); - } - - //utility - function vector3ToString(prop) { - if (!prop) return "0,0,0"; - - const x = - typeof prop.x === "number" ? - prop.x : - typeof prop._x === "number" ? - prop._x : - JSON.stringify(prop).includes("X") ? - prop : - 0; - const y = typeof prop.y === "number" ? prop.y : typeof prop.y === "number" ? prop._y : 0; - const z = typeof prop.z === "number" ? prop.z : typeof prop.z === "number" ? prop.z : 0; - - return [x, y, z]; - } - - //objects - function createObject(name, content, parentName) { - let object = getObject(name, true); - if (object) { - removeObject(name); - alerts ? alert(name + " already exsisted, will replace!") : null; - } - content.name = name; - content.rotation._order = eulerOrder; - parentName === scene.name ? (object = scene) : (object = getObject(parentName)); - content.physics = false; - - object.add(content); - } - - function removeObject(name) { - let object = getObject(name); - if (!object) return; - - scene.remove(object); - - if (object.rigidBody) { - physicsWorld.removeCollider(object.collider, true); - physicsWorld.removeRigidBody(object.rigidBody, true); - object.rigidBody = null; - object.collider = null; - } - if (object.isLight) { - delete lights[name]; - } - } - - function getObject(name, isNew) { - let object = null; - if (!scene) { - alerts ? alert("Can not get " + name + ". Create a scene first!") : null; - return; - } - object = scene.getObjectByName(name); - if (!object && !isNew) { - alerts ? alert(name + " does not exist! Add it to scene") : null; - return; - } - return object; - } - - //materials - function encodeCostume(name) { - if (name.startsWith("data:image/")) return name; - return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); - } - - function setTexutre(texture, mode, style, x, y) { - texture.colorSpace = THREE.SRGBColorSpace; - - if (mode === "Pixelate") { - texture.minFilter = THREE.NearestFilter; - texture.magFilter = THREE.NearestFilter; - } else { - //Blur - texture.minFilter = THREE.NearestMipmapLinearFilter; - texture.magFilter = THREE.NearestMipmapLinearFilter; - } - - if (style === "Repeat") { - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; - texture.repeat.set(x, y); - } - - texture.generateMipmaps = true; - } - async function resizeImageToSquare(uri, size = 256) { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext("2d"); - - // clear + draw image scaled to fit canvas - ctx.clearRect(0, 0, size, size); - ctx.drawImage(img, 0, 0, size, size); - - resolve(canvas.toDataURL()); // return normalized Data URI - //delete canvas? - }; - img.src = uri; - }); - } - //light - function updateShadowFrustum(light, focusPos) { - if (light.type !== "DirectionalLight") return; - - // Frustum Size - Increase this value to cover a larger area. - const d = 50; - - // Update Orthographic Shadow Camera Frustum - const shadowCamera = light.shadow.camera; - - // Set the width/height of the frustum - shadowCamera.left = -d; - shadowCamera.right = d; - shadowCamera.top = d; - shadowCamera.bottom = -d; - - // Determine ranges - shadowCamera.near = 0.1; - shadowCamera.far = 500; - - // Position the Light and its Target - light.target.position.copy(focusPos); - const direction = light.position.clone().sub(light.target.position).normalize(); - light.position.copy(focusPos.clone().add(direction.multiplyScalar(100))); - - // Ensure matrices are updated. - light.target.updateMatrixWorld(); - light.shadow.camera.updateProjectionMatrix(); - light.shadow.needsUpdate = true; - } - //composer - function updateComposers() { - if (!camera || !scene) return; // nothing to do yet - - // always recreate the RenderPass to point to the current scene/camera - passes["Render"] = new RenderPass(scene, camera); - - // ensure composer has a RenderPass as the first pass - const hasRender = composer.passes.some((p) => p && p.scene); - if (!hasRender) composer.addPass(passes["Render"]); - else { - // if composer already has one, replace it so it references current scene/camera - const idx = composer.passes.findIndex((p) => p && p.scene); - composer.passes[idx] = passes["Render"]; - } - } - //utility - function getMouseNDC(event) { - // Use threeRenderer.domElement for correct offset - const rect = threeRenderer.domElement.getBoundingClientRect(); - const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - return [x, y]; - } - - function checkCanvasSize() { - const { - width, - height - } = canvas; - if (width !== lastWidth || height !== lastHeight) { - lastWidth = width; - lastHeight = height; - resize(); - } - requestAnimationFrame(checkCanvasSize); //rerun next frame - } - //physics - function computeWorldBoundingBox(mesh) { - // Create a Box3 in world coordinates - const box = new THREE.Box3().setFromObject(mesh); - const size = new THREE.Vector3(); - box.getSize(size); - const center = new THREE.Vector3(); - box.getCenter(center); - return { - size, - center, - }; - } - - function createCuboidCollider(mesh) { - const { - size - } = computeWorldBoundingBox(mesh); - const collider = RAPIER.ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2); - return collider; - } - - function createBallCollider(mesh) { - const { - size - } = computeWorldBoundingBox(mesh); - // radius = 1/2 of the largest verticie - const radius = Math.max(size.x, size.y, size.z) / 2; - const collider = RAPIER.ColliderDesc.ball(radius); - return collider; //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) - } - - function createConvexHullCollider(mesh) { - mesh.updateWorldMatrix(true, false); - - const position = mesh.geometry.attributes.position; - const vertices = []; - const vertex = new THREE.Vector3(); - - // Matrix for scale only - const scaleMatrix = new THREE.Matrix4().makeScale(mesh.scale.x, mesh.scale.y, mesh.scale.z); - - for (let i = 0; i < position.count; i++) { - vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); - vertices.push(vertex.x, vertex.y, vertex.z); - } - - const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); - return collider; - } - - function TriMesh(mesh) { - // Get the positions array (from your geoPoints function) - const positions = mesh.geometry.attributes.position.array; - const numVertices = positions.length / 3; - - // Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] - const indices = Array.from({ - length: numVertices, - }, - (_, i) => i - ); - - const collider = RAPIER.ColliderDesc.trimesh(positions, new Uint32Array(indices)); - - return collider; - } - - function getModel(model, name) { - const file = runtime - .getTargetForStage() - .getSounds() - .find((c) => c.name === model); - if (!file) return; - - return new Promise((resolve, reject) => { - gltf.parse( - file.asset.data.buffer, - "", - (gltf) => { - const root = gltf.scene; - root.traverse((child) => { - if (child.isMesh) { - child.castShadow = true; - child.receiveShadow = true; - } - }); - - const mixer = new THREE.AnimationMixer(root); - const actions = {}; - gltf.animations.forEach((clip) => { - const act = mixer.clipAction(clip); - act.clampWhenFinished = true; - actions[clip.name] = act; - }); - - models[name] = { - root, - mixer, - actions, - }; - resolve(root); - }, - (error) => { - console.error("Error parsing GLB model:", error); - reject(error); - } - ); - }); - } - async function openFileExplorer(format) { - return new Promise((resolve) => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = format; - input.multiple = false; - input.onchange = () => { - resolve(input.files); - input.remove(); - }; - input.click(); - }); - } - - function getMeshesUsingTexture(scene, targetTexture) { - const meshes = []; - - scene.traverse((object) => { - if (object.material) { - const materials = Array.isArray(object.material) ? object.material : [object.material]; - for (const material of materials) { - if (material.map === targetTexture) { - meshes.push(object); - break; - } - } - } - }); - - return meshes; - } - - function getAsset(path) { - if (typeof path == "string") { - //string? - if (path.includes("/")) { - //has the /? - const value = path.split("/"); - console.log(value[0], value[1]); - return assets[value[0]][value[1]]; - } - } - - return JSON.parse(path); //boolean or number - } - - let mouseNDC = [0, 0]; - //loops/init - function stopLoop() { - if (!running) return; - running = false; - - if (loopId) { - cancelAnimationFrame(loopId); - loopId = null; - if (threeRenderer) threeRenderer.clear(); - } - } - async function load() { - if (!THREE) { - // @ts-ignore -<<<<<<< HEAD - THREE = await import("https://esm.sh/three@0.180.0") - window._THREE_ = THREE -======= - THREE = await import("https://esm.sh/three@0.180.0"); ->>>>>>> e4a038b (Update threejsD.js) - //Addons - GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js"); - OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js"); - BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js"); - TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js"); - const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js"); - fontLoad = new FontLoader.FontLoader(); - - const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8"); - const { - EffectComposer, - EffectPass, - RenderPass, - - Effect, - BloomEffect, - GodRaysEffect, - DotScreenEffect, - DepthOfFieldEffect, - - BlendFunction, - } = POSTPROCESSING; - //so i can use them later as global - window.EffectComposer = EffectComposer; - window.EffectPass = EffectPass; - window.RenderPass = RenderPass; - window.Effect = Effect; - window.BloomEffect = BloomEffect; - window.GodRaysEffect = GodRaysEffect; - window.DotScreenEffect = DotScreenEffect; - window.DepthOfFieldEffect = DepthOfFieldEffect; - window.BlendFunction = BlendFunction; - - RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0"); - await RAPIER.init(); - - threeRenderer = new THREE.WebGLRenderer({ - powerPreference: "high-performance", - antialias: false, - stencil: false, - depth: true, - }); - threeRenderer.setPixelRatio(window.devicePixelRatio); - threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors - threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) - //threeRenderer.toneMappingExposure = 1.0 //(test) - - threeRenderer.shadowMap.enabled = true; - threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap; // (optional) - threeRenderer.domElement.style.pointerEvents = "auto"; //will disable turbowarp mouse events, but enable threejs's - - gltf = new GLTFLoader.GLTFLoader(); - clock = new THREE.Clock(); - - // Example: create a composer - composer = new EffectComposer(threeRenderer, { - frameBufferType: THREE.HalfFloatType, - }); - - renderer.addOverlay(threeRenderer.domElement, "manual"); - renderer.addOverlay(canvas, "manual"); - renderer.setBackgroundColor(1, 1, 1, 0); - - resize(); - - window.addEventListener("mousedown", (e) => { - if (e.button === 0) isMouseDown.left = true; - if (e.button === 1) isMouseDown.middle = true; - if (e.button === 2) isMouseDown.right = true; - }); - window.addEventListener("mouseup", (e) => { - if (e.button === 0) isMouseDown.left = false; - prevMouse.left = false; - if (e.button === 1) isMouseDown.middle = false; - prevMouse.middle = false; - if (e.button === 2) isMouseDown.right = false; - prevMouse.right = false; - }); - // prevent contextmenu on right click - threeRenderer.domElement.addEventListener("contextmenu", (e) => e.preventDefault()); - - threeRenderer.domElement.addEventListener("mousemove", (event) => { - mouseNDC = getMouseNDC(event); - }); - - running = false; - load(); - -<<<<<<< HEAD - startRenderLoop() - runtime.on('PROJECT_START', () => startRenderLoop()) - runtime.on('PROJECT_STOP_ALL', () => stopLoop()) - runtime.on('STAGE_SIZE_CHANGED', () => {requestAnimationFrame(() => resize())}) - checkCanvasSize() -======= - startRenderLoop(); - runtime.on("PROJECT_START", () => startRenderLoop()); - runtime.on("PROJECT_STOP_ALL", () => stopLoop()); - runtime.on("STAGE_SIZE_CHANGED", () => { - requestAnimationFrame(() => resize()); - }); - //if (!runtime.isPackaged) checkCanvasSize() //only in editor ->>>>>>> e4a038b (Update threejsD.js) - } - } - - function startRenderLoop() { - if (running) return; - running = true; - - const loop = () => { - if (!running) return; - //RAPIER - if (physicsWorld && scene) { - physicsWorld.step(); - - scene.children.forEach((obj) => { - if (!obj.isMesh || !obj.physics) return; - if (obj.rigidBody) { - obj.position.copy(obj.rigidBody.translation()); - obj.quaternion.copy(obj.rigidBody.rotation()); - } - }); - } - if (scene && camera) { - if (controls) controls.update(); - - const delta = clock.getDelta(); - Object.values(models).forEach((model) => { - if (model) model.mixer.update(delta); - }); - - Object.values(lights).forEach((light) => updateShadowFrustum(light, camera.position)); - - //update custom effects time - customEffects.forEach((e) => { - if (e.uniforms.get("time")) { - e.uniforms.get("time").value += delta; - } - }); - Object.values(renderTargets).forEach((t) => { - if (t.camera.type == "PerspectiveCamera") { - t.camera.aspect = t.target.width / t.target.height; - t.camera.updateProjectionMatrix(); - } - // get meshes using the texture associated with this target - const displayMeshes = getMeshesUsingTexture(scene, t.target.texture); - - displayMeshes.forEach((mesh) => { - mesh.visible = false; - }); - - if (t.camera.type == "PerspectiveCamera") { - threeRenderer.setRenderTarget(t.target); - threeRenderer.clear(true, true, true); - threeRenderer.render(scene, t.camera); - } else { - t.target.clear(threeRenderer); - t.camera.update(threeRenderer, scene); //cubeCamera - } - - displayMeshes.forEach((mesh) => { - mesh.visible = true; - }); - }); - - camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height; - camera.updateProjectionMatrix(); - threeRenderer.setRenderTarget(null); - composer.render(delta); - } - - loopId = requestAnimationFrame(loop); - }; - - loopId = requestAnimationFrame(loop); - } - - function resize() { - const w = canvas.width; - const h = canvas.height; - - threeRenderer.setSize(w, h); - composer.setSize(w, h); - customEffects.forEach((e) => { - if (e.uniforms.get("resolution")) { - e.uniforms.get("resolution").value.set(w, h); - } - }); - - if (camera) { - camera.aspect = w / h; - camera.updateProjectionMatrix(); - } - } - //wait until all packages are loaded - Promise.resolve(load()).then(() => { - class threejsExtension { - getInfo() { - return { - id: "threejsExtension", - name: "Extra 3D", - color1: "#222222", - color2: "#222222", - color3: "#11cc99", - menuIconURI, - blockIconURI: menuIconURI, - - blocks: [{ - blockType: Scratch.BlockType.BUTTON, - text: "Show Docs", - func: "openDocs", - }, - { - blockType: Scratch.BlockType.BUTTON, - text: "Toggle Alerts", - func: "alerts", - }, - ], - menus: {}, - }; - } - openDocs() { - open("https://civ3ro.github.io/extensions/Documentation/"); - } - alerts() { - alerts = !alerts; - alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!"); - } - } - Scratch.extensions.register(new threejsExtension()); - - class ThreeRenderer { - getInfo() { - return { - id: "threeRenderer", - name: "Three Renderer", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "setRendererRatio", - blockType: Scratch.BlockType.COMMAND, - text: "set Pixel Ratio to [VALUE]", - arguments: { - VALUE: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "1", - }, - }, - }, - { - opcode: "eulerOrder", - blockType: Scratch.BlockType.COMMAND, - text: "set euler order to [VALUE]", - arguments: { - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "YXZ", - }, - }, - }, - ], - menus: {}, - }; - } - - setRendererRatio(args) { - threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE); - } - eulerOrder(args) { - eulerOrder = args.VALUE; - console.log("euler order set to", eulerOrder); - } - } - Scratch.extensions.register(new ThreeRenderer()); - - class ThreeScene { - constructor() { - this.THREE = THREE; - this.scenes = {}; - } - - getInfo() { - return { - id: "threeScene", - name: "Three Scene", - color1: "#4638c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "newScene", - blockType: Scratch.BlockType.COMMAND, - text: "new Scene [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - }, - }, - - { - opcode: "setSceneProperty", - blockType: Scratch.BlockType.COMMAND, - text: "set Scene [PROPERTY] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "sceneProperties", - defaultValue: "background", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "new Color()", - exemptFromNormalization: true, - }, - }, - }, - "---", - { - opcode: "getSceneObjects", - blockType: Scratch.BlockType.REPORTER, - text: "get Scene [THING]", - arguments: { - THING: { - type: Scratch.ArgumentType.STRING, - menu: "sceneThings", - }, - }, - }, - { - opcode: "reset", - blockType: Scratch.BlockType.COMMAND, - text: "Reset Everything", - }, - ], - menus: { - sceneProperties: { - acceptReporters: false, - items: [{ - text: "Background", - value: "background", - }, - { - text: "Background Blurriness", - value: "backgroundBlurriness", - }, - { - text: "Background Intensity", - value: "backgroundIntensity", - }, - { - text: "Background Rotation", - value: "backgroundRotation", - }, - { - text: "Environment", - value: "environment", - }, - { - text: "Environment Intensity", - value: "environmentIntensity", - }, - { - text: "Environment Rotation", - value: "environmentRotation", - }, - { - text: "Fog", - value: "fog", - }, - ], - }, - sceneThings: { - acceptReporters: false, - items: ["Objects", "Materials", "Geometries", "Lights", "Scene Properties", "Other assets"], - }, - }, - }; - } - - newScene(args) { - scene = new THREE.Scene(); - scene.name = args.NAME; - scene.background = new THREE.Color("#222"); - //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - this.scenes = { - ...this.scenes, - ...scene, - }; - resetor(0); - } - - reset() { - resetor(1); - } - - async setSceneProperty(args) { - const property = args.PROPERTY; - const value = getAsset(args.VALUE); - - scene[property] = value; - } - getSceneObjects(args) { - const names = []; - if (args.THING === "Objects") { - scene.traverse((obj) => { - if (obj.name) names.push(obj.name); //if it has a name, add to list! - }); - } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); - else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); - else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); - else if (args.THING === "Scene Properties") { - console.log(scene); - return "check console"; - } else if (args.THING === "Other assets") return JSON.stringify(assets); - - return JSON.stringify(names); // if objects - } - } - Scratch.extensions.register(new ThreeScene()); - - class ThreeCameras { - getInfo() { - return { - id: "threeCameras", - name: "Three Cameras", - color1: "#38c59bff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "addCamera", - blockType: Scratch.BlockType.COMMAND, - text: "add camera [TYPE] [CAMERA] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "cameraTypes", - }, - }, - }, - { - opcode: "setCamera", - blockType: Scratch.BlockType.COMMAND, - text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "cameraProperties", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "0.1", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "getCamera", - blockType: Scratch.BlockType.REPORTER, - text: "get camera [PROPERTY] of [CAMERA]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "cameraProperties", - }, - }, - }, - "---", - { - opcode: "renderSceneCamera", - blockType: Scratch.BlockType.COMMAND, - text: "set rendering camera to [CAMERA]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - }, - }, - "---", - { - opcode: "cubeCamera", - blockType: Scratch.BlockType.COMMAND, - text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "cubeCamera", - }, - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - "---", - { - opcode: "renderTarget", - blockType: Scratch.BlockType.COMMAND, - text: "set a RenderTarget: [RT] for camera [CAMERA]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - { - opcode: "sizeTarget", - blockType: Scratch.BlockType.COMMAND, - text: "set RenderTarget [RT] size to [W] [H]", - arguments: { - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - W: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 480, - }, - H: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 360, - }, - }, - }, - { - opcode: "getTarget", - blockType: Scratch.BlockType.REPORTER, - text: "get RenderTarget: [RT] texture", - arguments: { - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - { - opcode: "removeTarget", - blockType: Scratch.BlockType.COMMAND, - text: "remove RenderTarget: [RT]", - arguments: { - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - ], - menus: { - cameraTypes: { - acceptReporters: false, - items: [{ - text: "Perspective", - value: "PerspectiveCamera", - }, ], - }, - cameraProperties: { - acceptReporters: false, - items: [{ - text: "Near", - value: "near", - }, - { - text: "Far", - value: "far", - }, - { - text: "FOV", - value: "fov", - }, - { - text: "Focus (nothing...)", - value: "focus", - }, - { - text: "Zoom", - value: "zoom", - }, - ], - }, - }, - }; - } - addCamera(args) { - let v2 = new THREE.Vector2(); - threeRenderer.getSize(v2); - const object = new THREE.PerspectiveCamera(90, v2.x / v2.y); - object.position.z = 3; - - createObject(args.CAMERA, object, args.GROUP); - } - setCamera(args) { - let object = getObject(args.CAMERA); - object[args.PROPERTY] = args.VALUE; - object.updateProjectionMatrix(); - } - getCamera(args) { - let object = getObject(args.CAMERA); - const value = JSON.stringify(object[args.PROPERTY]); - return value; - } - renderSceneCamera(args) { - let object = getObject(args.CAMERA); - if (!object) return; - camera = object; - //reset composer, else it does not update. - composer.passes = []; - passes = {}; - customEffects = []; - updateComposers(); - } - - cubeCamera(args) { - // Create cube render target - const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, { - generateMipmaps: true, - }); - // Create cube camera - const cubeCamera = new THREE.CubeCamera(0.1, 500, cubeRenderTarget); - createObject(args.CAMERA, cubeCamera, args.GROUP); - - renderTargets[args.RT] = { - target: cubeRenderTarget, - camera: cubeCamera, - }; - assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture; - } - - renderTarget(args) { - let object = getObject(args.CAMERA); - const renderTarget = new THREE.WebGLRenderTarget(360, 360, { - generateMipmaps: false, - }); - - renderTargets[args.RT] = { - target: renderTarget, - camera: object, - }; - assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; - } - sizeTarget(args) { - renderTargets[args.RT].target.setSize(args.W, args.H); - } - getTarget(args) { - const t = renderTargets[args.RT].target.texture; - console.log(t, renderTargets[args.RT]); - return `renderTargets/${t.uuid}`; - } - removeTarget(args) { - delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; - renderTargets[args.RT].target.dispose(); - delete renderTargets[args.RT]; - } - } - Scratch.extensions.register(new ThreeCameras()); - - class ThreeObjects { - getInfo() { - return { - id: "threeObjects", - name: "Three Objects", - color1: "#38c567ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "addObject", - blockType: Scratch.BlockType.COMMAND, - text: "add object [OBJECT3D] [TYPE] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "objectTypes", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - { - opcode: "cloneObject", - blockType: Scratch.BlockType.COMMAND, - text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myClone", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "setObject", - blockType: Scratch.BlockType.COMMAND, - text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", - arguments: { - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectProperties", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - }, - }, - { - opcode: "getObject", - blockType: Scratch.BlockType.REPORTER, - text: "get [PROPERTY] of object [OBJECT3D]", - arguments: { - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectProperties", - }, - }, - }, - { - opcode: "objectE", - blockType: Scratch.BlockType.BOOLEAN, - text: "is there an object [NAME]?", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "removeObject", - blockType: Scratch.BlockType.COMMAND, - text: "remove object [OBJECT3D] from scene", - arguments: { - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: " ↳ Transforms", - }, - { - opcode: "setObjectV3", - extensions: ["colours_motion"], - blockType: Scratch.BlockType.COMMAND, - text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectVector3", - defaultValue: "position", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - }, - }, - //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, - //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - { - opcode: "getObjectV3", - extensions: ["colours_motion"], - blockType: Scratch.BlockType.REPORTER, - text: "get [PROPERTY] of [OBJECT3D]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectVector3", - defaultValue: "position", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: "↳ Materials", - }, - { - opcode: "newMaterial", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "new material [NAME] [TYPE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "materialTypes", - defaultValue: "MeshStandardMaterial", - }, - }, - }, - { - opcode: "materialE", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.BOOLEAN, - text: "is there a material [NAME]?", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - }, - }, - { - opcode: "removeMaterial", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "remove material [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - }, - }, - { - opcode: "setMaterial", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "set material [PROPERTY] of [NAME] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "materialProperties", - defaultValue: "color", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "new Color()", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "setBlending", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "set material [NAME] blending to [VALUE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - }, - }, - }, - { - opcode: "setDepth", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "set material [NAME] depth to [VALUE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - menu: "depthModes", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: "↳ Geometries", - }, - { - opcode: "newGeometry", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "new geometry [NAME] [TYPE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "geometryTypes", - defaultValue: "BoxGeometry", - }, - }, - }, - { - opcode: "geometryE", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.BOOLEAN, - text: "is there a geometry [NAME]?", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - }, - }, - { - opcode: "removeGeometry", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "remove geometry [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - }, - }, - "---", - { - opcode: "newGeo", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "new empty geometry [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[points]", - }, - }, - }, - { - opcode: "geoPoints", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "set geometry [NAME] vertex points to [POINTS]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[points]", - }, - }, - }, - { - opcode: "geoUVs", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "set geometry [NAME] UVs to [POINTS]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[UVs]", - }, - }, - }, - "---", - { - opcode: "splines", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "create spline [NAME] from curve [CURVE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySpline", - }, - CURVE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[curve]", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "splineModel", - extensions: ["colours_operators"], - blockType: Scratch.BlockType.COMMAND, - text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySpline", - }, - MODEL: { - type: Scratch.ArgumentType.STRING, - menu: "modelsList", - }, - CURVE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[curve]", - exemptFromNormalization: true, - }, - SPACING: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - "---", - { - blockType: Scratch.BlockType.BUTTON, - text: "Convert font to JSON", - func: "openConv", - }, - { - blockType: Scratch.BlockType.BUTTON, - text: "Load JSON font file", - func: "loadFont", - }, - { - opcode: "text", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myText", - }, - TEXT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "C-369", - }, - FONT: { - type: Scratch.ArgumentType.STRING, - menu: "fonts", - }, - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - D: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.1, - }, - CS: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 6, - }, - }, - }, - ], - menus: { - objectVector3: { - acceptReporters: false, - items: [{ - text: "Positon", - value: "position", - }, - { - text: "Rotation", - value: "rotation", - }, - { - text: "Scale", - value: "scale", - }, - { - text: "Facing Direction (.up)", - value: "up", - }, - ], - }, - objectProperties: { - acceptReporters: false, - items: [{ - text: "Geometry", - value: "geometry", - }, - { - text: "Material", - value: "material", - }, - { - text: "Visible (true/false)", - value: "visible", - }, - ], - }, - objectTypes: { - acceptReporters: false, - items: [{ - text: "Mesh", - value: "Mesh", - }, - { - text: "Sprite", - value: "Sprite", - }, - { - text: "Points", - value: "Points", - }, - { - text: "Line", - value: "Line", - }, - { - text: "Group", - value: "Group", - }, - ], - }, - XYZ: { - acceptReporters: false, - items: [{ - text: "X", - value: "x", - }, - { - text: "Y", - value: "y", - }, - { - text: "Z", - value: "z", - }, - ], - }, - materialProperties: { - acceptReporters: false, - items: [ - "|GENERAL| <-- not a property", - { - text: "Color", - value: "color", - }, - { - text: "Map", - value: "map", - }, - { - text: "Opacity", - value: "opacity", - }, - { - text: "Transparent", - value: "transparent", - }, - { - text: "Alpha Map", - value: "alphaMap", - }, - { - text: "Alpha Test", - value: "alphaTest", - }, - { - text: "Depth Test", - value: "depthTest", - }, - { - text: "Depth Write", - value: "depthWrite", - }, - { - text: "Color Write", - value: "colorWrite", - }, - { - text: "Side", - value: "side", - }, - { - text: "Visible", - value: "visible", - }, - /* - { text: "Blending", value: "blending" }, - { text: "Blend Src", value: "blendSrc" }, - { text: "Blend Dst", value: "blendDst" }, - { text: "Blend Equation", value: "blendEquation" }, - { text: "Blend Src Alpha", value: "blendSrcAlpha" }, - { text: "Blend Dst Alpha", value: "blendDstAlpha" }, - { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ - { - text: "Blend Aplha", - value: "blendAplha", - }, - { - text: "Blend Color", - value: "blendColor", - }, - { - text: "Alpha Hash", - value: "alphaHash", - }, - { - text: "Premultiplied Alpha", - value: "premultipliedAlpha", - }, - - { - text: "Tone Mapped", - value: "toneMapped", - }, - { - text: "Fog", - value: "fog", - }, - { - text: "Flat Shading", - value: "flatShading", - }, - - "|MESH Standard / Physical| <-- not a property", - { - text: "Metalness", - value: "metalness", - }, - { - text: "Metalness Map", - value: "metalnessMap", - }, - { - text: "Roughness", - value: "roughness", - }, - { - text: "Reflectivity", - value: "reflectivity", - }, - { - text: "Roughness Map", - value: "roughnessMap", - }, - { - text: "Emissive", - value: "emissive", - }, - { - text: "Emissive Intensity", - value: "emissiveIntensity", - }, - { - text: "Emissive Map", - value: "emissiveMap", - }, - { - text: "Env Map", - value: "envMap", - }, - { - text: "Env Map Intensity", - value: "envMapIntensity", - }, - { - text: "Env Map Rotation", - value: "envMapRotation", - }, - { - text: "Ior", - value: "ior", - }, - { - text: "Refraction Ratio", - value: "refractionRatio", - }, - { - text: "Clearcoat", - value: "clearcoat", - }, - { - text: "Clearcoat Map", - value: "clearcoatMap", - }, - { - text: "Clearcoat Roughness", - value: "clearcoatRoughness", - }, - { - text: "Clearcoat Roughness Map", - value: "clearcoatRoughnessMap", - }, - { - text: "Dispersion", - value: "dispersion", - }, - { - text: "Sheen", - value: "sheen", - }, - { - text: "Sheen Color", - value: "sheenColor", - }, - { - text: "Sheen Color Map", - value: "sheenColorMap", - }, - { - text: "Sheen Roughness", - value: "sheenRoughness", - }, - { - text: "Sheen Roughness Map", - value: "sheenRoughnessMap", - }, - { - text: "Specular Color", - value: "specularColor", - }, - { - text: "Specular Color Map", - value: "specularColorMap", - }, - { - text: "Specular Intensity", - value: "specularIntensity", - }, - { - text: "Specular Intensity Map", - value: "specularIntensityMap", - }, - { - text: "Transmission", - value: "transmission", - }, - { - text: "Transmission Map", - value: "transmissionMap", - }, - { - text: "Thickness", - value: "thickness", - }, - { - text: "Thickness Map", - value: "thicknessMap", - }, - { - text: "Anisotropy", - value: "anisotropy", - }, - { - text: "Anisotropy Map", - value: "anisotropyMap", - }, - { - text: "Anisotropy Rotation", - value: "anisotropyRotation", - }, - { - text: "Attenuation Distance", - value: "attenuationDistance", - }, - { - text: "Attenuation Color", - value: "attenuationColor", - }, - { - text: "Thickness", - value: "thickness", - }, - { - text: "Iridescence", - value: "iridescence", - }, - { - text: "Iridescence Ior", - value: "iridescenceIOR", - }, - { - text: "Iridescence Map", - value: "iridescenceMap", - }, - { - text: "Iridescence Thickness Range", - value: "iridescenceThicknessRange", - }, - - "|MESH Displacement / Normal / Bump| <-- not a property", - { - text: "Displacement Map", - value: "displacementMap", - }, - { - text: "Displacement Scale", - value: "displacementScale", - }, - { - text: "Displacement Bias", - value: "displacementBias", - }, - { - text: "Bump Map", - value: "bumpMap", - }, - { - text: "Bump Scale", - value: "bumpScale", - }, - { - text: "Normal Map Type", - value: "normalMapType", - }, - - "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", - { - text: "Shininess", - value: "shininess", - }, - - { - text: "Wireframe", - value: "wireframe", - }, - { - text: "Wireframe Linewidth", - value: "wireframeLinewidth", - }, - { - text: "Wireframe Linecap", - value: "wireframeLinecap", - }, - { - text: "Wireframe Linejoin", - value: "wireframeLinejoin", - }, - - "|POINTS| <-- not a property", - { - text: "Size", - value: "size", - }, - { - text: "Size Attenuation", - value: "sizeAttenuation", - }, - - "|LINES| <-- not a property", - { - text: "Scale", - value: "scale", - }, - { - text: "Dash Size", - value: "dashSize", - }, - { - text: "Gap Size", - value: "gapSize", - }, - - "|SPRITES| <-- not a property", - { - text: "Rotation", - value: "rotation", - }, - ], - }, - blendModes: { - acceptReporters: false, - items: [{ - text: "No Blending", - value: "NoBlending", - }, - { - text: "Normal Blending", - value: "NormalBlending", - }, - { - text: "Additive Blending", - value: "AdditiveBlending", - }, - { - text: "Subtractive Blending", - value: "SubtractiveBlending", - }, - { - text: "Multiply Blending", - value: "MultiplyBlending", - }, - { - text: "Custom Blending", - value: "CustomBlending", - }, - ], - }, - depthModes: { - acceptReporters: false, - items: [{ - text: "Never Depth", - value: "NeverDepth", - }, - { - text: "Always Depth", - value: "AlwaysDepth", - }, - { - text: "Equal Depth", - value: "EqualDepth", - }, - { - text: "Less Depth", - value: "LessDepth", - }, - { - text: "Less Equal Depth", - value: "LessEqualDepth", - }, - { - text: "Greater Equal Depth", - value: "GreaterEqualDepth", - }, - { - text: "Greater Depth", - value: "GreaterDepth", - }, - { - text: "Not Equal Depth", - value: "NotEqualDepth", - }, - ], - }, - materialTypes: { - acceptReporters: false, - items: [{ - text: "Mesh Basic Material", - value: "MeshBasicMaterial", - }, - { - text: "Mesh Standard Material", - value: "MeshStandardMaterial", - }, - { - text: "Mesh Physical Material", - value: "MeshPhysicalMaterial", - }, - { - text: "Mesh Lambert Material", - value: "MeshLambertMaterial", - }, - { - text: "Mesh Phong Material", - value: "MeshPhongMaterial", - }, - { - text: "Mesh Depth Material", - value: "MeshDepthMaterial", - }, - { - text: "Mesh Normal Material", - value: "MeshNormalMaterial", - }, - { - text: "Mesh Matcap Material", - value: "MeshMatcapMaterial", - }, - { - text: "Mesh Toon Material", - value: "MeshToonMaterial", - }, - { - text: "Line Basic Material", - value: "LineBasicMaterial", - }, - { - text: "Line Dashed Material", - value: "LineDashedMaterial", - }, - { - text: "Points Material", - value: "PointsMaterial", - }, - { - text: "Sprite Material", - value: "SpriteMaterial", - }, - { - text: "Shadow Material", - value: "ShadowMaterial", - }, - ], - }, - textureModes: { - acceptReporters: false, - items: ["Pixelate", "Blur"], - }, - textureStyles: { - acceptReporters: false, - items: ["Repeat", "Clamp"], - }, - geometryTypes: { - acceptReporters: false, - items: [{ - text: "Box Geometry", - value: "BoxGeometry", - }, - { - text: "Sphere Geometry", - value: "SphereGeometry", - }, - { - text: "Cylinder Geometry", - value: "CylinderGeometry", - }, - { - text: "Plane Geometry", - value: "PlaneGeometry", - }, - { - text: "Circle Geometry", - value: "CircleGeometry", - }, - { - text: "Torus Geometry", - value: "TorusGeometry", - }, - { - text: "Torus Knot Geometry", - value: "TorusKnotGeometry", - }, - ], - }, - modelsList: { - acceptReporters: false, - items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime - .getTargetForStage() - .getSounds() - .filter((e) => e.name && e.name.endsWith(".glb")); - if (models.length < 1) return [ - ["Load a model! (GLB Loader category)"] - ]; - - // @ts-ignore - return models.map((m) => [m.name]); - }, - }, - fonts: { - acceptReporters: false, - items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime - .getTargetForStage() - .getSounds() - .filter((e) => e.name && e.name.endsWith(".json")); - if (models.length < 1) return [ - ["Load a font!"] - ]; - - // @ts-ignore - return models.map((m) => [m.name]); - }, - }, - }, - }; - } - - addObject(args) { - const object = new THREE[args.TYPE](); - - object.castShadow = true; - object.receiveShadow = true; - - createObject(args.OBJECT3D, object, args.GROUP); - } - cloneObject(args) { - let object = getObject(args.OBJECT3D); - const clone = object.clone(true); - clone.name; - createObject(args.NAME, clone, args.GROUP); - } - setObjectV3(args) { - let object = getObject(args.OBJECT3D); - let values = JSON.parse(args.VALUE); - - function degToRad(deg) { - return (deg * Math.PI) / 180; - } - - if (object.rigidBody) { - const x = values[0]; - const y = values[1]; - const z = values[2]; - if (args.PROPERTY === "rotation") { - const euler = new THREE.Euler(degToRad(x), degToRad(y), degToRad(z), "YXZ"); - const quaternion = new THREE.Quaternion(); - quaternion.setFromEuler(euler); - - object.rigidBody.setRotation({ - x: quaternion.x, - y: quaternion.y, - z: quaternion.z, - w: quaternion.w, - }); - } else if (args.PROPERTY === "position") { - object.rigidBody.setTranslation({ - x: x, - y: y, - z: z, - }, - true - ); - } - return; - } - - if (object.isCamera == true && controls) {} - - if (args.PROPERTY === "rotation") { - values = values.map((v) => (v * Math.PI) / 180); - object.rotation.set(0, 0, 0); - } - if (object.isDirectionalLight == true) { - object.pos = new THREE.Vector3(...values); - console.log(true, values, object.pos); - return; - } - object[args.PROPERTY].set(...values); - - if (object.type == "CubeCamera") object.updateCoordinateSystem(); - } - /* - changeObjectV3(args) { - getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) - - if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.x += values[0] - object.rotation.y += values[1] - object.rotation.z += values[2] - } - else { - object[args.PROPERTY].add(...values); - } - } - changeObjectXV3(args) { - getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "rotation") value = value * Math.PI / 180 - - object[args.PROPERTY][args.X] += value - } - */ - getObjectV3(args) { - let object = getObject(args.OBJECT3D); - if (!object) return; - let values = vector3ToString(object[args.PROPERTY]); - if (args.PROPERTY === "rotation") { - const toDeg = Math.PI / 180; - values = [values[0] / toDeg, values[1] / toDeg, values[2] / toDeg]; - } - - return JSON.stringify(values); - } - setObject(args) { - let object = getObject(args.OBJECT3D); - let value = args.VALUE; - if (args.PROPERTY === "material") { - const mat = materials[args.NAME]; - if (mat) value = mat; - else value = undefined; - } else if (args.PROPERTY === "geometry") { - const geo = geometries[args.NAME]; - if (geo) value = geo; - else value = undefined; - } else value = !!value; - - if (value == undefined) return; //invalid geo/mat - object[args.PROPERTY] = value; - } - getObject(args) { - let object = getObject(args.OBJECT3D); - if (!object) return; - let value; - if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; - else value = object.visible; - - return value; - } - removeObject(args) { - removeObject(args.OBJECT3D); - } - objectE(args) { - return scene.children.map((o) => o.name).includes(args.NAME); - } - - //defines - newMaterial(args) { - if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); - const mat = new THREE[args.TYPE](); - mat.name = args.NAME; - - materials[args.NAME] = mat; - } - async setMaterial(args) { - if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; - const mat = materials[args.NAME]; - - let value = args.VALUE; - - if (args.VALUE == "false") value = false; - - if (args.PROPERTY == "side") { - value = args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide; - } else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)); - else value = getAsset(value); - - console.log("o:", args.VALUE, typeof args.VALUE); - console.log("r:", value, typeof value); - - mat[args.PROPERTY] = await value; //await incase its a texture - mat.needsUpdate = true; - } - setBlending(args) { - const mat = materials[args.NAME]; - mat.blending = THREE[args.VALUE]; - mat.premultipliedAlpha = true; - mat.needsUpdate = true; - } - setDepth(args) { - const mat = materials[args.NAME]; - mat.depthFunc = THREE[args.VALUE]; - mat.needsUpdate = true; - } - removeMaterial(args) { - const mat = materials[args.NAME]; - mat.dispose(); - delete materials[args.NAME]; - } - materialE(args) { - return materials[args.NAME] ? true : false; - } - - newGeometry(args) { - if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); - const geo = new THREE[args.TYPE](); - geo.name = args.NAME; - - geometries[args.NAME] = geo; - } - setGeometry(args) { - const geo = geometries[args.NAME]; - geo[args.PROPERTY] = args.VALUE; - - geo.needsUpdate = true; - } - removeGeometry(args) { - const geo = geometries[args.NAME]; - geo.dispose(); - delete geometries[args.NAME]; - } - geometryE(args) { - return geometries[args.NAME] ? true : false; - } - - newGeo(args) { - const geometry = new THREE.BufferGeometry(); - geometry.name = args.NAME; - geometries[args.NAME] = geometry; - } - async geoPoints(args) { - const geometry = geometries[args.NAME]; - const positions = args.POINTS.split(" ") - .map((v) => JSON.parse(v)) - .flat(); //array of v3 of each vertex of each triangle - - geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3)); - geometry.computeVertexNormals(); - - geometry.needsUpdate = true; - } - geoUVs(args) { - const geometry = geometries[args.NAME]; - const UVs = args.POINTS.split(" ") - .map((v) => JSON.parse(v)) - .flat(); //array of v2 of each UV of each triangle - - geometry.setAttribute("uv", new THREE.BufferAttribute(new Float32Array(UVs), 2)); - geometry.needsUpdate = true; - } - - splines(args) { - const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); - geometry.name = args.NAME; - - geometries[args.NAME] = geometry; - } - - async splineModel(args) { - const model = await getModel(args.MODEL, args.NAME); - if (!model) return console.warn("Model not found:", args.MODEL); - - const curve = getAsset(args.CURVE); - const spacing = parseFloat(args.SPACING) || 1; - const curveLength = curve.getLength(); - const divisions = Math.floor(curveLength / spacing); - - const geomList = []; - const matList = []; - - for (let i = 0; i <= divisions; i++) { - const t = i / divisions; - const pos = curve.getPointAt(t); - const tangent = curve.getTangentAt(t); - - const temp = model.clone(true); - temp.position.copy(pos); - - const up = new THREE.Vector3(0, 0, 1); - const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()); - temp.quaternion.copy(quat); - - temp.updateMatrixWorld(true); - - temp.traverse((child) => { - if (child.isMesh && child.geometry) { - const geom = child.geometry.clone(); - geom.applyMatrix4(child.matrixWorld); - geomList.push(geom); - matList.push(child.material); //.clone() ? - } - }); - } - - const validGeoms = geomList.filter((g) => { - const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position; - if (!ok) console.warn("geometry skipped:", g); - return ok; - }); - - const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true); - merged.computeBoundingBox(); - merged.computeBoundingSphere(); - - merged.name = args.NAME; - geometries[args.NAME] = merged; - matList.name = args.NAME; - materials[args.NAME] = matList; - } - - async text(args) { - const fontFile = runtime - .getTargetForStage() - .getSounds() - .find((c) => c.name === args.FONT); - if (!fontFile) return; - - const json = new TextDecoder().decode(fontFile.asset.data.buffer); - const fontData = JSON.parse(json); - - const font = fontLoad.parse(fontData); - - const params = { - font: font, - size: JSON.parse(args.S), - height: JSON.parse(args.D), - curveSegments: JSON.parse(args.CS), - bevelEnabled: false, - }; - const geometry = new TextGeometry.TextGeometry(args.TEXT, params); - geometry.computeVertexNormals(); - geometry.center(); // optional, recenters the text - - geometry.name = args.NAME; - - geometries[args.NAME] = geometry; - } - - async loadFont() { - openFileExplorer(".json").then((files) => { - const file = files[0]; - const reader = new FileReader(); - - reader.onload = async (e) => { - const arrayBuffer = e.target.result; - - // From lily's assets - // // Thank you PenguinMod for providing this code. - - const targetId = runtime.getTargetForStage().id; //util.target.id not working! - const assetName = Cast.toString(file.name); - - const buffer = arrayBuffer; - - const storage = runtime.storage; - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ); - - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ); - alert("Font loaded successfully!"); - } catch (e) { - console.error(e); - alert("Error loading font."); - } - - // End of PenguinMod - }; - - reader.readAsArrayBuffer(file); - }); - } - openConv() { - { - open("https://gero3.github.io/facetype.js/"); - } - } - } - Scratch.extensions.register(new ThreeObjects()); - - class ThreeLights { - getInfo() { - return { - id: "threeLights", - name: "Three Lights", - color1: "#c7a22aff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "addLight", - blockType: Scratch.BlockType.COMMAND, - text: "add light [NAME] type [TYPE] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myLight", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "lightTypes", - }, - }, - }, - { - opcode: "setLight", - blockType: Scratch.BlockType.COMMAND, - text: "set light [NAME][PROPERTY] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "lightProperties", - defaultValue: "intensity", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myLight", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - exemptFromNormalization: true, - }, - }, - }, - ], - menus: { - lightTypes: { - acceptReporters: false, - items: [{ - text: "Ambient Light", - value: "AmbientLight", - }, - { - text: "Directional Light", - value: "DirectionalLight", - }, - { - text: "Point Light", - value: "PointLight", - }, - { - text: "Hemisphere Light", - value: "HemisphereLight", - }, - { - text: "Spot Light", - value: "SpotLight", - }, - ], - }, - lightProperties: { - acceptReporters: false, - items: [{ - text: "Color", - value: "color", - }, - { - text: "Intensity", - value: "intensity", - }, - { - text: "Cast Shadow?", - value: "castShadow", - }, - { - text: "Ground Color (HemisphereLight)", - value: "groundColor", - }, - { - text: "Map (SpotLight)", - value: "map", - }, - { - text: "Distance (SpotLight)", - value: "distance", - }, - { - text: "Decay (SpotLight)", - value: "decay", - }, - { - text: "Penumbra (SpotLight)", - value: "penumbra", - }, - { - text: "Angle/Size (SpotLight)", - value: "angle", - }, - { - text: "Power (SpotLight)", - value: "power", - }, - { - text: "Target Position (Directional/SpotLight)", - value: "target", - }, - ], - }, - }, - }; - } - - addLight(args) { - const light = new THREE[args.TYPE](0xffffff, 1); - - createObject(args.NAME, light, args.GROUP); - lights[args.NAME] = light; - if (light.type === "AmbientLight" || "HemisphereLight") return; - - light.castShadow = true; - if (light.type === "PointLight") return; - //Directional & Spot Light - light.target.position.set(0, 0, 0); - scene.add(light.target); - - light.pos = new THREE.Vector3(0, 0, 0); - - light.shadow.mapSize.width = 4096; - light.shadow.mapSize.height = 2048; - - if (light.type === "SpotLight") { - light.decay = 0; - light.shadow.camera.near = 500; - light.shadow.camera.far = 4000; - light.shadow.camera.fov = 30; - } - light.shadow.needsUpdate = true; - light.needsUpdate = true; - } - - setLight(args) { - const light = lights[args.NAME]; - if (!args.PROPERTY) return; - if (args.PROPERTY === "target") { - light.target.position.set(...JSON.parse(args.VALUE)); //vector3 - light.target.updateMatrixWorld(); - } else { - light[args.PROPERTY] = getAsset(args.VALUE); - } - light.needsUpdate = true; - - if (light.type === "AmbientLight" || "HemisphereLight") return; - - light.shadow.camera.updateProjectionMatrix(); - light.shadow.needsUpdate = true; - } - } - Scratch.extensions.register(new ThreeLights()); - - class ThreeUtilities { - getInfo() { - return { - id: "threeUtility", - name: "Three Utilities", - color1: "#3875c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "newVector2", - blockType: Scratch.BlockType.REPORTER, - text: "New Vector [X] [Y]", - arguments: { - X: { - type: Scratch.ArgumentType.NUMBER, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - }, - }, - }, - { - opcode: "newVector3", - blockType: Scratch.BlockType.REPORTER, - text: "New Vector [X] [Y] [Z]", - arguments: { - X: { - type: Scratch.ArgumentType.NUMBER, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - }, - Z: { - type: Scratch.ArgumentType.NUMBER, - }, - }, - }, - "---", - { - opcode: "operateV3", - blockType: Scratch.BlockType.REPORTER, - text: "do [V3] [O] [V32]", - arguments: { - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - O: { - type: Scratch.ArgumentType.STRING, - menu: "operators", - }, - V32: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[1,0,0]", - }, - }, - }, - { - opcode: "moveVector3", - blockType: Scratch.BlockType.REPORTER, - text: "move [S] steps in vector [V3] in direction [D3]", - arguments: { - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - D3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[1,0,0]", - }, - }, - }, - { - opcode: "directionTo", - blockType: Scratch.BlockType.REPORTER, - text: "direction from [V3] to [T3]", - arguments: { - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,3]", - }, - T3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - }, - }, - "---", - { - opcode: "newColor", - blockType: Scratch.BlockType.REPORTER, - text: "New Color [HEX]", - arguments: { - HEX: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#9966ff", - }, - }, - }, - { - opcode: "newFog", - blockType: Scratch.BlockType.REPORTER, - text: "New Fog [COLOR] [NEAR] [FAR]", - arguments: { - COLOR: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#9966ff", - exemptFromNormalization: true, - }, - NEAR: { - type: Scratch.ArgumentType.NUMBER, - }, - FAR: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 10, - }, - }, - }, - { - opcode: "newTexture", - blockType: Scratch.BlockType.REPORTER, - text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", - arguments: { - COSTUME: { - type: Scratch.ArgumentType.COSTUME, - }, - MODE: { - type: Scratch.ArgumentType.STRING, - menu: "textureModes", - }, - STYLE: { - type: Scratch.ArgumentType.STRING, - menu: "textureStyles", - }, - X: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - { - opcode: "newCubeTexture", - blockType: Scratch.BlockType.REPORTER, - text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", - arguments: { - COSTUMEX0: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEX1: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEY0: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEY1: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEZ0: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEZ1: { - type: Scratch.ArgumentType.COSTUME, - }, - MODE: { - type: Scratch.ArgumentType.STRING, - menu: "textureModes", - }, - STYLE: { - type: Scratch.ArgumentType.STRING, - menu: "textureStyles", - }, - X: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - { - opcode: "newEquirectangularTexture", - blockType: Scratch.BlockType.REPORTER, - text: "New Equirectangular Texture [COSTUME] [MODE]", - arguments: { - COSTUME: { - type: Scratch.ArgumentType.COSTUME, - }, - MODE: { - type: Scratch.ArgumentType.STRING, - menu: "textureModes", - }, - }, - }, - "---", - { - opcode: "curve", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.REPORTER, - text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", - arguments: { - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "curveTypes", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]", - }, - CLOSED: { - type: Scratch.ArgumentType.STRING, - defaultValue: "true", - }, - }, - }, - "---", - { - opcode: "mouseDown", - extensions: ["colours_sensing"], - blockType: Scratch.BlockType.BOOLEAN, - text: "mouse [BUTTON] [action]?", - arguments: { - BUTTON: { - type: Scratch.ArgumentType.STRING, - menu: "mouseButtons", - }, - action: { - type: Scratch.ArgumentType.STRING, - menu: "mouseAction", - }, - }, - }, - { - opcode: "mousePos", - extensions: ["colours_sensing"], - blockType: Scratch.BlockType.REPORTER, - text: "mouse position", - arguments: {}, - }, - "---", - { - opcode: "getItem", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.REPORTER, - text: "get item [ITEM] of [ARRAY]", - arguments: { - ITEM: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - }, - ARRAY: { - type: Scratch.ArgumentType.STRING, - defaultValue: `["myObject", "myLight"]`, - }, - }, - }, - { - blockType: Scratch.BlockType.LABEL, - text: "↳ Raycasting", - }, - { - opcode: "raycast", - blockType: Scratch.BlockType.COMMAND, - text: "Raycast from [V3] in direction [D3]", - arguments: { - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,3]", - }, - D3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,1]", - }, - }, - }, - { - opcode: "getRaycast", - blockType: Scratch.BlockType.REPORTER, - text: "get raycast [PROPERTY]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "raycastProperties", - }, - }, - }, - ], - menus: { - materialProperties: { - acceptReporters: false, - items: [{ - text: "Color", - value: "color", - }, - { - text: "Map (texture)", - value: "map", - }, - { - text: "Alpha Map (texture)", - value: "alphaMap", - }, - { - text: "Alpha Test (0-1)", - value: "alphaTest", - }, - { - text: "Side (front/back/double)", - value: "side", - }, - { - text: "Bump Map (texture)", - value: "bumpMap", - }, - { - text: "Bump Scale", - value: "bumpScale", - }, - ], - }, - textureModes: { - acceptReporters: false, - items: ["Pixelate", "Blur"], - }, - textureStyles: { - acceptReporters: false, - items: ["Repeat", "Clamp"], - }, - raycastProperties: { - acceptReporters: false, - items: [{ - text: "Intersected Object Names", - value: "name", - }, - { - text: "Number of Objects", - value: "number", - }, - { - text: "Intersected Objects distances", - value: "distance", - }, - ], - }, - mouseButtons: { - acceptReporters: false, - items: ["left", "middle", "right"], - }, - mouseAction: { - acceptReporters: false, - items: ["Down", "Clicked"], - }, - curveTypes: { - acceptReporters: false, - items: ["CatmullRomCurve3"], - }, - operators: { - acceptReporters: false, - items: ["+", "-", "*", "/", "=", "max", "min", "dot", "cross", "distance to", "angle to", "apply euler"], - }, - }, - }; - } - mouseDown(args) { - if (args.action === "Down") return isMouseDown[args.BUTTON]; - if (args.action === "Clicked") { - if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false; - else prevMouse[args.BUTTON] = true; - return true; - } - } - mousePos(event) { - return JSON.stringify(mouseNDC); - } - newVector3(args) { - return JSON.stringify([args.X, args.Y, args.Z]); - } - operateV3(args) { - const v3 = new THREE.Vector3(...JSON.parse(args.V3)); - const v32 = new THREE.Vector3(...JSON.parse(args.V32)); - - let r; - if (args.O == "+") r = v3.add(v32); - else if (args.O == "-") r = v3.sub(v32); - else if (args.O == "*") r = v3.multiply(v32); - else if (args.O == "/") r = v3.divide(v32); - else if (args.O == "=") r = v3.equals(v32); - else if (args.O == "max") r = v3.max(v32); - else if (args.O == "min") r = v3.min(v32); - else if (args.O == "dot") r = v3.dot(v32); - else if (args.O == "cross") r = v3.cross(v32); - else if (args.O == "distance to") r = v3.distanceTo(v32); - else if (args.O == "angle to") r = v3.angleTo(v32); - else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)); - - if (typeof r == "object") return JSON.stringify([r.x, r.y, r.z]); - else return JSON.stringify(r); - } - - newVector2(args) { - return JSON.stringify([args.X, args.Y]); - } - - moveVector3(args) { - const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); - const steps = Number(args.S); - - const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); - - const yaw = THREE.MathUtils.degToRad(yawInputDeg); - const pitch = THREE.MathUtils.degToRad(pitchInputDeg); - const roll = THREE.MathUtils.degToRad(rollInputDeg); - - const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); - - const forwardVector = new THREE.Vector3(0, 0, -1); - const direction = forwardVector.applyEuler(euler).normalize(); - - const newPos = currentPos.add(direction.multiplyScalar(steps)); - return JSON.stringify([newPos.x, newPos.y, newPos.z]); - } - - directionTo(args) { - const v3 = new THREE.Vector3(...JSON.parse(args.V3)); - const toV3 = new THREE.Vector3(...JSON.parse(args.T3)); - - const direction = toV3.clone().sub(v3).normalize(); - // Pitch (X) - const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)); - // Yaw (Y) - const yaw = Math.atan2(direction.x, direction.z); - - // Roll always 0 - return JSON.stringify([180 + THREE.MathUtils.radToDeg(pitch), THREE.MathUtils.radToDeg(yaw), 0]); - } - - newColor(args) { - const color = new THREE.Color(args.HEX); - const uuid = crypto.randomUUID(); - assets.colors[uuid] = color; - return `colors/${uuid}`; - } - newFog(args) { - const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR); - const uuid = crypto.randomUUID(); - assets.fogs[uuid] = fog; - return `fogs/${uuid}`; - } - async newTexture(args) { - const textureURI = encodeCostume(args.COSTUME); - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); - assets.textures[texture.uuid] = texture; - return `textures/${texture.uuid}`; - } - async newCubeTexture(args) { - const uris = [ - encodeCostume(args.COSTUMEX0), - encodeCostume(args.COSTUMEX1), - encodeCostume(args.COSTUMEY0), - encodeCostume(args.COSTUMEY1), - encodeCostume(args.COSTUMEZ0), - encodeCostume(args.COSTUMEZ1), - ]; - const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); - const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); - - texture.name = "CubeTexture" + args.COSTUMEX0; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); - assets.textures[texture.uuid] = texture; - return `textures/${texture.uuid}`; - } - async newEquirectangularTexture(args) { - const textureURI = encodeCostume(args.COSTUME); - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME; - texture.mapping = THREE.EquirectangularReflectionMapping; - - setTexutre(texture, args.MODE); - assets.textures[texture.uuid] = texture; - return `textures/${texture.uuid}`; - } - - curve(args) { - function parsePoints(input) { - // Match all [x,y,z] groups - const matches = input.match(/\[([^\]]+)\]/g); - if (!matches) return []; - - return matches.map((str) => { - const nums = str - .replace(/[\[\]\s]/g, "") - .split(",") - .map(Number); - return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0); - }); - } - const points = parsePoints(args.POINTS); - const curve = new THREE[args.TYPE](points); - curve.closed = JSON.parse(args.CLOSED); - - const uuid = crypto.randomUUID(); - assets.curves[uuid] = curve; - return `curves/${uuid}`; - } - - getItem(args) { - const items = JSON.parse(args.ARRAY); - const item = items[args.ITEM - 1]; - if (!item) return "0"; - return item; - } - - raycast(args) { - const origin = new THREE.Vector3(...JSON.parse(args.V3)); - // rotation is in degrees => convert to radians first - const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); - - const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder); - const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize(); - - const raycaster = new THREE.Raycaster(); - //const camera = getObject(args.CAMERA) - raycaster.set(origin, direction); - - const intersects = raycaster.intersectObjects(scene.children, true); - - raycastResult = intersects; - } - getRaycast(args) { - if (args.PROPERTY === "number") return raycastResult.length; - if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map((i) => i.distance)); - return JSON.stringify(raycastResult.map((i) => i.object[args.PROPERTY])); - } - } - Scratch.extensions.register(new ThreeUtilities()); - - class ThreeGLB { - getInfo() { - return { - id: "threeGLB", - name: "Three GLB Loader", - color1: "#c53838ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - blockType: Scratch.BlockType.BUTTON, - text: "Load GLB File", - func: "loadModelFile", - }, - { - opcode: "addModel", - blockType: Scratch.BlockType.COMMAND, - text: "add [ITEM] as [NAME] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - ITEM: { - type: Scratch.ArgumentType.STRING, - menu: "modelsList", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - }, - }, - { - opcode: "getModel", - blockType: Scratch.BlockType.REPORTER, - text: "get object [PROPERTY] of [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "modelProperties", - }, - }, - }, - { - opcode: "playAnimation", - blockType: Scratch.BlockType.COMMAND, - text: "play animation [ANAME] of [NAME], [TIMES] times", - arguments: { - TIMES: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "0", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - ANAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "walk", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "pauseAnimation", - blockType: Scratch.BlockType.COMMAND, - text: "set [TOGGLE] animation [ANAME] of [NAME]", - arguments: { - TOGGLE: { - type: Scratch.ArgumentType.NUMBER, - menu: "pauseUn", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - ANAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "walk", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "stopAnimation", - blockType: Scratch.BlockType.COMMAND, - text: "stop animation [ANAME] of [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - ANAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "walk", - exemptFromNormalization: true, - }, - }, - }, - ], - menus: { - modelProperties: { - acceptReporters: false, - items: [{ - text: "Animations", - value: "animations", - }, ], - }, - pauseUn: { - acceptReporters: true, - items: [{ - text: "Pause", - value: "true", - }, - { - text: "Unpasue", - value: "false", - }, - ], - }, - modelsList: { - acceptReporters: false, - items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime - .getTargetForStage() - .getSounds() - .filter((e) => e.name && e.name.endsWith(".glb")); - if (models.length < 1) return [ - ["Load a model!"] - ]; - - // @ts-ignore - return models.map((m) => [m.name]); - }, - }, - }, - }; - } - - async loadModelFile() { - openFileExplorer(".glb").then((files) => { - const file = files[0]; - const reader = new FileReader(); - - reader.onload = async (e) => { - const arrayBuffer = e.target.result; - - { - // From lily's assets - - // Thank you PenguinMod for providing this code. - { - const targetId = runtime.getTargetForStage().id; //util.target.id not working! - const assetName = Cast.toString(file.name); - - //const res = await Scratch.fetch(args.URL); - //const buffer = await res.arrayBuffer(); - const buffer = arrayBuffer; - - const storage = runtime.storage; - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ); - - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ); - alert("Model loaded successfully!"); - } catch (e) { - console.error(e); - alert("Error loading model."); - } - } - // End of PenguinMod - } - }; - - reader.readAsArrayBuffer(file); - }); - } - async addModel(args) { - const group = await getModel(args.ITEM, args.NAME); - - createObject(args.NAME, group, args.GROUP); - } - getModel(args) { - if (!models[args.NAME]) return; - return Object.keys(models[args.NAME].actions).toString(); - } - - playAnimation(args) { - const model = models[args.NAME]; - if (!model) { - console.log("no model!"); - return; - } - - const action = model.actions[args.ANAME]; //clones of models dont have a stored actions! - if (!action) { - console.log("no action!"); - return; - } - - args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity); - - action.reset().play(); - } - stopAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.stop(); - } - pauseAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.paused = args.TOGGLE; - } - } - Scratch.extensions.register(new ThreeGLB()); - - class ThreeAddons { - getInfo() { - return { - id: "threeAddons", - name: "Three Addons", - color1: "#c538a2ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - blockType: Scratch.BlockType.LABEL, - text: "Orbit Control", - }, - { - opcode: "OrbitControl", - blockType: Scratch.BlockType.COMMAND, - text: "set addon Orbit Control [STATE]", - arguments: { - STATE: { - type: Scratch.ArgumentType.STRING, - menu: "onoff", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: "Post Processing", - }, - { - opcode: "resetComposer", - blockType: Scratch.BlockType.COMMAND, - text: "reset composer", - }, - { - opcode: "bloom", - blockType: Scratch.BlockType.COMMAND, - text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", - arguments: { - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - I: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.5, - }, - T: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.5, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "SCREEN", - }, - }, - }, - { - opcode: "godRays", - blockType: Scratch.BlockType.COMMAND, - text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", - arguments: { - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "SCREEN", - }, - DEC: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.95, - }, - DENS: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - EXP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.1, - }, - WEI: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.4, - }, - RES: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - SAMP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 64, - }, - }, - }, - { - opcode: "dots", - blockType: Scratch.BlockType.COMMAND, - text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", - arguments: { - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - A: { - type: Scratch.ArgumentType.ANGLE, - defaultValue: 0, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "SCREEN", - }, - }, - }, - { - opcode: "depth", - blockType: Scratch.BlockType.COMMAND, - text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", - arguments: { - FD: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 3, - }, - FL: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.001, - }, - BS: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 4, - }, - H: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 240, - }, - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "NORMAL", - }, - }, - }, - "---", - { - opcode: "custom", - blockType: Scratch.BlockType.COMMAND, - text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myShader", - }, - FRA: { - type: Scratch.ArgumentType.STRING, - }, - VER: { - type: Scratch.ArgumentType.STRING, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "NORMAL", - }, - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - ], - menus: { - onoff: { - acceptReporters: true, - items: [{ - text: "enabled", - value: "1", - }, - { - text: "disabled", - value: "0", - }, - ], - }, - blendModes: { - acceptReporters: false, - items: [ - "SKIP", - "SET", - "ADD", - "ALPHA", - "AVERAGE", - "COLOR", - "COLOR_BURN", - "COLOR_DODGE", - "DARKEN", - "DIFFERENCE", - "DIVIDE", - "DST", - "EXCLUSION", - "HARD_LIGHT", - "HARD_MIX", - "HUE", - "INVERT", - "INVERT_RGB", - "LIGHTEN", - "LINEAR_BURN", - "LINEAR_DODGE", - "LINEAR_LIGHT", - "LUMINOSITY", - "MULTIPLY", - "NEGATION", - "NORMAL", - "OVERLAY", - "PIN_LIGHT", - "REFLECT", - "SCREEN", - "SRC", - "SATURATION", - "SOFT_LIGHT", - "SUBTRACT", - "VIVID_LIGHT", - ], - }, - }, - }; - } - - OrbitControl(args) { - if (controls) controls.dispose(); - - console.log("creating...", OrbitControls); - controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); - controls.enableDamping = true; - - controls.enabled = !!args.STATE; - console.log(controls); - } - - resetComposer() { - composer.passes = []; - passes = {}; - customEffects = []; - updateComposers(); - } - - bloom(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - const bloomEffect = new BloomEffect({ - intensity: args.I, - luminanceThreshold: args.T, // ← correct key - luminanceSmoothing: args.S, - blendFunction: BlendFunction[args.BLEND], - }); - bloomEffect.blendMode.opacity.value = args.OP; - - const pass = new EffectPass(camera, bloomEffect); - - composer.addPass(pass); - } - - godRays(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - let object = getObject(args.NAME); - const sun = object; - - const godRays = new GodRaysEffect(camera, sun, { - resolutionScale: args.RES, - density: args.DENS, // ray density - decay: args.DEC, // fade out - weight: args.WEI, // brightness of rays - exposure: args.EXP, - samples: args.SAMP, - blendFunction: BlendFunction[args.BLEND], - }); - godRays.blendMode.opacity.value = args.OP; - const pass = new EffectPass(camera, godRays); - composer.addPass(pass); - } - - dots(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - const dot = new DotScreenEffect({ - angle: args.A, - scale: args.S, - blendFunction: BlendFunction[args.BLEND], - }); - dot.blendMode.opacity.value = args.OP; - const pass = new EffectPass(camera, dot); - composer.addPass(pass); - } - - depth(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - const dofEffect = new DepthOfFieldEffect(camera, { - focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) - focalLength: args.FL, // lens focal length in meters - bokehScale: args.BS, // strength/size of the blur circles - height: args.H, // resolution hint (affects quality/perf) - blendFunction: BlendFunction[args.BLEND], - }); - dofEffect.blendMode.opacity.value = args.OP; - - const dofPass = new EffectPass(camera, dofEffect); - composer.addPass(dofPass); - } - - async custom(args) { - function cleanGLSL(glslCode) { - //delete multilines comments - let cleanedCode = glslCode - .replace(/\/\*[\s\S]*?\*\//g, " ") - .replace(/ /g, "\n") - .replace(/\/\/.*$/gm, " ") - .replace(/; /g, ";\n"); - - return cleanedCode; - } - - let fs = cleanGLSL(` - ${args.FRA} - `); - if (!args.FRA.trim()) { - fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`; - } - const vs = cleanGLSL(` - ${args.VER} - `); - console.log(fs); - console.log(vs); - - const effect = new Effect("Custom", fs, { - blendFunction: BlendFunction[args.BLEND], - vertexShader: vs, - uniforms: new Map([ - //uniforms usually in shaders... open to more! - ["time", new THREE.Uniform(0.0)], - [ - "resolution", - new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height)), - ], - ]), - defines: new Map([ - ["USE_TIME", "1"], - ["USE_VERTEX_TRANSFORM", ""], - ]), - }); - - effect.blendMode.opacity.value = args.OP; - - const pass = new EffectPass(camera, effect); - composer.addPass(pass); - - customEffects.push(effect); - } - } - Scratch.extensions.register(new ThreeAddons()); - - class RapierPhysics { - getInfo() { - return { - id: "rapierPhysics", - name: "RAPIER Physics", - color1: "#222222", - color2: "#203024ff", - color3: "#78f07eff", - blocks: [{ - opcode: "createWorld", - blockType: Scratch.BlockType.COMMAND, - text: "create world | gravity:[G]", - arguments: { - G: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,-9.81,0]", - }, - }, - }, - { - opcode: "getWorld", - blockType: Scratch.BlockType.REPORTER, - text: "get world [PROPERTY]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "wProp", - }, - }, - }, - "---", - { - opcode: "objectPhysics", - blockType: Scratch.BlockType.COMMAND, - text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", - arguments: { - state2: { - type: Scratch.ArgumentType.STRING, - menu: "state2", - }, - state: { - type: Scratch.ArgumentType.STRING, - menu: "state", - defaultValue: "true", - }, - type: { - type: Scratch.ArgumentType.STRING, - menu: "objectTypes", - defaultValue: "dynamic", - }, - collider: { - type: Scratch.ArgumentType.STRING, - menu: "colliderTypes", - defaultValue: "cuboid", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - mass: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "1", - }, - density: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "1", - }, - friction: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "0.5", - }, - }, - }, - "---", - { - blockType: Scratch.BlockType.LABEL, - text: "- RigidBody", - }, - { - opcode: "setRB", - blockType: Scratch.BlockType.COMMAND, - text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "rigidBodySets", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - }, - }, - }, - { - opcode: "getRB", - blockType: Scratch.BlockType.REPORTER, - text: "get rigidbody [PROPERTY] of [OBJECT]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "rigidBodyProperties", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "lockObjectAxis", - blockType: Scratch.BlockType.COMMAND, - text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", - arguments: { - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "lockAxes", - }, - X: { - type: Scratch.ArgumentType.STRING, - menu: "tf", - }, - Y: { - type: Scratch.ArgumentType.STRING, - menu: "tf", - }, - Z: { - type: Scratch.ArgumentType.STRING, - menu: "tf", - }, - }, - }, - "---", - { - opcode: "addForce", - blockType: Scratch.BlockType.COMMAND, - text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", - arguments: { - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,10,0]", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "forces", - defaultValue: "addForce", - }, - SPACE: { - type: Scratch.ArgumentType.STRING, - menu: "spaces", - defaultValue: "world", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - { - opcode: "resetForces", - blockType: Scratch.BlockType.COMMAND, - text: "reset [PROPERTY] of [OBJECT]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "resetF", - defaultValue: "resetForces", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "enableCCD", - blockType: Scratch.BlockType.COMMAND, - text: "enable Continuous Collision Detection for [OBJECT] [state]", - arguments: { - state: { - type: Scratch.ArgumentType.STRING, - menu: "state", - defaultValue: "true", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "oPropS", - defaultValue: "physics", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "fixedJoint", - blockType: Scratch.BlockType.COMMAND, - text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", - arguments: { - ObjA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - ObjB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObjectB", - }, - VA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - VB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,1,0]", - }, - RA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - RB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - }, - }, - { - opcode: "sphericalJoint", - blockType: Scratch.BlockType.COMMAND, - text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", - arguments: { - ObjA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - ObjB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObjectB", - }, - VA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - VB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,1,0]", - }, - }, - }, - { - opcode: "revoluteJoint", - blockType: Scratch.BlockType.COMMAND, - text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", - arguments: { - ObjA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - ObjB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObjectB", - }, - VA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - VB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,1,0]", - }, - X: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[1,0,0]", - }, - }, - }, - "---", - { - blockType: Scratch.BlockType.LABEL, - text: "- Collider", - }, - { - opcode: "setC", - blockType: Scratch.BlockType.COMMAND, - text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "colliderSets", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - }, - }, - }, - { - opcode: "getC", - blockType: Scratch.BlockType.REPORTER, - text: "get collider [PROPERTY] of [OBJECT]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "colliderProperties", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "sensorSingle", - blockType: Scratch.BlockType.BOOLEAN, - text: "is sensor [SENSOR] touching [OBJECT]?", - arguments: { - SENSOR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySensor", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - { - opcode: "sensorAll", - blockType: Scratch.BlockType.REPORTER, - text: "objects touching sensor [SENSOR]", - arguments: { - SENSOR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySensor", - }, - }, - }, - ], - menus: { - wProp: { - acceptReporters: false, - items: [{ - text: "Gravity", - value: "gravity", - }, - { - text: "log to console", - value: "log", - }, - ], - }, - tf: { - acceptReporters: true, - items: [{ - text: "false", - value: "false", - }, - { - text: "true", - value: "true", - }, - ], - }, - lockAxes: { - acceptReporters: false, - items: [{ - text: "Translation", - value: "setEnabledTranslations", - }, - { - text: "Rotation", - value: "setEnabledRotations", - }, - ], - }, - rigidBodyProperties: { - acceptReporters: false, - items: [{ - text: "Type", - value: "bodyType", - }, - { - text: "Linear Velocity", - value: "linvel", - }, - { - text: "Angular Velocity", - value: "angvel", - }, - { - text: "Translation (position)", - value: "translation", - }, - { - text: "Rotation (quaternion)", - value: "rotation", - }, - { - text: "Mass", - value: "mass", - }, - //{text: "Center of Mass", value: "centerOfMass"}, - { - text: "Linear Damping", - value: "linearDamping", - }, - { - text: "Angular Damping", - value: "angularDamping", - }, - { - text: "Is Sleeping?", - value: "isSleeping", - }, - //{text: "Can Sleep?", value: "isCanSleep"}, - { - text: "Gravity Scale", - value: "gravityScale", - }, - { - text: "Is Fixed?", - value: "isFixed", - }, - { - text: "Is Dynamic?", - value: "isDynamic", - }, - { - text: "Is Kinematic?", - value: "isKinematic", - }, - //{text: "Sleeping", value: "sleeping"} - ], - }, - rigidBodySets: { - acceptReporters: false, - items: [ - //{text: "Linear Velocity", value: "setLinvel"}, - //{text: "Angular Velocity", value: "setAngvel"}, - //{text: "Mass", value: "setMass"}, - { - text: "Gravity Scale", - value: "setGravityScale", - }, - //{text: "Can Sleep?", value: "setCanSleep"}, - //{text: "Sleeping", value: "sleeping"}, - { - text: "Linear Damping", - value: "setLinearDamping", - }, - { - text: "Angular Damping", - value: "setAngularDamping", - }, - { - text: "Is Fixed?", - value: "isFixed", - }, - { - text: "Is Dynamic?", - value: "isDynamic", - }, - { - text: "Is Kinematic?", - value: "isKinematic", - }, - ], - }, - colliderProperties: { - acceptReporters: false, - items: [ - //{text: "Collider Type", value: "type"}, - { - text: "Is Sensor?", - value: "isSensor", - }, - { - text: "Friction", - value: "friction", - }, - { - text: "Restitution", - value: "restitution", - }, - { - text: "Density", - value: "density", - }, - { - text: "Mass", - value: "mass", - }, - { - text: "Position", - value: "translation", - }, - { - text: "Rotation", - value: "rotation", - }, - //{text: "Area", value: "area"}, - { - text: "Volume", - value: "volume", - }, - { - text: "Collision Groups", - value: "collisionGroups", - }, - //{text: "Collision Mask", value: "collisionMask"}, - //{text: "Is Enabled?", value: "enabled"}, - //{text: "Contact Count", value: "contactCount"}, - //{text: "RigidBody Handle", value: "rigidBody"} - ], - }, - colliderSets: { - acceptReporters: false, - items: [{ - text: "Friction", - value: "setFriction", - }, - { - text: "Restitution", - value: "setRestitution", - }, - { - text: "Density", - value: "setDensity", - }, - { - text: "Is Sensor?", - value: "setSensor", - }, - { - text: "Collision Groups", - value: "setCollisionGroups", - }, - //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool - //{text: "Position Offset", value: "setTranslation"}, - //{text: "Rotation Offset", value: "setRotation"} - ], - }, - state: { - acceptReporters: true, - items: [{ - text: "on", - value: "true", - }, - { - text: "off", - value: "false", - }, - ], - }, - state2: { - acceptReporters: true, - items: [{ - text: "false", - value: "false", - }, - { - text: "true (must be fixed)", - value: "true", - }, - ], - }, - spaces: { - acceptReporters: false, - items: [{ - text: "World", - value: "world", - }, - { - text: "Local", - value: "local", - }, - ], - }, - objectTypes: { - acceptReporters: false, - items: [{ - text: "Dynamic", - value: "dynamic", - }, - { - text: "Fixed", - value: "fixed", - }, - { - text: "Kinematic Position Based", - value: "kinematicPositionBased", - }, - ], - }, - colliderTypes: { - acceptReporters: false, - items: [{ - text: "Box, Rectangle, cuboid", - value: "cuboid", - }, - { - text: "Sphere, ball", - value: "ball", - }, - { - text: "Custom, complex simple shapes, convexHull", - value: "convexHull", - }, - { - text: "Precision, TriMesh", - value: "trimesh", - }, - ], - }, - forces: { - acceptReporters: false, - items: [{ - text: "Force", - value: "addForce", - }, - { - text: "Torque (rotation)", - value: "addTorque", - }, - { - text: "Apply Impulse", - value: "applyImpulse", - }, - { - text: "Apply Torque Impulse (rotation)", - value: "applyTorqueImpulse", - }, - { - text: "Linear Velocity", - value: "setLinvel", - }, - { - text: "Angular Velocity", - value: "setAngvel", - }, - ], - }, - resetF: { - acceptReporters: false, - items: [{ - text: "Forces", - value: "resetForces", - }, - { - text: "Torques", - value: "resetTorques", - }, - ], - }, - }, - }; - } - joint(jointData, bodyA, bodyB) { - physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); - } - - fixedJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - let RA = JSON.parse(args.RA).map(Number); - let RB = JSON.parse(args.RB).map(Number); - - RA = new THREE.Quaternion().setFromEuler( - new THREE.Euler( - THREE.MathUtils.degToRad(RA[0]), - THREE.MathUtils.degToRad(RA[1]), - THREE.MathUtils.degToRad(RA[2]) - ) - ); - RB = new THREE.Quaternion().setFromEuler( - new THREE.Euler( - THREE.MathUtils.degToRad(RB[0]), - THREE.MathUtils.degToRad(RB[1]), - THREE.MathUtils.degToRad(RB[2]) - ) - ); - - const data = RAPIER.JointData.fixed({ - x: VA[0], - y: VA[1], - z: VA[2], - }, - RA, { - x: VB[0], - y: VB[1], - z: VB[2], - }, - RB - ); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - sphericalJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - - const data = RAPIER.JointData.spherical({ - x: VA[0], - y: VA[1], - z: VA[2], - }, { - x: VB[0], - y: VB[1], - z: VB[2], - }); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - revoluteJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - const x = JSON.parse(args.X).map(Number); - - const data = RAPIER.JointData.revolute({ - x: VA[0], - y: VA[1], - z: VA[2], - }, { - x: VB[0], - y: VB[1], - z: VB[2], - }, { - x: x[0], - y: x[1], - z: x[2], - }); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - prismaticJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - const x = JSON.parse(args.X).map(Number); - - const data = RAPIER.JointData.prismatic({ - x: VA[0], - y: VA[1], - z: VA[2], - }, { - x: VB[0], - y: VB[1], - z: VB[2], - }, { - x: x[0], - y: x[1], - z: x[2], - }); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - createWorld(args) { - const v3 = JSON.parse(args.G).map(Number); - const gravity = { - x: v3[0], - y: v3[1], - z: v3[2], - }; - physicsWorld = new RAPIER.World(gravity); - - console.log(physicsWorld); - } - - getWorld(args) { - if (args.PROPERTY === "log") { - console.log(physicsWorld); - return "logged"; - } - return JSON.stringify(physicsWorld[args.PROPERTY]); - } - - setRB(args) { - let value = args.VALUE; - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; - let object = getObject(args.OBJECT); - object.rigidBody[args.PROPERTY](value); - } - setC(args) { - let value = args.VALUE; - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; - let object = getObject(args.OBJECT); - object.collider[args.PROPERTY](value); - } - - getRB(args) { - let object = getObject(args.OBJECT); - return JSON.stringify(object.rigidBody[args.PROPERTY]()); - } - getC(args) { - let object = getObject(args.OBJECT); - return JSON.stringify(object.collider[args.PROPERTY]()); - } - - lockObjectAxis(args) { - let object = getObject(args.OBJECT); - const x = !JSON.parse(args.X); - const y = !JSON.parse(args.Y); - const z = !JSON.parse(args.Z); - object.rigidBody[args.PROPERTY](x, y, z, true); //changes is xyz, wake up - } - - objectPhysics(args) { - let object = getObject(args.OBJECT); - object.physics = JSON.parse(args.state); - - if (JSON.parse(args.state)) { - //if already exists delete: - if (object.rigidBody) { - physicsWorld.removeRigidBody(object.rigidBody); - object.rigidBody = null; - object.collider = null; - } - /*asing a rigidbody and collider to object and add them to physicsWorld*/ - let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() - .setTranslation(object.position.x, object.position.y, object.position.z) - .setRotation({ - w: object.quaternion._w, - x: object.quaternion._x, - y: object.quaternion._y, - z: object.quaternion._z, - }); - - let colliderDesc; - switch (args.collider) { - case "cuboid": - colliderDesc = createCuboidCollider(object); - break; - case "ball": - colliderDesc = createBallCollider(object); - break; - case "convexHull": - colliderDesc = createConvexHullCollider(object); - break; - case "trimesh": - colliderDesc = TriMesh(object); - break; - } - colliderDesc - .setSensor(JSON.parse(args.state2)) - .setMass(args.mass) - .setDensity(args.density) - .setFriction(args.friction); - - let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc); - let collider = physicsWorld.createCollider(colliderDesc, rigidBody); - - object.rigidBody = rigidBody; - object.collider = collider; - } else { - /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ - physicsWorld.removeRigidBody(object.rigidBody); - object.rigidBody = null; - object.collider = null; - } - } - - enableCCD(args) { - let object = getObject(args.OBJECT); - if (object.physics) { - let rigidBody = object.rigidBody; - rigidBody.enableCcd(JSON.parse(args.state)); - } - } - - addForce(args) { - let object = getObject(args.OBJECT); - const vector = JSON.parse(args.VALUE).map(Number); - - let force = new THREE.Vector3(vector[0], vector[1], vector[2]); - if (args.SPACE === "local") { - force.applyQuaternion(object.quaternion); - } - - object.rigidBody[args.PROPERTY](force, true); - } - - resetForces(args) { - rigidBody[args.PROPERTY](true); - } - - sensorSingle(args) { - const sensor = getObject(args.SENSOR); - - let object = getObject(args.OBJECT); - - let touching = false; - physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { - if (otherCollider === object.collider) touching = true; - }); - - return touching; - } - - sensorAll(args) { - const sensor = getObject(args.SENSOR); - - const touchedObjects = []; - - // loop thruogh every collider touching sensor - physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { - // find owner of collider - const otherObject = scene.children.find((o) => o.collider === otherCollider); - console.log(otherCollider); - if (otherObject) touchedObjects.push(otherObject.name); - }); - - return JSON.stringify(touchedObjects); - } - } - Scratch.extensions.register(new RapierPhysics()); - - //Thanks to the PointerLock extension of Turbowarp - const mouse = vm.runtime.ioDevices.mouse; - let isLocked = false; - let isPointerLockEnabled = false; - - let rect = threeRenderer.domElement.getBoundingClientRect(); - document.addEventListener("resize", () => { - rect = threeRenderer.domElement.getBoundingClientRect(); - }); - - const postMouseData = (e, isDown) => { - const { - movementX, - movementY - } = e; - const { - width, - height - } = rect; - const x = mouse._clientX + movementX; - const y = mouse._clientY - movementY; - mouse._clientX = x; - mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); - mouse._clientY = y; - mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); - if (typeof isDown === "boolean") { - const data = { - button: e.button, - isDown, - }; - originalPostIOData(data); - } - }; - - const mouseDevice = vm.runtime.ioDevices.mouse; - const originalPostIOData = mouseDevice.postData.bind(mouseDevice); - mouseDevice.postData = (data) => { - if (!isPointerLockEnabled) { - return originalPostIOData(data); - } - }; - - document.addEventListener( - "mousedown", - (e) => { - // @ts-expect-error - if (threeRenderer.domElement.contains(e.target)) { - if (isLocked) { - postMouseData(e, true); - } else if (isPointerLockEnabled) { - threeRenderer.domElement.requestPointerLock(); - } - } - }, - true - ); - document.addEventListener( - "mouseup", - (e) => { - if (isLocked) { - postMouseData(e, false); - // @ts-expect-error - } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { - threeRenderer.domElement.requestPointerLock(); - } - }, - true - ); - document.addEventListener( - "mousemove", - (e) => { - if (isLocked) { - postMouseData(e); - } - }, - true - ); - - document.addEventListener("pointerlockchange", () => { - isLocked = document.pointerLockElement === threeRenderer.domElement; - }); - document.addEventListener("pointerlockerror", (e) => { - console.error("Pointer lock error", e); - }); - - const oldStep = vm.runtime._step; - vm.runtime._step = function(...args) { - const ret = oldStep.call(this, ...args); - if (isPointerLockEnabled) { - const { - width, - height - } = rect; - mouse._clientX = width / 2; - mouse._clientY = height / 2; - mouse._scratchX = 0; - mouse._scratchY = 0; - } - return ret; - }; - - vm.runtime.on("PROJECT_LOADED", () => { - isPointerLockEnabled = false; - if (isLocked) { - document.exitPointerLock(); - } - }); - - class Pointerlock { - getInfo() { - return { - id: "threepointerlockmod", - name: "Pointerlock for Extra 3D", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "setLocked", - blockType: Scratch.BlockType.COMMAND, - text: "set pointer lock [enabled]", - arguments: { - enabled: { - type: Scratch.ArgumentType.STRING, - defaultValue: "true", - menu: "enabled", - }, - }, - }, - { - opcode: "isLocked", - blockType: Scratch.BlockType.BOOLEAN, - text: "pointer locked?", - }, - ], - menus: { - enabled: { - acceptReporters: true, - items: [{ - text: "enabled", - value: "true", - }, - { - text: "disabled", - value: "false", - }, - ], - }, - }, - }; - } - - setLocked(args) { - isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; - if (!isPointerLockEnabled && isLocked) { - document.exitPointerLock(); - } - } - - isLocked() { - return isLocked; - } - } - Scratch.extensions.register(new Pointerlock()); - }); -})(Scratch); diff --git a/threejsD_BACKUP_13852.js b/threejsD_BACKUP_13852.js deleted file mode 100644 index d1db442..0000000 --- a/threejsD_BACKUP_13852.js +++ /dev/null @@ -1,5029 +0,0 @@ -/* jshint esversion: 11 */ -// Name: Extra 3D -// ID: threejsExtension -// Description: Use three js inside Turbowarp! A 3D graphics library. -// By: Civero -// License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors - -(function(Scratch) { - "use strict"; - - if (!Scratch.extensions.unsandboxed) { - throw new Error("Three-D extension must run unsandboxed"); - } - - if (Scratch.vm.runtime.isPackaged) { - alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); - return; - } - //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return - - const vm = Scratch.vm; - const runtime = vm.runtime; - const renderer = Scratch.renderer; - const canvas = renderer.canvas; - const Cast = Scratch.Cast; - const menuIconURI = - "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; - - let alerts = false; - console.log("alerts are " + (alerts ? "enabled" : "disabled")); - - let isMouseDown = { - left: false, - middle: false, - right: false, - }; - let prevMouse = { - left: false, - middle: false, - right: false, - }; - - let lastWidth = 0; - let lastHeight = 0; - - let THREE; - let clock; - let running; - let loopId; - //Addons - let GLTFLoader; - let gltf; - let OrbitControls; - let controls; - let BufferGeometryUtils; - let TextGeometry; - let fontLoad; - //Physics - let RAPIER; - let physicsWorld; - - let threeRenderer; - let scene; - let camera; - let eulerOrder = "YXZ"; - - let composer; - let passes = {}; - let customEffects = []; - let renderTargets = {}; - - let materials = {}; - let geometries = {}; - let lights = {}; - let models = {}; - - let assets = { - //should i place materials, geometries; inside too? - textures: {}, - colors: {}, - fogs: {}, - curves: {}, - renderTargets: {}, //not the same as the global one! this one only stores textures - }; - - let raycastResult = []; - - function resetor(level) { - camera = undefined; - composer.reset(); - - passes = {}; - customEffects = []; - renderTargets = {}; - - materials = {}; - geometries = {}; - lights = {}; - models = {}; - - if (level > 0) { - assets = { - textures: {}, - colors: {}, - fogs: {}, - curves: {}, - renderTargets: {}, - }; - } - - updateComposers(); - } - - //utility - function vector3ToString(prop) { - if (!prop) return "0,0,0"; - - const x = - typeof prop.x === "number" ? - prop.x : - typeof prop._x === "number" ? - prop._x : - JSON.stringify(prop).includes("X") ? - prop : - 0; - const y = typeof prop.y === "number" ? prop.y : typeof prop.y === "number" ? prop._y : 0; - const z = typeof prop.z === "number" ? prop.z : typeof prop.z === "number" ? prop.z : 0; - - return [x, y, z]; - } - - //objects - function createObject(name, content, parentName) { - let object = getObject(name, true); - if (object) { - removeObject(name); - alerts ? alert(name + " already exsisted, will replace!") : null; - } - content.name = name; - content.rotation._order = eulerOrder; - parentName === scene.name ? (object = scene) : (object = getObject(parentName)); - content.physics = false; - - object.add(content); - } - - function removeObject(name) { - let object = getObject(name); - if (!object) return; - - scene.remove(object); - - if (object.rigidBody) { - physicsWorld.removeCollider(object.collider, true); - physicsWorld.removeRigidBody(object.rigidBody, true); - object.rigidBody = null; - object.collider = null; - } - if (object.isLight) { - delete lights[name]; - } - } - - function getObject(name, isNew) { - let object = null; - if (!scene) { - alerts ? alert("Can not get " + name + ". Create a scene first!") : null; - return; - } - object = scene.getObjectByName(name); - if (!object && !isNew) { - alerts ? alert(name + " does not exist! Add it to scene") : null; - return; - } - return object; - } - - //materials - function encodeCostume(name) { - if (name.startsWith("data:image/")) return name; - return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); - } - - function setTexutre(texture, mode, style, x, y) { - texture.colorSpace = THREE.SRGBColorSpace; - - if (mode === "Pixelate") { - texture.minFilter = THREE.NearestFilter; - texture.magFilter = THREE.NearestFilter; - } else { - //Blur - texture.minFilter = THREE.NearestMipmapLinearFilter; - texture.magFilter = THREE.NearestMipmapLinearFilter; - } - - if (style === "Repeat") { - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; - texture.repeat.set(x, y); - } - - texture.generateMipmaps = true; - } - async function resizeImageToSquare(uri, size = 256) { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext("2d"); - - // clear + draw image scaled to fit canvas - ctx.clearRect(0, 0, size, size); - ctx.drawImage(img, 0, 0, size, size); - - resolve(canvas.toDataURL()); // return normalized Data URI - //delete canvas? - }; - img.src = uri; - }); - } - //light - function updateShadowFrustum(light, focusPos) { - if (light.type !== "DirectionalLight") return; - - // Frustum Size - Increase this value to cover a larger area. - const d = 50; - - // Update Orthographic Shadow Camera Frustum - const shadowCamera = light.shadow.camera; - - // Set the width/height of the frustum - shadowCamera.left = -d; - shadowCamera.right = d; - shadowCamera.top = d; - shadowCamera.bottom = -d; - - // Determine ranges - shadowCamera.near = 0.1; - shadowCamera.far = 500; - - // Position the Light and its Target - light.target.position.copy(focusPos); - const direction = light.position.clone().sub(light.target.position).normalize(); - light.position.copy(focusPos.clone().add(direction.multiplyScalar(100))); - - // Ensure matrices are updated. - light.target.updateMatrixWorld(); - light.shadow.camera.updateProjectionMatrix(); - light.shadow.needsUpdate = true; - } - //composer - function updateComposers() { - if (!camera || !scene) return; // nothing to do yet - - // always recreate the RenderPass to point to the current scene/camera - passes["Render"] = new RenderPass(scene, camera); - - // ensure composer has a RenderPass as the first pass - const hasRender = composer.passes.some((p) => p && p.scene); - if (!hasRender) composer.addPass(passes["Render"]); - else { - // if composer already has one, replace it so it references current scene/camera - const idx = composer.passes.findIndex((p) => p && p.scene); - composer.passes[idx] = passes["Render"]; - } - } - //utility - function getMouseNDC(event) { - // Use threeRenderer.domElement for correct offset - const rect = threeRenderer.domElement.getBoundingClientRect(); - const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - return [x, y]; - } - - function checkCanvasSize() { - const { - width, - height - } = canvas; - if (width !== lastWidth || height !== lastHeight) { - lastWidth = width; - lastHeight = height; - resize(); - } - requestAnimationFrame(checkCanvasSize); //rerun next frame - } - //physics - function computeWorldBoundingBox(mesh) { - // Create a Box3 in world coordinates - const box = new THREE.Box3().setFromObject(mesh); - const size = new THREE.Vector3(); - box.getSize(size); - const center = new THREE.Vector3(); - box.getCenter(center); - return { - size, - center, - }; - } - - function createCuboidCollider(mesh) { - const { - size - } = computeWorldBoundingBox(mesh); - const collider = RAPIER.ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2); - return collider; - } - - function createBallCollider(mesh) { - const { - size - } = computeWorldBoundingBox(mesh); - // radius = 1/2 of the largest verticie - const radius = Math.max(size.x, size.y, size.z) / 2; - const collider = RAPIER.ColliderDesc.ball(radius); - return collider; //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) - } - - function createConvexHullCollider(mesh) { - mesh.updateWorldMatrix(true, false); - - const position = mesh.geometry.attributes.position; - const vertices = []; - const vertex = new THREE.Vector3(); - - // Matrix for scale only - const scaleMatrix = new THREE.Matrix4().makeScale(mesh.scale.x, mesh.scale.y, mesh.scale.z); - - for (let i = 0; i < position.count; i++) { - vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); - vertices.push(vertex.x, vertex.y, vertex.z); - } - - const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); - return collider; - } - - function TriMesh(mesh) { - // Get the positions array (from your geoPoints function) - const positions = mesh.geometry.attributes.position.array; - const numVertices = positions.length / 3; - - // Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] - const indices = Array.from({ - length: numVertices, - }, - (_, i) => i - ); - - const collider = RAPIER.ColliderDesc.trimesh(positions, new Uint32Array(indices)); - - return collider; - } - - function getModel(model, name) { - const file = runtime - .getTargetForStage() - .getSounds() - .find((c) => c.name === model); - if (!file) return; - - return new Promise((resolve, reject) => { - gltf.parse( - file.asset.data.buffer, - "", - (gltf) => { - const root = gltf.scene; - root.traverse((child) => { - if (child.isMesh) { - child.castShadow = true; - child.receiveShadow = true; - } - }); - - const mixer = new THREE.AnimationMixer(root); - const actions = {}; - gltf.animations.forEach((clip) => { - const act = mixer.clipAction(clip); - act.clampWhenFinished = true; - actions[clip.name] = act; - }); - - models[name] = { - root, - mixer, - actions, - }; - resolve(root); - }, - (error) => { - console.error("Error parsing GLB model:", error); - reject(error); - } - ); - }); - } - async function openFileExplorer(format) { - return new Promise((resolve) => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = format; - input.multiple = false; - input.onchange = () => { - resolve(input.files); - input.remove(); - }; - input.click(); - }); - } - - function getMeshesUsingTexture(scene, targetTexture) { - const meshes = []; - - scene.traverse((object) => { - if (object.material) { - const materials = Array.isArray(object.material) ? object.material : [object.material]; - for (const material of materials) { - if (material.map === targetTexture) { - meshes.push(object); - break; - } - } - } - }); - - return meshes; - } - - function getAsset(path) { - if (typeof path == "string") { - //string? - if (path.includes("/")) { - //has the /? - const value = path.split("/"); - console.log(value[0], value[1]); - return assets[value[0]][value[1]]; - } - } - - return JSON.parse(path); //boolean or number - } - - let mouseNDC = [0, 0]; - //loops/init - function stopLoop() { - if (!running) return; - running = false; - - if (loopId) { - cancelAnimationFrame(loopId); - loopId = null; - if (threeRenderer) threeRenderer.clear(); - } - } - async function load() { - if (!THREE) { - // @ts-ignore -<<<<<<< HEAD - THREE = await import("https://esm.sh/three@0.180.0") - window._THREE_ = THREE -======= - THREE = await import("https://esm.sh/three@0.180.0"); ->>>>>>> e4a038b (Update threejsD.js) - //Addons - GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js"); - OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js"); - BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js"); - TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js"); - const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js"); - fontLoad = new FontLoader.FontLoader(); - - const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8"); - const { - EffectComposer, - EffectPass, - RenderPass, - - Effect, - BloomEffect, - GodRaysEffect, - DotScreenEffect, - DepthOfFieldEffect, - - BlendFunction, - } = POSTPROCESSING; - //so i can use them later as global - window.EffectComposer = EffectComposer; - window.EffectPass = EffectPass; - window.RenderPass = RenderPass; - window.Effect = Effect; - window.BloomEffect = BloomEffect; - window.GodRaysEffect = GodRaysEffect; - window.DotScreenEffect = DotScreenEffect; - window.DepthOfFieldEffect = DepthOfFieldEffect; - window.BlendFunction = BlendFunction; - - RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0"); - await RAPIER.init(); - - threeRenderer = new THREE.WebGLRenderer({ - powerPreference: "high-performance", - antialias: false, - stencil: false, - depth: true, - }); - threeRenderer.setPixelRatio(window.devicePixelRatio); - threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors - threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) - //threeRenderer.toneMappingExposure = 1.0 //(test) - - threeRenderer.shadowMap.enabled = true; - threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap; // (optional) - threeRenderer.domElement.style.pointerEvents = "auto"; //will disable turbowarp mouse events, but enable threejs's - - gltf = new GLTFLoader.GLTFLoader(); - clock = new THREE.Clock(); - - // Example: create a composer - composer = new EffectComposer(threeRenderer, { - frameBufferType: THREE.HalfFloatType, - }); - - renderer.addOverlay(threeRenderer.domElement, "manual"); - renderer.addOverlay(canvas, "manual"); - renderer.setBackgroundColor(1, 1, 1, 0); - - resize(); - - window.addEventListener("mousedown", (e) => { - if (e.button === 0) isMouseDown.left = true; - if (e.button === 1) isMouseDown.middle = true; - if (e.button === 2) isMouseDown.right = true; - }); - window.addEventListener("mouseup", (e) => { - if (e.button === 0) isMouseDown.left = false; - prevMouse.left = false; - if (e.button === 1) isMouseDown.middle = false; - prevMouse.middle = false; - if (e.button === 2) isMouseDown.right = false; - prevMouse.right = false; - }); - // prevent contextmenu on right click - threeRenderer.domElement.addEventListener("contextmenu", (e) => e.preventDefault()); - - threeRenderer.domElement.addEventListener("mousemove", (event) => { - mouseNDC = getMouseNDC(event); - }); - - running = false; - load(); - -<<<<<<< HEAD - startRenderLoop() - runtime.on('PROJECT_START', () => startRenderLoop()) - runtime.on('PROJECT_STOP_ALL', () => stopLoop()) - runtime.on('STAGE_SIZE_CHANGED', () => {requestAnimationFrame(() => resize())}) - checkCanvasSize() -======= - startRenderLoop(); - runtime.on("PROJECT_START", () => startRenderLoop()); - runtime.on("PROJECT_STOP_ALL", () => stopLoop()); - runtime.on("STAGE_SIZE_CHANGED", () => { - requestAnimationFrame(() => resize()); - }); - //if (!runtime.isPackaged) checkCanvasSize() //only in editor ->>>>>>> e4a038b (Update threejsD.js) - } - } - - function startRenderLoop() { - if (running) return; - running = true; - - const loop = () => { - if (!running) return; - //RAPIER - if (physicsWorld && scene) { - physicsWorld.step(); - - scene.children.forEach((obj) => { - if (!obj.isMesh || !obj.physics) return; - if (obj.rigidBody) { - obj.position.copy(obj.rigidBody.translation()); - obj.quaternion.copy(obj.rigidBody.rotation()); - } - }); - } - if (scene && camera) { - if (controls) controls.update(); - - const delta = clock.getDelta(); - Object.values(models).forEach((model) => { - if (model) model.mixer.update(delta); - }); - - Object.values(lights).forEach((light) => updateShadowFrustum(light, camera.position)); - - //update custom effects time - customEffects.forEach((e) => { - if (e.uniforms.get("time")) { - e.uniforms.get("time").value += delta; - } - }); - Object.values(renderTargets).forEach((t) => { - if (t.camera.type == "PerspectiveCamera") { - t.camera.aspect = t.target.width / t.target.height; - t.camera.updateProjectionMatrix(); - } - // get meshes using the texture associated with this target - const displayMeshes = getMeshesUsingTexture(scene, t.target.texture); - - displayMeshes.forEach((mesh) => { - mesh.visible = false; - }); - - if (t.camera.type == "PerspectiveCamera") { - threeRenderer.setRenderTarget(t.target); - threeRenderer.clear(true, true, true); - threeRenderer.render(scene, t.camera); - } else { - t.target.clear(threeRenderer); - t.camera.update(threeRenderer, scene); //cubeCamera - } - - displayMeshes.forEach((mesh) => { - mesh.visible = true; - }); - }); - - camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height; - camera.updateProjectionMatrix(); - threeRenderer.setRenderTarget(null); - composer.render(delta); - } - - loopId = requestAnimationFrame(loop); - }; - - loopId = requestAnimationFrame(loop); - } - - function resize() { - const w = canvas.width; - const h = canvas.height; - - threeRenderer.setSize(w, h); - composer.setSize(w, h); - customEffects.forEach((e) => { - if (e.uniforms.get("resolution")) { - e.uniforms.get("resolution").value.set(w, h); - } - }); - - if (camera) { - camera.aspect = w / h; - camera.updateProjectionMatrix(); - } - } - //wait until all packages are loaded - Promise.resolve(load()).then(() => { - class threejsExtension { - getInfo() { - return { - id: "threejsExtension", - name: "Extra 3D", - color1: "#222222", - color2: "#222222", - color3: "#11cc99", - menuIconURI, - blockIconURI: menuIconURI, - - blocks: [{ - blockType: Scratch.BlockType.BUTTON, - text: "Show Docs", - func: "openDocs", - }, - { - blockType: Scratch.BlockType.BUTTON, - text: "Toggle Alerts", - func: "alerts", - }, - ], - menus: {}, - }; - } - openDocs() { - open("https://civ3ro.github.io/extensions/Documentation/"); - } - alerts() { - alerts = !alerts; - alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!"); - } - } - Scratch.extensions.register(new threejsExtension()); - - class ThreeRenderer { - getInfo() { - return { - id: "threeRenderer", - name: "Three Renderer", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "setRendererRatio", - blockType: Scratch.BlockType.COMMAND, - text: "set Pixel Ratio to [VALUE]", - arguments: { - VALUE: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "1", - }, - }, - }, - { - opcode: "eulerOrder", - blockType: Scratch.BlockType.COMMAND, - text: "set euler order to [VALUE]", - arguments: { - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "YXZ", - }, - }, - }, - ], - menus: {}, - }; - } - - setRendererRatio(args) { - threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE); - } - eulerOrder(args) { - eulerOrder = args.VALUE; - console.log("euler order set to", eulerOrder); - } - } - Scratch.extensions.register(new ThreeRenderer()); - - class ThreeScene { - constructor() { - this.THREE = THREE; - this.scenes = {}; - } - - getInfo() { - return { - id: "threeScene", - name: "Three Scene", - color1: "#4638c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "newScene", - blockType: Scratch.BlockType.COMMAND, - text: "new Scene [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - }, - }, - - { - opcode: "setSceneProperty", - blockType: Scratch.BlockType.COMMAND, - text: "set Scene [PROPERTY] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "sceneProperties", - defaultValue: "background", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "new Color()", - exemptFromNormalization: true, - }, - }, - }, - "---", - { - opcode: "getSceneObjects", - blockType: Scratch.BlockType.REPORTER, - text: "get Scene [THING]", - arguments: { - THING: { - type: Scratch.ArgumentType.STRING, - menu: "sceneThings", - }, - }, - }, - { - opcode: "reset", - blockType: Scratch.BlockType.COMMAND, - text: "Reset Everything", - }, - ], - menus: { - sceneProperties: { - acceptReporters: false, - items: [{ - text: "Background", - value: "background", - }, - { - text: "Background Blurriness", - value: "backgroundBlurriness", - }, - { - text: "Background Intensity", - value: "backgroundIntensity", - }, - { - text: "Background Rotation", - value: "backgroundRotation", - }, - { - text: "Environment", - value: "environment", - }, - { - text: "Environment Intensity", - value: "environmentIntensity", - }, - { - text: "Environment Rotation", - value: "environmentRotation", - }, - { - text: "Fog", - value: "fog", - }, - ], - }, - sceneThings: { - acceptReporters: false, - items: ["Objects", "Materials", "Geometries", "Lights", "Scene Properties", "Other assets"], - }, - }, - }; - } - - newScene(args) { - scene = new THREE.Scene(); - scene.name = args.NAME; - scene.background = new THREE.Color("#222"); - //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - this.scenes = { - ...this.scenes, - ...scene, - }; - resetor(0); - } - - reset() { - resetor(1); - } - - async setSceneProperty(args) { - const property = args.PROPERTY; - const value = getAsset(args.VALUE); - - scene[property] = value; - } - getSceneObjects(args) { - const names = []; - if (args.THING === "Objects") { - scene.traverse((obj) => { - if (obj.name) names.push(obj.name); //if it has a name, add to list! - }); - } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); - else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); - else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); - else if (args.THING === "Scene Properties") { - console.log(scene); - return "check console"; - } else if (args.THING === "Other assets") return JSON.stringify(assets); - - return JSON.stringify(names); // if objects - } - } - Scratch.extensions.register(new ThreeScene()); - - class ThreeCameras { - getInfo() { - return { - id: "threeCameras", - name: "Three Cameras", - color1: "#38c59bff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "addCamera", - blockType: Scratch.BlockType.COMMAND, - text: "add camera [TYPE] [CAMERA] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "cameraTypes", - }, - }, - }, - { - opcode: "setCamera", - blockType: Scratch.BlockType.COMMAND, - text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "cameraProperties", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "0.1", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "getCamera", - blockType: Scratch.BlockType.REPORTER, - text: "get camera [PROPERTY] of [CAMERA]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "cameraProperties", - }, - }, - }, - "---", - { - opcode: "renderSceneCamera", - blockType: Scratch.BlockType.COMMAND, - text: "set rendering camera to [CAMERA]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - }, - }, - "---", - { - opcode: "cubeCamera", - blockType: Scratch.BlockType.COMMAND, - text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "cubeCamera", - }, - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - "---", - { - opcode: "renderTarget", - blockType: Scratch.BlockType.COMMAND, - text: "set a RenderTarget: [RT] for camera [CAMERA]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - { - opcode: "sizeTarget", - blockType: Scratch.BlockType.COMMAND, - text: "set RenderTarget [RT] size to [W] [H]", - arguments: { - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - W: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 480, - }, - H: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 360, - }, - }, - }, - { - opcode: "getTarget", - blockType: Scratch.BlockType.REPORTER, - text: "get RenderTarget: [RT] texture", - arguments: { - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - { - opcode: "removeTarget", - blockType: Scratch.BlockType.COMMAND, - text: "remove RenderTarget: [RT]", - arguments: { - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - ], - menus: { - cameraTypes: { - acceptReporters: false, - items: [{ - text: "Perspective", - value: "PerspectiveCamera", - }, ], - }, - cameraProperties: { - acceptReporters: false, - items: [{ - text: "Near", - value: "near", - }, - { - text: "Far", - value: "far", - }, - { - text: "FOV", - value: "fov", - }, - { - text: "Focus (nothing...)", - value: "focus", - }, - { - text: "Zoom", - value: "zoom", - }, - ], - }, - }, - }; - } - addCamera(args) { - let v2 = new THREE.Vector2(); - threeRenderer.getSize(v2); - const object = new THREE.PerspectiveCamera(90, v2.x / v2.y); - object.position.z = 3; - - createObject(args.CAMERA, object, args.GROUP); - } - setCamera(args) { - let object = getObject(args.CAMERA); - object[args.PROPERTY] = args.VALUE; - object.updateProjectionMatrix(); - } - getCamera(args) { - let object = getObject(args.CAMERA); - const value = JSON.stringify(object[args.PROPERTY]); - return value; - } - renderSceneCamera(args) { - let object = getObject(args.CAMERA); - if (!object) return; - camera = object; - //reset composer, else it does not update. - composer.passes = []; - passes = {}; - customEffects = []; - updateComposers(); - } - - cubeCamera(args) { - // Create cube render target - const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, { - generateMipmaps: true, - }); - // Create cube camera - const cubeCamera = new THREE.CubeCamera(0.1, 500, cubeRenderTarget); - createObject(args.CAMERA, cubeCamera, args.GROUP); - - renderTargets[args.RT] = { - target: cubeRenderTarget, - camera: cubeCamera, - }; - assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture; - } - - renderTarget(args) { - let object = getObject(args.CAMERA); - const renderTarget = new THREE.WebGLRenderTarget(360, 360, { - generateMipmaps: false, - }); - - renderTargets[args.RT] = { - target: renderTarget, - camera: object, - }; - assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; - } - sizeTarget(args) { - renderTargets[args.RT].target.setSize(args.W, args.H); - } - getTarget(args) { - const t = renderTargets[args.RT].target.texture; - console.log(t, renderTargets[args.RT]); - return `renderTargets/${t.uuid}`; - } - removeTarget(args) { - delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; - renderTargets[args.RT].target.dispose(); - delete renderTargets[args.RT]; - } - } - Scratch.extensions.register(new ThreeCameras()); - - class ThreeObjects { - getInfo() { - return { - id: "threeObjects", - name: "Three Objects", - color1: "#38c567ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "addObject", - blockType: Scratch.BlockType.COMMAND, - text: "add object [OBJECT3D] [TYPE] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "objectTypes", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - { - opcode: "cloneObject", - blockType: Scratch.BlockType.COMMAND, - text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myClone", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "setObject", - blockType: Scratch.BlockType.COMMAND, - text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", - arguments: { - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectProperties", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - }, - }, - { - opcode: "getObject", - blockType: Scratch.BlockType.REPORTER, - text: "get [PROPERTY] of object [OBJECT3D]", - arguments: { - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectProperties", - }, - }, - }, - { - opcode: "objectE", - blockType: Scratch.BlockType.BOOLEAN, - text: "is there an object [NAME]?", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "removeObject", - blockType: Scratch.BlockType.COMMAND, - text: "remove object [OBJECT3D] from scene", - arguments: { - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: " ↳ Transforms", - }, - { - opcode: "setObjectV3", - extensions: ["colours_motion"], - blockType: Scratch.BlockType.COMMAND, - text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectVector3", - defaultValue: "position", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - }, - }, - //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, - //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - { - opcode: "getObjectV3", - extensions: ["colours_motion"], - blockType: Scratch.BlockType.REPORTER, - text: "get [PROPERTY] of [OBJECT3D]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectVector3", - defaultValue: "position", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: "↳ Materials", - }, - { - opcode: "newMaterial", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "new material [NAME] [TYPE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "materialTypes", - defaultValue: "MeshStandardMaterial", - }, - }, - }, - { - opcode: "materialE", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.BOOLEAN, - text: "is there a material [NAME]?", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - }, - }, - { - opcode: "removeMaterial", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "remove material [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - }, - }, - { - opcode: "setMaterial", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "set material [PROPERTY] of [NAME] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "materialProperties", - defaultValue: "color", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "new Color()", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "setBlending", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "set material [NAME] blending to [VALUE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - }, - }, - }, - { - opcode: "setDepth", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "set material [NAME] depth to [VALUE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - menu: "depthModes", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: "↳ Geometries", - }, - { - opcode: "newGeometry", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "new geometry [NAME] [TYPE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "geometryTypes", - defaultValue: "BoxGeometry", - }, - }, - }, - { - opcode: "geometryE", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.BOOLEAN, - text: "is there a geometry [NAME]?", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - }, - }, - { - opcode: "removeGeometry", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "remove geometry [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - }, - }, - "---", - { - opcode: "newGeo", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "new empty geometry [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[points]", - }, - }, - }, - { - opcode: "geoPoints", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "set geometry [NAME] vertex points to [POINTS]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[points]", - }, - }, - }, - { - opcode: "geoUVs", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "set geometry [NAME] UVs to [POINTS]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[UVs]", - }, - }, - }, - "---", - { - opcode: "splines", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "create spline [NAME] from curve [CURVE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySpline", - }, - CURVE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[curve]", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "splineModel", - extensions: ["colours_operators"], - blockType: Scratch.BlockType.COMMAND, - text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySpline", - }, - MODEL: { - type: Scratch.ArgumentType.STRING, - menu: "modelsList", - }, - CURVE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[curve]", - exemptFromNormalization: true, - }, - SPACING: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - "---", - { - blockType: Scratch.BlockType.BUTTON, - text: "Convert font to JSON", - func: "openConv", - }, - { - blockType: Scratch.BlockType.BUTTON, - text: "Load JSON font file", - func: "loadFont", - }, - { - opcode: "text", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myText", - }, - TEXT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "C-369", - }, - FONT: { - type: Scratch.ArgumentType.STRING, - menu: "fonts", - }, - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - D: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.1, - }, - CS: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 6, - }, - }, - }, - ], - menus: { - objectVector3: { - acceptReporters: false, - items: [{ - text: "Positon", - value: "position", - }, - { - text: "Rotation", - value: "rotation", - }, - { - text: "Scale", - value: "scale", - }, - { - text: "Facing Direction (.up)", - value: "up", - }, - ], - }, - objectProperties: { - acceptReporters: false, - items: [{ - text: "Geometry", - value: "geometry", - }, - { - text: "Material", - value: "material", - }, - { - text: "Visible (true/false)", - value: "visible", - }, - ], - }, - objectTypes: { - acceptReporters: false, - items: [{ - text: "Mesh", - value: "Mesh", - }, - { - text: "Sprite", - value: "Sprite", - }, - { - text: "Points", - value: "Points", - }, - { - text: "Line", - value: "Line", - }, - { - text: "Group", - value: "Group", - }, - ], - }, - XYZ: { - acceptReporters: false, - items: [{ - text: "X", - value: "x", - }, - { - text: "Y", - value: "y", - }, - { - text: "Z", - value: "z", - }, - ], - }, - materialProperties: { - acceptReporters: false, - items: [ - "|GENERAL| <-- not a property", - { - text: "Color", - value: "color", - }, - { - text: "Map", - value: "map", - }, - { - text: "Opacity", - value: "opacity", - }, - { - text: "Transparent", - value: "transparent", - }, - { - text: "Alpha Map", - value: "alphaMap", - }, - { - text: "Alpha Test", - value: "alphaTest", - }, - { - text: "Depth Test", - value: "depthTest", - }, - { - text: "Depth Write", - value: "depthWrite", - }, - { - text: "Color Write", - value: "colorWrite", - }, - { - text: "Side", - value: "side", - }, - { - text: "Visible", - value: "visible", - }, - /* - { text: "Blending", value: "blending" }, - { text: "Blend Src", value: "blendSrc" }, - { text: "Blend Dst", value: "blendDst" }, - { text: "Blend Equation", value: "blendEquation" }, - { text: "Blend Src Alpha", value: "blendSrcAlpha" }, - { text: "Blend Dst Alpha", value: "blendDstAlpha" }, - { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ - { - text: "Blend Aplha", - value: "blendAplha", - }, - { - text: "Blend Color", - value: "blendColor", - }, - { - text: "Alpha Hash", - value: "alphaHash", - }, - { - text: "Premultiplied Alpha", - value: "premultipliedAlpha", - }, - - { - text: "Tone Mapped", - value: "toneMapped", - }, - { - text: "Fog", - value: "fog", - }, - { - text: "Flat Shading", - value: "flatShading", - }, - - "|MESH Standard / Physical| <-- not a property", - { - text: "Metalness", - value: "metalness", - }, - { - text: "Metalness Map", - value: "metalnessMap", - }, - { - text: "Roughness", - value: "roughness", - }, - { - text: "Reflectivity", - value: "reflectivity", - }, - { - text: "Roughness Map", - value: "roughnessMap", - }, - { - text: "Emissive", - value: "emissive", - }, - { - text: "Emissive Intensity", - value: "emissiveIntensity", - }, - { - text: "Emissive Map", - value: "emissiveMap", - }, - { - text: "Env Map", - value: "envMap", - }, - { - text: "Env Map Intensity", - value: "envMapIntensity", - }, - { - text: "Env Map Rotation", - value: "envMapRotation", - }, - { - text: "Ior", - value: "ior", - }, - { - text: "Refraction Ratio", - value: "refractionRatio", - }, - { - text: "Clearcoat", - value: "clearcoat", - }, - { - text: "Clearcoat Map", - value: "clearcoatMap", - }, - { - text: "Clearcoat Roughness", - value: "clearcoatRoughness", - }, - { - text: "Clearcoat Roughness Map", - value: "clearcoatRoughnessMap", - }, - { - text: "Dispersion", - value: "dispersion", - }, - { - text: "Sheen", - value: "sheen", - }, - { - text: "Sheen Color", - value: "sheenColor", - }, - { - text: "Sheen Color Map", - value: "sheenColorMap", - }, - { - text: "Sheen Roughness", - value: "sheenRoughness", - }, - { - text: "Sheen Roughness Map", - value: "sheenRoughnessMap", - }, - { - text: "Specular Color", - value: "specularColor", - }, - { - text: "Specular Color Map", - value: "specularColorMap", - }, - { - text: "Specular Intensity", - value: "specularIntensity", - }, - { - text: "Specular Intensity Map", - value: "specularIntensityMap", - }, - { - text: "Transmission", - value: "transmission", - }, - { - text: "Transmission Map", - value: "transmissionMap", - }, - { - text: "Thickness", - value: "thickness", - }, - { - text: "Thickness Map", - value: "thicknessMap", - }, - { - text: "Anisotropy", - value: "anisotropy", - }, - { - text: "Anisotropy Map", - value: "anisotropyMap", - }, - { - text: "Anisotropy Rotation", - value: "anisotropyRotation", - }, - { - text: "Attenuation Distance", - value: "attenuationDistance", - }, - { - text: "Attenuation Color", - value: "attenuationColor", - }, - { - text: "Thickness", - value: "thickness", - }, - { - text: "Iridescence", - value: "iridescence", - }, - { - text: "Iridescence Ior", - value: "iridescenceIOR", - }, - { - text: "Iridescence Map", - value: "iridescenceMap", - }, - { - text: "Iridescence Thickness Range", - value: "iridescenceThicknessRange", - }, - - "|MESH Displacement / Normal / Bump| <-- not a property", - { - text: "Displacement Map", - value: "displacementMap", - }, - { - text: "Displacement Scale", - value: "displacementScale", - }, - { - text: "Displacement Bias", - value: "displacementBias", - }, - { - text: "Bump Map", - value: "bumpMap", - }, - { - text: "Bump Scale", - value: "bumpScale", - }, - { - text: "Normal Map Type", - value: "normalMapType", - }, - - "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", - { - text: "Shininess", - value: "shininess", - }, - - { - text: "Wireframe", - value: "wireframe", - }, - { - text: "Wireframe Linewidth", - value: "wireframeLinewidth", - }, - { - text: "Wireframe Linecap", - value: "wireframeLinecap", - }, - { - text: "Wireframe Linejoin", - value: "wireframeLinejoin", - }, - - "|POINTS| <-- not a property", - { - text: "Size", - value: "size", - }, - { - text: "Size Attenuation", - value: "sizeAttenuation", - }, - - "|LINES| <-- not a property", - { - text: "Scale", - value: "scale", - }, - { - text: "Dash Size", - value: "dashSize", - }, - { - text: "Gap Size", - value: "gapSize", - }, - - "|SPRITES| <-- not a property", - { - text: "Rotation", - value: "rotation", - }, - ], - }, - blendModes: { - acceptReporters: false, - items: [{ - text: "No Blending", - value: "NoBlending", - }, - { - text: "Normal Blending", - value: "NormalBlending", - }, - { - text: "Additive Blending", - value: "AdditiveBlending", - }, - { - text: "Subtractive Blending", - value: "SubtractiveBlending", - }, - { - text: "Multiply Blending", - value: "MultiplyBlending", - }, - { - text: "Custom Blending", - value: "CustomBlending", - }, - ], - }, - depthModes: { - acceptReporters: false, - items: [{ - text: "Never Depth", - value: "NeverDepth", - }, - { - text: "Always Depth", - value: "AlwaysDepth", - }, - { - text: "Equal Depth", - value: "EqualDepth", - }, - { - text: "Less Depth", - value: "LessDepth", - }, - { - text: "Less Equal Depth", - value: "LessEqualDepth", - }, - { - text: "Greater Equal Depth", - value: "GreaterEqualDepth", - }, - { - text: "Greater Depth", - value: "GreaterDepth", - }, - { - text: "Not Equal Depth", - value: "NotEqualDepth", - }, - ], - }, - materialTypes: { - acceptReporters: false, - items: [{ - text: "Mesh Basic Material", - value: "MeshBasicMaterial", - }, - { - text: "Mesh Standard Material", - value: "MeshStandardMaterial", - }, - { - text: "Mesh Physical Material", - value: "MeshPhysicalMaterial", - }, - { - text: "Mesh Lambert Material", - value: "MeshLambertMaterial", - }, - { - text: "Mesh Phong Material", - value: "MeshPhongMaterial", - }, - { - text: "Mesh Depth Material", - value: "MeshDepthMaterial", - }, - { - text: "Mesh Normal Material", - value: "MeshNormalMaterial", - }, - { - text: "Mesh Matcap Material", - value: "MeshMatcapMaterial", - }, - { - text: "Mesh Toon Material", - value: "MeshToonMaterial", - }, - { - text: "Line Basic Material", - value: "LineBasicMaterial", - }, - { - text: "Line Dashed Material", - value: "LineDashedMaterial", - }, - { - text: "Points Material", - value: "PointsMaterial", - }, - { - text: "Sprite Material", - value: "SpriteMaterial", - }, - { - text: "Shadow Material", - value: "ShadowMaterial", - }, - ], - }, - textureModes: { - acceptReporters: false, - items: ["Pixelate", "Blur"], - }, - textureStyles: { - acceptReporters: false, - items: ["Repeat", "Clamp"], - }, - geometryTypes: { - acceptReporters: false, - items: [{ - text: "Box Geometry", - value: "BoxGeometry", - }, - { - text: "Sphere Geometry", - value: "SphereGeometry", - }, - { - text: "Cylinder Geometry", - value: "CylinderGeometry", - }, - { - text: "Plane Geometry", - value: "PlaneGeometry", - }, - { - text: "Circle Geometry", - value: "CircleGeometry", - }, - { - text: "Torus Geometry", - value: "TorusGeometry", - }, - { - text: "Torus Knot Geometry", - value: "TorusKnotGeometry", - }, - ], - }, - modelsList: { - acceptReporters: false, - items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime - .getTargetForStage() - .getSounds() - .filter((e) => e.name && e.name.endsWith(".glb")); - if (models.length < 1) return [ - ["Load a model! (GLB Loader category)"] - ]; - - // @ts-ignore - return models.map((m) => [m.name]); - }, - }, - fonts: { - acceptReporters: false, - items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime - .getTargetForStage() - .getSounds() - .filter((e) => e.name && e.name.endsWith(".json")); - if (models.length < 1) return [ - ["Load a font!"] - ]; - - // @ts-ignore - return models.map((m) => [m.name]); - }, - }, - }, - }; - } - - addObject(args) { - const object = new THREE[args.TYPE](); - - object.castShadow = true; - object.receiveShadow = true; - - createObject(args.OBJECT3D, object, args.GROUP); - } - cloneObject(args) { - let object = getObject(args.OBJECT3D); - const clone = object.clone(true); - clone.name; - createObject(args.NAME, clone, args.GROUP); - } - setObjectV3(args) { - let object = getObject(args.OBJECT3D); - let values = JSON.parse(args.VALUE); - - function degToRad(deg) { - return (deg * Math.PI) / 180; - } - - if (object.rigidBody) { - const x = values[0]; - const y = values[1]; - const z = values[2]; - if (args.PROPERTY === "rotation") { - const euler = new THREE.Euler(degToRad(x), degToRad(y), degToRad(z), "YXZ"); - const quaternion = new THREE.Quaternion(); - quaternion.setFromEuler(euler); - - object.rigidBody.setRotation({ - x: quaternion.x, - y: quaternion.y, - z: quaternion.z, - w: quaternion.w, - }); - } else if (args.PROPERTY === "position") { - object.rigidBody.setTranslation({ - x: x, - y: y, - z: z, - }, - true - ); - } - return; - } - - if (object.isCamera == true && controls) {} - - if (args.PROPERTY === "rotation") { - values = values.map((v) => (v * Math.PI) / 180); - object.rotation.set(0, 0, 0); - } - if (object.isDirectionalLight == true) { - object.pos = new THREE.Vector3(...values); - console.log(true, values, object.pos); - return; - } - object[args.PROPERTY].set(...values); - - if (object.type == "CubeCamera") object.updateCoordinateSystem(); - } - /* - changeObjectV3(args) { - getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) - - if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.x += values[0] - object.rotation.y += values[1] - object.rotation.z += values[2] - } - else { - object[args.PROPERTY].add(...values); - } - } - changeObjectXV3(args) { - getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "rotation") value = value * Math.PI / 180 - - object[args.PROPERTY][args.X] += value - } - */ - getObjectV3(args) { - let object = getObject(args.OBJECT3D); - if (!object) return; - let values = vector3ToString(object[args.PROPERTY]); - if (args.PROPERTY === "rotation") { - const toDeg = Math.PI / 180; - values = [values[0] / toDeg, values[1] / toDeg, values[2] / toDeg]; - } - - return JSON.stringify(values); - } - setObject(args) { - let object = getObject(args.OBJECT3D); - let value = args.VALUE; - if (args.PROPERTY === "material") { - const mat = materials[args.NAME]; - if (mat) value = mat; - else value = undefined; - } else if (args.PROPERTY === "geometry") { - const geo = geometries[args.NAME]; - if (geo) value = geo; - else value = undefined; - } else value = !!value; - - if (value == undefined) return; //invalid geo/mat - object[args.PROPERTY] = value; - } - getObject(args) { - let object = getObject(args.OBJECT3D); - if (!object) return; - let value; - if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; - else value = object.visible; - - return value; - } - removeObject(args) { - removeObject(args.OBJECT3D); - } - objectE(args) { - return scene.children.map((o) => o.name).includes(args.NAME); - } - - //defines - newMaterial(args) { - if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); - const mat = new THREE[args.TYPE](); - mat.name = args.NAME; - - materials[args.NAME] = mat; - } - async setMaterial(args) { - if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; - const mat = materials[args.NAME]; - - let value = args.VALUE; - - if (args.VALUE == "false") value = false; - - if (args.PROPERTY == "side") { - value = args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide; - } else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)); - else value = getAsset(value); - - console.log("o:", args.VALUE, typeof args.VALUE); - console.log("r:", value, typeof value); - - mat[args.PROPERTY] = await value; //await incase its a texture - mat.needsUpdate = true; - } - setBlending(args) { - const mat = materials[args.NAME]; - mat.blending = THREE[args.VALUE]; - mat.premultipliedAlpha = true; - mat.needsUpdate = true; - } - setDepth(args) { - const mat = materials[args.NAME]; - mat.depthFunc = THREE[args.VALUE]; - mat.needsUpdate = true; - } - removeMaterial(args) { - const mat = materials[args.NAME]; - mat.dispose(); - delete materials[args.NAME]; - } - materialE(args) { - return materials[args.NAME] ? true : false; - } - - newGeometry(args) { - if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); - const geo = new THREE[args.TYPE](); - geo.name = args.NAME; - - geometries[args.NAME] = geo; - } - setGeometry(args) { - const geo = geometries[args.NAME]; - geo[args.PROPERTY] = args.VALUE; - - geo.needsUpdate = true; - } - removeGeometry(args) { - const geo = geometries[args.NAME]; - geo.dispose(); - delete geometries[args.NAME]; - } - geometryE(args) { - return geometries[args.NAME] ? true : false; - } - - newGeo(args) { - const geometry = new THREE.BufferGeometry(); - geometry.name = args.NAME; - geometries[args.NAME] = geometry; - } - async geoPoints(args) { - const geometry = geometries[args.NAME]; - const positions = args.POINTS.split(" ") - .map((v) => JSON.parse(v)) - .flat(); //array of v3 of each vertex of each triangle - - geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3)); - geometry.computeVertexNormals(); - - geometry.needsUpdate = true; - } - geoUVs(args) { - const geometry = geometries[args.NAME]; - const UVs = args.POINTS.split(" ") - .map((v) => JSON.parse(v)) - .flat(); //array of v2 of each UV of each triangle - - geometry.setAttribute("uv", new THREE.BufferAttribute(new Float32Array(UVs), 2)); - geometry.needsUpdate = true; - } - - splines(args) { - const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); - geometry.name = args.NAME; - - geometries[args.NAME] = geometry; - } - - async splineModel(args) { - const model = await getModel(args.MODEL, args.NAME); - if (!model) return console.warn("Model not found:", args.MODEL); - - const curve = getAsset(args.CURVE); - const spacing = parseFloat(args.SPACING) || 1; - const curveLength = curve.getLength(); - const divisions = Math.floor(curveLength / spacing); - - const geomList = []; - const matList = []; - - for (let i = 0; i <= divisions; i++) { - const t = i / divisions; - const pos = curve.getPointAt(t); - const tangent = curve.getTangentAt(t); - - const temp = model.clone(true); - temp.position.copy(pos); - - const up = new THREE.Vector3(0, 0, 1); - const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()); - temp.quaternion.copy(quat); - - temp.updateMatrixWorld(true); - - temp.traverse((child) => { - if (child.isMesh && child.geometry) { - const geom = child.geometry.clone(); - geom.applyMatrix4(child.matrixWorld); - geomList.push(geom); - matList.push(child.material); //.clone() ? - } - }); - } - - const validGeoms = geomList.filter((g) => { - const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position; - if (!ok) console.warn("geometry skipped:", g); - return ok; - }); - - const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true); - merged.computeBoundingBox(); - merged.computeBoundingSphere(); - - merged.name = args.NAME; - geometries[args.NAME] = merged; - matList.name = args.NAME; - materials[args.NAME] = matList; - } - - async text(args) { - const fontFile = runtime - .getTargetForStage() - .getSounds() - .find((c) => c.name === args.FONT); - if (!fontFile) return; - - const json = new TextDecoder().decode(fontFile.asset.data.buffer); - const fontData = JSON.parse(json); - - const font = fontLoad.parse(fontData); - - const params = { - font: font, - size: JSON.parse(args.S), - height: JSON.parse(args.D), - curveSegments: JSON.parse(args.CS), - bevelEnabled: false, - }; - const geometry = new TextGeometry.TextGeometry(args.TEXT, params); - geometry.computeVertexNormals(); - geometry.center(); // optional, recenters the text - - geometry.name = args.NAME; - - geometries[args.NAME] = geometry; - } - - async loadFont() { - openFileExplorer(".json").then((files) => { - const file = files[0]; - const reader = new FileReader(); - - reader.onload = async (e) => { - const arrayBuffer = e.target.result; - - // From lily's assets - // // Thank you PenguinMod for providing this code. - - const targetId = runtime.getTargetForStage().id; //util.target.id not working! - const assetName = Cast.toString(file.name); - - const buffer = arrayBuffer; - - const storage = runtime.storage; - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ); - - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ); - alert("Font loaded successfully!"); - } catch (e) { - console.error(e); - alert("Error loading font."); - } - - // End of PenguinMod - }; - - reader.readAsArrayBuffer(file); - }); - } - openConv() { - { - open("https://gero3.github.io/facetype.js/"); - } - } - } - Scratch.extensions.register(new ThreeObjects()); - - class ThreeLights { - getInfo() { - return { - id: "threeLights", - name: "Three Lights", - color1: "#c7a22aff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "addLight", - blockType: Scratch.BlockType.COMMAND, - text: "add light [NAME] type [TYPE] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myLight", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "lightTypes", - }, - }, - }, - { - opcode: "setLight", - blockType: Scratch.BlockType.COMMAND, - text: "set light [NAME][PROPERTY] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "lightProperties", - defaultValue: "intensity", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myLight", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - exemptFromNormalization: true, - }, - }, - }, - ], - menus: { - lightTypes: { - acceptReporters: false, - items: [{ - text: "Ambient Light", - value: "AmbientLight", - }, - { - text: "Directional Light", - value: "DirectionalLight", - }, - { - text: "Point Light", - value: "PointLight", - }, - { - text: "Hemisphere Light", - value: "HemisphereLight", - }, - { - text: "Spot Light", - value: "SpotLight", - }, - ], - }, - lightProperties: { - acceptReporters: false, - items: [{ - text: "Color", - value: "color", - }, - { - text: "Intensity", - value: "intensity", - }, - { - text: "Cast Shadow?", - value: "castShadow", - }, - { - text: "Ground Color (HemisphereLight)", - value: "groundColor", - }, - { - text: "Map (SpotLight)", - value: "map", - }, - { - text: "Distance (SpotLight)", - value: "distance", - }, - { - text: "Decay (SpotLight)", - value: "decay", - }, - { - text: "Penumbra (SpotLight)", - value: "penumbra", - }, - { - text: "Angle/Size (SpotLight)", - value: "angle", - }, - { - text: "Power (SpotLight)", - value: "power", - }, - { - text: "Target Position (Directional/SpotLight)", - value: "target", - }, - ], - }, - }, - }; - } - - addLight(args) { - const light = new THREE[args.TYPE](0xffffff, 1); - - createObject(args.NAME, light, args.GROUP); - lights[args.NAME] = light; - if (light.type === "AmbientLight" || "HemisphereLight") return; - - light.castShadow = true; - if (light.type === "PointLight") return; - //Directional & Spot Light - light.target.position.set(0, 0, 0); - scene.add(light.target); - - light.pos = new THREE.Vector3(0, 0, 0); - - light.shadow.mapSize.width = 4096; - light.shadow.mapSize.height = 2048; - - if (light.type === "SpotLight") { - light.decay = 0; - light.shadow.camera.near = 500; - light.shadow.camera.far = 4000; - light.shadow.camera.fov = 30; - } - light.shadow.needsUpdate = true; - light.needsUpdate = true; - } - - setLight(args) { - const light = lights[args.NAME]; - if (!args.PROPERTY) return; - if (args.PROPERTY === "target") { - light.target.position.set(...JSON.parse(args.VALUE)); //vector3 - light.target.updateMatrixWorld(); - } else { - light[args.PROPERTY] = getAsset(args.VALUE); - } - light.needsUpdate = true; - - if (light.type === "AmbientLight" || "HemisphereLight") return; - - light.shadow.camera.updateProjectionMatrix(); - light.shadow.needsUpdate = true; - } - } - Scratch.extensions.register(new ThreeLights()); - - class ThreeUtilities { - getInfo() { - return { - id: "threeUtility", - name: "Three Utilities", - color1: "#3875c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "newVector2", - blockType: Scratch.BlockType.REPORTER, - text: "New Vector [X] [Y]", - arguments: { - X: { - type: Scratch.ArgumentType.NUMBER, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - }, - }, - }, - { - opcode: "newVector3", - blockType: Scratch.BlockType.REPORTER, - text: "New Vector [X] [Y] [Z]", - arguments: { - X: { - type: Scratch.ArgumentType.NUMBER, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - }, - Z: { - type: Scratch.ArgumentType.NUMBER, - }, - }, - }, - "---", - { - opcode: "operateV3", - blockType: Scratch.BlockType.REPORTER, - text: "do [V3] [O] [V32]", - arguments: { - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - O: { - type: Scratch.ArgumentType.STRING, - menu: "operators", - }, - V32: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[1,0,0]", - }, - }, - }, - { - opcode: "moveVector3", - blockType: Scratch.BlockType.REPORTER, - text: "move [S] steps in vector [V3] in direction [D3]", - arguments: { - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - D3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[1,0,0]", - }, - }, - }, - { - opcode: "directionTo", - blockType: Scratch.BlockType.REPORTER, - text: "direction from [V3] to [T3]", - arguments: { - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,3]", - }, - T3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - }, - }, - "---", - { - opcode: "newColor", - blockType: Scratch.BlockType.REPORTER, - text: "New Color [HEX]", - arguments: { - HEX: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#9966ff", - }, - }, - }, - { - opcode: "newFog", - blockType: Scratch.BlockType.REPORTER, - text: "New Fog [COLOR] [NEAR] [FAR]", - arguments: { - COLOR: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#9966ff", - exemptFromNormalization: true, - }, - NEAR: { - type: Scratch.ArgumentType.NUMBER, - }, - FAR: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 10, - }, - }, - }, - { - opcode: "newTexture", - blockType: Scratch.BlockType.REPORTER, - text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", - arguments: { - COSTUME: { - type: Scratch.ArgumentType.COSTUME, - }, - MODE: { - type: Scratch.ArgumentType.STRING, - menu: "textureModes", - }, - STYLE: { - type: Scratch.ArgumentType.STRING, - menu: "textureStyles", - }, - X: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - { - opcode: "newCubeTexture", - blockType: Scratch.BlockType.REPORTER, - text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", - arguments: { - COSTUMEX0: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEX1: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEY0: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEY1: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEZ0: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEZ1: { - type: Scratch.ArgumentType.COSTUME, - }, - MODE: { - type: Scratch.ArgumentType.STRING, - menu: "textureModes", - }, - STYLE: { - type: Scratch.ArgumentType.STRING, - menu: "textureStyles", - }, - X: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - { - opcode: "newEquirectangularTexture", - blockType: Scratch.BlockType.REPORTER, - text: "New Equirectangular Texture [COSTUME] [MODE]", - arguments: { - COSTUME: { - type: Scratch.ArgumentType.COSTUME, - }, - MODE: { - type: Scratch.ArgumentType.STRING, - menu: "textureModes", - }, - }, - }, - "---", - { - opcode: "curve", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.REPORTER, - text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", - arguments: { - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "curveTypes", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]", - }, - CLOSED: { - type: Scratch.ArgumentType.STRING, - defaultValue: "true", - }, - }, - }, - "---", - { - opcode: "mouseDown", - extensions: ["colours_sensing"], - blockType: Scratch.BlockType.BOOLEAN, - text: "mouse [BUTTON] [action]?", - arguments: { - BUTTON: { - type: Scratch.ArgumentType.STRING, - menu: "mouseButtons", - }, - action: { - type: Scratch.ArgumentType.STRING, - menu: "mouseAction", - }, - }, - }, - { - opcode: "mousePos", - extensions: ["colours_sensing"], - blockType: Scratch.BlockType.REPORTER, - text: "mouse position", - arguments: {}, - }, - "---", - { - opcode: "getItem", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.REPORTER, - text: "get item [ITEM] of [ARRAY]", - arguments: { - ITEM: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - }, - ARRAY: { - type: Scratch.ArgumentType.STRING, - defaultValue: `["myObject", "myLight"]`, - }, - }, - }, - { - blockType: Scratch.BlockType.LABEL, - text: "↳ Raycasting", - }, - { - opcode: "raycast", - blockType: Scratch.BlockType.COMMAND, - text: "Raycast from [V3] in direction [D3]", - arguments: { - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,3]", - }, - D3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,1]", - }, - }, - }, - { - opcode: "getRaycast", - blockType: Scratch.BlockType.REPORTER, - text: "get raycast [PROPERTY]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "raycastProperties", - }, - }, - }, - ], - menus: { - materialProperties: { - acceptReporters: false, - items: [{ - text: "Color", - value: "color", - }, - { - text: "Map (texture)", - value: "map", - }, - { - text: "Alpha Map (texture)", - value: "alphaMap", - }, - { - text: "Alpha Test (0-1)", - value: "alphaTest", - }, - { - text: "Side (front/back/double)", - value: "side", - }, - { - text: "Bump Map (texture)", - value: "bumpMap", - }, - { - text: "Bump Scale", - value: "bumpScale", - }, - ], - }, - textureModes: { - acceptReporters: false, - items: ["Pixelate", "Blur"], - }, - textureStyles: { - acceptReporters: false, - items: ["Repeat", "Clamp"], - }, - raycastProperties: { - acceptReporters: false, - items: [{ - text: "Intersected Object Names", - value: "name", - }, - { - text: "Number of Objects", - value: "number", - }, - { - text: "Intersected Objects distances", - value: "distance", - }, - ], - }, - mouseButtons: { - acceptReporters: false, - items: ["left", "middle", "right"], - }, - mouseAction: { - acceptReporters: false, - items: ["Down", "Clicked"], - }, - curveTypes: { - acceptReporters: false, - items: ["CatmullRomCurve3"], - }, - operators: { - acceptReporters: false, - items: ["+", "-", "*", "/", "=", "max", "min", "dot", "cross", "distance to", "angle to", "apply euler"], - }, - }, - }; - } - mouseDown(args) { - if (args.action === "Down") return isMouseDown[args.BUTTON]; - if (args.action === "Clicked") { - if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false; - else prevMouse[args.BUTTON] = true; - return true; - } - } - mousePos(event) { - return JSON.stringify(mouseNDC); - } - newVector3(args) { - return JSON.stringify([args.X, args.Y, args.Z]); - } - operateV3(args) { - const v3 = new THREE.Vector3(...JSON.parse(args.V3)); - const v32 = new THREE.Vector3(...JSON.parse(args.V32)); - - let r; - if (args.O == "+") r = v3.add(v32); - else if (args.O == "-") r = v3.sub(v32); - else if (args.O == "*") r = v3.multiply(v32); - else if (args.O == "/") r = v3.divide(v32); - else if (args.O == "=") r = v3.equals(v32); - else if (args.O == "max") r = v3.max(v32); - else if (args.O == "min") r = v3.min(v32); - else if (args.O == "dot") r = v3.dot(v32); - else if (args.O == "cross") r = v3.cross(v32); - else if (args.O == "distance to") r = v3.distanceTo(v32); - else if (args.O == "angle to") r = v3.angleTo(v32); - else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)); - - if (typeof r == "object") return JSON.stringify([r.x, r.y, r.z]); - else return JSON.stringify(r); - } - - newVector2(args) { - return JSON.stringify([args.X, args.Y]); - } - - moveVector3(args) { - const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); - const steps = Number(args.S); - - const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); - - const yaw = THREE.MathUtils.degToRad(yawInputDeg); - const pitch = THREE.MathUtils.degToRad(pitchInputDeg); - const roll = THREE.MathUtils.degToRad(rollInputDeg); - - const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); - - const forwardVector = new THREE.Vector3(0, 0, -1); - const direction = forwardVector.applyEuler(euler).normalize(); - - const newPos = currentPos.add(direction.multiplyScalar(steps)); - return JSON.stringify([newPos.x, newPos.y, newPos.z]); - } - - directionTo(args) { - const v3 = new THREE.Vector3(...JSON.parse(args.V3)); - const toV3 = new THREE.Vector3(...JSON.parse(args.T3)); - - const direction = toV3.clone().sub(v3).normalize(); - // Pitch (X) - const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)); - // Yaw (Y) - const yaw = Math.atan2(direction.x, direction.z); - - // Roll always 0 - return JSON.stringify([180 + THREE.MathUtils.radToDeg(pitch), THREE.MathUtils.radToDeg(yaw), 0]); - } - - newColor(args) { - const color = new THREE.Color(args.HEX); - const uuid = crypto.randomUUID(); - assets.colors[uuid] = color; - return `colors/${uuid}`; - } - newFog(args) { - const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR); - const uuid = crypto.randomUUID(); - assets.fogs[uuid] = fog; - return `fogs/${uuid}`; - } - async newTexture(args) { - const textureURI = encodeCostume(args.COSTUME); - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); - assets.textures[texture.uuid] = texture; - return `textures/${texture.uuid}`; - } - async newCubeTexture(args) { - const uris = [ - encodeCostume(args.COSTUMEX0), - encodeCostume(args.COSTUMEX1), - encodeCostume(args.COSTUMEY0), - encodeCostume(args.COSTUMEY1), - encodeCostume(args.COSTUMEZ0), - encodeCostume(args.COSTUMEZ1), - ]; - const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); - const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); - - texture.name = "CubeTexture" + args.COSTUMEX0; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); - assets.textures[texture.uuid] = texture; - return `textures/${texture.uuid}`; - } - async newEquirectangularTexture(args) { - const textureURI = encodeCostume(args.COSTUME); - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME; - texture.mapping = THREE.EquirectangularReflectionMapping; - - setTexutre(texture, args.MODE); - assets.textures[texture.uuid] = texture; - return `textures/${texture.uuid}`; - } - - curve(args) { - function parsePoints(input) { - // Match all [x,y,z] groups - const matches = input.match(/\[([^\]]+)\]/g); - if (!matches) return []; - - return matches.map((str) => { - const nums = str - .replace(/[\[\]\s]/g, "") - .split(",") - .map(Number); - return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0); - }); - } - const points = parsePoints(args.POINTS); - const curve = new THREE[args.TYPE](points); - curve.closed = JSON.parse(args.CLOSED); - - const uuid = crypto.randomUUID(); - assets.curves[uuid] = curve; - return `curves/${uuid}`; - } - - getItem(args) { - const items = JSON.parse(args.ARRAY); - const item = items[args.ITEM - 1]; - if (!item) return "0"; - return item; - } - - raycast(args) { - const origin = new THREE.Vector3(...JSON.parse(args.V3)); - // rotation is in degrees => convert to radians first - const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); - - const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder); - const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize(); - - const raycaster = new THREE.Raycaster(); - //const camera = getObject(args.CAMERA) - raycaster.set(origin, direction); - - const intersects = raycaster.intersectObjects(scene.children, true); - - raycastResult = intersects; - } - getRaycast(args) { - if (args.PROPERTY === "number") return raycastResult.length; - if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map((i) => i.distance)); - return JSON.stringify(raycastResult.map((i) => i.object[args.PROPERTY])); - } - } - Scratch.extensions.register(new ThreeUtilities()); - - class ThreeGLB { - getInfo() { - return { - id: "threeGLB", - name: "Three GLB Loader", - color1: "#c53838ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - blockType: Scratch.BlockType.BUTTON, - text: "Load GLB File", - func: "loadModelFile", - }, - { - opcode: "addModel", - blockType: Scratch.BlockType.COMMAND, - text: "add [ITEM] as [NAME] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - ITEM: { - type: Scratch.ArgumentType.STRING, - menu: "modelsList", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - }, - }, - { - opcode: "getModel", - blockType: Scratch.BlockType.REPORTER, - text: "get object [PROPERTY] of [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "modelProperties", - }, - }, - }, - { - opcode: "playAnimation", - blockType: Scratch.BlockType.COMMAND, - text: "play animation [ANAME] of [NAME], [TIMES] times", - arguments: { - TIMES: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "0", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - ANAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "walk", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "pauseAnimation", - blockType: Scratch.BlockType.COMMAND, - text: "set [TOGGLE] animation [ANAME] of [NAME]", - arguments: { - TOGGLE: { - type: Scratch.ArgumentType.NUMBER, - menu: "pauseUn", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - ANAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "walk", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "stopAnimation", - blockType: Scratch.BlockType.COMMAND, - text: "stop animation [ANAME] of [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - ANAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "walk", - exemptFromNormalization: true, - }, - }, - }, - ], - menus: { - modelProperties: { - acceptReporters: false, - items: [{ - text: "Animations", - value: "animations", - }, ], - }, - pauseUn: { - acceptReporters: true, - items: [{ - text: "Pause", - value: "true", - }, - { - text: "Unpasue", - value: "false", - }, - ], - }, - modelsList: { - acceptReporters: false, - items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime - .getTargetForStage() - .getSounds() - .filter((e) => e.name && e.name.endsWith(".glb")); - if (models.length < 1) return [ - ["Load a model!"] - ]; - - // @ts-ignore - return models.map((m) => [m.name]); - }, - }, - }, - }; - } - - async loadModelFile() { - openFileExplorer(".glb").then((files) => { - const file = files[0]; - const reader = new FileReader(); - - reader.onload = async (e) => { - const arrayBuffer = e.target.result; - - { - // From lily's assets - - // Thank you PenguinMod for providing this code. - { - const targetId = runtime.getTargetForStage().id; //util.target.id not working! - const assetName = Cast.toString(file.name); - - //const res = await Scratch.fetch(args.URL); - //const buffer = await res.arrayBuffer(); - const buffer = arrayBuffer; - - const storage = runtime.storage; - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ); - - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ); - alert("Model loaded successfully!"); - } catch (e) { - console.error(e); - alert("Error loading model."); - } - } - // End of PenguinMod - } - }; - - reader.readAsArrayBuffer(file); - }); - } - async addModel(args) { - const group = await getModel(args.ITEM, args.NAME); - - createObject(args.NAME, group, args.GROUP); - } - getModel(args) { - if (!models[args.NAME]) return; - return Object.keys(models[args.NAME].actions).toString(); - } - - playAnimation(args) { - const model = models[args.NAME]; - if (!model) { - console.log("no model!"); - return; - } - - const action = model.actions[args.ANAME]; //clones of models dont have a stored actions! - if (!action) { - console.log("no action!"); - return; - } - - args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity); - - action.reset().play(); - } - stopAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.stop(); - } - pauseAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.paused = args.TOGGLE; - } - } - Scratch.extensions.register(new ThreeGLB()); - - class ThreeAddons { - getInfo() { - return { - id: "threeAddons", - name: "Three Addons", - color1: "#c538a2ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - blockType: Scratch.BlockType.LABEL, - text: "Orbit Control", - }, - { - opcode: "OrbitControl", - blockType: Scratch.BlockType.COMMAND, - text: "set addon Orbit Control [STATE]", - arguments: { - STATE: { - type: Scratch.ArgumentType.STRING, - menu: "onoff", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: "Post Processing", - }, - { - opcode: "resetComposer", - blockType: Scratch.BlockType.COMMAND, - text: "reset composer", - }, - { - opcode: "bloom", - blockType: Scratch.BlockType.COMMAND, - text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", - arguments: { - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - I: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.5, - }, - T: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.5, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "SCREEN", - }, - }, - }, - { - opcode: "godRays", - blockType: Scratch.BlockType.COMMAND, - text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", - arguments: { - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "SCREEN", - }, - DEC: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.95, - }, - DENS: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - EXP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.1, - }, - WEI: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.4, - }, - RES: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - SAMP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 64, - }, - }, - }, - { - opcode: "dots", - blockType: Scratch.BlockType.COMMAND, - text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", - arguments: { - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - A: { - type: Scratch.ArgumentType.ANGLE, - defaultValue: 0, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "SCREEN", - }, - }, - }, - { - opcode: "depth", - blockType: Scratch.BlockType.COMMAND, - text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", - arguments: { - FD: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 3, - }, - FL: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.001, - }, - BS: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 4, - }, - H: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 240, - }, - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "NORMAL", - }, - }, - }, - "---", - { - opcode: "custom", - blockType: Scratch.BlockType.COMMAND, - text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myShader", - }, - FRA: { - type: Scratch.ArgumentType.STRING, - }, - VER: { - type: Scratch.ArgumentType.STRING, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "NORMAL", - }, - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - ], - menus: { - onoff: { - acceptReporters: true, - items: [{ - text: "enabled", - value: "1", - }, - { - text: "disabled", - value: "0", - }, - ], - }, - blendModes: { - acceptReporters: false, - items: [ - "SKIP", - "SET", - "ADD", - "ALPHA", - "AVERAGE", - "COLOR", - "COLOR_BURN", - "COLOR_DODGE", - "DARKEN", - "DIFFERENCE", - "DIVIDE", - "DST", - "EXCLUSION", - "HARD_LIGHT", - "HARD_MIX", - "HUE", - "INVERT", - "INVERT_RGB", - "LIGHTEN", - "LINEAR_BURN", - "LINEAR_DODGE", - "LINEAR_LIGHT", - "LUMINOSITY", - "MULTIPLY", - "NEGATION", - "NORMAL", - "OVERLAY", - "PIN_LIGHT", - "REFLECT", - "SCREEN", - "SRC", - "SATURATION", - "SOFT_LIGHT", - "SUBTRACT", - "VIVID_LIGHT", - ], - }, - }, - }; - } - - OrbitControl(args) { - if (controls) controls.dispose(); - - console.log("creating...", OrbitControls); - controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); - controls.enableDamping = true; - - controls.enabled = !!args.STATE; - console.log(controls); - } - - resetComposer() { - composer.passes = []; - passes = {}; - customEffects = []; - updateComposers(); - } - - bloom(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - const bloomEffect = new BloomEffect({ - intensity: args.I, - luminanceThreshold: args.T, // ← correct key - luminanceSmoothing: args.S, - blendFunction: BlendFunction[args.BLEND], - }); - bloomEffect.blendMode.opacity.value = args.OP; - - const pass = new EffectPass(camera, bloomEffect); - - composer.addPass(pass); - } - - godRays(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - let object = getObject(args.NAME); - const sun = object; - - const godRays = new GodRaysEffect(camera, sun, { - resolutionScale: args.RES, - density: args.DENS, // ray density - decay: args.DEC, // fade out - weight: args.WEI, // brightness of rays - exposure: args.EXP, - samples: args.SAMP, - blendFunction: BlendFunction[args.BLEND], - }); - godRays.blendMode.opacity.value = args.OP; - const pass = new EffectPass(camera, godRays); - composer.addPass(pass); - } - - dots(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - const dot = new DotScreenEffect({ - angle: args.A, - scale: args.S, - blendFunction: BlendFunction[args.BLEND], - }); - dot.blendMode.opacity.value = args.OP; - const pass = new EffectPass(camera, dot); - composer.addPass(pass); - } - - depth(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - const dofEffect = new DepthOfFieldEffect(camera, { - focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) - focalLength: args.FL, // lens focal length in meters - bokehScale: args.BS, // strength/size of the blur circles - height: args.H, // resolution hint (affects quality/perf) - blendFunction: BlendFunction[args.BLEND], - }); - dofEffect.blendMode.opacity.value = args.OP; - - const dofPass = new EffectPass(camera, dofEffect); - composer.addPass(dofPass); - } - - async custom(args) { - function cleanGLSL(glslCode) { - //delete multilines comments - let cleanedCode = glslCode - .replace(/\/\*[\s\S]*?\*\//g, " ") - .replace(/ /g, "\n") - .replace(/\/\/.*$/gm, " ") - .replace(/; /g, ";\n"); - - return cleanedCode; - } - - let fs = cleanGLSL(` - ${args.FRA} - `); - if (!args.FRA.trim()) { - fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`; - } - const vs = cleanGLSL(` - ${args.VER} - `); - console.log(fs); - console.log(vs); - - const effect = new Effect("Custom", fs, { - blendFunction: BlendFunction[args.BLEND], - vertexShader: vs, - uniforms: new Map([ - //uniforms usually in shaders... open to more! - ["time", new THREE.Uniform(0.0)], - [ - "resolution", - new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height)), - ], - ]), - defines: new Map([ - ["USE_TIME", "1"], - ["USE_VERTEX_TRANSFORM", ""], - ]), - }); - - effect.blendMode.opacity.value = args.OP; - - const pass = new EffectPass(camera, effect); - composer.addPass(pass); - - customEffects.push(effect); - } - } - Scratch.extensions.register(new ThreeAddons()); - - class RapierPhysics { - getInfo() { - return { - id: "rapierPhysics", - name: "RAPIER Physics", - color1: "#222222", - color2: "#203024ff", - color3: "#78f07eff", - blocks: [{ - opcode: "createWorld", - blockType: Scratch.BlockType.COMMAND, - text: "create world | gravity:[G]", - arguments: { - G: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,-9.81,0]", - }, - }, - }, - { - opcode: "getWorld", - blockType: Scratch.BlockType.REPORTER, - text: "get world [PROPERTY]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "wProp", - }, - }, - }, - "---", - { - opcode: "objectPhysics", - blockType: Scratch.BlockType.COMMAND, - text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", - arguments: { - state2: { - type: Scratch.ArgumentType.STRING, - menu: "state2", - }, - state: { - type: Scratch.ArgumentType.STRING, - menu: "state", - defaultValue: "true", - }, - type: { - type: Scratch.ArgumentType.STRING, - menu: "objectTypes", - defaultValue: "dynamic", - }, - collider: { - type: Scratch.ArgumentType.STRING, - menu: "colliderTypes", - defaultValue: "cuboid", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - mass: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "1", - }, - density: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "1", - }, - friction: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "0.5", - }, - }, - }, - "---", - { - blockType: Scratch.BlockType.LABEL, - text: "- RigidBody", - }, - { - opcode: "setRB", - blockType: Scratch.BlockType.COMMAND, - text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "rigidBodySets", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - }, - }, - }, - { - opcode: "getRB", - blockType: Scratch.BlockType.REPORTER, - text: "get rigidbody [PROPERTY] of [OBJECT]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "rigidBodyProperties", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "lockObjectAxis", - blockType: Scratch.BlockType.COMMAND, - text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", - arguments: { - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "lockAxes", - }, - X: { - type: Scratch.ArgumentType.STRING, - menu: "tf", - }, - Y: { - type: Scratch.ArgumentType.STRING, - menu: "tf", - }, - Z: { - type: Scratch.ArgumentType.STRING, - menu: "tf", - }, - }, - }, - "---", - { - opcode: "addForce", - blockType: Scratch.BlockType.COMMAND, - text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", - arguments: { - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,10,0]", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "forces", - defaultValue: "addForce", - }, - SPACE: { - type: Scratch.ArgumentType.STRING, - menu: "spaces", - defaultValue: "world", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - { - opcode: "resetForces", - blockType: Scratch.BlockType.COMMAND, - text: "reset [PROPERTY] of [OBJECT]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "resetF", - defaultValue: "resetForces", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "enableCCD", - blockType: Scratch.BlockType.COMMAND, - text: "enable Continuous Collision Detection for [OBJECT] [state]", - arguments: { - state: { - type: Scratch.ArgumentType.STRING, - menu: "state", - defaultValue: "true", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "oPropS", - defaultValue: "physics", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "fixedJoint", - blockType: Scratch.BlockType.COMMAND, - text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", - arguments: { - ObjA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - ObjB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObjectB", - }, - VA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - VB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,1,0]", - }, - RA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - RB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - }, - }, - { - opcode: "sphericalJoint", - blockType: Scratch.BlockType.COMMAND, - text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", - arguments: { - ObjA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - ObjB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObjectB", - }, - VA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - VB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,1,0]", - }, - }, - }, - { - opcode: "revoluteJoint", - blockType: Scratch.BlockType.COMMAND, - text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", - arguments: { - ObjA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - ObjB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObjectB", - }, - VA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - VB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,1,0]", - }, - X: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[1,0,0]", - }, - }, - }, - "---", - { - blockType: Scratch.BlockType.LABEL, - text: "- Collider", - }, - { - opcode: "setC", - blockType: Scratch.BlockType.COMMAND, - text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "colliderSets", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - }, - }, - }, - { - opcode: "getC", - blockType: Scratch.BlockType.REPORTER, - text: "get collider [PROPERTY] of [OBJECT]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "colliderProperties", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "sensorSingle", - blockType: Scratch.BlockType.BOOLEAN, - text: "is sensor [SENSOR] touching [OBJECT]?", - arguments: { - SENSOR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySensor", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - { - opcode: "sensorAll", - blockType: Scratch.BlockType.REPORTER, - text: "objects touching sensor [SENSOR]", - arguments: { - SENSOR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySensor", - }, - }, - }, - ], - menus: { - wProp: { - acceptReporters: false, - items: [{ - text: "Gravity", - value: "gravity", - }, - { - text: "log to console", - value: "log", - }, - ], - }, - tf: { - acceptReporters: true, - items: [{ - text: "false", - value: "false", - }, - { - text: "true", - value: "true", - }, - ], - }, - lockAxes: { - acceptReporters: false, - items: [{ - text: "Translation", - value: "setEnabledTranslations", - }, - { - text: "Rotation", - value: "setEnabledRotations", - }, - ], - }, - rigidBodyProperties: { - acceptReporters: false, - items: [{ - text: "Type", - value: "bodyType", - }, - { - text: "Linear Velocity", - value: "linvel", - }, - { - text: "Angular Velocity", - value: "angvel", - }, - { - text: "Translation (position)", - value: "translation", - }, - { - text: "Rotation (quaternion)", - value: "rotation", - }, - { - text: "Mass", - value: "mass", - }, - //{text: "Center of Mass", value: "centerOfMass"}, - { - text: "Linear Damping", - value: "linearDamping", - }, - { - text: "Angular Damping", - value: "angularDamping", - }, - { - text: "Is Sleeping?", - value: "isSleeping", - }, - //{text: "Can Sleep?", value: "isCanSleep"}, - { - text: "Gravity Scale", - value: "gravityScale", - }, - { - text: "Is Fixed?", - value: "isFixed", - }, - { - text: "Is Dynamic?", - value: "isDynamic", - }, - { - text: "Is Kinematic?", - value: "isKinematic", - }, - //{text: "Sleeping", value: "sleeping"} - ], - }, - rigidBodySets: { - acceptReporters: false, - items: [ - //{text: "Linear Velocity", value: "setLinvel"}, - //{text: "Angular Velocity", value: "setAngvel"}, - //{text: "Mass", value: "setMass"}, - { - text: "Gravity Scale", - value: "setGravityScale", - }, - //{text: "Can Sleep?", value: "setCanSleep"}, - //{text: "Sleeping", value: "sleeping"}, - { - text: "Linear Damping", - value: "setLinearDamping", - }, - { - text: "Angular Damping", - value: "setAngularDamping", - }, - { - text: "Is Fixed?", - value: "isFixed", - }, - { - text: "Is Dynamic?", - value: "isDynamic", - }, - { - text: "Is Kinematic?", - value: "isKinematic", - }, - ], - }, - colliderProperties: { - acceptReporters: false, - items: [ - //{text: "Collider Type", value: "type"}, - { - text: "Is Sensor?", - value: "isSensor", - }, - { - text: "Friction", - value: "friction", - }, - { - text: "Restitution", - value: "restitution", - }, - { - text: "Density", - value: "density", - }, - { - text: "Mass", - value: "mass", - }, - { - text: "Position", - value: "translation", - }, - { - text: "Rotation", - value: "rotation", - }, - //{text: "Area", value: "area"}, - { - text: "Volume", - value: "volume", - }, - { - text: "Collision Groups", - value: "collisionGroups", - }, - //{text: "Collision Mask", value: "collisionMask"}, - //{text: "Is Enabled?", value: "enabled"}, - //{text: "Contact Count", value: "contactCount"}, - //{text: "RigidBody Handle", value: "rigidBody"} - ], - }, - colliderSets: { - acceptReporters: false, - items: [{ - text: "Friction", - value: "setFriction", - }, - { - text: "Restitution", - value: "setRestitution", - }, - { - text: "Density", - value: "setDensity", - }, - { - text: "Is Sensor?", - value: "setSensor", - }, - { - text: "Collision Groups", - value: "setCollisionGroups", - }, - //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool - //{text: "Position Offset", value: "setTranslation"}, - //{text: "Rotation Offset", value: "setRotation"} - ], - }, - state: { - acceptReporters: true, - items: [{ - text: "on", - value: "true", - }, - { - text: "off", - value: "false", - }, - ], - }, - state2: { - acceptReporters: true, - items: [{ - text: "false", - value: "false", - }, - { - text: "true (must be fixed)", - value: "true", - }, - ], - }, - spaces: { - acceptReporters: false, - items: [{ - text: "World", - value: "world", - }, - { - text: "Local", - value: "local", - }, - ], - }, - objectTypes: { - acceptReporters: false, - items: [{ - text: "Dynamic", - value: "dynamic", - }, - { - text: "Fixed", - value: "fixed", - }, - { - text: "Kinematic Position Based", - value: "kinematicPositionBased", - }, - ], - }, - colliderTypes: { - acceptReporters: false, - items: [{ - text: "Box, Rectangle, cuboid", - value: "cuboid", - }, - { - text: "Sphere, ball", - value: "ball", - }, - { - text: "Custom, complex simple shapes, convexHull", - value: "convexHull", - }, - { - text: "Precision, TriMesh", - value: "trimesh", - }, - ], - }, - forces: { - acceptReporters: false, - items: [{ - text: "Force", - value: "addForce", - }, - { - text: "Torque (rotation)", - value: "addTorque", - }, - { - text: "Apply Impulse", - value: "applyImpulse", - }, - { - text: "Apply Torque Impulse (rotation)", - value: "applyTorqueImpulse", - }, - { - text: "Linear Velocity", - value: "setLinvel", - }, - { - text: "Angular Velocity", - value: "setAngvel", - }, - ], - }, - resetF: { - acceptReporters: false, - items: [{ - text: "Forces", - value: "resetForces", - }, - { - text: "Torques", - value: "resetTorques", - }, - ], - }, - }, - }; - } - joint(jointData, bodyA, bodyB) { - physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); - } - - fixedJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - let RA = JSON.parse(args.RA).map(Number); - let RB = JSON.parse(args.RB).map(Number); - - RA = new THREE.Quaternion().setFromEuler( - new THREE.Euler( - THREE.MathUtils.degToRad(RA[0]), - THREE.MathUtils.degToRad(RA[1]), - THREE.MathUtils.degToRad(RA[2]) - ) - ); - RB = new THREE.Quaternion().setFromEuler( - new THREE.Euler( - THREE.MathUtils.degToRad(RB[0]), - THREE.MathUtils.degToRad(RB[1]), - THREE.MathUtils.degToRad(RB[2]) - ) - ); - - const data = RAPIER.JointData.fixed({ - x: VA[0], - y: VA[1], - z: VA[2], - }, - RA, { - x: VB[0], - y: VB[1], - z: VB[2], - }, - RB - ); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - sphericalJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - - const data = RAPIER.JointData.spherical({ - x: VA[0], - y: VA[1], - z: VA[2], - }, { - x: VB[0], - y: VB[1], - z: VB[2], - }); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - revoluteJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - const x = JSON.parse(args.X).map(Number); - - const data = RAPIER.JointData.revolute({ - x: VA[0], - y: VA[1], - z: VA[2], - }, { - x: VB[0], - y: VB[1], - z: VB[2], - }, { - x: x[0], - y: x[1], - z: x[2], - }); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - prismaticJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - const x = JSON.parse(args.X).map(Number); - - const data = RAPIER.JointData.prismatic({ - x: VA[0], - y: VA[1], - z: VA[2], - }, { - x: VB[0], - y: VB[1], - z: VB[2], - }, { - x: x[0], - y: x[1], - z: x[2], - }); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - createWorld(args) { - const v3 = JSON.parse(args.G).map(Number); - const gravity = { - x: v3[0], - y: v3[1], - z: v3[2], - }; - physicsWorld = new RAPIER.World(gravity); - - console.log(physicsWorld); - } - - getWorld(args) { - if (args.PROPERTY === "log") { - console.log(physicsWorld); - return "logged"; - } - return JSON.stringify(physicsWorld[args.PROPERTY]); - } - - setRB(args) { - let value = args.VALUE; - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; - let object = getObject(args.OBJECT); - object.rigidBody[args.PROPERTY](value); - } - setC(args) { - let value = args.VALUE; - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; - let object = getObject(args.OBJECT); - object.collider[args.PROPERTY](value); - } - - getRB(args) { - let object = getObject(args.OBJECT); - return JSON.stringify(object.rigidBody[args.PROPERTY]()); - } - getC(args) { - let object = getObject(args.OBJECT); - return JSON.stringify(object.collider[args.PROPERTY]()); - } - - lockObjectAxis(args) { - let object = getObject(args.OBJECT); - const x = !JSON.parse(args.X); - const y = !JSON.parse(args.Y); - const z = !JSON.parse(args.Z); - object.rigidBody[args.PROPERTY](x, y, z, true); //changes is xyz, wake up - } - - objectPhysics(args) { - let object = getObject(args.OBJECT); - object.physics = JSON.parse(args.state); - - if (JSON.parse(args.state)) { - //if already exists delete: - if (object.rigidBody) { - physicsWorld.removeRigidBody(object.rigidBody); - object.rigidBody = null; - object.collider = null; - } - /*asing a rigidbody and collider to object and add them to physicsWorld*/ - let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() - .setTranslation(object.position.x, object.position.y, object.position.z) - .setRotation({ - w: object.quaternion._w, - x: object.quaternion._x, - y: object.quaternion._y, - z: object.quaternion._z, - }); - - let colliderDesc; - switch (args.collider) { - case "cuboid": - colliderDesc = createCuboidCollider(object); - break; - case "ball": - colliderDesc = createBallCollider(object); - break; - case "convexHull": - colliderDesc = createConvexHullCollider(object); - break; - case "trimesh": - colliderDesc = TriMesh(object); - break; - } - colliderDesc - .setSensor(JSON.parse(args.state2)) - .setMass(args.mass) - .setDensity(args.density) - .setFriction(args.friction); - - let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc); - let collider = physicsWorld.createCollider(colliderDesc, rigidBody); - - object.rigidBody = rigidBody; - object.collider = collider; - } else { - /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ - physicsWorld.removeRigidBody(object.rigidBody); - object.rigidBody = null; - object.collider = null; - } - } - - enableCCD(args) { - let object = getObject(args.OBJECT); - if (object.physics) { - let rigidBody = object.rigidBody; - rigidBody.enableCcd(JSON.parse(args.state)); - } - } - - addForce(args) { - let object = getObject(args.OBJECT); - const vector = JSON.parse(args.VALUE).map(Number); - - let force = new THREE.Vector3(vector[0], vector[1], vector[2]); - if (args.SPACE === "local") { - force.applyQuaternion(object.quaternion); - } - - object.rigidBody[args.PROPERTY](force, true); - } - - resetForces(args) { - rigidBody[args.PROPERTY](true); - } - - sensorSingle(args) { - const sensor = getObject(args.SENSOR); - - let object = getObject(args.OBJECT); - - let touching = false; - physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { - if (otherCollider === object.collider) touching = true; - }); - - return touching; - } - - sensorAll(args) { - const sensor = getObject(args.SENSOR); - - const touchedObjects = []; - - // loop thruogh every collider touching sensor - physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { - // find owner of collider - const otherObject = scene.children.find((o) => o.collider === otherCollider); - console.log(otherCollider); - if (otherObject) touchedObjects.push(otherObject.name); - }); - - return JSON.stringify(touchedObjects); - } - } - Scratch.extensions.register(new RapierPhysics()); - - //Thanks to the PointerLock extension of Turbowarp - const mouse = vm.runtime.ioDevices.mouse; - let isLocked = false; - let isPointerLockEnabled = false; - - let rect = threeRenderer.domElement.getBoundingClientRect(); - document.addEventListener("resize", () => { - rect = threeRenderer.domElement.getBoundingClientRect(); - }); - - const postMouseData = (e, isDown) => { - const { - movementX, - movementY - } = e; - const { - width, - height - } = rect; - const x = mouse._clientX + movementX; - const y = mouse._clientY - movementY; - mouse._clientX = x; - mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); - mouse._clientY = y; - mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); - if (typeof isDown === "boolean") { - const data = { - button: e.button, - isDown, - }; - originalPostIOData(data); - } - }; - - const mouseDevice = vm.runtime.ioDevices.mouse; - const originalPostIOData = mouseDevice.postData.bind(mouseDevice); - mouseDevice.postData = (data) => { - if (!isPointerLockEnabled) { - return originalPostIOData(data); - } - }; - - document.addEventListener( - "mousedown", - (e) => { - // @ts-expect-error - if (threeRenderer.domElement.contains(e.target)) { - if (isLocked) { - postMouseData(e, true); - } else if (isPointerLockEnabled) { - threeRenderer.domElement.requestPointerLock(); - } - } - }, - true - ); - document.addEventListener( - "mouseup", - (e) => { - if (isLocked) { - postMouseData(e, false); - // @ts-expect-error - } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { - threeRenderer.domElement.requestPointerLock(); - } - }, - true - ); - document.addEventListener( - "mousemove", - (e) => { - if (isLocked) { - postMouseData(e); - } - }, - true - ); - - document.addEventListener("pointerlockchange", () => { - isLocked = document.pointerLockElement === threeRenderer.domElement; - }); - document.addEventListener("pointerlockerror", (e) => { - console.error("Pointer lock error", e); - }); - - const oldStep = vm.runtime._step; - vm.runtime._step = function(...args) { - const ret = oldStep.call(this, ...args); - if (isPointerLockEnabled) { - const { - width, - height - } = rect; - mouse._clientX = width / 2; - mouse._clientY = height / 2; - mouse._scratchX = 0; - mouse._scratchY = 0; - } - return ret; - }; - - vm.runtime.on("PROJECT_LOADED", () => { - isPointerLockEnabled = false; - if (isLocked) { - document.exitPointerLock(); - } - }); - - class Pointerlock { - getInfo() { - return { - id: "threepointerlockmod", - name: "Pointerlock for Extra 3D", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "setLocked", - blockType: Scratch.BlockType.COMMAND, - text: "set pointer lock [enabled]", - arguments: { - enabled: { - type: Scratch.ArgumentType.STRING, - defaultValue: "true", - menu: "enabled", - }, - }, - }, - { - opcode: "isLocked", - blockType: Scratch.BlockType.BOOLEAN, - text: "pointer locked?", - }, - ], - menus: { - enabled: { - acceptReporters: true, - items: [{ - text: "enabled", - value: "true", - }, - { - text: "disabled", - value: "false", - }, - ], - }, - }, - }; - } - - setLocked(args) { - isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; - if (!isPointerLockEnabled && isLocked) { - document.exitPointerLock(); - } - } - - isLocked() { - return isLocked; - } - } - Scratch.extensions.register(new Pointerlock()); - }); -})(Scratch); diff --git a/threejsD_BASE_13852.js b/threejsD_BASE_13852.js deleted file mode 100644 index ced1928..0000000 --- a/threejsD_BASE_13852.js +++ /dev/null @@ -1,2413 +0,0 @@ -// Name: Extra 3D -// ID: threejsExtension -// Description: Use three js inside Turbowarp! A 3D graphics library. -// By: Civero -// License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors - -(function (Scratch) { - "use strict"; - - if (!Scratch.extensions.unsandboxed) { - throw new Error("Three-D extension must run unsandboxed"); - } - - if (Scratch.vm.runtime.isPackaged) {alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); return} - //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return - - const vm = Scratch.vm; - const runtime = vm.runtime - const renderer = Scratch.renderer; - const canvas = renderer.canvas - const Cast = Scratch.Cast; - const menuIconURI = "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; - - let alerts = false - console.log("alerts are "+ (alerts ? "enabled" : "disabled")) - - let isMouseDown = { left: false, middle: false, right: false } - let prevMouse = { left: false, middle: false, right: false } - - let lastWidth = 0 - let lastHeight = 0 - - let THREE - let clock - let running - let loopId - //Addons - let GLTFLoader - let gltf - let OrbitControls - let controls - let BufferGeometryUtils - let TextGeometry - let fontLoad - //Physics - let RAPIER - let physicsWorld - - let threeRenderer - let scene - let camera - let eulerOrder = "YXZ" - - let composer - let passes = {} - let customEffects = [] - let renderTargets = {} - - let materials = {} - let geometries = {} - let lights = {} - let models = {} - - let assets = { //should i place materials, geometries; inside too? - textures: {}, - colors: {}, - fogs: {}, - curves: {}, - renderTargets: {}, //not the same as the global one! this one only stores textures - } - - let raycastResult = [] - - function resetor(level) { - camera = undefined - composer.reset() - - passes = {} - customEffects = [] - renderTargets = {} - - materials = {} - geometries = {} - lights = {} - models = {} - - if (level > 0) { - assets = { - textures: {}, - colors: {}, - fogs: {}, - curves: {}, - renderTargets: {}, - } - } - - updateComposers() - } - -//utility - function vector3ToString(prop) { - if (!prop) return "0,0,0"; - - const x = (typeof(prop.x) === "number") ? prop.x : (typeof(prop._x) === "number") ? prop._x : (JSON.stringify(prop).includes("X")) ? prop : 0 - const y = (typeof(prop.y) === "number") ? prop.y : (typeof(prop.y) === "number") ? prop._y : 0 - const z = (typeof(prop.z) === "number") ? prop.z : (typeof(prop.z) === "number") ? prop.z : 0 - - return [x, y, z] - } - -//objects - function createObject(name, content, parentName) { - let object = getObject(name, true) - if (object) { - removeObject(name) - alerts ? alert(name + " already exsisted, will replace!") : null - } - content.name = name - content.rotation._order = eulerOrder - parentName === scene.name ? object = scene : object = getObject(parentName) - content.physics = false - - object.add(content) - } - function removeObject(name) { - let object = getObject(name) - if (!object) return - - scene.remove(object) - - if (object.rigidBody) { - physicsWorld.removeCollider(object.collider, true) - physicsWorld.removeRigidBody(object.rigidBody, true) - object.rigidBody = null - object.collider = null - } - if (object.isLight) { - delete(lights[name]) - } - } - function getObject(name, isNew) { - let object = null - if (!scene) { - alerts ? alert("Can not get " + name + ". Create a scene first!") : null; return;} - object = scene.getObjectByName(name) - if (!object && !isNew) {alerts ? alert(name + " does not exist! Add it to scene"):null; return;} - return object - } - -//materials - function encodeCostume (name) { - if (name.startsWith("data:image/")) return name - return Scratch.vm.editingTarget.sprite.costumes.find(c => c.name === name).asset.encodeDataURI() - } - function setTexutre (texture, mode, style, x, y) { - texture.colorSpace = THREE.SRGBColorSpace - - if (mode === "Pixelate") { - texture.minFilter = THREE.NearestFilter; - texture.magFilter = THREE.NearestFilter; - } else { //Blur - texture.minFilter = THREE.NearestMipmapLinearFilter - texture.magFilter = THREE.NearestMipmapLinearFilter - } - - if (style === "Repeat") { - texture.wrapS = THREE.RepeatWrapping - texture.wrapT = THREE.RepeatWrapping - texture.repeat.set(x, y) - } - - texture.generateMipmaps = true; - } - async function resizeImageToSquare(uri, size = 256) { - return new Promise((resolve) => { - const img = new Image() - img.onload = () => { - const canvas = document.createElement('canvas') - canvas.width = size - canvas.height = size - const ctx = canvas.getContext('2d') - - // clear + draw image scaled to fit canvas - ctx.clearRect(0, 0, size, size) - ctx.drawImage(img, 0, 0, size, size) - - resolve(canvas.toDataURL()) // return normalized Data URI - //delete canvas? - }; - img.src = uri - }); -} -//light -function updateShadowFrustum(light, focusPos) { - if (light.type !== "DirectionalLight") return - - // Frustum Size - Increase this value to cover a larger area. - const d = 50; - - // Update Orthographic Shadow Camera Frustum - const shadowCamera = light.shadow.camera; - - // Set the width/height of the frustum - shadowCamera.left = -d; - shadowCamera.right = d; - shadowCamera.top = d; - shadowCamera.bottom = -d; - - // Determine ranges - shadowCamera.near = 0.1 - shadowCamera.far = 500 - - // Position the Light and its Target - light.target.position.copy(focusPos); - const direction = light.position.clone().sub(light.target.position).normalize(); - light.position.copy(focusPos.clone().add(direction.multiplyScalar(100))); - - // Ensure matrices are updated. - light.target.updateMatrixWorld(); - light.shadow.camera.updateProjectionMatrix() - light.shadow.needsUpdate = true; -} -//composer -function updateComposers() { - if (!camera || !scene) return; // nothing to do yet - - // always recreate the RenderPass to point to the current scene/camera - passes["Render"] = new RenderPass(scene, camera); - - // ensure composer has a RenderPass as the first pass - const hasRender = composer.passes.some(p => p && p.scene); - if (!hasRender) composer.addPass(passes["Render"]); - else { - // if composer already has one, replace it so it references current scene/camera - const idx = composer.passes.findIndex(p => p && p.scene); - composer.passes[idx] = passes["Render"]; - } -} -//utility -function getMouseNDC(event) { - // Use threeRenderer.domElement for correct offset - const rect = threeRenderer.domElement.getBoundingClientRect(); - const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - return [x, y]; -} -function checkCanvasSize() { - const { width, height } = canvas - if (width !== lastWidth || height !== lastHeight) { - lastWidth = width - lastHeight = height - resize() - } - requestAnimationFrame(checkCanvasSize) //rerun next frame -} -//physics -function computeWorldBoundingBox(mesh) { - // Create a Box3 in world coordinates - const box = new THREE.Box3().setFromObject(mesh); - const size = new THREE.Vector3(); - box.getSize(size); - const center = new THREE.Vector3(); - box.getCenter(center); - return { size, center }; -} -function createCuboidCollider(mesh) { - const { size } = computeWorldBoundingBox(mesh); - const collider = RAPIER.ColliderDesc.cuboid( - size.x / 2, - size.y / 2, - size.z / 2 - ) - return collider; -} -function createBallCollider(mesh) { - const { size } = computeWorldBoundingBox(mesh); - // radius = 1/2 of the largest verticie - const radius = Math.max(size.x, size.y, size.z) / 2; - const collider = RAPIER.ColliderDesc.ball(radius) - return collider //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) -} -function createConvexHullCollider(mesh) { - mesh.updateWorldMatrix(true, false); - - const position = mesh.geometry.attributes.position; - const vertices = []; - const vertex = new THREE.Vector3(); - - // Matrix for scale only - const scaleMatrix = new THREE.Matrix4().makeScale( - mesh.scale.x, - mesh.scale.y, - mesh.scale.z - ); - - for (let i = 0; i < position.count; i++) { - vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); - vertices.push(vertex.x, vertex.y, vertex.z); - } - - const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); - return collider; -} -function TriMesh(mesh) { - // Get the positions array (from your geoPoints function) -const positions = mesh.geometry.attributes.position.array; -const numVertices = positions.length / 3; - -// Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] -const indices = Array.from({ length: numVertices }, (_, i) => i); - -const collider = RAPIER.ColliderDesc.trimesh( - positions, - new Uint32Array(indices) -); - -return collider -} -function getModel(model, name) { - const file = runtime.getTargetForStage().getSounds().find(c => c.name === model) - if (!file) return - -return new Promise((resolve, reject) => { - gltf.parse( - file.asset.data.buffer, - "", - gltf => { - const root = gltf.scene - root.traverse(child => { - if (child.isMesh) { - child.castShadow = true - child.receiveShadow = true - } - }); - - const mixer = new THREE.AnimationMixer(root) - const actions = {} - gltf.animations.forEach(clip => { - const act = mixer.clipAction(clip) - act.clampWhenFinished = true - actions[clip.name] = act - }); - - models[name] = { root, mixer, actions } - resolve(root) - }, - error => { - console.error("Error parsing GLB model:", error) - reject(error) - } - )}) -} -async function openFileExplorer(format) { - return new Promise((resolve) => { - const input = document.createElement("input"); - input.type = "file" - input.accept = format - input.multiple = false - input.onchange = () => { - resolve(input.files) - input.remove() - }; - input.click(); - }) -} -function getMeshesUsingTexture(scene, targetTexture) { - const meshes = [] - - scene.traverse(object => { - if (object.material) { - const materials = Array.isArray(object.material) ? object.material : [object.material] - for (const material of materials) { - if (material.map === targetTexture) { - meshes.push(object) - break - } - } - } - }) - - return meshes -} -function getAsset(path) { - if (typeof(path) == "string") { //string? - if (path.includes("/")) { //has the /? - const value = path.split("/") - console.log(value[0], value[1]) - return assets[value[0]][value[1]] - } - } - - return JSON.parse(path) //boolean or number -} - -let mouseNDC = [0, 0] -//loops/init -function stopLoop() { - if (!running) return - running = false - - if (loopId) { - cancelAnimationFrame(loopId) - loopId = null - if (threeRenderer) threeRenderer.clear(); - } -} -async function load() { - if (!THREE) { - - // @ts-ignore - THREE = await import("https://esm.sh/three@0.180.0") - //Addons - GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js") - OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js") - BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js") - TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js") - const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js") - fontLoad = new FontLoader.FontLoader() - - const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8") - const { - EffectComposer, - EffectPass, - RenderPass, - - Effect, - BloomEffect, - GodRaysEffect, - DotScreenEffect, - DepthOfFieldEffect, - - BlendFunction - } = POSTPROCESSING - //so i can use them later as global - window.EffectComposer = EffectComposer; - window.EffectPass = EffectPass; - window.RenderPass = RenderPass; - window.Effect = Effect; - window.BloomEffect = BloomEffect; - window.GodRaysEffect = GodRaysEffect; - window.DotScreenEffect = DotScreenEffect; - window.DepthOfFieldEffect = DepthOfFieldEffect; - window.BlendFunction = BlendFunction; - - RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0") - await RAPIER.init() - - threeRenderer = new THREE.WebGLRenderer({ - powerPreference: "high-performance", - antialias: false, - stencil: false, - depth: true - }) - threeRenderer.setPixelRatio(window.devicePixelRatio) - threeRenderer.outputColorSpace = THREE.SRGBColorSpace // correct colors - threeRenderer.toneMapping = THREE.ACESFilmicToneMapping // HDR look (test) - //threeRenderer.toneMappingExposure = 1.0 //(test) - - threeRenderer.shadowMap.enabled = true - threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap // (optional) - threeRenderer.domElement.style.pointerEvents = 'auto' //will disable turbowarp mouse events, but enable threejs's - - gltf = new GLTFLoader.GLTFLoader() - clock = new THREE.Clock() - - // Example: create a composer - composer = new EffectComposer(threeRenderer, {frameBufferType: THREE.HalfFloatType}) - - renderer.addOverlay( threeRenderer.domElement, "manual" ) - renderer.addOverlay(canvas, "manual") - renderer.setBackgroundColor(1, 1, 1, 0) - - resize() - - window.addEventListener("mousedown", e => { - if (e.button === 0) isMouseDown.left = true - if (e.button === 1) isMouseDown.middle = true - if (e.button === 2) isMouseDown.right = true - }) - window.addEventListener("mouseup", e => { - if (e.button === 0) isMouseDown.left = false; prevMouse.left = false - if (e.button === 1) isMouseDown.middle = false; prevMouse.middle = false - if (e.button === 2) isMouseDown.right = false; prevMouse.right = false - }) - // prevent contextmenu on right click - threeRenderer.domElement.addEventListener("contextmenu", e => e.preventDefault()); - - threeRenderer.domElement.addEventListener('mousemove', (event) => { - mouseNDC = getMouseNDC(event); - }) - - running = false - load() - - startRenderLoop() - runtime.on('PROJECT_START', () => startRenderLoop()) - runtime.on('PROJECT_STOP_ALL', () => stopLoop()) - runtime.on('STAGE_SIZE_CHANGED', () => {requestAnimationFrame(() => resize())}) - //if (!runtime.isPackaged) checkCanvasSize() //only in editor - } - } -function startRenderLoop() { - if (running) return - running = true - - const loop = () => { - if (!running) return - //RAPIER - if (physicsWorld && scene) { - physicsWorld.step() - - scene.children.forEach(obj => { - if (!(obj.isMesh) || !(obj.physics)) return - if (obj.rigidBody) { - obj.position.copy(obj.rigidBody.translation()) - obj.quaternion.copy(obj.rigidBody.rotation()) - } - }) - - } - if (scene && camera) { - if (controls) controls.update() - - const delta = clock.getDelta() - Object.values(models).forEach( model => { if (model) model.mixer.update(delta) } ) - - Object.values(lights).forEach(light => updateShadowFrustum(light, camera.position)) - - //update custom effects time - customEffects.forEach(e => { - if (e.uniforms.get('time')) { - e.uniforms.get('time').value += delta - } - }) - Object.values(renderTargets).forEach(t => { - if ( t.camera.type == "PerspectiveCamera") { - t.camera.aspect = t.target.width / t.target.height - t.camera.updateProjectionMatrix() - } - // get meshes using the texture associated with this target - const displayMeshes = getMeshesUsingTexture(scene, t.target.texture) - - displayMeshes.forEach(mesh => { - mesh.visible = false - }) - - if (t.camera.type == "PerspectiveCamera") { - threeRenderer.setRenderTarget(t.target) - threeRenderer.clear(true, true, true) - threeRenderer.render(scene, t.camera) - } else { - t.target.clear(threeRenderer) - t.camera.update( threeRenderer, scene ) //cubeCamera - } - - displayMeshes.forEach(mesh => { - mesh.visible = true - }) - }) - - camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height - camera.updateProjectionMatrix() - threeRenderer.setRenderTarget(null) - composer.render(delta) - } - - loopId = requestAnimationFrame(loop) - } - - loopId = requestAnimationFrame(loop) -} - -function resize() { - const w = canvas.width - const h = canvas.height - - threeRenderer.setSize(w, h) - composer.setSize(w, h) - customEffects.forEach(e => { - if (e.uniforms.get('resolution')) { - e.uniforms.get('resolution').value.set(w,h) - } - }) - - if (camera) { - camera.aspect = w / h - camera.updateProjectionMatrix() -} -} -//wait until all packages are loaded -Promise.resolve(load()).then(() => { - - class threejsExtension { - getInfo() { - return { - id: "threejsExtension", - name: "Extra 3D", - color1: "#222222", - color2: "#222222", - color3: "#11cc99", - menuIconURI, - blockIconURI: menuIconURI, - - blocks: [ - {blockType: Scratch.BlockType.BUTTON, text: "Show Docs", func: "openDocs"}, - {blockType: Scratch.BlockType.BUTTON, text: "Toggle Alerts", func: "alerts"}, - ], - menus: {} - }} - openDocs(){ - open("https://civ3ro.github.io/extensions/Documentation/") - } - alerts() {alerts = !alerts; alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!")} - } - Scratch.extensions.register(new threejsExtension()) - - class ThreeRenderer { - getInfo() { - return { - id: "threeRenderer", - name: "Three Renderer", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "setRendererRatio", blockType: Scratch.BlockType.COMMAND, text: "set Pixel Ratio to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"}}}, - {opcode: "eulerOrder", blockType: Scratch.BlockType.COMMAND, text: "set euler order to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "YXZ"}}}, - ], - menus: {} - }} - - setRendererRatio(args) { - threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE) - } - eulerOrder(args) { - eulerOrder = args.VALUE - console.log("euler order set to", eulerOrder) - } - - } - Scratch.extensions.register(new ThreeRenderer()) - - class ThreeScene { - constructor() { - this.THREE = THREE; - this.scenes = {}; - } - - getInfo() { - return { - id: "threeScene", - name: "Three Scene", - color1: "#4638c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "newScene", blockType: Scratch.BlockType.COMMAND, text: "new Scene [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}}}, - - {opcode: "setSceneProperty", blockType: Scratch.BlockType.COMMAND, text: "set Scene [PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "sceneProperties", defaultValue: "background"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, - "---", - {opcode: "getSceneObjects", blockType: Scratch.BlockType.REPORTER, text: "get Scene [THING]", arguments:{THING: {type: Scratch.ArgumentType.STRING, menu: "sceneThings"}}}, - {opcode: "reset", blockType: Scratch.BlockType.COMMAND, text: "Reset Everything"} - ], - menus: { - sceneProperties: {acceptReporters: false, items: [ - {text: "Background", value: "background"},{text: "Background Blurriness", value: "backgroundBlurriness"},{text: "Background Intensity", value: "backgroundIntensity"},{text: "Background Rotation", value: "backgroundRotation"}, - {text: "Environment", value: "environment"},{text: "Environment Intensity", value: "environmentIntensity"},{text: "Environment Rotation", value: "environmentRotation"},{text: "Fog", value: "fog"}, - ]}, - sceneThings: {acceptReporters: false, items: ["Objects", "Materials", "Geometries","Lights","Scene Properties","Other assets"]}, - - } - }} - - newScene(args) { - scene = new THREE.Scene(); - scene.name = args.NAME - scene.background = new THREE.Color("#222") - //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - this.scenes = {...this.scenes, {scene}}; - resetor(0) - } - - reset() { - resetor(1) - } - - async setSceneProperty(args) { - const property = args.PROPERTY; - const value = getAsset(args.VALUE); - - scene[property] = value; - } - getSceneObjects(args){ - const names = []; - if (args.THING === "Objects") { - scene.traverse(obj => { - if (obj.name) names.push(obj.name); //if it has a name, add to list! - }); - } - else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)) - else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)) - else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)) - else if (args.THING === "Scene Properties") {console.log(scene); return "check console"} - else if (args.THING === "Other assets") return JSON.stringify(assets) - - return JSON.stringify(names); // if objects - } - - } - Scratch.extensions.register(new ThreeScene()) - - class ThreeCameras { - getInfo() { - return { - id: "threeCameras", - name: "Three Cameras", - color1: "#38c59bff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addCamera", blockType: Scratch.BlockType.COMMAND, text: "add camera [TYPE] [CAMERA] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "cameraTypes"}}}, - {opcode: "setCamera", blockType: Scratch.BlockType.COMMAND, text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "0.1", exemptFromNormalization: true}}}, - {opcode: "getCamera", blockType: Scratch.BlockType.REPORTER, text: "get camera [PROPERTY] of [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}}}, - "---", - {opcode: "renderSceneCamera", blockType: Scratch.BlockType.COMMAND, text: "set rendering camera to [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}}}, - "---", - {opcode: "cubeCamera", blockType: Scratch.BlockType.COMMAND, text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "cubeCamera"}, GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, - "---", - {opcode: "renderTarget", blockType: Scratch.BlockType.COMMAND, text: "set a RenderTarget: [RT] for camera [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, - {opcode: "sizeTarget", blockType: Scratch.BlockType.COMMAND, text: "set RenderTarget [RT] size to [W] [H]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, W: {type: Scratch.ArgumentType.NUMBER, defaultValue: 480}, H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 360},} }, - {opcode: "getTarget", blockType: Scratch.BlockType.REPORTER, text: "get RenderTarget: [RT] texture", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, - {opcode: "removeTarget", blockType: Scratch.BlockType.COMMAND, text: "remove RenderTarget: [RT]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, - ], - menus: { - cameraTypes: {acceptReporters: false, items: [ - {text: "Perspective", value: "PerspectiveCamera"}, - ]}, - cameraProperties: {acceptReporters: false, items: [ - {text: "Near", value: "near"},{text: "Far", value: "far"},{text: "FOV", value: "fov"},{text: "Focus (nothing...)", value: "focus"},{text: "Zoom", value: "zoom"}, - ]}, - } - }} - addCamera(args) { - let v2 = new THREE.Vector2() - threeRenderer.getSize(v2) - const object = new THREE.PerspectiveCamera(90, v2.x / v2.y ) - object.position.z = 3 - - createObject(args.CAMERA, object, args.GROUP) - } - setCamera(args) { - let object = getObject(args.CAMERA) - object[args.PROPERTY] = args.VALUE - object.updateProjectionMatrix() - } - getCamera(args) { - let object = getObject(args.CAMERA) - const value = JSON.stringify(object[args.PROPERTY]) - return value - } - renderSceneCamera(args) { - let object = getObject(args.CAMERA) - if (!object) return - camera = object - //reset composer, else it does not update. - composer.passes = [] - passes = {} - customEffects = [] - updateComposers() - } - - cubeCamera(args) { - // Create cube render target - const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true } ) - // Create cube camera - const cubeCamera = new THREE.CubeCamera( 0.1, 500, cubeRenderTarget ) - createObject(args.CAMERA, cubeCamera, args.GROUP) - - renderTargets[args.RT] = {target: cubeRenderTarget, camera: cubeCamera} - assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture - } - - renderTarget(args) { - let object = getObject(args.CAMERA) - const renderTarget = new THREE.WebGLRenderTarget( - 360, - 360, - { - generateMipmaps: false - } - ) - - renderTargets[args.RT] = {target: renderTarget, camera: object} - assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture - } - sizeTarget(args) { - renderTargets[args.RT].target.setSize(args.W, args.H) - } - getTarget(args) { - const t = renderTargets[args.RT].target.texture - console.log(t, renderTargets[args.RT]) - return `renderTargets/${t.uuid}` - } - removeTarget(args) { - delete(assets.renderTargets[renderTargets[args.RT].target.texture.uuid]) - renderTargets[args.RT].target.dispose() - delete(renderTargets[args.RT]) - } - } - Scratch.extensions.register(new ThreeCameras()) - - class ThreeObjects { - getInfo() { - return { - id: "threeObjects", - name: "Three Objects", - color1: "#38c567ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addObject", blockType: Scratch.BlockType.COMMAND, text: "add object [OBJECT3D] [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},TYPE: {type: Scratch.ArgumentType.STRING, menu: "objectTypes"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "cloneObject", blockType: Scratch.BlockType.COMMAND, text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myClone"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "setObject", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - {opcode: "getObject", blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of object [OBJECT3D]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}}}, - {opcode: "objectE", blockType: Scratch.BlockType.BOOLEAN, text: "is there an object [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "removeObject", blockType: Scratch.BlockType.COMMAND, text: "remove object [OBJECT3D] from scene", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - - {blockType: Scratch.BlockType.LABEL, text: " ↳ Transforms"}, - {opcode: "setObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, - //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, - //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of [OBJECT3D]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - - {blockType: Scratch.BlockType.LABEL, text: "↳ Materials"}, - {opcode: "newMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "new material [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "materialTypes", defaultValue: "MeshStandardMaterial"}}}, - {opcode: "materialE",extensions: ["colours_looks"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a material [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, - {opcode: "removeMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "remove material [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, - {opcode: "setMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [PROPERTY] of [NAME] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "materialProperties", defaultValue: "color"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, - {opcode: "setBlending",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] blending to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "blendModes"}}}, - {opcode: "setDepth",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] depth to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "depthModes"}}}, - - {blockType: Scratch.BlockType.LABEL, text: "↳ Geometries"}, - {opcode: "newGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new geometry [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "geometryTypes", defaultValue: "BoxGeometry"}}}, - {opcode: "geometryE",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a geometry [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - {opcode: "removeGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "remove geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - "---", - {opcode: "newGeo",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new empty geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, - {opcode: "geoPoints",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] vertex points to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, - {opcode: "geoUVs",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] UVs to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[UVs]"}}}, - "---", - {opcode: "splines", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create spline [NAME] from curve [CURVE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}}}, - {opcode: "splineModel", extensions: ["colours_operators"], blockType: Scratch.BlockType.COMMAND, text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, MODEL: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}, SPACING: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, - "---", - {blockType: Scratch.BlockType.BUTTON, text: "Convert font to JSON", func: "openConv"}, - {blockType: Scratch.BlockType.BUTTON, text: "Load JSON font file", func: "loadFont"}, - {opcode: "text", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myText"}, TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "C-369"}, FONT: {type: Scratch.ArgumentType.STRING, menu: "fonts"}, S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, D: {type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1}, CS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 6}}}, - ], - menus: { - objectVector3: {acceptReporters: false, items: [ - {text: "Positon", value: "position"},{text: "Rotation", value: "rotation"},{text: "Scale", value: "scale"},{text: "Facing Direction (.up)", value: "up"} - ]}, - objectProperties: {acceptReporters: false, items: [ - {text: "Geometry", value: "geometry"},{text: "Material", value: "material"},{text: "Visible (true/false)", value: "visible"}, - ]}, - objectTypes: { acceptReporters: false, items: [ - { text: "Mesh", value: "Mesh" }, { text: "Sprite", value: "Sprite" }, { text: "Points", value: "Points" }, { text: "Line", value: "Line" }, { text: "Group", value: "Group" } - ]}, - XYZ: {acceptReporters: false, items: [{text: "X", value: "x"},{text: "Y", value: "y"},{text: "Z", value: "z"}]}, - materialProperties: {acceptReporters: false, items: [ - "|GENERAL| <-- not a property", - { text: "Color", value: "color" }, - { text: "Map", value: "map" }, - { text: "Opacity", value: "opacity" }, - { text: "Transparent", value: "transparent" }, - { text: "Alpha Map", value: "alphaMap" }, - { text: "Alpha Test", value: "alphaTest" }, - { text: "Depth Test", value: "depthTest" }, - { text: "Depth Write", value: "depthWrite" }, - { text: "Color Write", value: "colorWrite" }, - { text: "Side", value: "side" }, - { text: "Visible", value: "visible" },/* - { text: "Blending", value: "blending" }, - { text: "Blend Src", value: "blendSrc" }, - { text: "Blend Dst", value: "blendDst" }, - { text: "Blend Equation", value: "blendEquation" }, - { text: "Blend Src Alpha", value: "blendSrcAlpha" }, - { text: "Blend Dst Alpha", value: "blendDstAlpha" }, - { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ - { text: "Blend Aplha", value: "blendAplha" }, - { text: "Blend Color", value: "blendColor" }, - { text: "Alpha Hash", value: "alphaHash" }, - { text: "Premultiplied Alpha", value: "premultipliedAlpha" }, - - { text: "Tone Mapped", value: "toneMapped" }, - { text: "Fog", value: "fog" }, - { text: "Flat Shading", value: "flatShading" }, - - "|MESH Standard / Physical| <-- not a property", - { text: "Metalness", value: "metalness" }, - { text: "Metalness Map", value: "metalnessMap" }, - { text: "Roughness", value: "roughness" }, - { text: "Reflectivity", value: "reflectivity" }, - { text: "Roughness Map", value: "roughnessMap" }, - { text: "Emissive", value: "emissive" }, - { text: "Emissive Intensity", value: "emissiveIntensity" }, - { text: "Emissive Map", value: "emissiveMap" }, - { text: "Env Map", value: "envMap" }, - { text: "Env Map Intensity", value: "envMapIntensity" }, - { text: "Env Map Rotation", value: "envMapRotation" }, - { text: "Ior", value: "ior" }, - { text: "Refraction Ratio", value: "refractionRatio" }, - { text: "Clearcoat", value: "clearcoat" }, - { text: "Clearcoat Map", value: "clearcoatMap" }, - { text: "Clearcoat Roughness", value: "clearcoatRoughness" }, - { text: "Clearcoat Roughness Map", value: "clearcoatRoughnessMap" }, - { text: "Dispersion", value: "dispersion" }, - { text: "Sheen", value: "sheen" }, - { text: "Sheen Color", value: "sheenColor" }, - { text: "Sheen Color Map", value: "sheenColorMap" }, - { text: "Sheen Roughness", value: "sheenRoughness" }, - { text: "Sheen Roughness Map", value: "sheenRoughnessMap" }, - { text: "Specular Color", value: "specularColor" }, - { text: "Specular Color Map", value: "specularColorMap" }, - { text: "Specular Intensity", value: "specularIntensity" }, - { text: "Specular Intensity Map", value: "specularIntensityMap" }, - { text: "Transmission", value: "transmission" }, - { text: "Transmission Map", value: "transmissionMap" }, - { text: "Thickness", value: "thickness" }, - { text: "Thickness Map", value: "thicknessMap" }, - { text: "Anisotropy", value: "anisotropy" }, - { text: "Anisotropy Map", value: "anisotropyMap" }, - { text: "Anisotropy Rotation", value: "anisotropyRotation" }, - { text: "Attenuation Distance", value: "attenuationDistance" }, - { text: "Attenuation Color", value: "attenuationColor" }, - { text: "Thickness", value: "thickness" }, - { text: "Iridescence", value: "iridescence" }, - { text: "Iridescence Ior", value: "iridescenceIOR" }, - { text: "Iridescence Map", value: "iridescenceMap" }, - { text: "Iridescence Thickness Range", value: "iridescenceThicknessRange" }, - - "|MESH Displacement / Normal / Bump| <-- not a property", - { text: "Displacement Map", value: "displacementMap" }, - { text: "Displacement Scale", value: "displacementScale" }, - { text: "Displacement Bias", value: "displacementBias" }, - { text: "Bump Map", value: "bumpMap" }, - { text: "Bump Scale", value: "bumpScale" }, - { text: "Normal Map Type", value: "normalMapType" }, - - "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", - { text: "Shininess", value: "shininess" }, - - { text: "Wireframe", value: "wireframe" }, - { text: "Wireframe Linewidth", value: "wireframeLinewidth" }, - { text: "Wireframe Linecap", value: "wireframeLinecap" }, - { text: "Wireframe Linejoin", value: "wireframeLinejoin" }, - - "|POINTS| <-- not a property", - { text: "Size", value: "size" }, - { text: "Size Attenuation", value: "sizeAttenuation" }, - - "|LINES| <-- not a property", - { text: "Scale", value: "scale" }, - { text: "Dash Size", value: "dashSize" }, - { text: "Gap Size", value: "gapSize" }, - - "|SPRITES| <-- not a property", - { text: "Rotation", value: "rotation" } -]}, - blendModes: {acceptReporters: false, items: [ - { text: "No Blending", value: "NoBlending" },{ text: "Normal Blending", value: "NormalBlending" },{ text: "Additive Blending", value: "AdditiveBlending" },{ text: "Subtractive Blending", value: "SubtractiveBlending" },{ text: "Multiply Blending", value: "MultiplyBlending" },{ text: "Custom Blending", value: "CustomBlending" } - ]}, - depthModes: {acceptReporters: false, items: [ - { text: "Never Depth", value: "NeverDepth" },{ text: "Always Depth", value: "AlwaysDepth" },{ text: "Equal Depth", value: "EqualDepth" },{ text: "Less Depth", value: "LessDepth" },{ text: "Less Equal Depth", value: "LessEqualDepth" },{ text: "Greater Equal Depth", value: "GreaterEqualDepth" },{ text: "Greater Depth", value: "GreaterDepth" },{ text: "Not Equal Depth", value: "NotEqualDepth" } - ]}, - materialTypes:{acceptReporters: false, items: [ - {text:"Mesh Basic Material",value:"MeshBasicMaterial"},{text:"Mesh Standard Material",value:"MeshStandardMaterial"},{text:"Mesh Physical Material",value:"MeshPhysicalMaterial"},{text:"Mesh Lambert Material",value:"MeshLambertMaterial"},{text:"Mesh Phong Material",value:"MeshPhongMaterial"},{text:"Mesh Depth Material",value:"MeshDepthMaterial"},{text:"Mesh Normal Material",value:"MeshNormalMaterial"},{text:"Mesh Matcap Material",value:"MeshMatcapMaterial"},{text:"Mesh Toon Material",value:"MeshToonMaterial"},{text:"Line Basic Material",value:"LineBasicMaterial"},{text:"Line Dashed Material",value:"LineDashedMaterial"},{text:"Points Material",value:"PointsMaterial"},{text:"Sprite Material",value:"SpriteMaterial"},{text:"Shadow Material",value:"ShadowMaterial"} - ]}, - textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, - textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, - geometryTypes: {acceptReporters: false, items: [ - {text: "Box Geometry", value: "BoxGeometry"},{text: "Sphere Geometry", value: "SphereGeometry"},{text: "Cylinder Geometry", value: "CylinderGeometry"},{text: "Plane Geometry", value: "PlaneGeometry"},{text: "Circle Geometry", value: "CircleGeometry"},{text: "Torus Geometry", value: "TorusGeometry"},{text: "Torus Knot Geometry", value: "TorusKnotGeometry"}, - ]}, - modelsList: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) - if (models.length < 1) return [["Load a model! (GLB Loader category)"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - fonts: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.json')) - if (models.length < 1) return [["Load a font!"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - - } - }} - - addObject(args) { - const object = new THREE[args.TYPE](); - - object.castShadow = true - object.receiveShadow = true - - createObject(args.OBJECT3D, object, args.GROUP) - } - cloneObject(args) { - let object = getObject(args.OBJECT3D) - const clone = object.clone(true) - clone.name - createObject(args.NAME, clone, args.GROUP) - } - setObjectV3(args) { - let object = getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) - - function degToRad(deg) { - return deg * Math.PI / 180; - } - - - if (object.rigidBody) { - const x = values[0] - const y = values[1] - const z = values[2] - if (args.PROPERTY === "rotation") { - const euler = new THREE.Euler( - degToRad(x), - degToRad(y), - degToRad(z), - 'YXZ' - ) - const quaternion = new THREE.Quaternion() - quaternion.setFromEuler(euler) - - object.rigidBody.setRotation({ - x: quaternion.x, - y: quaternion.y, - z: quaternion.z, - w: quaternion.w - }); - } else if (args.PROPERTY === "position") { - object.rigidBody.setTranslation({ x: x, y: y, z: z }, true) - } - return - } - - if (object.isCamera == true && controls) { - - } - - if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.set(0,0,0) - } - if (object.isDirectionalLight == true) {object.pos = new THREE.Vector3(...values); console.log(true, values, object.pos); return} - object[args.PROPERTY].set(...values); - - if (object.type == "CubeCamera") object.updateCoordinateSystem() - } - /* - changeObjectV3(args) { - getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) - - if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.x += values[0] - object.rotation.y += values[1] - object.rotation.z += values[2] - } - else { - object[args.PROPERTY].add(...values); - } - } - changeObjectXV3(args) { - getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "rotation") value = value * Math.PI / 180 - - object[args.PROPERTY][args.X] += value - } - */ - getObjectV3(args) { - let object = getObject(args.OBJECT3D) - if (!object) return - let values = vector3ToString(object[args.PROPERTY]) - if (args.PROPERTY === "rotation") { - const toDeg = Math.PI/180 - values = [values[0]/toDeg,values[1]/toDeg,values[2]/toDeg,] - } - - return JSON.stringify(values) - } - setObject(args){ - let object = getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "material") {const mat = materials[args.NAME]; if (mat) value = mat; else value = undefined} - else if (args.PROPERTY === "geometry") {const geo = geometries[args.NAME]; if (geo) value = geo; else value = undefined} - else value = !!value - - if (value == undefined) return //invalid geo/mat - object[args.PROPERTY] = value - } - getObject(args){ - let object = getObject(args.OBJECT3D) - if (!object) return - let value - if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; - else value = object.visible; - - return value - } - removeObject(args) { - removeObject(args.OBJECT3D) - } - objectE(args) { - return scene.children.map(o => o.name).includes(args.NAME) - } - -//defines - newMaterial(args) { - if (materials[args.NAME] && alerts) alert ("material already exists! will replace...") - const mat = new THREE[args.TYPE](); - mat.name = args.NAME; - - materials[args.NAME] = mat; - } - async setMaterial(args) { - if (typeof(args.VALUE) == "string" && args.VALUE.at(0) == "|") return - const mat = materials[args.NAME] - - let value = args.VALUE - - if (args.VALUE == "false") value = false - - if (args.PROPERTY == "side") {value = (args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide)} - else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)) - else value = getAsset(value) - - - console.log("o:", args.VALUE, typeof(args.VALUE)) - console.log("r:", value, typeof(value)) - - mat[args.PROPERTY] = await (value) //await incase its a texture - mat.needsUpdate = true - } - setBlending(args) { - const mat = materials[args.NAME] - mat.blending = THREE[args.VALUE] - mat.premultipliedAlpha = true - mat.needsUpdate = true - } - setDepth(args) { - const mat = materials[args.NAME] - mat.depthFunc = THREE[args.VALUE] - mat.needsUpdate = true - } - removeMaterial(args){ - const mat = materials[args.NAME] - mat.dispose() - delete(materials[args.NAME]) - } - materialE(args) { - return materials[args.NAME] ? true : false - } - - newGeometry(args) { - if (geometries[args.NAME] && alerts) alert ("geometry already exists! will replace...") - const geo = new THREE[args.TYPE]() - geo.name = args.NAME - - geometries[args.NAME] = geo - } - setGeometry(args) { - const geo = geometries[args.NAME] - geo[args.PROPERTY] = (args.VALUE) - - geo.needsUpdate = true; - } - removeGeometry(args){ - const geo = geometries[args.NAME] - geo.dispose() - delete(geometries[args.NAME]) - } - geometryE(args) { - return geometries[args.NAME] ? true : false - } - - newGeo(args) { - const geometry = new THREE.BufferGeometry() - geometry.name = args.NAME - geometries[args.NAME] = geometry - } - async geoPoints(args) { - const geometry = geometries[args.NAME] - const positions = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v3 of each vertex of each triangle - - geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3)) - geometry.computeVertexNormals() - - geometry.needsUpdate = true - } - geoUVs(args) { - const geometry = geometries[args.NAME] - const UVs = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v2 of each UV of each triangle - - geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(UVs), 2)) - geometry.needsUpdate = true - } - - splines(args) { - const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)) - geometry.name = args.NAME - - geometries[args.NAME] = geometry - } - - async splineModel(args) { - const model = await getModel(args.MODEL, args.NAME) - if (!model) return console.warn("Model not found:", args.MODEL) - - const curve = getAsset(args.CURVE) - const spacing = parseFloat(args.SPACING) || 1 - const curveLength = curve.getLength() - const divisions = Math.floor(curveLength / spacing) - - const geomList = [] - const matList = [] - - for (let i = 0; i <= divisions; i++) { - const t = i / divisions - const pos = curve.getPointAt(t) - const tangent = curve.getTangentAt(t) - - const temp = model.clone(true) - temp.position.copy(pos) - - const up = new THREE.Vector3(0, 0, 1) - const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()) - temp.quaternion.copy(quat) - - temp.updateMatrixWorld(true) - - temp.traverse(child => { - if (child.isMesh && child.geometry) { - const geom = child.geometry.clone() - geom.applyMatrix4(child.matrixWorld) - geomList.push(geom) - matList.push(child.material) //.clone() ? - } - }) - } - - const validGeoms = geomList.filter(g => { - const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position - if (!ok) console.warn("geometry skipped:", g) - return ok - }) - - const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true) - merged.computeBoundingBox() - merged.computeBoundingSphere() - - merged.name = args.NAME - geometries[args.NAME] = merged - matList.name = args.NAME - materials[args.NAME] = matList - } - - async text(args) { - const fontFile = runtime.getTargetForStage().getSounds().find(c => c.name === args.FONT) - if (!fontFile) return - - const json = new TextDecoder().decode(fontFile.asset.data.buffer) - const fontData = JSON.parse(json) - - const font = fontLoad.parse(fontData) - - const params = {font: font, size: JSON.parse(args.S), height: JSON.parse(args.D), curveSegments: JSON.parse(args.CS), bevelEnabled: false} - const geometry = new TextGeometry.TextGeometry(args.TEXT, params) - geometry.computeVertexNormals() - geometry.center() // optional, recenters the text - - - geometry.name = args.NAME - - geometries[args.NAME] = geometry - } - - async loadFont() { - openFileExplorer(".json").then(files => { - const file = files[0] - const reader = new FileReader() - - reader.onload = async (e) => { - const arrayBuffer = e.target.result - - // From lily's assets - // // Thank you PenguinMod for providing this code. - - const targetId = runtime.getTargetForStage().id //util.target.id not working! - const assetName = Cast.toString(file.name) - - const buffer = arrayBuffer - - const storage = runtime.storage - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ) - - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ) - alert("Font loaded successfully!") - } catch (e) { - console.error(e) - alert("Error loading font.") - } - - // End of PenguinMod - } - - reader.readAsArrayBuffer(file); - }) - } - openConv() {{open("https://gero3.github.io/facetype.js/")}} - - } - Scratch.extensions.register(new ThreeObjects()) - - class ThreeLights { - getInfo() { - return { - id: "threeLights", - name: "Three Lights", - color1: "#c7a22aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addLight", blockType: Scratch.BlockType.COMMAND, text: "add light [NAME] type [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "lightTypes"}}}, - {opcode: "setLight", blockType: Scratch.BlockType.COMMAND, text: "set light [NAME][PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lightProperties", defaultValue: "intensity"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1", exemptFromNormalization: true}}}, - ], - menus: { - lightTypes: {acceptReporters: false, items: [ - {text: "Ambient Light", value: "AmbientLight"},{text: "Directional Light", value: "DirectionalLight"},{text: "Point Light", value: "PointLight"},{text: "Hemisphere Light", value: "HemisphereLight"},{text: "Spot Light", value: "SpotLight"}, - ]}, - lightProperties: {acceptReporters: false, items: [ - {text: "Color", value: "color"},{text: "Intensity", value: "intensity"},{text: "Cast Shadow?", value: "castShadow"}, - {text: "Ground Color (HemisphereLight)", value: "groundColor"}, - {text: "Map (SpotLight)", value: "map"},{text: "Distance (SpotLight)", value: "distance"},{text: "Decay (SpotLight)", value: "decay"},{text: "Penumbra (SpotLight)", value: "penumbra"},{text: "Angle/Size (SpotLight)", value: "angle"},{text: "Power (SpotLight)", value: "power"}, - {text: "Target Position (Directional/SpotLight)", value: "target"}, - ]}, - } - }} - - addLight(args) { - const light = new THREE[args.TYPE](0xffffff, 1) - - createObject(args.NAME, light, args.GROUP) - lights[args.NAME] = light - if (light.type === "AmbientLight" || "HemisphereLight") return - - light.castShadow = true - if (light.type === "PointLight") return - //Directional & Spot Light - light.target.position.set(0, 0, 0) - scene.add(light.target) - - light.pos = new THREE.Vector3(0,0,0) - - light.shadow.mapSize.width = 4096 - light.shadow.mapSize.height = 2048 - - if (light.type === "SpotLight") { - light.decay = 0 - light.shadow.camera.near = 500; - light.shadow.camera.far = 4000; - light.shadow.camera.fov = 30; - } - light.shadow.needsUpdate = true - light.needsUpdate = true - } - - setLight(args) { - const light = lights[args.NAME] - if (!args.PROPERTY) return - if (args.PROPERTY === "target") { - light.target.position.set(...JSON.parse(args.VALUE)) //vector3 - light.target.updateMatrixWorld(); - } - else { - light[args.PROPERTY] = getAsset(args.VALUE) - } - light.needsUpdate = true - - if (light.type === "AmbientLight" || "HemisphereLight") return - - light.shadow.camera.updateProjectionMatrix(); - light.shadow.needsUpdate = true - } - - } - Scratch.extensions.register(new ThreeLights()) - - class ThreeUtilities { - getInfo() { - return { - id: "threeUtility", - name: "Three Utilities", - color1: "#3875c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "newVector2", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}}}, - {opcode: "newVector3", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y] [Z]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}, Z: {type: Scratch.ArgumentType.NUMBER}}}, - "---", - {opcode: "operateV3", blockType: Scratch.BlockType.REPORTER, text: "do [V3] [O] [V32]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, O: {type: Scratch.ArgumentType.STRING, menu: "operators"}, V32: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, - {opcode: "moveVector3", blockType: Scratch.BlockType.REPORTER, text: "move [S] steps in vector [V3] in direction [D3]", arguments: {S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, - {opcode: "directionTo", blockType: Scratch.BlockType.REPORTER, text: "direction from [V3] to [T3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, T3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, - "---", - {opcode: "newColor", blockType: Scratch.BlockType.REPORTER, text: "New Color [HEX]", arguments: {HEX: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff"}}}, - {opcode: "newFog", blockType: Scratch.BlockType.REPORTER, text: "New Fog [COLOR] [NEAR] [FAR]", arguments: {COLOR: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff", exemptFromNormalization: true}, NEAR: {type: Scratch.ArgumentType.NUMBER}, FAR: {type: Scratch.ArgumentType.NUMBER, defaultValue: 10}}}, - {opcode: "newTexture", blockType: Scratch.BlockType.REPORTER, text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, - {opcode: "newCubeTexture", blockType: Scratch.BlockType.REPORTER, text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", arguments: {"COSTUMEX0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEX1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ1": {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, - {opcode: "newEquirectangularTexture", blockType: Scratch.BlockType.REPORTER, text: "New Equirectangular Texture [COSTUME] [MODE]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"}}}, - "---", - {opcode: "curve", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", arguments: {TYPE: {type: Scratch.ArgumentType.STRING, menu: "curveTypes"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]"}, CLOSED: {type: Scratch.ArgumentType.STRING, defaultValue: "true"}}}, - "---", - {opcode:"mouseDown",extensions: ["colours_sensing"], blockType: Scratch.BlockType.BOOLEAN, text: "mouse [BUTTON] [action]?", arguments: {BUTTON: {type: Scratch.ArgumentType.STRING, menu: "mouseButtons"},action: {type: Scratch.ArgumentType.STRING, menu: "mouseAction"}}}, - {opcode: "mousePos",extensions: ["colours_sensing"], blockType: Scratch.BlockType.REPORTER, text: "mouse position", arguments: {}}, - "---", - {opcode: "getItem",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "get item [ITEM] of [ARRAY]", arguments: {ITEM: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}, ARRAY: {type: Scratch.ArgumentType.STRING, defaultValue: `["myObject", "myLight"]`}}}, - {blockType: Scratch.BlockType.LABEL, text: "↳ Raycasting"}, - {opcode: "raycast", blockType: Scratch.BlockType.COMMAND, text: "Raycast from [V3] in direction [D3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,1]"}}}, - {opcode: "getRaycast", blockType: Scratch.BlockType.REPORTER, text: "get raycast [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "raycastProperties"}}}, - - ], - menus: { - materialProperties: {acceptReporters: false, items: [ - {text: "Color", value: "color"},{text: "Map (texture)", value: "map"},{text: "Alpha Map (texture)", value: "alphaMap"},{text: "Alpha Test (0-1)", value: "alphaTest"},{text: "Side (front/back/double)", value: "side"},{text: "Bump Map (texture)", value: "bumpMap"},{text: "Bump Scale", value: "bumpScale"}, - ]}, - textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, - textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, - raycastProperties: {acceptReporters: false, items: [ - {text: "Intersected Object Names", value: "name"},{text: "Number of Objects", value: "number"},{text: "Intersected Objects distances", value: "distance"}, - ]}, - mouseButtons: {acceptReporters: false, items: ["left","middle","right"]}, - mouseAction: {acceptReporters: false, items: ["Down","Clicked"]}, - curveTypes: {acceptReporters: false, items: ["CatmullRomCurve3"]}, - operators: {acceptReporters: false, items: [ - "+","-","*","/","=","max","min","dot","cross","distance to","angle to","apply euler", - ]} - } - }} - mouseDown(args) { - if (args.action === "Down") return isMouseDown[args.BUTTON] - if (args.action === "Clicked") { - if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false - else prevMouse[args.BUTTON] = true; return true - } - } - mousePos(event) { - return JSON.stringify(mouseNDC) - } - newVector3(args) { - return JSON.stringify([args.X, args.Y, args.Z]) - } - operateV3(args){ - const v3 = new THREE.Vector3(...JSON.parse(args.V3)) - const v32 = new THREE.Vector3(...JSON.parse(args.V32)) - - let r - if (args.O == "+") r = v3.add(v32) - else if (args.O == "-") r = v3.sub(v32) - else if (args.O == "*") r = v3.multiply(v32) - else if (args.O == "/") r = v3.divide(v32) - else if (args.O == "=") r = v3.equals(v32) - else if (args.O == "max") r = v3.max(v32) - else if (args.O == "min") r = v3.min(v32) - else if (args.O == "dot") r = v3.dot(v32) - else if (args.O == "cross") r = v3.cross(v32) - else if (args.O == "distance to") r = v3.distanceTo(v32) - else if (args.O == "angle to") r = v3.angleTo(v32) - else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)) - - if (typeof(r) == "object") return JSON.stringify([r.x, r.y, r.z]) - else return JSON.stringify(r) - } - - newVector2(args) { - return JSON.stringify([args.X, args.Y]) - } - - moveVector3(args) { - const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); - const steps = Number(args.S); - - const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); - - const yaw = THREE.MathUtils.degToRad(yawInputDeg); - const pitch = THREE.MathUtils.degToRad(pitchInputDeg); - const roll = THREE.MathUtils.degToRad(rollInputDeg); - - const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); - - const forwardVector = new THREE.Vector3(0, 0, -1); - const direction = forwardVector.applyEuler(euler).normalize(); - - const newPos = currentPos.add(direction.multiplyScalar(steps)); - return JSON.stringify([newPos.x, newPos.y, newPos.z]); - } - - directionTo(args) { - const v3 = new THREE.Vector3(...JSON.parse(args.V3)) - const toV3 = new THREE.Vector3(...JSON.parse(args.T3)) - - const direction = toV3.clone().sub(v3).normalize(); - // Pitch (X) - const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x*direction.x + direction.z*direction.z)); - // Yaw (Y) - const yaw = Math.atan2(direction.x, direction.z); - - // Roll always 0 - return JSON.stringify([180+THREE.MathUtils.radToDeg(pitch),THREE.MathUtils.radToDeg(yaw),0]) - } - - newColor(args) { - const color = new THREE.Color(args.HEX) - const uuid = crypto.randomUUID() - assets.colors[uuid] = color - return `colors/${uuid}` - } - newFog(args) { - const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR) - const uuid = crypto.randomUUID() - assets.fogs[uuid] = fog - return `fogs/${uuid}` - } - async newTexture(args) { - const textureURI = encodeCostume(args.COSTUME) - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - async newCubeTexture(args) { - const uris = [encodeCostume(args.COSTUMEX0),encodeCostume(args.COSTUMEX1), encodeCostume(args.COSTUMEY0),encodeCostume(args.COSTUMEY1), encodeCostume(args.COSTUMEZ0),encodeCostume(args.COSTUMEZ1)] - const normalized = await Promise.all(uris.map(uri => resizeImageToSquare(uri, 256))); - const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); - - texture.name = "CubeTexture" + args.COSTUMEX0; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - async newEquirectangularTexture(args) { - const textureURI = encodeCostume(args.COSTUME) - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME - texture.mapping = THREE.EquirectangularReflectionMapping - - setTexutre(texture, args.MODE) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - - curve(args) { - function parsePoints(input) { - // Match all [x,y,z] groups - const matches = input.match(/\[([^\]]+)\]/g) - if (!matches) return [] - - return matches.map(str => { - const nums = str - .replace(/[\[\]\s]/g, '') - .split(',') - .map(Number) - return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0) - }) - } - const points = parsePoints(args.POINTS) - const curve = new THREE[args.TYPE](points) - curve.closed = JSON.parse(args.CLOSED) - - const uuid = crypto.randomUUID() - assets.curves[uuid] = curve - return `curves/${uuid}` - } - - getItem(args) { - const items = JSON.parse(args.ARRAY) - const item = items[args.ITEM - 1] - if (!item) return "0" - return item - } - - raycast(args) { - const origin = new THREE.Vector3(...JSON.parse(args.V3)) - // rotation is in degrees => convert to radians first - const rot = JSON.parse(args.D3).map(v => v * Math.PI / 180) - - const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder) - const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize() - - const raycaster = new THREE.Raycaster() - //const camera = getObject(args.CAMERA) - raycaster.set( origin, direction ); - - const intersects = raycaster.intersectObjects( scene.children, true ) - - raycastResult = intersects - } - getRaycast(args) { - if (args.PROPERTY === "number") return raycastResult.length - if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map(i => i.distance)) - return JSON.stringify(raycastResult.map(i => i.object[args.PROPERTY])) - } - - } - Scratch.extensions.register(new ThreeUtilities()) - - class ThreeGLB { - getInfo() { - return { - id: "threeGLB", - name: "Three GLB Loader", - color1: "#c53838ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {blockType: Scratch.BlockType.BUTTON, text: "Load GLB File", func: "loadModelFile"}, - {opcode: "addModel", blockType: Scratch.BlockType.COMMAND, text: "add [ITEM] as [NAME] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},ITEM: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}}}, - {opcode: "getModel", blockType: Scratch.BlockType.REPORTER, text: "get object [PROPERTY] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "modelProperties"}}}, - {opcode: "playAnimation", blockType: Scratch.BlockType.COMMAND, text: "play animation [ANAME] of [NAME], [TIMES] times", arguments: {TIMES: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - {opcode: "pauseAnimation", blockType: Scratch.BlockType.COMMAND, text: "set [TOGGLE] animation [ANAME] of [NAME]", arguments: {TOGGLE: {type: Scratch.ArgumentType.NUMBER, menu: "pauseUn"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - {opcode: "stopAnimation", blockType: Scratch.BlockType.COMMAND, text: "stop animation [ANAME] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - - ], - menus: { - modelProperties: {acceptReporters: false, items: [ - {text: "Animations", value: "animations"}, - ]}, - pauseUn: {acceptReporters: true, items: [{text: "Pause", value: "true"},{text: "Unpasue", value: "false"},]}, - modelsList: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) - if (models.length < 1) return [["Load a model!"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - } - }} - - async loadModelFile() { - - openFileExplorer(".glb").then(files => { - const file = files[0]; - const reader = new FileReader(); - - reader.onload = async (e) => { - const arrayBuffer = e.target.result; - - { // From lily's assets - - // Thank you PenguinMod for providing this code. - { - const targetId = runtime.getTargetForStage().id; //util.target.id not working! - const assetName = Cast.toString(file.name); - - //const res = await Scratch.fetch(args.URL); - //const buffer = await res.arrayBuffer(); - const buffer = arrayBuffer - - const storage = runtime.storage; - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ); - - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ); - alert("Model loaded successfully!"); - } catch (e) { - console.error(e); - alert("Error loading model."); - } - } - // End of PenguinMod - } - }; - - reader.readAsArrayBuffer(file); - }) - - } - async addModel(args) { - const group = await getModel(args.ITEM, args.NAME) - - createObject(args.NAME, group, args.GROUP) - } - getModel(args){ - if (!models[args.NAME]) return; - return Object.keys(models[args.NAME].actions).toString() - } - - playAnimation(args) { - const model = models[args.NAME] - if (!model) {console.log("no model!"); return} - - const action = model.actions[args.ANAME] //clones of models dont have a stored actions! - if (!action) { - console.log("no action!") - return - } - - args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity) - - action.reset() - .play() - } - stopAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.stop(); - } - pauseAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.paused = args.TOGGLE - } - - } - Scratch.extensions.register(new ThreeGLB()) - - class ThreeAddons { - getInfo() { - return { - id: "threeAddons", - name: "Three Addons", - color1: "#c538a2ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {blockType: Scratch.BlockType.LABEL, text: "Orbit Control"}, - {opcode: "OrbitControl", blockType: Scratch.BlockType.COMMAND, text: "set addon Orbit Control [STATE]", arguments: {STATE: {type: Scratch.ArgumentType.STRING, menu: "onoff"},}}, - - {blockType: Scratch.BlockType.LABEL, text: "Post Processing"}, - {opcode: "resetComposer", blockType: Scratch.BlockType.COMMAND, text: "reset composer"}, - {opcode: "bloom", blockType: Scratch.BlockType.COMMAND, text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},I: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, T:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, - {opcode: "godRays", blockType: Scratch.BlockType.COMMAND, text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}, DEC:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.95}, DENS:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},EXP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1},WEI:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.4},RES:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},SAMP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 64},}}, - {opcode: "dots", blockType: Scratch.BlockType.COMMAND, text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, A: {type: Scratch.ArgumentType.ANGLE, defaultValue: 0},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, - {opcode: "depth", blockType: Scratch.BlockType.COMMAND, text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", arguments: {FD: {type: Scratch.ArgumentType.NUMBER, defaultValue: (3)},FL: {type: Scratch.ArgumentType.NUMBER, defaultValue: (0.001)},BS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 4},H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 240},OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}}}, - "---", - {opcode: "custom", blockType: Scratch.BlockType.COMMAND, text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myShader"}, FRA: {type: Scratch.ArgumentType.STRING}, VER: {type: Scratch.ArgumentType.STRING}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}, OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, - ], - menus: { - onoff: {acceptReporters: true, items: [{text: "enabled", value: "1"},{text: "disabled", value: "0"},]}, - blendModes: {acceptReporters: false, items: [ - "SKIP","SET","ADD","ALPHA","AVERAGE","COLOR","COLOR_BURN","COLOR_DODGE", - "DARKEN","DIFFERENCE","DIVIDE","DST","EXCLUSION","HARD_LIGHT","HARD_MIX", - "HUE","INVERT","INVERT_RGB","LIGHTEN","LINEAR_BURN","LINEAR_DODGE", - "LINEAR_LIGHT","LUMINOSITY","MULTIPLY","NEGATION","NORMAL","OVERLAY", - "PIN_LIGHT","REFLECT","SCREEN","SRC","SATURATION","SOFT_LIGHT","SUBTRACT", - "VIVID_LIGHT" - ]}, - } - }} - - OrbitControl(args) { - if (controls) controls.dispose() - - console.log("creating...", OrbitControls) - controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); - controls.enableDamping = true - - controls.enabled = !!args.STATE - console.log(controls) - } - - resetComposer() { - composer.passes = [] - passes = {} - customEffects = [] - updateComposers() - } - - bloom(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const bloomEffect = new BloomEffect({ - intensity: args.I, - luminanceThreshold: args.T, // ← correct key - luminanceSmoothing: args.S, - blendFunction: BlendFunction[args.BLEND], - }) - bloomEffect.blendMode.opacity.value = args.OP - - const pass = new EffectPass(camera, bloomEffect) - - composer.addPass(pass) - } - - godRays(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - let object = getObject(args.NAME) - const sun = object - - const godRays = new GodRaysEffect(camera, sun, { - resolutionScale: args.RES, - density: args.DENS, // ray density - decay: args.DEC, // fade out - weight: args.WEI, // brightness of rays - exposure: args.EXP, - samples: args.SAMP, - blendFunction: BlendFunction[args.BLEND], - }) - godRays.blendMode.opacity.value = args.OP - const pass = new EffectPass(camera, godRays) - composer.addPass(pass) - } - - dots(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const dot = new DotScreenEffect({ - angle: args.A, - scale: args.S, - blendFunction: BlendFunction[args.BLEND], - }) - dot.blendMode.opacity.value = args.OP - const pass = new EffectPass(camera, dot) - composer.addPass(pass) - } - - depth(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const dofEffect = new DepthOfFieldEffect(camera, { - focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) - focalLength: args.FL, // lens focal length in meters - bokehScale: args.BS, // strength/size of the blur circles - height: args.H, // resolution hint (affects quality/perf) - blendFunction: BlendFunction[args.BLEND], - }) - dofEffect.blendMode.opacity.value = args.OP - - const dofPass = new EffectPass(camera, dofEffect) - composer.addPass(dofPass) - } - - async custom(args) { - function cleanGLSL(glslCode) { - //delete multilines comments - let cleanedCode = glslCode.replace(/\/\*[\s\S]*?\*\//g, ' ') - .replace(/ /g, '\n') - .replace(/\/\/.*$/gm, ' ') - .replace(/; /g, ';\n') - - return cleanedCode; - } - - let fs = cleanGLSL(` - ${args.FRA} - `) - if (!args.FRA.trim()) {fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`} - const vs = cleanGLSL(` - ${args.VER} - `) - console.log(fs) - console.log(vs) - - const effect = new Effect( - "Custom", - fs, - { - blendFunction: BlendFunction[args.BLEND], - vertexShader: vs, - uniforms: new Map([ //uniforms usually in shaders... open to more! - ['time', new THREE.Uniform(0.0)], - ['resolution', new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height))] - ]), - defines: new Map([['USE_TIME', '1'], ['USE_VERTEX_TRANSFORM', '']]), - } - ); - - effect.blendMode.opacity.value = args.OP - - const pass = new EffectPass(camera, effect); - composer.addPass(pass); - - customEffects.push(effect); - } - - } - Scratch.extensions.register(new ThreeAddons()) - - class RapierPhysics { - getInfo() { - return { - id: "rapierPhysics", - name: "RAPIER Physics", - color1: "#222222", - color2: "#203024ff", - color3: "#78f07eff", - blocks: [ - {opcode: "createWorld", blockType: Scratch.BlockType.COMMAND, text: "create world | gravity:[G]", arguments: {G: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,-9.81,0]"}}}, - {opcode: "getWorld", blockType: Scratch.BlockType.REPORTER, text: "get world [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "wProp"}}}, - "---", - {opcode: "objectPhysics", blockType: Scratch.BlockType.COMMAND, text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", arguments: {state2: {type: Scratch.ArgumentType.STRING, menu: "state2"},state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"}, type: {type: Scratch.ArgumentType.STRING, menu: "objectTypes", defaultValue: "dynamic"}, collider: {type: Scratch.ArgumentType.STRING, menu: "colliderTypes", defaultValue: "cuboid"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},mass: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},density: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},friction: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0.5"}}}, - "---", - {blockType: Scratch.BlockType.LABEL, text: "- RigidBody"}, - {opcode: "setRB", blockType: Scratch.BlockType.COMMAND, text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodySets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getRB", blockType: Scratch.BlockType.REPORTER, text: "get rigidbody [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodyProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "lockObjectAxis", blockType: Scratch.BlockType.COMMAND, text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", arguments: {OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lockAxes"}, X: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Y: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Z: {type: Scratch.ArgumentType.STRING, menu: "tf"}}}, - "---", - {opcode: "addForce", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,10,0]"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "forces", defaultValue: "addForce"},SPACE: {type: Scratch.ArgumentType.STRING, menu: "spaces", defaultValue: "world"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "resetForces", blockType: Scratch.BlockType.COMMAND, text: "reset [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "resetF", defaultValue: "resetForces"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "enableCCD", blockType: Scratch.BlockType.COMMAND, text: "enable Continuous Collision Detection for [OBJECT] [state]", arguments: {state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "oPropS", defaultValue: "physics"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "fixedJoint", blockType: Scratch.BlockType.COMMAND, text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},RA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},RB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},}}, - {opcode: "sphericalJoint", blockType: Scratch.BlockType.COMMAND, text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},}}, - {opcode: "revoluteJoint", blockType: Scratch.BlockType.COMMAND, text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},X: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"},}}, - "---", - {blockType: Scratch.BlockType.LABEL, text: "- Collider"}, - {opcode: "setC", blockType: Scratch.BlockType.COMMAND, text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderSets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getC", blockType: Scratch.BlockType.REPORTER, text: "get collider [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "sensorSingle", blockType: Scratch.BlockType.BOOLEAN, text: "is sensor [SENSOR] touching [OBJECT]?", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "sensorAll", blockType: Scratch.BlockType.REPORTER, text: "objects touching sensor [SENSOR]", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}}} - ], - menus: { - wProp: {acceptReporters: false, items: [ - {text: "Gravity", value: "gravity"}, {text: "log to console", value: "log"} - ]}, - tf: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true", value: "true"}]}, - lockAxes: {acceptReporters: false, items: [ - {text: "Translation", value: "setEnabledTranslations"}, {text: "Rotation", value: "setEnabledRotations"} - ]}, - rigidBodyProperties: {acceptReporters: false, items: [ - {text: "Type", value: "bodyType"}, - {text: "Linear Velocity", value: "linvel"}, - {text: "Angular Velocity", value: "angvel"}, - {text: "Translation (position)", value: "translation"}, - {text: "Rotation (quaternion)", value: "rotation"}, - {text: "Mass", value: "mass"}, - //{text: "Center of Mass", value: "centerOfMass"}, - {text: "Linear Damping", value: "linearDamping"}, - {text: "Angular Damping", value: "angularDamping"}, - {text: "Is Sleeping?", value: "isSleeping"}, - //{text: "Can Sleep?", value: "isCanSleep"}, - {text: "Gravity Scale", value: "gravityScale"}, - {text: "Is Fixed?", value: "isFixed"}, - {text: "Is Dynamic?", value: "isDynamic"}, - {text: "Is Kinematic?", value: "isKinematic"}, - //{text: "Sleeping", value: "sleeping"} - ]}, - rigidBodySets: {acceptReporters: false, items: [ - //{text: "Linear Velocity", value: "setLinvel"}, - //{text: "Angular Velocity", value: "setAngvel"}, - //{text: "Mass", value: "setMass"}, - {text: "Gravity Scale", value: "setGravityScale"}, - //{text: "Can Sleep?", value: "setCanSleep"}, - //{text: "Sleeping", value: "sleeping"}, - {text: "Linear Damping", value: "setLinearDamping"}, - {text: "Angular Damping", value: "setAngularDamping"}, - {text: "Is Fixed?", value: "isFixed"}, - {text: "Is Dynamic?", value: "isDynamic"}, - {text: "Is Kinematic?", value: "isKinematic"} - ]}, - colliderProperties: {acceptReporters: false, items: [ - //{text: "Collider Type", value: "type"}, - {text: "Is Sensor?", value: "isSensor"}, - {text: "Friction", value: "friction"}, - {text: "Restitution", value: "restitution"}, - {text: "Density", value: "density"}, - {text: "Mass", value: "mass"}, - {text: "Position", value: "translation"}, - {text: "Rotation", value: "rotation"}, - //{text: "Area", value: "area"}, - {text: "Volume", value: "volume"}, - {text: "Collision Groups", value: "collisionGroups"}, - //{text: "Collision Mask", value: "collisionMask"}, - //{text: "Is Enabled?", value: "enabled"}, - //{text: "Contact Count", value: "contactCount"}, - //{text: "RigidBody Handle", value: "rigidBody"} - ]}, - colliderSets: {acceptReporters: false, items: [ - {text: "Friction", value: "setFriction"}, - {text: "Restitution", value: "setRestitution"}, - {text: "Density", value: "setDensity"}, - {text: "Is Sensor?", value: "setSensor"}, - {text: "Collision Groups", value: "setCollisionGroups"}, - //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool - //{text: "Position Offset", value: "setTranslation"}, - //{text: "Rotation Offset", value: "setRotation"} - ]}, - state: {acceptReporters: true, items: [{text: "on", value: "true"},{text: "off", value: "false"}]}, - state2: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true (must be fixed)", value: "true"}]}, - spaces: {acceptReporters: false, items: [{text: "World", value: "world"},{text: "Local", value: "local"}]}, - objectTypes: {acceptReporters: false, items: [{text: "Dynamic", value: "dynamic"},{text: "Fixed", value: "fixed"},{text: "Kinematic Position Based",value: "kinematicPositionBased"}]}, - colliderTypes: {acceptReporters: false, items: [{text: "Box, Rectangle, cuboid", value: "cuboid"},{text: "Sphere, ball", value: "ball"},{text: "Custom, complex simple shapes, convexHull", value: "convexHull"},{text:"Precision, TriMesh",value:"trimesh"}]}, - forces: {acceptReporters: false, items: [{text: "Force", value: "addForce"},{text: "Torque (rotation)", value: "addTorque"},{text: "Apply Impulse", value: "applyImpulse"},{text: "Apply Torque Impulse (rotation)", value: "applyTorqueImpulse"},{text: "Linear Velocity", value: "setLinvel"},{text: "Angular Velocity", value: "setAngvel"},]}, - resetF: {acceptReporters: false, items: [{text:"Forces", value: "resetForces"},{text:"Torques", value: "resetTorques"},]} - } - } - } - joint(jointData, bodyA, bodyB) { - physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true) - } - - fixedJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - let RA = JSON.parse(args.RA).map(Number) - let RB = JSON.parse(args.RB).map(Number) - - RA = new THREE.Quaternion().setFromEuler( - new THREE.Euler( - THREE.MathUtils.degToRad(RA[0]), - THREE.MathUtils.degToRad(RA[1]), - THREE.MathUtils.degToRad(RA[2]) - ) - ) - RB = new THREE.Quaternion().setFromEuler( - new THREE.Euler( - THREE.MathUtils.degToRad(RB[0]), - THREE.MathUtils.degToRad(RB[1]), - THREE.MathUtils.degToRad(RB[2]) - ) - ) - - const data = RAPIER.JointData.fixed( - { x: VA[0], y: VA[1], z: VA[2] }, RA, - { x: VB[0], y: VB[1], z: VB[2] }, RB - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) - } - - sphericalJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - - const data = RAPIER.JointData.spherical( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] } - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) - } - - revoluteJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - const x = JSON.parse(args.X).map(Number) - - const data = RAPIER.JointData.revolute( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) - } - - prismaticJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - const x = JSON.parse(args.X).map(Number) - - const data = RAPIER.JointData.prismatic( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) - } - - createWorld(args) { - const v3 = JSON.parse(args.G).map(Number) - const gravity = { x: v3[0], y: v3[1], z: v3[2]} - physicsWorld = new RAPIER.World(gravity) - - console.log(physicsWorld) - } - - getWorld(args) { - if (args.PROPERTY === "log") {console.log(physicsWorld); return "logged"} - return JSON.stringify(physicsWorld[args.PROPERTY]) - } - - setRB(args) { - let value = args.VALUE - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE - let object = getObject(args.OBJECT) - object.rigidBody[args.PROPERTY](value) - } - setC(args) { - let value = args.VALUE - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE - let object = getObject(args.OBJECT) - object.collider[args.PROPERTY](value) - } - - getRB(args) { - let object = getObject(args.OBJECT) - return JSON.stringify(object.rigidBody[args.PROPERTY]()) - } - getC(args) { - let object = getObject(args.OBJECT) - return JSON.stringify(object.collider[args.PROPERTY]()) - } - - lockObjectAxis(args) { - let object = getObject(args.OBJECT) - const x = !JSON.parse(args.X) - const y = !JSON.parse(args.Y) - const z = !JSON.parse(args.Z) - object.rigidBody[args.PROPERTY](x,y,z,true) //changes is xyz, wake up - } - - objectPhysics(args) { - let object = getObject(args.OBJECT) - object.physics = JSON.parse(args.state) - - if (JSON.parse(args.state)) { - //if already exists delete: - if (object.rigidBody) { - physicsWorld.removeRigidBody(object.rigidBody) - object.rigidBody = null - object.collider = null - } - /*asing a rigidbody and collider to object and add them to physicsWorld*/ - let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() - .setTranslation(object.position.x, object.position.y, object.position.z) - .setRotation({w: object.quaternion._w, x: object.quaternion._x, y: object.quaternion._y, z: object.quaternion._z}) - - let colliderDesc - switch(args.collider) { - case "cuboid": colliderDesc = createCuboidCollider(object,); break - case "ball": colliderDesc = createBallCollider(object); break - case "convexHull": colliderDesc = createConvexHullCollider(object); break - case "trimesh": colliderDesc = TriMesh(object); break - } - colliderDesc.setSensor(JSON.parse(args.state2)).setMass(args.mass).setDensity(args.density).setFriction(args.friction) - - let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc) - let collider = physicsWorld.createCollider(colliderDesc, rigidBody) - - object.rigidBody = rigidBody - object.collider = collider - } else { - /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ - physicsWorld.removeRigidBody(object.rigidBody) - object.rigidBody = null - object.collider = null - } - - } - - enableCCD(args) { - let object = getObject(args.OBJECT) - if (object.physics) { - let rigidBody = object.rigidBody - rigidBody.enableCcd(JSON.parse(args.state)) - } - } - - addForce(args) { - let object = getObject(args.OBJECT) - const vector = JSON.parse(args.VALUE).map(Number) - - let force = new THREE.Vector3(vector[0],vector[1],vector[2]) - if (args.SPACE === "local") { - force.applyQuaternion(object.quaternion); - } - - object.rigidBody[args.PROPERTY](force,true) - } - - resetForces(args) { - rigidBody[args.PROPERTY](true) - } - - sensorSingle(args) { - const sensor = getObject(args.SENSOR) - - let object = getObject(args.OBJECT) - - let touching = false - physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { - if (otherCollider === object.collider) touching = true - }) - - return touching - } - - sensorAll(args) { - const sensor = getObject(args.SENSOR) - - const touchedObjects = [] - - // loop thruogh every collider touching sensor - physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { - // find owner of collider - const otherObject = scene.children.find(o => o.collider === otherCollider) - console.log(otherCollider) - if (otherObject) touchedObjects.push(otherObject.name) - }) - - return JSON.stringify(touchedObjects) - } - - } - Scratch.extensions.register(new RapierPhysics()) - - //Thanks to the PointerLock extension of Turbowarp - const mouse = vm.runtime.ioDevices.mouse; - let isLocked = false; - let isPointerLockEnabled = false; - - let rect = threeRenderer.domElement.getBoundingClientRect(); - document.addEventListener("resize", () => { - rect = threeRenderer.domElement.getBoundingClientRect(); - }); - - const postMouseData = (e, isDown) => { - const { movementX, movementY } = e; - const { width, height } = rect; - const x = mouse._clientX + movementX; - const y = mouse._clientY - movementY; - mouse._clientX = x; - mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); - mouse._clientY = y; - mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); - if (typeof isDown === "boolean") { - const data = { - button: e.button, - isDown, - }; - originalPostIOData(data); - } - }; - - const mouseDevice = vm.runtime.ioDevices.mouse; - const originalPostIOData = mouseDevice.postData.bind(mouseDevice); - mouseDevice.postData = (data) => { - if (!isPointerLockEnabled) { - return originalPostIOData(data); - } - }; - - document.addEventListener( - "mousedown", - (e) => { - // @ts-expect-error - if (threeRenderer.domElement.contains(e.target)) { - if (isLocked) { - postMouseData(e, true); - } else if (isPointerLockEnabled) { - threeRenderer.domElement.requestPointerLock(); - } - } - }, - true - ); - document.addEventListener( - "mouseup", - (e) => { - if (isLocked) { - postMouseData(e, false); - // @ts-expect-error - } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { - threeRenderer.domElement.requestPointerLock(); - } - }, - true - ); - document.addEventListener( - "mousemove", - (e) => { - if (isLocked) { - postMouseData(e); - } - }, - true - ); - - document.addEventListener("pointerlockchange", () => { - isLocked = document.pointerLockElement === threeRenderer.domElement; - }); - document.addEventListener("pointerlockerror", (e) => { - console.error("Pointer lock error", e); - }); - - const oldStep = vm.runtime._step; - vm.runtime._step = function (...args) { - const ret = oldStep.call(this, ...args); - if (isPointerLockEnabled) { - const { width, height } = rect; - mouse._clientX = width / 2; - mouse._clientY = height / 2; - mouse._scratchX = 0; - mouse._scratchY = 0; - } - return ret; - }; - - vm.runtime.on("PROJECT_LOADED", () => { - isPointerLockEnabled = false; - if (isLocked) { - document.exitPointerLock(); - } - }); - - class Pointerlock { - getInfo() { - return { - id: "threepointerlockmod", - name: "Pointerlock for Extra 3D", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "setLocked", blockType: Scratch.BlockType.COMMAND, text: "set pointer lock [enabled]", arguments: { enabled: { type: Scratch.ArgumentType.STRING, defaultValue: "true", menu: "enabled"}},}, - {opcode: "isLocked", blockType: Scratch.BlockType.BOOLEAN, text: "pointer locked?",}, - ], - menus: { - enabled: {acceptReporters: true, items: [ - {text: "enabled", value: "true"},{text: "disabled", value: "false"}, - ]} - }, - } - } - - setLocked(args) { - isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; - if (!isPointerLockEnabled && isLocked) { - document.exitPointerLock(); - } - } - - isLocked() { - return isLocked; - } - } -Scratch.extensions.register(new Pointerlock()) - - }) - - - - -})(Scratch); diff --git a/threejsD_LOCAL_13852.js b/threejsD_LOCAL_13852.js deleted file mode 100644 index 27ba380..0000000 --- a/threejsD_LOCAL_13852.js +++ /dev/null @@ -1,2414 +0,0 @@ -// Name: Extra 3D -// ID: threejsExtension -// Description: Use three js inside Turbowarp! A 3D graphics library. -// By: Civero -// License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors - -(function (Scratch) { - "use strict"; - - if (!Scratch.extensions.unsandboxed) { - throw new Error("Three-D extension must run unsandboxed"); - } - - if (Scratch.vm.runtime.isPackaged) {alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); return} - //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return - - const vm = Scratch.vm; - const runtime = vm.runtime - const renderer = Scratch.renderer; - const canvas = renderer.canvas - const Cast = Scratch.Cast; - const menuIconURI = "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; - - let alerts = false - console.log("alerts are "+ (alerts ? "enabled" : "disabled")) - - let isMouseDown = { left: false, middle: false, right: false } - let prevMouse = { left: false, middle: false, right: false } - - let lastWidth = 0 - let lastHeight = 0 - - let THREE - let clock - let running - let loopId - //Addons - let GLTFLoader - let gltf - let OrbitControls - let controls - let BufferGeometryUtils - let TextGeometry - let fontLoad - //Physics - let RAPIER - let physicsWorld - - let threeRenderer - let scene - let camera - let eulerOrder = "YXZ" - - let composer - let passes = {} - let customEffects = [] - let renderTargets = {} - - let materials = {} - let geometries = {} - let lights = {} - let models = {} - - let assets = { //should i place materials, geometries; inside too? - textures: {}, - colors: {}, - fogs: {}, - curves: {}, - renderTargets: {}, //not the same as the global one! this one only stores textures - } - - let raycastResult = [] - - function resetor(level) { - camera = undefined - composer.reset() - - passes = {} - customEffects = [] - renderTargets = {} - - materials = {} - geometries = {} - lights = {} - models = {} - - if (level > 0) { - assets = { - textures: {}, - colors: {}, - fogs: {}, - curves: {}, - renderTargets: {}, - } - } - - updateComposers() - } - -//utility - function vector3ToString(prop) { - if (!prop) return "0,0,0"; - - const x = (typeof(prop.x) === "number") ? prop.x : (typeof(prop._x) === "number") ? prop._x : (JSON.stringify(prop).includes("X")) ? prop : 0 - const y = (typeof(prop.y) === "number") ? prop.y : (typeof(prop.y) === "number") ? prop._y : 0 - const z = (typeof(prop.z) === "number") ? prop.z : (typeof(prop.z) === "number") ? prop.z : 0 - - return [x, y, z] - } - -//objects - function createObject(name, content, parentName) { - let object = getObject(name, true) - if (object) { - removeObject(name) - alerts ? alert(name + " already exsisted, will replace!") : null - } - content.name = name - content.rotation._order = eulerOrder - parentName === scene.name ? object = scene : object = getObject(parentName) - content.physics = false - - object.add(content) - } - function removeObject(name) { - let object = getObject(name) - if (!object) return - - scene.remove(object) - - if (object.rigidBody) { - physicsWorld.removeCollider(object.collider, true) - physicsWorld.removeRigidBody(object.rigidBody, true) - object.rigidBody = null - object.collider = null - } - if (object.isLight) { - delete(lights[name]) - } - } - function getObject(name, isNew) { - let object = null - if (!scene) { - alerts ? alert("Can not get " + name + ". Create a scene first!") : null; return;} - object = scene.getObjectByName(name) - if (!object && !isNew) {alerts ? alert(name + " does not exist! Add it to scene"):null; return;} - return object - } - -//materials - function encodeCostume (name) { - if (name.startsWith("data:image/")) return name - return Scratch.vm.editingTarget.sprite.costumes.find(c => c.name === name).asset.encodeDataURI() - } - function setTexutre (texture, mode, style, x, y) { - texture.colorSpace = THREE.SRGBColorSpace - - if (mode === "Pixelate") { - texture.minFilter = THREE.NearestFilter; - texture.magFilter = THREE.NearestFilter; - } else { //Blur - texture.minFilter = THREE.NearestMipmapLinearFilter - texture.magFilter = THREE.NearestMipmapLinearFilter - } - - if (style === "Repeat") { - texture.wrapS = THREE.RepeatWrapping - texture.wrapT = THREE.RepeatWrapping - texture.repeat.set(x, y) - } - - texture.generateMipmaps = true; - } - async function resizeImageToSquare(uri, size = 256) { - return new Promise((resolve) => { - const img = new Image() - img.onload = () => { - const canvas = document.createElement('canvas') - canvas.width = size - canvas.height = size - const ctx = canvas.getContext('2d') - - // clear + draw image scaled to fit canvas - ctx.clearRect(0, 0, size, size) - ctx.drawImage(img, 0, 0, size, size) - - resolve(canvas.toDataURL()) // return normalized Data URI - //delete canvas? - }; - img.src = uri - }); -} -//light -function updateShadowFrustum(light, focusPos) { - if (light.type !== "DirectionalLight") return - - // Frustum Size - Increase this value to cover a larger area. - const d = 50; - - // Update Orthographic Shadow Camera Frustum - const shadowCamera = light.shadow.camera; - - // Set the width/height of the frustum - shadowCamera.left = -d; - shadowCamera.right = d; - shadowCamera.top = d; - shadowCamera.bottom = -d; - - // Determine ranges - shadowCamera.near = 0.1 - shadowCamera.far = 500 - - // Position the Light and its Target - light.target.position.copy(focusPos); - const direction = light.position.clone().sub(light.target.position).normalize(); - light.position.copy(focusPos.clone().add(direction.multiplyScalar(100))); - - // Ensure matrices are updated. - light.target.updateMatrixWorld(); - light.shadow.camera.updateProjectionMatrix() - light.shadow.needsUpdate = true; -} -//composer -function updateComposers() { - if (!camera || !scene) return; // nothing to do yet - - // always recreate the RenderPass to point to the current scene/camera - passes["Render"] = new RenderPass(scene, camera); - - // ensure composer has a RenderPass as the first pass - const hasRender = composer.passes.some(p => p && p.scene); - if (!hasRender) composer.addPass(passes["Render"]); - else { - // if composer already has one, replace it so it references current scene/camera - const idx = composer.passes.findIndex(p => p && p.scene); - composer.passes[idx] = passes["Render"]; - } -} -//utility -function getMouseNDC(event) { - // Use threeRenderer.domElement for correct offset - const rect = threeRenderer.domElement.getBoundingClientRect(); - const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - return [x, y]; -} -function checkCanvasSize() { - const { width, height } = canvas - if (width !== lastWidth || height !== lastHeight) { - lastWidth = width - lastHeight = height - resize() - } - requestAnimationFrame(checkCanvasSize) //rerun next frame -} -//physics -function computeWorldBoundingBox(mesh) { - // Create a Box3 in world coordinates - const box = new THREE.Box3().setFromObject(mesh); - const size = new THREE.Vector3(); - box.getSize(size); - const center = new THREE.Vector3(); - box.getCenter(center); - return { size, center }; -} -function createCuboidCollider(mesh) { - const { size } = computeWorldBoundingBox(mesh); - const collider = RAPIER.ColliderDesc.cuboid( - size.x / 2, - size.y / 2, - size.z / 2 - ) - return collider; -} -function createBallCollider(mesh) { - const { size } = computeWorldBoundingBox(mesh); - // radius = 1/2 of the largest verticie - const radius = Math.max(size.x, size.y, size.z) / 2; - const collider = RAPIER.ColliderDesc.ball(radius) - return collider //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) -} -function createConvexHullCollider(mesh) { - mesh.updateWorldMatrix(true, false); - - const position = mesh.geometry.attributes.position; - const vertices = []; - const vertex = new THREE.Vector3(); - - // Matrix for scale only - const scaleMatrix = new THREE.Matrix4().makeScale( - mesh.scale.x, - mesh.scale.y, - mesh.scale.z - ); - - for (let i = 0; i < position.count; i++) { - vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); - vertices.push(vertex.x, vertex.y, vertex.z); - } - - const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); - return collider; -} -function TriMesh(mesh) { - // Get the positions array (from your geoPoints function) -const positions = mesh.geometry.attributes.position.array; -const numVertices = positions.length / 3; - -// Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] -const indices = Array.from({ length: numVertices }, (_, i) => i); - -const collider = RAPIER.ColliderDesc.trimesh( - positions, - new Uint32Array(indices) -); - -return collider -} -function getModel(model, name) { - const file = runtime.getTargetForStage().getSounds().find(c => c.name === model) - if (!file) return - -return new Promise((resolve, reject) => { - gltf.parse( - file.asset.data.buffer, - "", - gltf => { - const root = gltf.scene - root.traverse(child => { - if (child.isMesh) { - child.castShadow = true - child.receiveShadow = true - } - }); - - const mixer = new THREE.AnimationMixer(root) - const actions = {} - gltf.animations.forEach(clip => { - const act = mixer.clipAction(clip) - act.clampWhenFinished = true - actions[clip.name] = act - }); - - models[name] = { root, mixer, actions } - resolve(root) - }, - error => { - console.error("Error parsing GLB model:", error) - reject(error) - } - )}) -} -async function openFileExplorer(format) { - return new Promise((resolve) => { - const input = document.createElement("input"); - input.type = "file" - input.accept = format - input.multiple = false - input.onchange = () => { - resolve(input.files) - input.remove() - }; - input.click(); - }) -} -function getMeshesUsingTexture(scene, targetTexture) { - const meshes = [] - - scene.traverse(object => { - if (object.material) { - const materials = Array.isArray(object.material) ? object.material : [object.material] - for (const material of materials) { - if (material.map === targetTexture) { - meshes.push(object) - break - } - } - } - }) - - return meshes -} -function getAsset(path) { - if (typeof(path) == "string") { //string? - if (path.includes("/")) { //has the /? - const value = path.split("/") - console.log(value[0], value[1]) - return assets[value[0]][value[1]] - } - } - - return JSON.parse(path) //boolean or number -} - -let mouseNDC = [0, 0] -//loops/init -function stopLoop() { - if (!running) return - running = false - - if (loopId) { - cancelAnimationFrame(loopId) - loopId = null - if (threeRenderer) threeRenderer.clear(); - } -} -async function load() { - if (!THREE) { - - // @ts-ignore - THREE = await import("https://esm.sh/three@0.180.0") - window._THREE_ = THREE - //Addons - GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js") - OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js") - BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js") - TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js") - const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js") - fontLoad = new FontLoader.FontLoader() - - const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8") - const { - EffectComposer, - EffectPass, - RenderPass, - - Effect, - BloomEffect, - GodRaysEffect, - DotScreenEffect, - DepthOfFieldEffect, - - BlendFunction - } = POSTPROCESSING - //so i can use them later as global - window.EffectComposer = EffectComposer; - window.EffectPass = EffectPass; - window.RenderPass = RenderPass; - window.Effect = Effect; - window.BloomEffect = BloomEffect; - window.GodRaysEffect = GodRaysEffect; - window.DotScreenEffect = DotScreenEffect; - window.DepthOfFieldEffect = DepthOfFieldEffect; - window.BlendFunction = BlendFunction; - - RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0") - await RAPIER.init() - - threeRenderer = new THREE.WebGLRenderer({ - powerPreference: "high-performance", - antialias: false, - stencil: false, - depth: true - }) - threeRenderer.setPixelRatio(window.devicePixelRatio) - threeRenderer.outputColorSpace = THREE.SRGBColorSpace // correct colors - threeRenderer.toneMapping = THREE.ACESFilmicToneMapping // HDR look (test) - //threeRenderer.toneMappingExposure = 1.0 //(test) - - threeRenderer.shadowMap.enabled = true - threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap // (optional) - threeRenderer.domElement.style.pointerEvents = 'auto' //will disable turbowarp mouse events, but enable threejs's - - gltf = new GLTFLoader.GLTFLoader() - clock = new THREE.Clock() - - // Example: create a composer - composer = new EffectComposer(threeRenderer, {frameBufferType: THREE.HalfFloatType}) - - renderer.addOverlay( threeRenderer.domElement, "manual" ) - renderer.addOverlay(canvas, "manual") - renderer.setBackgroundColor(1, 1, 1, 0) - - resize() - - window.addEventListener("mousedown", e => { - if (e.button === 0) isMouseDown.left = true - if (e.button === 1) isMouseDown.middle = true - if (e.button === 2) isMouseDown.right = true - }) - window.addEventListener("mouseup", e => { - if (e.button === 0) isMouseDown.left = false; prevMouse.left = false - if (e.button === 1) isMouseDown.middle = false; prevMouse.middle = false - if (e.button === 2) isMouseDown.right = false; prevMouse.right = false - }) - // prevent contextmenu on right click - threeRenderer.domElement.addEventListener("contextmenu", e => e.preventDefault()); - - threeRenderer.domElement.addEventListener('mousemove', (event) => { - mouseNDC = getMouseNDC(event); - }) - - running = false - load() - - startRenderLoop() - runtime.on('PROJECT_START', () => startRenderLoop()) - runtime.on('PROJECT_STOP_ALL', () => stopLoop()) - runtime.on('STAGE_SIZE_CHANGED', () => {requestAnimationFrame(() => resize())}) - checkCanvasSize() - } - } -function startRenderLoop() { - if (running) return - running = true - - const loop = () => { - if (!running) return - //RAPIER - if (physicsWorld && scene) { - physicsWorld.step() - - scene.children.forEach(obj => { - if (!(obj.isMesh) || !(obj.physics)) return - if (obj.rigidBody) { - obj.position.copy(obj.rigidBody.translation()) - obj.quaternion.copy(obj.rigidBody.rotation()) - } - }) - - } - if (scene && camera) { - if (controls) controls.update() - - const delta = clock.getDelta() - Object.values(models).forEach( model => { if (model) model.mixer.update(delta) } ) - - Object.values(lights).forEach(light => updateShadowFrustum(light, camera.position)) - - //update custom effects time - customEffects.forEach(e => { - if (e.uniforms.get('time')) { - e.uniforms.get('time').value += delta - } - }) - Object.values(renderTargets).forEach(t => { - if ( t.camera.type == "PerspectiveCamera") { - t.camera.aspect = t.target.width / t.target.height - t.camera.updateProjectionMatrix() - } - // get meshes using the texture associated with this target - const displayMeshes = getMeshesUsingTexture(scene, t.target.texture) - - displayMeshes.forEach(mesh => { - mesh.visible = false - }) - - if (t.camera.type == "PerspectiveCamera") { - threeRenderer.setRenderTarget(t.target) - threeRenderer.clear(true, true, true) - threeRenderer.render(scene, t.camera) - } else { - t.target.clear(threeRenderer) - t.camera.update( threeRenderer, scene ) //cubeCamera - } - - displayMeshes.forEach(mesh => { - mesh.visible = true - }) - }) - - camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height - camera.updateProjectionMatrix() - threeRenderer.setRenderTarget(null) - composer.render(delta) - } - - loopId = requestAnimationFrame(loop) - } - - loopId = requestAnimationFrame(loop) -} - -function resize() { - const w = canvas.width - const h = canvas.height - - threeRenderer.setSize(w, h) - composer.setSize(w, h) - customEffects.forEach(e => { - if (e.uniforms.get('resolution')) { - e.uniforms.get('resolution').value.set(w,h) - } - }) - - if (camera) { - camera.aspect = w / h - camera.updateProjectionMatrix() -} -} -//wait until all packages are loaded -Promise.resolve(load()).then(() => { - - class threejsExtension { - getInfo() { - return { - id: "threejsExtension", - name: "Extra 3D", - color1: "#222222", - color2: "#222222", - color3: "#11cc99", - menuIconURI, - blockIconURI: menuIconURI, - - blocks: [ - {blockType: Scratch.BlockType.BUTTON, text: "Show Docs", func: "openDocs"}, - {blockType: Scratch.BlockType.BUTTON, text: "Toggle Alerts", func: "alerts"}, - ], - menus: {} - }} - openDocs(){ - open("https://civ3ro.github.io/extensions/Documentation/") - } - alerts() {alerts = !alerts; alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!")} - } - Scratch.extensions.register(new threejsExtension()) - - class ThreeRenderer { - getInfo() { - return { - id: "threeRenderer", - name: "Three Renderer", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "setRendererRatio", blockType: Scratch.BlockType.COMMAND, text: "set Pixel Ratio to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"}}}, - {opcode: "eulerOrder", blockType: Scratch.BlockType.COMMAND, text: "set euler order to [VALUE]", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "YXZ"}}}, - ], - menus: {} - }} - - setRendererRatio(args) { - threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE) - } - eulerOrder(args) { - eulerOrder = args.VALUE - console.log("euler order set to", eulerOrder) - } - - } - Scratch.extensions.register(new ThreeRenderer()) - - class ThreeScene { - constructor() { - this.THREE = THREE; - this.scenes = {}; - } - - getInfo() { - return { - id: "threeScene", - name: "Three Scene", - color1: "#4638c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "newScene", blockType: Scratch.BlockType.COMMAND, text: "new Scene [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}}}, - - {opcode: "setSceneProperty", blockType: Scratch.BlockType.COMMAND, text: "set Scene [PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "sceneProperties", defaultValue: "background"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, - "---", - {opcode: "getSceneObjects", blockType: Scratch.BlockType.REPORTER, text: "get Scene [THING]", arguments:{THING: {type: Scratch.ArgumentType.STRING, menu: "sceneThings"}}}, - {opcode: "reset", blockType: Scratch.BlockType.COMMAND, text: "Reset Everything"} - ], - menus: { - sceneProperties: {acceptReporters: false, items: [ - {text: "Background", value: "background"},{text: "Background Blurriness", value: "backgroundBlurriness"},{text: "Background Intensity", value: "backgroundIntensity"},{text: "Background Rotation", value: "backgroundRotation"}, - {text: "Environment", value: "environment"},{text: "Environment Intensity", value: "environmentIntensity"},{text: "Environment Rotation", value: "environmentRotation"},{text: "Fog", value: "fog"}, - ]}, - sceneThings: {acceptReporters: false, items: ["Objects", "Materials", "Geometries","Lights","Scene Properties","Other assets"]}, - - } - }} - - newScene(args) { - scene = new THREE.Scene(); - scene.name = args.NAME - scene.background = new THREE.Color("#222") - //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - this.scenes = {...this.scenes, {scene}}; - resetor(0) - } - - reset() { - resetor(1) - } - - async setSceneProperty(args) { - const property = args.PROPERTY; - const value = getAsset(args.VALUE); - - scene[property] = value; - } - getSceneObjects(args){ - const names = []; - if (args.THING === "Objects") { - scene.traverse(obj => { - if (obj.name) names.push(obj.name); //if it has a name, add to list! - }); - } - else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)) - else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)) - else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)) - else if (args.THING === "Scene Properties") {console.log(scene); return "check console"} - else if (args.THING === "Other assets") return JSON.stringify(assets) - - return JSON.stringify(names); // if objects - } - - } - Scratch.extensions.register(new ThreeScene()) - - class ThreeCameras { - getInfo() { - return { - id: "threeCameras", - name: "Three Cameras", - color1: "#38c59bff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addCamera", blockType: Scratch.BlockType.COMMAND, text: "add camera [TYPE] [CAMERA] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "cameraTypes"}}}, - {opcode: "setCamera", blockType: Scratch.BlockType.COMMAND, text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "0.1", exemptFromNormalization: true}}}, - {opcode: "getCamera", blockType: Scratch.BlockType.REPORTER, text: "get camera [PROPERTY] of [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "cameraProperties"}}}, - "---", - {opcode: "renderSceneCamera", blockType: Scratch.BlockType.COMMAND, text: "set rendering camera to [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}}}, - "---", - {opcode: "cubeCamera", blockType: Scratch.BlockType.COMMAND, text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "cubeCamera"}, GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, - "---", - {opcode: "renderTarget", blockType: Scratch.BlockType.COMMAND, text: "set a RenderTarget: [RT] for camera [CAMERA]", arguments: {CAMERA: {type: Scratch.ArgumentType.STRING, defaultValue: "myCamera"}, RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, } }, - {opcode: "sizeTarget", blockType: Scratch.BlockType.COMMAND, text: "set RenderTarget [RT] size to [W] [H]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}, W: {type: Scratch.ArgumentType.NUMBER, defaultValue: 480}, H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 360},} }, - {opcode: "getTarget", blockType: Scratch.BlockType.REPORTER, text: "get RenderTarget: [RT] texture", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, - {opcode: "removeTarget", blockType: Scratch.BlockType.COMMAND, text: "remove RenderTarget: [RT]", arguments: {RT: {type: Scratch.ArgumentType.STRING, defaultValue: "myTarget"}} }, - ], - menus: { - cameraTypes: {acceptReporters: false, items: [ - {text: "Perspective", value: "PerspectiveCamera"}, - ]}, - cameraProperties: {acceptReporters: false, items: [ - {text: "Near", value: "near"},{text: "Far", value: "far"},{text: "FOV", value: "fov"},{text: "Focus (nothing...)", value: "focus"},{text: "Zoom", value: "zoom"}, - ]}, - } - }} - addCamera(args) { - let v2 = new THREE.Vector2() - threeRenderer.getSize(v2) - const object = new THREE.PerspectiveCamera(90, v2.x / v2.y ) - object.position.z = 3 - - createObject(args.CAMERA, object, args.GROUP) - } - setCamera(args) { - let object = getObject(args.CAMERA) - object[args.PROPERTY] = args.VALUE - object.updateProjectionMatrix() - } - getCamera(args) { - let object = getObject(args.CAMERA) - const value = JSON.stringify(object[args.PROPERTY]) - return value - } - renderSceneCamera(args) { - let object = getObject(args.CAMERA) - if (!object) return - camera = object - //reset composer, else it does not update. - composer.passes = [] - passes = {} - customEffects = [] - updateComposers() - } - - cubeCamera(args) { - // Create cube render target - const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true } ) - // Create cube camera - const cubeCamera = new THREE.CubeCamera( 0.1, 500, cubeRenderTarget ) - createObject(args.CAMERA, cubeCamera, args.GROUP) - - renderTargets[args.RT] = {target: cubeRenderTarget, camera: cubeCamera} - assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture - } - - renderTarget(args) { - let object = getObject(args.CAMERA) - const renderTarget = new THREE.WebGLRenderTarget( - 360, - 360, - { - generateMipmaps: false - } - ) - - renderTargets[args.RT] = {target: renderTarget, camera: object} - assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture - } - sizeTarget(args) { - renderTargets[args.RT].target.setSize(args.W, args.H) - } - getTarget(args) { - const t = renderTargets[args.RT].target.texture - console.log(t, renderTargets[args.RT]) - return `renderTargets/${t.uuid}` - } - removeTarget(args) { - delete(assets.renderTargets[renderTargets[args.RT].target.texture.uuid]) - renderTargets[args.RT].target.dispose() - delete(renderTargets[args.RT]) - } - } - Scratch.extensions.register(new ThreeCameras()) - - class ThreeObjects { - getInfo() { - return { - id: "threeObjects", - name: "Three Objects", - color1: "#38c567ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addObject", blockType: Scratch.BlockType.COMMAND, text: "add object [OBJECT3D] [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},TYPE: {type: Scratch.ArgumentType.STRING, menu: "objectTypes"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "cloneObject", blockType: Scratch.BlockType.COMMAND, text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myClone"},OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "setObject", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - {opcode: "getObject", blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of object [OBJECT3D]", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectProperties"}}}, - {opcode: "objectE", blockType: Scratch.BlockType.BOOLEAN, text: "is there an object [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "removeObject", blockType: Scratch.BlockType.COMMAND, text: "remove object [OBJECT3D] from scene", arguments: {OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - - {blockType: Scratch.BlockType.LABEL, text: " ↳ Transforms"}, - {opcode: "setObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, - //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, - //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.REPORTER, text: "get [PROPERTY] of [OBJECT3D]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - - {blockType: Scratch.BlockType.LABEL, text: "↳ Materials"}, - {opcode: "newMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "new material [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "materialTypes", defaultValue: "MeshStandardMaterial"}}}, - {opcode: "materialE",extensions: ["colours_looks"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a material [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, - {opcode: "removeMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "remove material [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}}}, - {opcode: "setMaterial",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [PROPERTY] of [NAME] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "materialProperties", defaultValue: "color"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "new Color()", exemptFromNormalization: true}}}, - {opcode: "setBlending",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] blending to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "blendModes"}}}, - {opcode: "setDepth",extensions: ["colours_looks"], blockType: Scratch.BlockType.COMMAND, text: "set material [NAME] depth to [VALUE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myMaterial"}, VALUE: {type: Scratch.ArgumentType.STRING, menu: "depthModes"}}}, - - {blockType: Scratch.BlockType.LABEL, text: "↳ Geometries"}, - {opcode: "newGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new geometry [NAME] [TYPE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "geometryTypes", defaultValue: "BoxGeometry"}}}, - {opcode: "geometryE",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.BOOLEAN, text: "is there a geometry [NAME]?", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - {opcode: "removeGeometry",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "remove geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}}}, - "---", - {opcode: "newGeo",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "new empty geometry [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, - {opcode: "geoPoints",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] vertex points to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[points]"}}}, - {opcode: "geoUVs",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "set geometry [NAME] UVs to [POINTS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myGeometry"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[UVs]"}}}, - "---", - {opcode: "splines", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create spline [NAME] from curve [CURVE]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}}}, - {opcode: "splineModel", extensions: ["colours_operators"], blockType: Scratch.BlockType.COMMAND, text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "mySpline"}, MODEL: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, CURVE: {type: Scratch.ArgumentType.STRING, defaultValue: "[curve]", exemptFromNormalization: true}, SPACING: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, - "---", - {blockType: Scratch.BlockType.BUTTON, text: "Convert font to JSON", func: "openConv"}, - {blockType: Scratch.BlockType.BUTTON, text: "Load JSON font file", func: "loadFont"}, - {opcode: "text", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.COMMAND, text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myText"}, TEXT: {type: Scratch.ArgumentType.STRING, defaultValue: "C-369"}, FONT: {type: Scratch.ArgumentType.STRING, menu: "fonts"}, S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, D: {type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1}, CS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 6}}}, - ], - menus: { - objectVector3: {acceptReporters: false, items: [ - {text: "Positon", value: "position"},{text: "Rotation", value: "rotation"},{text: "Scale", value: "scale"},{text: "Facing Direction (.up)", value: "up"} - ]}, - objectProperties: {acceptReporters: false, items: [ - {text: "Geometry", value: "geometry"},{text: "Material", value: "material"},{text: "Visible (true/false)", value: "visible"}, - ]}, - objectTypes: { acceptReporters: false, items: [ - { text: "Mesh", value: "Mesh" }, { text: "Sprite", value: "Sprite" }, { text: "Points", value: "Points" }, { text: "Line", value: "Line" }, { text: "Group", value: "Group" } - ]}, - XYZ: {acceptReporters: false, items: [{text: "X", value: "x"},{text: "Y", value: "y"},{text: "Z", value: "z"}]}, - materialProperties: {acceptReporters: false, items: [ - "|GENERAL| <-- not a property", - { text: "Color", value: "color" }, - { text: "Map", value: "map" }, - { text: "Opacity", value: "opacity" }, - { text: "Transparent", value: "transparent" }, - { text: "Alpha Map", value: "alphaMap" }, - { text: "Alpha Test", value: "alphaTest" }, - { text: "Depth Test", value: "depthTest" }, - { text: "Depth Write", value: "depthWrite" }, - { text: "Color Write", value: "colorWrite" }, - { text: "Side", value: "side" }, - { text: "Visible", value: "visible" },/* - { text: "Blending", value: "blending" }, - { text: "Blend Src", value: "blendSrc" }, - { text: "Blend Dst", value: "blendDst" }, - { text: "Blend Equation", value: "blendEquation" }, - { text: "Blend Src Alpha", value: "blendSrcAlpha" }, - { text: "Blend Dst Alpha", value: "blendDstAlpha" }, - { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ - { text: "Blend Aplha", value: "blendAplha" }, - { text: "Blend Color", value: "blendColor" }, - { text: "Alpha Hash", value: "alphaHash" }, - { text: "Premultiplied Alpha", value: "premultipliedAlpha" }, - - { text: "Tone Mapped", value: "toneMapped" }, - { text: "Fog", value: "fog" }, - { text: "Flat Shading", value: "flatShading" }, - - "|MESH Standard / Physical| <-- not a property", - { text: "Metalness", value: "metalness" }, - { text: "Metalness Map", value: "metalnessMap" }, - { text: "Roughness", value: "roughness" }, - { text: "Reflectivity", value: "reflectivity" }, - { text: "Roughness Map", value: "roughnessMap" }, - { text: "Emissive", value: "emissive" }, - { text: "Emissive Intensity", value: "emissiveIntensity" }, - { text: "Emissive Map", value: "emissiveMap" }, - { text: "Env Map", value: "envMap" }, - { text: "Env Map Intensity", value: "envMapIntensity" }, - { text: "Env Map Rotation", value: "envMapRotation" }, - { text: "Ior", value: "ior" }, - { text: "Refraction Ratio", value: "refractionRatio" }, - { text: "Clearcoat", value: "clearcoat" }, - { text: "Clearcoat Map", value: "clearcoatMap" }, - { text: "Clearcoat Roughness", value: "clearcoatRoughness" }, - { text: "Clearcoat Roughness Map", value: "clearcoatRoughnessMap" }, - { text: "Dispersion", value: "dispersion" }, - { text: "Sheen", value: "sheen" }, - { text: "Sheen Color", value: "sheenColor" }, - { text: "Sheen Color Map", value: "sheenColorMap" }, - { text: "Sheen Roughness", value: "sheenRoughness" }, - { text: "Sheen Roughness Map", value: "sheenRoughnessMap" }, - { text: "Specular Color", value: "specularColor" }, - { text: "Specular Color Map", value: "specularColorMap" }, - { text: "Specular Intensity", value: "specularIntensity" }, - { text: "Specular Intensity Map", value: "specularIntensityMap" }, - { text: "Transmission", value: "transmission" }, - { text: "Transmission Map", value: "transmissionMap" }, - { text: "Thickness", value: "thickness" }, - { text: "Thickness Map", value: "thicknessMap" }, - { text: "Anisotropy", value: "anisotropy" }, - { text: "Anisotropy Map", value: "anisotropyMap" }, - { text: "Anisotropy Rotation", value: "anisotropyRotation" }, - { text: "Attenuation Distance", value: "attenuationDistance" }, - { text: "Attenuation Color", value: "attenuationColor" }, - { text: "Thickness", value: "thickness" }, - { text: "Iridescence", value: "iridescence" }, - { text: "Iridescence Ior", value: "iridescenceIOR" }, - { text: "Iridescence Map", value: "iridescenceMap" }, - { text: "Iridescence Thickness Range", value: "iridescenceThicknessRange" }, - - "|MESH Displacement / Normal / Bump| <-- not a property", - { text: "Displacement Map", value: "displacementMap" }, - { text: "Displacement Scale", value: "displacementScale" }, - { text: "Displacement Bias", value: "displacementBias" }, - { text: "Bump Map", value: "bumpMap" }, - { text: "Bump Scale", value: "bumpScale" }, - { text: "Normal Map Type", value: "normalMapType" }, - - "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", - { text: "Shininess", value: "shininess" }, - - { text: "Wireframe", value: "wireframe" }, - { text: "Wireframe Linewidth", value: "wireframeLinewidth" }, - { text: "Wireframe Linecap", value: "wireframeLinecap" }, - { text: "Wireframe Linejoin", value: "wireframeLinejoin" }, - - "|POINTS| <-- not a property", - { text: "Size", value: "size" }, - { text: "Size Attenuation", value: "sizeAttenuation" }, - - "|LINES| <-- not a property", - { text: "Scale", value: "scale" }, - { text: "Dash Size", value: "dashSize" }, - { text: "Gap Size", value: "gapSize" }, - - "|SPRITES| <-- not a property", - { text: "Rotation", value: "rotation" } -]}, - blendModes: {acceptReporters: false, items: [ - { text: "No Blending", value: "NoBlending" },{ text: "Normal Blending", value: "NormalBlending" },{ text: "Additive Blending", value: "AdditiveBlending" },{ text: "Subtractive Blending", value: "SubtractiveBlending" },{ text: "Multiply Blending", value: "MultiplyBlending" },{ text: "Custom Blending", value: "CustomBlending" } - ]}, - depthModes: {acceptReporters: false, items: [ - { text: "Never Depth", value: "NeverDepth" },{ text: "Always Depth", value: "AlwaysDepth" },{ text: "Equal Depth", value: "EqualDepth" },{ text: "Less Depth", value: "LessDepth" },{ text: "Less Equal Depth", value: "LessEqualDepth" },{ text: "Greater Equal Depth", value: "GreaterEqualDepth" },{ text: "Greater Depth", value: "GreaterDepth" },{ text: "Not Equal Depth", value: "NotEqualDepth" } - ]}, - materialTypes:{acceptReporters: false, items: [ - {text:"Mesh Basic Material",value:"MeshBasicMaterial"},{text:"Mesh Standard Material",value:"MeshStandardMaterial"},{text:"Mesh Physical Material",value:"MeshPhysicalMaterial"},{text:"Mesh Lambert Material",value:"MeshLambertMaterial"},{text:"Mesh Phong Material",value:"MeshPhongMaterial"},{text:"Mesh Depth Material",value:"MeshDepthMaterial"},{text:"Mesh Normal Material",value:"MeshNormalMaterial"},{text:"Mesh Matcap Material",value:"MeshMatcapMaterial"},{text:"Mesh Toon Material",value:"MeshToonMaterial"},{text:"Line Basic Material",value:"LineBasicMaterial"},{text:"Line Dashed Material",value:"LineDashedMaterial"},{text:"Points Material",value:"PointsMaterial"},{text:"Sprite Material",value:"SpriteMaterial"},{text:"Shadow Material",value:"ShadowMaterial"} - ]}, - textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, - textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, - geometryTypes: {acceptReporters: false, items: [ - {text: "Box Geometry", value: "BoxGeometry"},{text: "Sphere Geometry", value: "SphereGeometry"},{text: "Cylinder Geometry", value: "CylinderGeometry"},{text: "Plane Geometry", value: "PlaneGeometry"},{text: "Circle Geometry", value: "CircleGeometry"},{text: "Torus Geometry", value: "TorusGeometry"},{text: "Torus Knot Geometry", value: "TorusKnotGeometry"}, - ]}, - modelsList: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) - if (models.length < 1) return [["Load a model! (GLB Loader category)"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - fonts: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.json')) - if (models.length < 1) return [["Load a font!"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - - } - }} - - addObject(args) { - const object = new THREE[args.TYPE](); - - object.castShadow = true - object.receiveShadow = true - - createObject(args.OBJECT3D, object, args.GROUP) - } - cloneObject(args) { - let object = getObject(args.OBJECT3D) - const clone = object.clone(true) - clone.name - createObject(args.NAME, clone, args.GROUP) - } - setObjectV3(args) { - let object = getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) - - function degToRad(deg) { - return deg * Math.PI / 180; - } - - - if (object.rigidBody) { - const x = values[0] - const y = values[1] - const z = values[2] - if (args.PROPERTY === "rotation") { - const euler = new THREE.Euler( - degToRad(x), - degToRad(y), - degToRad(z), - 'YXZ' - ) - const quaternion = new THREE.Quaternion() - quaternion.setFromEuler(euler) - - object.rigidBody.setRotation({ - x: quaternion.x, - y: quaternion.y, - z: quaternion.z, - w: quaternion.w - }); - } else if (args.PROPERTY === "position") { - object.rigidBody.setTranslation({ x: x, y: y, z: z }, true) - } - return - } - - if (object.isCamera == true && controls) { - - } - - if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.set(0,0,0) - } - if (object.isDirectionalLight == true) {object.pos = new THREE.Vector3(...values); console.log(true, values, object.pos); return} - object[args.PROPERTY].set(...values); - - if (object.type == "CubeCamera") object.updateCoordinateSystem() - } - /* - changeObjectV3(args) { - getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) - - if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.x += values[0] - object.rotation.y += values[1] - object.rotation.z += values[2] - } - else { - object[args.PROPERTY].add(...values); - } - } - changeObjectXV3(args) { - getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "rotation") value = value * Math.PI / 180 - - object[args.PROPERTY][args.X] += value - } - */ - getObjectV3(args) { - let object = getObject(args.OBJECT3D) - if (!object) return - let values = vector3ToString(object[args.PROPERTY]) - if (args.PROPERTY === "rotation") { - const toDeg = Math.PI/180 - values = [values[0]/toDeg,values[1]/toDeg,values[2]/toDeg,] - } - - return JSON.stringify(values) - } - setObject(args){ - let object = getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "material") {const mat = materials[args.NAME]; if (mat) value = mat; else value = undefined} - else if (args.PROPERTY === "geometry") {const geo = geometries[args.NAME]; if (geo) value = geo; else value = undefined} - else value = !!value - - if (value == undefined) return //invalid geo/mat - object[args.PROPERTY] = value - } - getObject(args){ - let object = getObject(args.OBJECT3D) - if (!object) return - let value - if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; - else value = object.visible; - - return value - } - removeObject(args) { - removeObject(args.OBJECT3D) - } - objectE(args) { - return scene.children.map(o => o.name).includes(args.NAME) - } - -//defines - newMaterial(args) { - if (materials[args.NAME] && alerts) alert ("material already exists! will replace...") - const mat = new THREE[args.TYPE](); - mat.name = args.NAME; - - materials[args.NAME] = mat; - } - async setMaterial(args) { - if (typeof(args.VALUE) == "string" && args.VALUE.at(0) == "|") return - const mat = materials[args.NAME] - - let value = args.VALUE - - if (args.VALUE == "false") value = false - - if (args.PROPERTY == "side") {value = (args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide)} - else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)) - else value = getAsset(value) - - - console.log("o:", args.VALUE, typeof(args.VALUE)) - console.log("r:", value, typeof(value)) - - mat[args.PROPERTY] = await (value) //await incase its a texture - mat.needsUpdate = true - } - setBlending(args) { - const mat = materials[args.NAME] - mat.blending = THREE[args.VALUE] - mat.premultipliedAlpha = true - mat.needsUpdate = true - } - setDepth(args) { - const mat = materials[args.NAME] - mat.depthFunc = THREE[args.VALUE] - mat.needsUpdate = true - } - removeMaterial(args){ - const mat = materials[args.NAME] - mat.dispose() - delete(materials[args.NAME]) - } - materialE(args) { - return materials[args.NAME] ? true : false - } - - newGeometry(args) { - if (geometries[args.NAME] && alerts) alert ("geometry already exists! will replace...") - const geo = new THREE[args.TYPE]() - geo.name = args.NAME - - geometries[args.NAME] = geo - } - setGeometry(args) { - const geo = geometries[args.NAME] - geo[args.PROPERTY] = (args.VALUE) - - geo.needsUpdate = true; - } - removeGeometry(args){ - const geo = geometries[args.NAME] - geo.dispose() - delete(geometries[args.NAME]) - } - geometryE(args) { - return geometries[args.NAME] ? true : false - } - - newGeo(args) { - const geometry = new THREE.BufferGeometry() - geometry.name = args.NAME - geometries[args.NAME] = geometry - } - async geoPoints(args) { - const geometry = geometries[args.NAME] - const positions = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v3 of each vertex of each triangle - - geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3)) - geometry.computeVertexNormals() - - geometry.needsUpdate = true - } - geoUVs(args) { - const geometry = geometries[args.NAME] - const UVs = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v2 of each UV of each triangle - - geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(UVs), 2)) - geometry.needsUpdate = true - } - - splines(args) { - const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)) - geometry.name = args.NAME - - geometries[args.NAME] = geometry - } - - async splineModel(args) { - const model = await getModel(args.MODEL, args.NAME) - if (!model) return console.warn("Model not found:", args.MODEL) - - const curve = getAsset(args.CURVE) - const spacing = parseFloat(args.SPACING) || 1 - const curveLength = curve.getLength() - const divisions = Math.floor(curveLength / spacing) - - const geomList = [] - const matList = [] - - for (let i = 0; i <= divisions; i++) { - const t = i / divisions - const pos = curve.getPointAt(t) - const tangent = curve.getTangentAt(t) - - const temp = model.clone(true) - temp.position.copy(pos) - - const up = new THREE.Vector3(0, 0, 1) - const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()) - temp.quaternion.copy(quat) - - temp.updateMatrixWorld(true) - - temp.traverse(child => { - if (child.isMesh && child.geometry) { - const geom = child.geometry.clone() - geom.applyMatrix4(child.matrixWorld) - geomList.push(geom) - matList.push(child.material) //.clone() ? - } - }) - } - - const validGeoms = geomList.filter(g => { - const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position - if (!ok) console.warn("geometry skipped:", g) - return ok - }) - - const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true) - merged.computeBoundingBox() - merged.computeBoundingSphere() - - merged.name = args.NAME - geometries[args.NAME] = merged - matList.name = args.NAME - materials[args.NAME] = matList - } - - async text(args) { - const fontFile = runtime.getTargetForStage().getSounds().find(c => c.name === args.FONT) - if (!fontFile) return - - const json = new TextDecoder().decode(fontFile.asset.data.buffer) - const fontData = JSON.parse(json) - - const font = fontLoad.parse(fontData) - - const params = {font: font, size: JSON.parse(args.S), height: JSON.parse(args.D), curveSegments: JSON.parse(args.CS), bevelEnabled: false} - const geometry = new TextGeometry.TextGeometry(args.TEXT, params) - geometry.computeVertexNormals() - geometry.center() // optional, recenters the text - - - geometry.name = args.NAME - - geometries[args.NAME] = geometry - } - - async loadFont() { - openFileExplorer(".json").then(files => { - const file = files[0] - const reader = new FileReader() - - reader.onload = async (e) => { - const arrayBuffer = e.target.result - - // From lily's assets - // // Thank you PenguinMod for providing this code. - - const targetId = runtime.getTargetForStage().id //util.target.id not working! - const assetName = Cast.toString(file.name) - - const buffer = arrayBuffer - - const storage = runtime.storage - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ) - - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ) - alert("Font loaded successfully!") - } catch (e) { - console.error(e) - alert("Error loading font.") - } - - // End of PenguinMod - } - - reader.readAsArrayBuffer(file); - }) - } - openConv() {{open("https://gero3.github.io/facetype.js/")}} - - } - Scratch.extensions.register(new ThreeObjects()) - - class ThreeLights { - getInfo() { - return { - id: "threeLights", - name: "Three Lights", - color1: "#c7a22aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "addLight", blockType: Scratch.BlockType.COMMAND, text: "add light [NAME] type [TYPE] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, TYPE: {type: Scratch.ArgumentType.STRING, menu: "lightTypes"}}}, - {opcode: "setLight", blockType: Scratch.BlockType.COMMAND, text: "set light [NAME][PROPERTY] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lightProperties", defaultValue: "intensity"},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myLight"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1", exemptFromNormalization: true}}}, - ], - menus: { - lightTypes: {acceptReporters: false, items: [ - {text: "Ambient Light", value: "AmbientLight"},{text: "Directional Light", value: "DirectionalLight"},{text: "Point Light", value: "PointLight"},{text: "Hemisphere Light", value: "HemisphereLight"},{text: "Spot Light", value: "SpotLight"}, - ]}, - lightProperties: {acceptReporters: false, items: [ - {text: "Color", value: "color"},{text: "Intensity", value: "intensity"},{text: "Cast Shadow?", value: "castShadow"}, - {text: "Ground Color (HemisphereLight)", value: "groundColor"}, - {text: "Map (SpotLight)", value: "map"},{text: "Distance (SpotLight)", value: "distance"},{text: "Decay (SpotLight)", value: "decay"},{text: "Penumbra (SpotLight)", value: "penumbra"},{text: "Angle/Size (SpotLight)", value: "angle"},{text: "Power (SpotLight)", value: "power"}, - {text: "Target Position (Directional/SpotLight)", value: "target"}, - ]}, - } - }} - - addLight(args) { - const light = new THREE[args.TYPE](0xffffff, 1) - - createObject(args.NAME, light, args.GROUP) - lights[args.NAME] = light - if (light.type === "AmbientLight" || "HemisphereLight") return - - light.castShadow = true - if (light.type === "PointLight") return - //Directional & Spot Light - light.target.position.set(0, 0, 0) - scene.add(light.target) - - light.pos = new THREE.Vector3(0,0,0) - - light.shadow.mapSize.width = 4096 - light.shadow.mapSize.height = 2048 - - if (light.type === "SpotLight") { - light.decay = 0 - light.shadow.camera.near = 500; - light.shadow.camera.far = 4000; - light.shadow.camera.fov = 30; - } - light.shadow.needsUpdate = true - light.needsUpdate = true - } - - setLight(args) { - const light = lights[args.NAME] - if (!args.PROPERTY) return - if (args.PROPERTY === "target") { - light.target.position.set(...JSON.parse(args.VALUE)) //vector3 - light.target.updateMatrixWorld(); - } - else { - light[args.PROPERTY] = getAsset(args.VALUE) - } - light.needsUpdate = true - - if (light.type === "AmbientLight" || "HemisphereLight") return - - light.shadow.camera.updateProjectionMatrix(); - light.shadow.needsUpdate = true - } - - } - Scratch.extensions.register(new ThreeLights()) - - class ThreeUtilities { - getInfo() { - return { - id: "threeUtility", - name: "Three Utilities", - color1: "#3875c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "newVector2", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}}}, - {opcode: "newVector3", blockType: Scratch.BlockType.REPORTER, text: "New Vector [X] [Y] [Z]", arguments: {X: {type: Scratch.ArgumentType.NUMBER}, Y: {type: Scratch.ArgumentType.NUMBER}, Z: {type: Scratch.ArgumentType.NUMBER}}}, - "---", - {opcode: "operateV3", blockType: Scratch.BlockType.REPORTER, text: "do [V3] [O] [V32]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, O: {type: Scratch.ArgumentType.STRING, menu: "operators"}, V32: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, - {opcode: "moveVector3", blockType: Scratch.BlockType.REPORTER, text: "move [S] steps in vector [V3] in direction [D3]", arguments: {S: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"}}}, - {opcode: "directionTo", blockType: Scratch.BlockType.REPORTER, text: "direction from [V3] to [T3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, T3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"}}}, - "---", - {opcode: "newColor", blockType: Scratch.BlockType.REPORTER, text: "New Color [HEX]", arguments: {HEX: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff"}}}, - {opcode: "newFog", blockType: Scratch.BlockType.REPORTER, text: "New Fog [COLOR] [NEAR] [FAR]", arguments: {COLOR: {type: Scratch.ArgumentType.COLOR, defaultValue: "#9966ff", exemptFromNormalization: true}, NEAR: {type: Scratch.ArgumentType.NUMBER}, FAR: {type: Scratch.ArgumentType.NUMBER, defaultValue: 10}}}, - {opcode: "newTexture", blockType: Scratch.BlockType.REPORTER, text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, - {opcode: "newCubeTexture", blockType: Scratch.BlockType.REPORTER, text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", arguments: {"COSTUMEX0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEX1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEY1": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ0": {type: Scratch.ArgumentType.COSTUME},"COSTUMEZ1": {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"},STYLE: {type: Scratch.ArgumentType.STRING, menu: "textureStyles"}, X: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1},Y: {type: Scratch.ArgumentType.NUMBER,defaultValue: 1}}}, - {opcode: "newEquirectangularTexture", blockType: Scratch.BlockType.REPORTER, text: "New Equirectangular Texture [COSTUME] [MODE]", arguments: {COSTUME: {type: Scratch.ArgumentType.COSTUME}, MODE: {type: Scratch.ArgumentType.STRING, menu: "textureModes"}}}, - "---", - {opcode: "curve", extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", arguments: {TYPE: {type: Scratch.ArgumentType.STRING, menu: "curveTypes"}, POINTS: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]"}, CLOSED: {type: Scratch.ArgumentType.STRING, defaultValue: "true"}}}, - "---", - {opcode:"mouseDown",extensions: ["colours_sensing"], blockType: Scratch.BlockType.BOOLEAN, text: "mouse [BUTTON] [action]?", arguments: {BUTTON: {type: Scratch.ArgumentType.STRING, menu: "mouseButtons"},action: {type: Scratch.ArgumentType.STRING, menu: "mouseAction"}}}, - {opcode: "mousePos",extensions: ["colours_sensing"], blockType: Scratch.BlockType.REPORTER, text: "mouse position", arguments: {}}, - "---", - {opcode: "getItem",extensions: ["colours_data_lists"], blockType: Scratch.BlockType.REPORTER, text: "get item [ITEM] of [ARRAY]", arguments: {ITEM: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}, ARRAY: {type: Scratch.ArgumentType.STRING, defaultValue: `["myObject", "myLight"]`}}}, - {blockType: Scratch.BlockType.LABEL, text: "↳ Raycasting"}, - {opcode: "raycast", blockType: Scratch.BlockType.COMMAND, text: "Raycast from [V3] in direction [D3]", arguments: {V3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,3]"}, D3: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,1]"}}}, - {opcode: "getRaycast", blockType: Scratch.BlockType.REPORTER, text: "get raycast [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "raycastProperties"}}}, - - ], - menus: { - materialProperties: {acceptReporters: false, items: [ - {text: "Color", value: "color"},{text: "Map (texture)", value: "map"},{text: "Alpha Map (texture)", value: "alphaMap"},{text: "Alpha Test (0-1)", value: "alphaTest"},{text: "Side (front/back/double)", value: "side"},{text: "Bump Map (texture)", value: "bumpMap"},{text: "Bump Scale", value: "bumpScale"}, - ]}, - textureModes: {acceptReporters: false, items: ["Pixelate","Blur"]}, - textureStyles: {acceptReporters: false, items: ["Repeat","Clamp"]}, - raycastProperties: {acceptReporters: false, items: [ - {text: "Intersected Object Names", value: "name"},{text: "Number of Objects", value: "number"},{text: "Intersected Objects distances", value: "distance"}, - ]}, - mouseButtons: {acceptReporters: false, items: ["left","middle","right"]}, - mouseAction: {acceptReporters: false, items: ["Down","Clicked"]}, - curveTypes: {acceptReporters: false, items: ["CatmullRomCurve3"]}, - operators: {acceptReporters: false, items: [ - "+","-","*","/","=","max","min","dot","cross","distance to","angle to","apply euler", - ]} - } - }} - mouseDown(args) { - if (args.action === "Down") return isMouseDown[args.BUTTON] - if (args.action === "Clicked") { - if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false - else prevMouse[args.BUTTON] = true; return true - } - } - mousePos(event) { - return JSON.stringify(mouseNDC) - } - newVector3(args) { - return JSON.stringify([args.X, args.Y, args.Z]) - } - operateV3(args){ - const v3 = new THREE.Vector3(...JSON.parse(args.V3)) - const v32 = new THREE.Vector3(...JSON.parse(args.V32)) - - let r - if (args.O == "+") r = v3.add(v32) - else if (args.O == "-") r = v3.sub(v32) - else if (args.O == "*") r = v3.multiply(v32) - else if (args.O == "/") r = v3.divide(v32) - else if (args.O == "=") r = v3.equals(v32) - else if (args.O == "max") r = v3.max(v32) - else if (args.O == "min") r = v3.min(v32) - else if (args.O == "dot") r = v3.dot(v32) - else if (args.O == "cross") r = v3.cross(v32) - else if (args.O == "distance to") r = v3.distanceTo(v32) - else if (args.O == "angle to") r = v3.angleTo(v32) - else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)) - - if (typeof(r) == "object") return JSON.stringify([r.x, r.y, r.z]) - else return JSON.stringify(r) - } - - newVector2(args) { - return JSON.stringify([args.X, args.Y]) - } - - moveVector3(args) { - const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); - const steps = Number(args.S); - - const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); - - const yaw = THREE.MathUtils.degToRad(yawInputDeg); - const pitch = THREE.MathUtils.degToRad(pitchInputDeg); - const roll = THREE.MathUtils.degToRad(rollInputDeg); - - const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); - - const forwardVector = new THREE.Vector3(0, 0, -1); - const direction = forwardVector.applyEuler(euler).normalize(); - - const newPos = currentPos.add(direction.multiplyScalar(steps)); - return JSON.stringify([newPos.x, newPos.y, newPos.z]); - } - - directionTo(args) { - const v3 = new THREE.Vector3(...JSON.parse(args.V3)) - const toV3 = new THREE.Vector3(...JSON.parse(args.T3)) - - const direction = toV3.clone().sub(v3).normalize(); - // Pitch (X) - const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x*direction.x + direction.z*direction.z)); - // Yaw (Y) - const yaw = Math.atan2(direction.x, direction.z); - - // Roll always 0 - return JSON.stringify([180+THREE.MathUtils.radToDeg(pitch),THREE.MathUtils.radToDeg(yaw),0]) - } - - newColor(args) { - const color = new THREE.Color(args.HEX) - const uuid = crypto.randomUUID() - assets.colors[uuid] = color - return `colors/${uuid}` - } - newFog(args) { - const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR) - const uuid = crypto.randomUUID() - assets.fogs[uuid] = fog - return `fogs/${uuid}` - } - async newTexture(args) { - const textureURI = encodeCostume(args.COSTUME) - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - async newCubeTexture(args) { - const uris = [encodeCostume(args.COSTUMEX0),encodeCostume(args.COSTUMEX1), encodeCostume(args.COSTUMEY0),encodeCostume(args.COSTUMEY1), encodeCostume(args.COSTUMEZ0),encodeCostume(args.COSTUMEZ1)] - const normalized = await Promise.all(uris.map(uri => resizeImageToSquare(uri, 256))); - const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); - - texture.name = "CubeTexture" + args.COSTUMEX0; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - async newEquirectangularTexture(args) { - const textureURI = encodeCostume(args.COSTUME) - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME - texture.mapping = THREE.EquirectangularReflectionMapping - - setTexutre(texture, args.MODE) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - - curve(args) { - function parsePoints(input) { - // Match all [x,y,z] groups - const matches = input.match(/\[([^\]]+)\]/g) - if (!matches) return [] - - return matches.map(str => { - const nums = str - .replace(/[\[\]\s]/g, '') - .split(',') - .map(Number) - return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0) - }) - } - const points = parsePoints(args.POINTS) - const curve = new THREE[args.TYPE](points) - curve.closed = JSON.parse(args.CLOSED) - - const uuid = crypto.randomUUID() - assets.curves[uuid] = curve - return `curves/${uuid}` - } - - getItem(args) { - const items = JSON.parse(args.ARRAY) - const item = items[args.ITEM - 1] - if (!item) return "0" - return item - } - - raycast(args) { - const origin = new THREE.Vector3(...JSON.parse(args.V3)) - // rotation is in degrees => convert to radians first - const rot = JSON.parse(args.D3).map(v => v * Math.PI / 180) - - const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder) - const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize() - - const raycaster = new THREE.Raycaster() - //const camera = getObject(args.CAMERA) - raycaster.set( origin, direction ); - - const intersects = raycaster.intersectObjects( scene.children, true ) - - raycastResult = intersects - } - getRaycast(args) { - if (args.PROPERTY === "number") return raycastResult.length - if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map(i => i.distance)) - return JSON.stringify(raycastResult.map(i => i.object[args.PROPERTY])) - } - - } - Scratch.extensions.register(new ThreeUtilities()) - - class ThreeGLB { - getInfo() { - return { - id: "threeGLB", - name: "Three GLB Loader", - color1: "#c53838ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {blockType: Scratch.BlockType.BUTTON, text: "Load GLB File", func: "loadModelFile"}, - {opcode: "addModel", blockType: Scratch.BlockType.COMMAND, text: "add [ITEM] as [NAME] to [GROUP]", arguments: {GROUP: {type: Scratch.ArgumentType.STRING, defaultValue: "scene"},ITEM: {type: Scratch.ArgumentType.STRING, menu: "modelsList"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}}}, - {opcode: "getModel", blockType: Scratch.BlockType.REPORTER, text: "get object [PROPERTY] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "modelProperties"}}}, - {opcode: "playAnimation", blockType: Scratch.BlockType.COMMAND, text: "play animation [ANAME] of [NAME], [TIMES] times", arguments: {TIMES: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - {opcode: "pauseAnimation", blockType: Scratch.BlockType.COMMAND, text: "set [TOGGLE] animation [ANAME] of [NAME]", arguments: {TOGGLE: {type: Scratch.ArgumentType.NUMBER, menu: "pauseUn"}, NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - {opcode: "stopAnimation", blockType: Scratch.BlockType.COMMAND, text: "stop animation [ANAME] of [NAME]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myModel"}, ANAME: {type: Scratch.ArgumentType.STRING, defaultValue: "walk",exemptFromNormalization: true}}}, - - ], - menus: { - modelProperties: {acceptReporters: false, items: [ - {text: "Animations", value: "animations"}, - ]}, - pauseUn: {acceptReporters: true, items: [{text: "Pause", value: "true"},{text: "Unpasue", value: "false"},]}, - modelsList: {acceptReporters: false, items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime.getTargetForStage().getSounds().filter(e => e.name && e.name.endsWith('.glb')) - if (models.length < 1) return [["Load a model!"]] - - // @ts-ignore - return models.map( m => [m.name] ) - }}, - } - }} - - async loadModelFile() { - - openFileExplorer(".glb").then(files => { - const file = files[0]; - const reader = new FileReader(); - - reader.onload = async (e) => { - const arrayBuffer = e.target.result; - - { // From lily's assets - - // Thank you PenguinMod for providing this code. - { - const targetId = runtime.getTargetForStage().id; //util.target.id not working! - const assetName = Cast.toString(file.name); - - //const res = await Scratch.fetch(args.URL); - //const buffer = await res.arrayBuffer(); - const buffer = arrayBuffer - - const storage = runtime.storage; - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ); - - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ); - alert("Model loaded successfully!"); - } catch (e) { - console.error(e); - alert("Error loading model."); - } - } - // End of PenguinMod - } - }; - - reader.readAsArrayBuffer(file); - }) - - } - async addModel(args) { - const group = await getModel(args.ITEM, args.NAME) - - createObject(args.NAME, group, args.GROUP) - } - getModel(args){ - if (!models[args.NAME]) return; - return Object.keys(models[args.NAME].actions).toString() - } - - playAnimation(args) { - const model = models[args.NAME] - if (!model) {console.log("no model!"); return} - - const action = model.actions[args.ANAME] //clones of models dont have a stored actions! - if (!action) { - console.log("no action!") - return - } - - args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity) - - action.reset() - .play() - } - stopAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.stop(); - } - pauseAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.paused = args.TOGGLE - } - - } - Scratch.extensions.register(new ThreeGLB()) - - class ThreeAddons { - getInfo() { - return { - id: "threeAddons", - name: "Three Addons", - color1: "#c538a2ff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {blockType: Scratch.BlockType.LABEL, text: "Orbit Control"}, - {opcode: "OrbitControl", blockType: Scratch.BlockType.COMMAND, text: "set addon Orbit Control [STATE]", arguments: {STATE: {type: Scratch.ArgumentType.STRING, menu: "onoff"},}}, - - {blockType: Scratch.BlockType.LABEL, text: "Post Processing"}, - {opcode: "resetComposer", blockType: Scratch.BlockType.COMMAND, text: "reset composer"}, - {opcode: "bloom", blockType: Scratch.BlockType.COMMAND, text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},I: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, T:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.5}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, - {opcode: "godRays", blockType: Scratch.BlockType.COMMAND, text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}, DEC:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.95}, DENS:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},EXP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.1},WEI:{type: Scratch.ArgumentType.NUMBER, defaultValue: 0.4},RES:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1},SAMP:{type: Scratch.ArgumentType.NUMBER, defaultValue: 64},}}, - {opcode: "dots", blockType: Scratch.BlockType.COMMAND, text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", arguments: {OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},S:{type: Scratch.ArgumentType.NUMBER, defaultValue: 1}, A: {type: Scratch.ArgumentType.ANGLE, defaultValue: 0},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "SCREEN"}}}, - {opcode: "depth", blockType: Scratch.BlockType.COMMAND, text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", arguments: {FD: {type: Scratch.ArgumentType.NUMBER, defaultValue: (3)},FL: {type: Scratch.ArgumentType.NUMBER, defaultValue: (0.001)},BS: {type: Scratch.ArgumentType.NUMBER, defaultValue: 4},H: {type: Scratch.ArgumentType.NUMBER, defaultValue: 240},OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1},BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}}}, - "---", - {opcode: "custom", blockType: Scratch.BlockType.COMMAND, text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", arguments: {NAME: {type: Scratch.ArgumentType.STRING, defaultValue: "myShader"}, FRA: {type: Scratch.ArgumentType.STRING}, VER: {type: Scratch.ArgumentType.STRING}, BLEND: {type: Scratch.ArgumentType.STRING, menu: "blendModes", defaultValue: "NORMAL"}, OP: {type: Scratch.ArgumentType.NUMBER, defaultValue: 1}}}, - ], - menus: { - onoff: {acceptReporters: true, items: [{text: "enabled", value: "1"},{text: "disabled", value: "0"},]}, - blendModes: {acceptReporters: false, items: [ - "SKIP","SET","ADD","ALPHA","AVERAGE","COLOR","COLOR_BURN","COLOR_DODGE", - "DARKEN","DIFFERENCE","DIVIDE","DST","EXCLUSION","HARD_LIGHT","HARD_MIX", - "HUE","INVERT","INVERT_RGB","LIGHTEN","LINEAR_BURN","LINEAR_DODGE", - "LINEAR_LIGHT","LUMINOSITY","MULTIPLY","NEGATION","NORMAL","OVERLAY", - "PIN_LIGHT","REFLECT","SCREEN","SRC","SATURATION","SOFT_LIGHT","SUBTRACT", - "VIVID_LIGHT" - ]}, - } - }} - - OrbitControl(args) { - if (controls) controls.dispose() - - console.log("creating...", OrbitControls) - controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); - controls.enableDamping = true - - controls.enabled = !!args.STATE - console.log(controls) - } - - resetComposer() { - composer.passes = [] - passes = {} - customEffects = [] - updateComposers() - } - - bloom(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const bloomEffect = new BloomEffect({ - intensity: args.I, - luminanceThreshold: args.T, // ← correct key - luminanceSmoothing: args.S, - blendFunction: BlendFunction[args.BLEND], - }) - bloomEffect.blendMode.opacity.value = args.OP - - const pass = new EffectPass(camera, bloomEffect) - - composer.addPass(pass) - } - - godRays(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - let object = getObject(args.NAME) - const sun = object - - const godRays = new GodRaysEffect(camera, sun, { - resolutionScale: args.RES, - density: args.DENS, // ray density - decay: args.DEC, // fade out - weight: args.WEI, // brightness of rays - exposure: args.EXP, - samples: args.SAMP, - blendFunction: BlendFunction[args.BLEND], - }) - godRays.blendMode.opacity.value = args.OP - const pass = new EffectPass(camera, godRays) - composer.addPass(pass) - } - - dots(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const dot = new DotScreenEffect({ - angle: args.A, - scale: args.S, - blendFunction: BlendFunction[args.BLEND], - }) - dot.blendMode.opacity.value = args.OP - const pass = new EffectPass(camera, dot) - composer.addPass(pass) - } - - depth(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const dofEffect = new DepthOfFieldEffect(camera, { - focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) - focalLength: args.FL, // lens focal length in meters - bokehScale: args.BS, // strength/size of the blur circles - height: args.H, // resolution hint (affects quality/perf) - blendFunction: BlendFunction[args.BLEND], - }) - dofEffect.blendMode.opacity.value = args.OP - - const dofPass = new EffectPass(camera, dofEffect) - composer.addPass(dofPass) - } - - async custom(args) { - function cleanGLSL(glslCode) { - //delete multilines comments - let cleanedCode = glslCode.replace(/\/\*[\s\S]*?\*\//g, ' ') - .replace(/ /g, '\n') - .replace(/\/\/.*$/gm, ' ') - .replace(/; /g, ';\n') - - return cleanedCode; - } - - let fs = cleanGLSL(` - ${args.FRA} - `) - if (!args.FRA.trim()) {fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`} - const vs = cleanGLSL(` - ${args.VER} - `) - console.log(fs) - console.log(vs) - - const effect = new Effect( - "Custom", - fs, - { - blendFunction: BlendFunction[args.BLEND], - vertexShader: vs, - uniforms: new Map([ //uniforms usually in shaders... open to more! - ['time', new THREE.Uniform(0.0)], - ['resolution', new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height))] - ]), - defines: new Map([['USE_TIME', '1'], ['USE_VERTEX_TRANSFORM', '']]), - } - ); - - effect.blendMode.opacity.value = args.OP - - const pass = new EffectPass(camera, effect); - composer.addPass(pass); - - customEffects.push(effect); - } - - } - Scratch.extensions.register(new ThreeAddons()) - - class RapierPhysics { - getInfo() { - return { - id: "rapierPhysics", - name: "RAPIER Physics", - color1: "#222222", - color2: "#203024ff", - color3: "#78f07eff", - blocks: [ - {opcode: "createWorld", blockType: Scratch.BlockType.COMMAND, text: "create world | gravity:[G]", arguments: {G: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,-9.81,0]"}}}, - {opcode: "getWorld", blockType: Scratch.BlockType.REPORTER, text: "get world [PROPERTY]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "wProp"}}}, - "---", - {opcode: "objectPhysics", blockType: Scratch.BlockType.COMMAND, text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", arguments: {state2: {type: Scratch.ArgumentType.STRING, menu: "state2"},state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"}, type: {type: Scratch.ArgumentType.STRING, menu: "objectTypes", defaultValue: "dynamic"}, collider: {type: Scratch.ArgumentType.STRING, menu: "colliderTypes", defaultValue: "cuboid"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},mass: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},density: {type: Scratch.ArgumentType.NUMBER, defaultValue: "1"},friction: {type: Scratch.ArgumentType.NUMBER, defaultValue: "0.5"}}}, - "---", - {blockType: Scratch.BlockType.LABEL, text: "- RigidBody"}, - {opcode: "setRB", blockType: Scratch.BlockType.COMMAND, text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodySets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getRB", blockType: Scratch.BlockType.REPORTER, text: "get rigidbody [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "rigidBodyProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "lockObjectAxis", blockType: Scratch.BlockType.COMMAND, text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", arguments: {OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "lockAxes"}, X: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Y: {type: Scratch.ArgumentType.STRING, menu: "tf"}, Z: {type: Scratch.ArgumentType.STRING, menu: "tf"}}}, - "---", - {opcode: "addForce", blockType: Scratch.BlockType.COMMAND, text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", arguments: {VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,10,0]"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "forces", defaultValue: "addForce"},SPACE: {type: Scratch.ArgumentType.STRING, menu: "spaces", defaultValue: "world"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "resetForces", blockType: Scratch.BlockType.COMMAND, text: "reset [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "resetF", defaultValue: "resetForces"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "enableCCD", blockType: Scratch.BlockType.COMMAND, text: "enable Continuous Collision Detection for [OBJECT] [state]", arguments: {state: {type: Scratch.ArgumentType.STRING, menu: "state", defaultValue: "true"},PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "oPropS", defaultValue: "physics"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "fixedJoint", blockType: Scratch.BlockType.COMMAND, text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},RA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},RB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},}}, - {opcode: "sphericalJoint", blockType: Scratch.BlockType.COMMAND, text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},}}, - {opcode: "revoluteJoint", blockType: Scratch.BlockType.COMMAND, text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", arguments: {ObjA: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"},ObjB: {type: Scratch.ArgumentType.STRING, defaultValue: "myObjectB"},VA: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,0,0]"},VB: {type: Scratch.ArgumentType.STRING, defaultValue: "[0,1,0]"},X: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,0,0]"},}}, - "---", - {blockType: Scratch.BlockType.LABEL, text: "- Collider"}, - {opcode: "setC", blockType: Scratch.BlockType.COMMAND, text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderSets"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - {opcode: "getC", blockType: Scratch.BlockType.REPORTER, text: "get collider [PROPERTY] of [OBJECT]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "colliderProperties"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - "---", - {opcode: "sensorSingle", blockType: Scratch.BlockType.BOOLEAN, text: "is sensor [SENSOR] touching [OBJECT]?", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}, OBJECT: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}}}, - {opcode: "sensorAll", blockType: Scratch.BlockType.REPORTER, text: "objects touching sensor [SENSOR]", arguments: {SENSOR: {type: Scratch.ArgumentType.STRING, defaultValue: "mySensor"}}} - ], - menus: { - wProp: {acceptReporters: false, items: [ - {text: "Gravity", value: "gravity"}, {text: "log to console", value: "log"} - ]}, - tf: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true", value: "true"}]}, - lockAxes: {acceptReporters: false, items: [ - {text: "Translation", value: "setEnabledTranslations"}, {text: "Rotation", value: "setEnabledRotations"} - ]}, - rigidBodyProperties: {acceptReporters: false, items: [ - {text: "Type", value: "bodyType"}, - {text: "Linear Velocity", value: "linvel"}, - {text: "Angular Velocity", value: "angvel"}, - {text: "Translation (position)", value: "translation"}, - {text: "Rotation (quaternion)", value: "rotation"}, - {text: "Mass", value: "mass"}, - //{text: "Center of Mass", value: "centerOfMass"}, - {text: "Linear Damping", value: "linearDamping"}, - {text: "Angular Damping", value: "angularDamping"}, - {text: "Is Sleeping?", value: "isSleeping"}, - //{text: "Can Sleep?", value: "isCanSleep"}, - {text: "Gravity Scale", value: "gravityScale"}, - {text: "Is Fixed?", value: "isFixed"}, - {text: "Is Dynamic?", value: "isDynamic"}, - {text: "Is Kinematic?", value: "isKinematic"}, - //{text: "Sleeping", value: "sleeping"} - ]}, - rigidBodySets: {acceptReporters: false, items: [ - //{text: "Linear Velocity", value: "setLinvel"}, - //{text: "Angular Velocity", value: "setAngvel"}, - //{text: "Mass", value: "setMass"}, - {text: "Gravity Scale", value: "setGravityScale"}, - //{text: "Can Sleep?", value: "setCanSleep"}, - //{text: "Sleeping", value: "sleeping"}, - {text: "Linear Damping", value: "setLinearDamping"}, - {text: "Angular Damping", value: "setAngularDamping"}, - {text: "Is Fixed?", value: "isFixed"}, - {text: "Is Dynamic?", value: "isDynamic"}, - {text: "Is Kinematic?", value: "isKinematic"} - ]}, - colliderProperties: {acceptReporters: false, items: [ - //{text: "Collider Type", value: "type"}, - {text: "Is Sensor?", value: "isSensor"}, - {text: "Friction", value: "friction"}, - {text: "Restitution", value: "restitution"}, - {text: "Density", value: "density"}, - {text: "Mass", value: "mass"}, - {text: "Position", value: "translation"}, - {text: "Rotation", value: "rotation"}, - //{text: "Area", value: "area"}, - {text: "Volume", value: "volume"}, - {text: "Collision Groups", value: "collisionGroups"}, - //{text: "Collision Mask", value: "collisionMask"}, - //{text: "Is Enabled?", value: "enabled"}, - //{text: "Contact Count", value: "contactCount"}, - //{text: "RigidBody Handle", value: "rigidBody"} - ]}, - colliderSets: {acceptReporters: false, items: [ - {text: "Friction", value: "setFriction"}, - {text: "Restitution", value: "setRestitution"}, - {text: "Density", value: "setDensity"}, - {text: "Is Sensor?", value: "setSensor"}, - {text: "Collision Groups", value: "setCollisionGroups"}, - //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool - //{text: "Position Offset", value: "setTranslation"}, - //{text: "Rotation Offset", value: "setRotation"} - ]}, - state: {acceptReporters: true, items: [{text: "on", value: "true"},{text: "off", value: "false"}]}, - state2: {acceptReporters: true, items: [{text: "false", value: "false"},{text: "true (must be fixed)", value: "true"}]}, - spaces: {acceptReporters: false, items: [{text: "World", value: "world"},{text: "Local", value: "local"}]}, - objectTypes: {acceptReporters: false, items: [{text: "Dynamic", value: "dynamic"},{text: "Fixed", value: "fixed"},{text: "Kinematic Position Based",value: "kinematicPositionBased"}]}, - colliderTypes: {acceptReporters: false, items: [{text: "Box, Rectangle, cuboid", value: "cuboid"},{text: "Sphere, ball", value: "ball"},{text: "Custom, complex simple shapes, convexHull", value: "convexHull"},{text:"Precision, TriMesh",value:"trimesh"}]}, - forces: {acceptReporters: false, items: [{text: "Force", value: "addForce"},{text: "Torque (rotation)", value: "addTorque"},{text: "Apply Impulse", value: "applyImpulse"},{text: "Apply Torque Impulse (rotation)", value: "applyTorqueImpulse"},{text: "Linear Velocity", value: "setLinvel"},{text: "Angular Velocity", value: "setAngvel"},]}, - resetF: {acceptReporters: false, items: [{text:"Forces", value: "resetForces"},{text:"Torques", value: "resetTorques"},]} - } - } - } - joint(jointData, bodyA, bodyB) { - physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true) - } - - fixedJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - let RA = JSON.parse(args.RA).map(Number) - let RB = JSON.parse(args.RB).map(Number) - - RA = new THREE.Quaternion().setFromEuler( - new THREE.Euler( - THREE.MathUtils.degToRad(RA[0]), - THREE.MathUtils.degToRad(RA[1]), - THREE.MathUtils.degToRad(RA[2]) - ) - ) - RB = new THREE.Quaternion().setFromEuler( - new THREE.Euler( - THREE.MathUtils.degToRad(RB[0]), - THREE.MathUtils.degToRad(RB[1]), - THREE.MathUtils.degToRad(RB[2]) - ) - ) - - const data = RAPIER.JointData.fixed( - { x: VA[0], y: VA[1], z: VA[2] }, RA, - { x: VB[0], y: VB[1], z: VB[2] }, RB - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) - } - - sphericalJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - - const data = RAPIER.JointData.spherical( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] } - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) - } - - revoluteJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - const x = JSON.parse(args.X).map(Number) - - const data = RAPIER.JointData.revolute( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) - } - - prismaticJoint(args) { - const VA = JSON.parse(args.VA).map(Number) - const VB = JSON.parse(args.VB).map(Number) - const x = JSON.parse(args.X).map(Number) - - const data = RAPIER.JointData.prismatic( - { x: VA[0], y: VA[1], z: VA[2] }, - { x: VB[0], y: VB[1], z: VB[2] }, { x: x[0], y: x[1], z: x[2] }, - ) - const objectA = getObject(args.ObjA) - let object = getObject(args.ObjB) - this.joint(data, objectA, object) - } - - createWorld(args) { - const v3 = JSON.parse(args.G).map(Number) - const gravity = { x: v3[0], y: v3[1], z: v3[2]} - physicsWorld = new RAPIER.World(gravity) - - console.log(physicsWorld) - } - - getWorld(args) { - if (args.PROPERTY === "log") {console.log(physicsWorld); return "logged"} - return JSON.stringify(physicsWorld[args.PROPERTY]) - } - - setRB(args) { - let value = args.VALUE - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE - let object = getObject(args.OBJECT) - object.rigidBody[args.PROPERTY](value) - } - setC(args) { - let value = args.VALUE - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE - let object = getObject(args.OBJECT) - object.collider[args.PROPERTY](value) - } - - getRB(args) { - let object = getObject(args.OBJECT) - return JSON.stringify(object.rigidBody[args.PROPERTY]()) - } - getC(args) { - let object = getObject(args.OBJECT) - return JSON.stringify(object.collider[args.PROPERTY]()) - } - - lockObjectAxis(args) { - let object = getObject(args.OBJECT) - const x = !JSON.parse(args.X) - const y = !JSON.parse(args.Y) - const z = !JSON.parse(args.Z) - object.rigidBody[args.PROPERTY](x,y,z,true) //changes is xyz, wake up - } - - objectPhysics(args) { - let object = getObject(args.OBJECT) - object.physics = JSON.parse(args.state) - - if (JSON.parse(args.state)) { - //if already exists delete: - if (object.rigidBody) { - physicsWorld.removeRigidBody(object.rigidBody) - object.rigidBody = null - object.collider = null - } - /*asing a rigidbody and collider to object and add them to physicsWorld*/ - let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() - .setTranslation(object.position.x, object.position.y, object.position.z) - .setRotation({w: object.quaternion._w, x: object.quaternion._x, y: object.quaternion._y, z: object.quaternion._z}) - - let colliderDesc - switch(args.collider) { - case "cuboid": colliderDesc = createCuboidCollider(object,); break - case "ball": colliderDesc = createBallCollider(object); break - case "convexHull": colliderDesc = createConvexHullCollider(object); break - case "trimesh": colliderDesc = TriMesh(object); break - } - colliderDesc.setSensor(JSON.parse(args.state2)).setMass(args.mass).setDensity(args.density).setFriction(args.friction) - - let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc) - let collider = physicsWorld.createCollider(colliderDesc, rigidBody) - - object.rigidBody = rigidBody - object.collider = collider - } else { - /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ - physicsWorld.removeRigidBody(object.rigidBody) - object.rigidBody = null - object.collider = null - } - - } - - enableCCD(args) { - let object = getObject(args.OBJECT) - if (object.physics) { - let rigidBody = object.rigidBody - rigidBody.enableCcd(JSON.parse(args.state)) - } - } - - addForce(args) { - let object = getObject(args.OBJECT) - const vector = JSON.parse(args.VALUE).map(Number) - - let force = new THREE.Vector3(vector[0],vector[1],vector[2]) - if (args.SPACE === "local") { - force.applyQuaternion(object.quaternion); - } - - object.rigidBody[args.PROPERTY](force,true) - } - - resetForces(args) { - rigidBody[args.PROPERTY](true) - } - - sensorSingle(args) { - const sensor = getObject(args.SENSOR) - - let object = getObject(args.OBJECT) - - let touching = false - physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { - if (otherCollider === object.collider) touching = true - }) - - return touching - } - - sensorAll(args) { - const sensor = getObject(args.SENSOR) - - const touchedObjects = [] - - // loop thruogh every collider touching sensor - physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { - // find owner of collider - const otherObject = scene.children.find(o => o.collider === otherCollider) - console.log(otherCollider) - if (otherObject) touchedObjects.push(otherObject.name) - }) - - return JSON.stringify(touchedObjects) - } - - } - Scratch.extensions.register(new RapierPhysics()) - - //Thanks to the PointerLock extension of Turbowarp - const mouse = vm.runtime.ioDevices.mouse; - let isLocked = false; - let isPointerLockEnabled = false; - - let rect = threeRenderer.domElement.getBoundingClientRect(); - document.addEventListener("resize", () => { - rect = threeRenderer.domElement.getBoundingClientRect(); - }); - - const postMouseData = (e, isDown) => { - const { movementX, movementY } = e; - const { width, height } = rect; - const x = mouse._clientX + movementX; - const y = mouse._clientY - movementY; - mouse._clientX = x; - mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); - mouse._clientY = y; - mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); - if (typeof isDown === "boolean") { - const data = { - button: e.button, - isDown, - }; - originalPostIOData(data); - } - }; - - const mouseDevice = vm.runtime.ioDevices.mouse; - const originalPostIOData = mouseDevice.postData.bind(mouseDevice); - mouseDevice.postData = (data) => { - if (!isPointerLockEnabled) { - return originalPostIOData(data); - } - }; - - document.addEventListener( - "mousedown", - (e) => { - // @ts-expect-error - if (threeRenderer.domElement.contains(e.target)) { - if (isLocked) { - postMouseData(e, true); - } else if (isPointerLockEnabled) { - threeRenderer.domElement.requestPointerLock(); - } - } - }, - true - ); - document.addEventListener( - "mouseup", - (e) => { - if (isLocked) { - postMouseData(e, false); - // @ts-expect-error - } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { - threeRenderer.domElement.requestPointerLock(); - } - }, - true - ); - document.addEventListener( - "mousemove", - (e) => { - if (isLocked) { - postMouseData(e); - } - }, - true - ); - - document.addEventListener("pointerlockchange", () => { - isLocked = document.pointerLockElement === threeRenderer.domElement; - }); - document.addEventListener("pointerlockerror", (e) => { - console.error("Pointer lock error", e); - }); - - const oldStep = vm.runtime._step; - vm.runtime._step = function (...args) { - const ret = oldStep.call(this, ...args); - if (isPointerLockEnabled) { - const { width, height } = rect; - mouse._clientX = width / 2; - mouse._clientY = height / 2; - mouse._scratchX = 0; - mouse._scratchY = 0; - } - return ret; - }; - - vm.runtime.on("PROJECT_LOADED", () => { - isPointerLockEnabled = false; - if (isLocked) { - document.exitPointerLock(); - } - }); - - class Pointerlock { - getInfo() { - return { - id: "threepointerlockmod", - name: "Pointerlock for Extra 3D", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [ - {opcode: "setLocked", blockType: Scratch.BlockType.COMMAND, text: "set pointer lock [enabled]", arguments: { enabled: { type: Scratch.ArgumentType.STRING, defaultValue: "true", menu: "enabled"}},}, - {opcode: "isLocked", blockType: Scratch.BlockType.BOOLEAN, text: "pointer locked?",}, - ], - menus: { - enabled: {acceptReporters: true, items: [ - {text: "enabled", value: "true"},{text: "disabled", value: "false"}, - ]} - }, - } - } - - setLocked(args) { - isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; - if (!isPointerLockEnabled && isLocked) { - document.exitPointerLock(); - } - } - - isLocked() { - return isLocked; - } - } -Scratch.extensions.register(new Pointerlock()) - - }) - - - - -})(Scratch); diff --git a/threejsD_REMOTE_13852.js b/threejsD_REMOTE_13852.js deleted file mode 100644 index 447584f..0000000 --- a/threejsD_REMOTE_13852.js +++ /dev/null @@ -1,5016 +0,0 @@ -/* jshint esversion: 11 */ -// Name: Extra 3D -// ID: threejsExtension -// Description: Use three js inside Turbowarp! A 3D graphics library. -// By: Civero -// License: MIT License Copyright (c) 2021-2024 TurboWarp Extensions Contributors - -(function(Scratch) { - "use strict"; - - if (!Scratch.extensions.unsandboxed) { - throw new Error("Three-D extension must run unsandboxed"); - } - - if (Scratch.vm.runtime.isPackaged) { - alert(`Uncheck the setting "Remove raw asset data after loading to save RAM" for package!`); - return; - } - //if (Scratch.vm.extensionManager._loadedExtensions.has("threejsExtension") && typeof scaffolding == "undefined") return - - const vm = Scratch.vm; - const runtime = vm.runtime; - const renderer = Scratch.renderer; - const canvas = renderer.canvas; - const Cast = Scratch.Cast; - const menuIconURI = - "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwLDAsMTc3LjIzLDE4MC40NzU3MSIgaGVpZ2h0PSIxODAuNDc1NzEiIHdpZHRoPSIxNzcuMjMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE2Ni4zODUsLTEwMS45OTQyOSkiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLXdpZHRoPSIxIiBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgZmlsbD0iI2ZmZmZmZiIgZD0iTTMxMS4wMjY0NCwxMzYuMzI5ODRjLTAuMDgxMzYsMC4zNDU3OCAtMC4xNDIzOCwwLjY5MTU2IC0wLjI0NDA4LDEuMDM3MzRjLTAuMzA1MSwxLjI4MTQyIC0wLjkzNTY0LDQuMzEyMDggLTEuNTY2MTgsMTAuMjMxMDJjMCwwLjEwMTcgMCwwLjE4MzA2IC0wLjAyMDM0LDAuMjQ0MDhjMy40NzgxNCwxMy45OTM5MyAtMi4zNzk3OCwyMi41MTY0IC02LjI2NDcyLDI2LjQwMTM0Yy0wLjI0NDA4LDAuMjY0NDIgLTAuNTA4NSwwLjUwODUxIC0wLjc5MzI2LDAuNzUyNTljLTMuODAzNTgsMy40NTc4MSAtMTAuNDU0NzcsNy41ODY4MyAtMjAuMzgwNyw3LjU4NjgzYy00Ljk0MjYzLDAgLTkuNTU5OCwtMS4wOTgzNyAtMTMuNTg3MTMsLTMuMTEyMDNjMC4xMDE3LDUuNTUyODMgMC4xNjI3MiwxMy4yMDA2NyAwLjE2MjcyLDIzLjgxODE2YzMuNjYxMiwxLjI4MTQyIDcuMDE3MzEsMy4zNTYxMSA5Ljg2NDkxLDYuMDgxNjdjNS42NTQ1Miw1LjQzMDc5IDguNzQ2MiwxMi42OTIxNyA4Ljc0NjIsMjAuNDQxNzFjMCwxMS41MTI0NSAtNi42MzA4NCwyMS41MTk3MyAtMTcuMzA5MzUsMjYuMDk2MjRjLTAuMjY0NDIsMC4xMjIwNCAtMC41NDkxOSwwLjI0NDA4IC0wLjgxMzYsMC4zNDU3OGMtMy41Nzk4NCwxLjM2Mjc4IC03LjYwNzE2LDIuMDM0IC0xMi4zMjYwNSwyLjAzNGMtMS43MDg1NiwwIC0zLjUzOTE2LC0wLjA4MTM2IC01LjUzMjQ4LC0wLjI2NDQyYy0xLjIyMDQsLTAuMDYxMDIgLTMuMDEwMzIsLTAuMDQwNjggLTUuMTI1NjksMC4wMjAzNGMtMy44NDQyNywwLjQyNzE0IC05LjI1NDcxLDAuODU0MjggLTE2LjQ5NTc2LDEuMjYxMDhjLTAuMTQyMzgsMCAtMC4yODQ3NiwwLjAyMDM0IC0wLjQ0NzQ4LDAuMDIwMzRjLTAuOTU1OTgsMC4wNDA2OCAtMS44NzEyOCwwLjA2MTAyIC0yLjc2NjI0LDAuMDYxMDJjLTEyLjk1NjU5LDAgLTIyLjQxNDY5LC00LjEwODY5IC0yOC4xMzAyNCwtMTIuMTgzNjdjLTAuMTIyMDQsLTAuMTYyNzIgLTAuMjIzNzQsLTAuMzI1NDQgLTAuMzI1NDQsLTAuNDg4MTZjLTUuODE3MjQsLTguNjg1MTggLTUuOTc5OTYsLTE5LjY2ODc5IC0wLjQ0NzQ4LC0yOC42Mzg3NGMwLjA0MDY4LC0wLjEwMTcgMC4xMDE3LC0wLjE4MzA2IDAuMTYyNzIsLTAuMjg0NzZjMy41MTg4MiwtNS41MzI0OSA4LjY2NDg0LC05LjQ3ODQ1IDE1LjMzNjM3LC0xMS43OTcyMWMwLjA4MTM2LC0zLjkyNTYyIDAuMDYxMDIsLTguODQ3OSAtMC4wNjEwMiwtMTQuNjg1NDljLTMuMzE1NDMsMS4zODMxMiAtNy4xMzkzNCwyLjE5NjcyIC0xMS40MzEwOSwyLjE5NjcyYy0xMS4zMjkzOSwwIC0yMC42ODU4LC02LjczMjU0IC0yMy45NDAyLC0xNi45NjM1N2MtMC42NzEyMiwtMi4wNzQ2OCAtMS4zMDE3NiwtNS4xMDUzNCAtMi43NjYyNCwtMTEuOTM5NTljLTAuMDYxMDIsLTAuMjQ0MDggLTAuMTAxNywtMC40ODgxNiAtMC4xNDIzOCwtMC43MzIyNGwtMy4wMTAzMiwtMTYuODIxMTljLTAuMTAxNywtMC4zNjYxMiAtMC4yNDQwOCwtMC43OTMyNiAtMC40MDY4LC0xLjI4MTQyYy0xLjU2NjE4LC00LjQ1NDQ2IC0yLjI5ODQzLC04LjIzNzcxIC0yLjI5ODQzLC0xMS44OTg5MWMwLC00LjUzNTgyIDEuMzIyMSwtMTEuMzkwNCA3LjU4NjgzLC0xOC4yMjQ2NWMzLjE1MjcsLTMuNDU3OCA4Ljg4ODU5LC03LjkzMjYxIDE4LjEyMjk1LC05LjM3Njc1YzEuMTM5MDQsLTAuMTgzMDYgMi4yOTg0MywtMC4yODQ3NiAzLjQ1NzgxLC0wLjI4NDc2aDIyLjQ5NjA2YzAuNTA4NSwwIDEuMDE3LDAuMDIwMzQgMS41MjU1LDAuMDYxMDJjOC41ODM0OCwwLjMwNTEgMTYuMjcyMDEsMC4yODQ3NiAyMi44NjIxOCwtMC4wMjAzNGM5LjIxNDAyLC0wLjQwNjggMTguNDA3NzEsLTEuMjAwMDYgMjcuNDE4MzQsLTIuMzU5NDRjMS4wMTcsLTAuMzI1NDQgMi4xMTUzNiwtMC42NTA4OCAzLjI5NTA4LC0wLjkzNTY0YzEuMTE4NywtMC4yODQ3NiAyLjI1Nzc1LC0wLjQ2NzgyIDMuMzk2NzksLTAuNTg5ODZjOC42NjQ4NSwtMC43OTMyNiAxNi43Mzk4MywxLjcwODU2IDIzLjAyNDksNy4wNzgzMmM3Ljc5MDIzLDYuNjkxODYgMTEuMjI3NjksMTYuODIxMTkgOS4xNzMzNSwyNy4xMTMyNHoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjExLjU5OCwyODAuNDdsLTQzLjIxMywtMTc0Ljk0bDE3My4yMyw0OS44NzR6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTI1NC45NjgsMTMwLjQ3MmwyMS41OTEsODcuNDk2bC04Ni41NjcsLTI0Ljk0NXoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjMzLjQ4OCwyMDQuODlsLTEwLjcyNCwtNDMuNDY1bDQzLjAwOCwxMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIxMi4wMzYsMTE4LjAxM2wxMC43MjQsNDMuNDY1bC00My4wMDgsLTEyLjM0NnoiPjwvcGF0aD48cGF0aCBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZT0iI2ZmZmZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBmaWxsPSIjMjIyMjIyIiBkPSJNMjk4LjA0OCwxNDIuNzlsMTAuNzI0LDQzLjQ2NWwtNDMuMDA4LC0xMi4zNDZ6Ij48L3BhdGg+PHBhdGggc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2U9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZmlsbD0iIzIyMjIyMiIgZD0iTTIzMy40OTMsMjA0LjkybDEwLjcyNCw0My40NjVsLTQzLjAwOCwtMTIuMzQ2eiI+PC9wYXRoPjxwYXRoIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS13aWR0aD0iNSIgc3Ryb2tlPSIjMmRmZmIyIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNmN2Y3ZjciIGQ9Ik0yODkuMDgzNjMsMTMxLjk0NDUzYy0wLjgzMzk0LDMuMzQxODcgLTEuNTQ5OTEsNy44NzE1OCAtMi4xNDU4NywxMy41OTczYy0wLjI0MjA1LDIuODYzODggLTAuMTI0MDcsNS4xODg3NCAwLjM1Nzk5LDYuOTc4NjZjMC44Mjk4OCwyLjk4Mzg4IDAuNzc0OTUsNC45NTI3OSAtMC4xNzksNS45MDQ3Yy0xLjMxMzk2LDEuMTkxOTIgLTMuMTAxODUsMS43ODc4OSAtNS4zNjc3MywxLjc4Nzg5Yy0yLjYyNTksMCAtNC4zNTg4NiwtMC43NzkwMiAtNS4xODg3NCwtMi4zNDExM2MtMC4xMjQwOCwtNC44MDQzMSAtMC4wNjMwNiwtOS4zMTE2NiAwLjE3ODk5LC0xMy41MTc5OGMwLjIzMzkxLC01LjE2NjM3IDAuMzU3OTksLTcuODY5NTUgMC4zNTc5OSwtOC4xMDk1N2MtMC4xMjQwOCwwIC0wLjMwMTAzLC0wLjEyMDAxIC0wLjUzNjk4LC0wLjM2MjA1Yy0xMS4wOTU0OCwwLjQ4MjA2IC0yMS41MzE5NCwxLjE5ODAzIC0zMS4zMTE0MiwyLjE1NDAxYy0wLjI0MDAxLDEuMTk4MDMgLTAuMjQwMDEsMy4xMDc5NiAwLDUuNzM1ODhjMC40NzM5MiwzLjcwNTk1IDAuNzE1OTYsNS44NTc5MiAwLjcxNTk2LDYuNDUxODVjLTAuNDc1OTUsMy43MDU5NSAtMC43MTU5Niw5LjIwMTgyIC0wLjcxNTk2LDE2LjQ5MzcyYzAuNDczOTIsMy4xMDc5NiAwLjcxNTk2LDE2LjQzNDczIDAuNzE1OTYsMzkuOTc4M3YxMy4wODg4YzAsMi4wMzE5NyAwLjI5NDkzLDMuNDY1OTQgMC44ODY4Myw0LjMwMTkxaDEwLjk4NTY0YzIuMDA3NTYsLTAuMjM3OTggMy42MzA2OSwwLjI0MDAxIDQuODczNDcsMS40MzE5NGMxLjIzODcsMS4xOTM5NiAxLjg1OTA4LDIuNjIzODYgMS44NTkwOCw0LjI5MTc0YzAsMi42MjE4MyAtMS4yNTA5MSw0LjQ3Mjc3IC0zLjc1NjgsNS41NDg3NmMtMS41NTE5NCwwLjU5MzkzIC00LjI5Mzc3LDAuNzE1OTcgLTguMjI5NTcsMC4zNTc5OWMtMS45MDc4OSwtMC4xMjIwNCAtNC43MTI3OSwtMC4xMjIwNCAtOC40MTA2LDBjLTMuMzM5ODMsMC40MTQ5NCAtOC43MDU1MiwwLjgzMTkxIC0xNi4xMDExNSwxLjI1MDkxYy02LjQ0MTY5LDAuMjM3OTggLTEwLjM3NzQ4LC0wLjY1Njk4IC0xMS44MDk0MSwtMi42ODI4NWMtMC45NTU5OCwtMS40Mjk5IC0wLjk1NTk4LC0yLjkyMjg2IDAsLTQuNDcyNzdjMS42Njc4OCwtMi42MjE4MyA2LjAyMjY3LC0zLjkzNTggMTMuMDYyMzUsLTMuOTM1OGMyLjUwMzg1LDAgNC4wNTE3NCwtMC4yMDc0NiA0LjY0OTczLC0wLjYyNDQzYzAuNTk1OTYsLTAuNDE2OTcgMC44OTQ5NiwtMS4yMjI0NCAwLjg5NDk2LC0yLjQxNDM2YzAsLTEuMDY5ODggMCwtMi4wODA3OCAwLC0zLjAzNDczYzAsLTEuNzgzODIgMCwtNC40MDM2MiAwLC03Ljg1NTMxYzAuMzU3OTksLTYuMDY3NDIgMC4zNTc5OSwtMTUuMTE2NyAwLC0yNy4xMzk2OWMtMC40Nzc5OSwtMTcuMjU4NTEgLTAuMjQwMDEsLTMyLjQzMjE1IDAuNzE1OTcsLTQ1LjUyNzA1Yy0wLjEyLC0wLjExNzk3IC0wLjI5OSwtMC4yOTY5NyAtMC41NDEwNCwtMC41MzY5OGMtNC4zMTgxOCwwLjI0MDAxIC0xMS4yNzQ0OCwwLjEyMDAxIC0yMC44Njg4NiwtMC4zNjAwMmMtMS4wODAwNSwwIC00Ljc0MTI1LDAuMjQyMDUgLTEwLjk3NTQ3LDAuNzE4MDFjMS41NDk5MSwxMC44NTU0NyAyLjUwMzg1LDE5LjAyNjA1IDIuODYxODQsMjQuNTExNzVjMCwwLjcxNTk3IC0wLjEyLDIuMzI2OSAtMC4zNTc5OCw0LjgzMDc1Yy0wLjEyLDEuNzg3ODggLTEuNjEwOTMsMi42ODI4NSAtNC40NzI3NywyLjY4Mjg1Yy0xLjU1MTk0LDAgLTIuNDQ0ODcsLTAuNTMyOSAtMi42ODI4NSwtMS41OTY2OWMtMC4xMiwtMC4yMzM5MSAtMC44MzU5NywtMy40MzEzNiAtMi4xNDc5LC05LjU4MjE4Yy0wLjcxNTk3LC00LjAyMTIyIC0xLjczMDk0LC05LjcwMDE1IC0zLjA0MDg0LC0xNy4wMzQ3NmMwLC0wLjQ2NzgyIC0wLjQxOSwtMS45NDY1NCAtMS4yNTA5MSwtNC40MzQxMmMtMC43MTE5LC0yLjAxMTYzIC0xLjA2Nzg1LC0zLjU0OTMzIC0xLjA2Nzg1LC00LjYxMzExYzAsLTAuODI1ODEgMC41NjM0MiwtMS44NjUxOCAxLjcwMDQyLC0zLjEwMzg5YzEuMTMyOTQsLTEuMjQwNzQgMi44MzMzNiwtMi4wNDIxMyA1LjA5OTI0LC0yLjM5ODA4YzAuMzU3OTksMCAwLjkyMTQsMCAxLjcwMDQyLDBjMC43NzI5MiwwIDEuMzk5MzksMCAxLjg3NzM5LDBjMTQuMTk1MjksMCAyMC4zOTY5NiwwIDE4LjYwNzA1LDBjOS42NjE1MSwwLjM2MDAyIDE4LjI0OTA3LDAuMzYwMDIgMjUuNzYyNjcsMGMxMC43MzM0MywtMC40NzM5MiAyMS4zNDY4NSwtMS40Mjk5MSAzMS44NDYzNiwtMi44NjE4NGMwLjcxNTk3LC0wLjM1Nzk4IDEuNzg3ODgsLTAuNzE1OTcgMy4yMTc3OSwtMS4wNzE5MmMyLjYyNTksLTAuMjM3OTggNC43NzE3NywwLjM1Nzk4IDYuNDQzNzIsMS43ODk5MmMxLjY2Nzg4LDEuNDI3ODcgMi4yNjM4NCwzLjMzNzggMS43ODc4OCw1LjcyMzY4eiI+PC9wYXRoPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjczLjYxNTAwMDAwMDAwMDAxOjc4LjAwNTcxMTMwMDg0OTk0LS0+"; - - let alerts = false; - console.log("alerts are " + (alerts ? "enabled" : "disabled")); - - let isMouseDown = { - left: false, - middle: false, - right: false, - }; - let prevMouse = { - left: false, - middle: false, - right: false, - }; - - let lastWidth = 0; - let lastHeight = 0; - - let THREE; - let clock; - let running; - let loopId; - //Addons - let GLTFLoader; - let gltf; - let OrbitControls; - let controls; - let BufferGeometryUtils; - let TextGeometry; - let fontLoad; - //Physics - let RAPIER; - let physicsWorld; - - let threeRenderer; - let scene; - let camera; - let eulerOrder = "YXZ"; - - let composer; - let passes = {}; - let customEffects = []; - let renderTargets = {}; - - let materials = {}; - let geometries = {}; - let lights = {}; - let models = {}; - - let assets = { - //should i place materials, geometries; inside too? - textures: {}, - colors: {}, - fogs: {}, - curves: {}, - renderTargets: {}, //not the same as the global one! this one only stores textures - }; - - let raycastResult = []; - - function resetor(level) { - camera = undefined; - composer.reset(); - - passes = {}; - customEffects = []; - renderTargets = {}; - - materials = {}; - geometries = {}; - lights = {}; - models = {}; - - if (level > 0) { - assets = { - textures: {}, - colors: {}, - fogs: {}, - curves: {}, - renderTargets: {}, - }; - } - - updateComposers(); - } - - //utility - function vector3ToString(prop) { - if (!prop) return "0,0,0"; - - const x = - typeof prop.x === "number" ? - prop.x : - typeof prop._x === "number" ? - prop._x : - JSON.stringify(prop).includes("X") ? - prop : - 0; - const y = typeof prop.y === "number" ? prop.y : typeof prop.y === "number" ? prop._y : 0; - const z = typeof prop.z === "number" ? prop.z : typeof prop.z === "number" ? prop.z : 0; - - return [x, y, z]; - } - - //objects - function createObject(name, content, parentName) { - let object = getObject(name, true); - if (object) { - removeObject(name); - alerts ? alert(name + " already exsisted, will replace!") : null; - } - content.name = name; - content.rotation._order = eulerOrder; - parentName === scene.name ? (object = scene) : (object = getObject(parentName)); - content.physics = false; - - object.add(content); - } - - function removeObject(name) { - let object = getObject(name); - if (!object) return; - - scene.remove(object); - - if (object.rigidBody) { - physicsWorld.removeCollider(object.collider, true); - physicsWorld.removeRigidBody(object.rigidBody, true); - object.rigidBody = null; - object.collider = null; - } - if (object.isLight) { - delete lights[name]; - } - } - - function getObject(name, isNew) { - let object = null; - if (!scene) { - alerts ? alert("Can not get " + name + ". Create a scene first!") : null; - return; - } - object = scene.getObjectByName(name); - if (!object && !isNew) { - alerts ? alert(name + " does not exist! Add it to scene") : null; - return; - } - return object; - } - - //materials - function encodeCostume(name) { - if (name.startsWith("data:image/")) return name; - return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); - } - - function setTexutre(texture, mode, style, x, y) { - texture.colorSpace = THREE.SRGBColorSpace; - - if (mode === "Pixelate") { - texture.minFilter = THREE.NearestFilter; - texture.magFilter = THREE.NearestFilter; - } else { - //Blur - texture.minFilter = THREE.NearestMipmapLinearFilter; - texture.magFilter = THREE.NearestMipmapLinearFilter; - } - - if (style === "Repeat") { - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; - texture.repeat.set(x, y); - } - - texture.generateMipmaps = true; - } - async function resizeImageToSquare(uri, size = 256) { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext("2d"); - - // clear + draw image scaled to fit canvas - ctx.clearRect(0, 0, size, size); - ctx.drawImage(img, 0, 0, size, size); - - resolve(canvas.toDataURL()); // return normalized Data URI - //delete canvas? - }; - img.src = uri; - }); - } - //light - function updateShadowFrustum(light, focusPos) { - if (light.type !== "DirectionalLight") return; - - // Frustum Size - Increase this value to cover a larger area. - const d = 50; - - // Update Orthographic Shadow Camera Frustum - const shadowCamera = light.shadow.camera; - - // Set the width/height of the frustum - shadowCamera.left = -d; - shadowCamera.right = d; - shadowCamera.top = d; - shadowCamera.bottom = -d; - - // Determine ranges - shadowCamera.near = 0.1; - shadowCamera.far = 500; - - // Position the Light and its Target - light.target.position.copy(focusPos); - const direction = light.position.clone().sub(light.target.position).normalize(); - light.position.copy(focusPos.clone().add(direction.multiplyScalar(100))); - - // Ensure matrices are updated. - light.target.updateMatrixWorld(); - light.shadow.camera.updateProjectionMatrix(); - light.shadow.needsUpdate = true; - } - //composer - function updateComposers() { - if (!camera || !scene) return; // nothing to do yet - - // always recreate the RenderPass to point to the current scene/camera - passes["Render"] = new RenderPass(scene, camera); - - // ensure composer has a RenderPass as the first pass - const hasRender = composer.passes.some((p) => p && p.scene); - if (!hasRender) composer.addPass(passes["Render"]); - else { - // if composer already has one, replace it so it references current scene/camera - const idx = composer.passes.findIndex((p) => p && p.scene); - composer.passes[idx] = passes["Render"]; - } - } - //utility - function getMouseNDC(event) { - // Use threeRenderer.domElement for correct offset - const rect = threeRenderer.domElement.getBoundingClientRect(); - const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - return [x, y]; - } - - function checkCanvasSize() { - const { - width, - height - } = canvas; - if (width !== lastWidth || height !== lastHeight) { - lastWidth = width; - lastHeight = height; - resize(); - } - requestAnimationFrame(checkCanvasSize); //rerun next frame - } - //physics - function computeWorldBoundingBox(mesh) { - // Create a Box3 in world coordinates - const box = new THREE.Box3().setFromObject(mesh); - const size = new THREE.Vector3(); - box.getSize(size); - const center = new THREE.Vector3(); - box.getCenter(center); - return { - size, - center, - }; - } - - function createCuboidCollider(mesh) { - const { - size - } = computeWorldBoundingBox(mesh); - const collider = RAPIER.ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2); - return collider; - } - - function createBallCollider(mesh) { - const { - size - } = computeWorldBoundingBox(mesh); - // radius = 1/2 of the largest verticie - const radius = Math.max(size.x, size.y, size.z) / 2; - const collider = RAPIER.ColliderDesc.ball(radius); - return collider; //there's a flaw with this, if the ball is deformed in just one axis it will be inaccurate. use convex instead (?) - } - - function createConvexHullCollider(mesh) { - mesh.updateWorldMatrix(true, false); - - const position = mesh.geometry.attributes.position; - const vertices = []; - const vertex = new THREE.Vector3(); - - // Matrix for scale only - const scaleMatrix = new THREE.Matrix4().makeScale(mesh.scale.x, mesh.scale.y, mesh.scale.z); - - for (let i = 0; i < position.count; i++) { - vertex.fromBufferAttribute(position, i).applyMatrix4(scaleMatrix); - vertices.push(vertex.x, vertex.y, vertex.z); - } - - const collider = RAPIER.ColliderDesc.convexHull(Float32Array.from(vertices)); - return collider; - } - - function TriMesh(mesh) { - // Get the positions array (from your geoPoints function) - const positions = mesh.geometry.attributes.position.array; - const numVertices = positions.length / 3; - - // Create an index array: [0, 1, 2, 3, 4, 5, ..., numVertices - 1] - const indices = Array.from({ - length: numVertices, - }, - (_, i) => i - ); - - const collider = RAPIER.ColliderDesc.trimesh(positions, new Uint32Array(indices)); - - return collider; - } - - function getModel(model, name) { - const file = runtime - .getTargetForStage() - .getSounds() - .find((c) => c.name === model); - if (!file) return; - - return new Promise((resolve, reject) => { - gltf.parse( - file.asset.data.buffer, - "", - (gltf) => { - const root = gltf.scene; - root.traverse((child) => { - if (child.isMesh) { - child.castShadow = true; - child.receiveShadow = true; - } - }); - - const mixer = new THREE.AnimationMixer(root); - const actions = {}; - gltf.animations.forEach((clip) => { - const act = mixer.clipAction(clip); - act.clampWhenFinished = true; - actions[clip.name] = act; - }); - - models[name] = { - root, - mixer, - actions, - }; - resolve(root); - }, - (error) => { - console.error("Error parsing GLB model:", error); - reject(error); - } - ); - }); - } - async function openFileExplorer(format) { - return new Promise((resolve) => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = format; - input.multiple = false; - input.onchange = () => { - resolve(input.files); - input.remove(); - }; - input.click(); - }); - } - - function getMeshesUsingTexture(scene, targetTexture) { - const meshes = []; - - scene.traverse((object) => { - if (object.material) { - const materials = Array.isArray(object.material) ? object.material : [object.material]; - for (const material of materials) { - if (material.map === targetTexture) { - meshes.push(object); - break; - } - } - } - }); - - return meshes; - } - - function getAsset(path) { - if (typeof path == "string") { - //string? - if (path.includes("/")) { - //has the /? - const value = path.split("/"); - console.log(value[0], value[1]); - return assets[value[0]][value[1]]; - } - } - - return JSON.parse(path); //boolean or number - } - - let mouseNDC = [0, 0]; - //loops/init - function stopLoop() { - if (!running) return; - running = false; - - if (loopId) { - cancelAnimationFrame(loopId); - loopId = null; - if (threeRenderer) threeRenderer.clear(); - } - } - async function load() { - if (!THREE) { - // @ts-ignore - THREE = await import("https://esm.sh/three@0.180.0"); - //Addons - GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js"); - OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js"); - BufferGeometryUtils = await import("https://esm.sh/three@0.180.0/examples/jsm/utils/BufferGeometryUtils.js"); - TextGeometry = await import("https://esm.sh/three@0.158.0/examples/jsm/geometries/TextGeometry.js"); - const FontLoader = await import("https://esm.sh/three@0.158.0/examples/jsm/loaders/FontLoader.js"); - fontLoad = new FontLoader.FontLoader(); - - const POSTPROCESSING = await import("https://esm.sh/postprocessing@6.37.8"); - const { - EffectComposer, - EffectPass, - RenderPass, - - Effect, - BloomEffect, - GodRaysEffect, - DotScreenEffect, - DepthOfFieldEffect, - - BlendFunction, - } = POSTPROCESSING; - //so i can use them later as global - window.EffectComposer = EffectComposer; - window.EffectPass = EffectPass; - window.RenderPass = RenderPass; - window.Effect = Effect; - window.BloomEffect = BloomEffect; - window.GodRaysEffect = GodRaysEffect; - window.DotScreenEffect = DotScreenEffect; - window.DepthOfFieldEffect = DepthOfFieldEffect; - window.BlendFunction = BlendFunction; - - RAPIER = await import("https://esm.sh/@dimforge/rapier3d-compat@0.19.0"); - await RAPIER.init(); - - threeRenderer = new THREE.WebGLRenderer({ - powerPreference: "high-performance", - antialias: false, - stencil: false, - depth: true, - }); - threeRenderer.setPixelRatio(window.devicePixelRatio); - threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors - threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) - //threeRenderer.toneMappingExposure = 1.0 //(test) - - threeRenderer.shadowMap.enabled = true; - threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap; // (optional) - threeRenderer.domElement.style.pointerEvents = "auto"; //will disable turbowarp mouse events, but enable threejs's - - gltf = new GLTFLoader.GLTFLoader(); - clock = new THREE.Clock(); - - // Example: create a composer - composer = new EffectComposer(threeRenderer, { - frameBufferType: THREE.HalfFloatType, - }); - - renderer.addOverlay(threeRenderer.domElement, "manual"); - renderer.addOverlay(canvas, "manual"); - renderer.setBackgroundColor(1, 1, 1, 0); - - resize(); - - window.addEventListener("mousedown", (e) => { - if (e.button === 0) isMouseDown.left = true; - if (e.button === 1) isMouseDown.middle = true; - if (e.button === 2) isMouseDown.right = true; - }); - window.addEventListener("mouseup", (e) => { - if (e.button === 0) isMouseDown.left = false; - prevMouse.left = false; - if (e.button === 1) isMouseDown.middle = false; - prevMouse.middle = false; - if (e.button === 2) isMouseDown.right = false; - prevMouse.right = false; - }); - // prevent contextmenu on right click - threeRenderer.domElement.addEventListener("contextmenu", (e) => e.preventDefault()); - - threeRenderer.domElement.addEventListener("mousemove", (event) => { - mouseNDC = getMouseNDC(event); - }); - - running = false; - load(); - - startRenderLoop(); - runtime.on("PROJECT_START", () => startRenderLoop()); - runtime.on("PROJECT_STOP_ALL", () => stopLoop()); - runtime.on("STAGE_SIZE_CHANGED", () => { - requestAnimationFrame(() => resize()); - }); - //if (!runtime.isPackaged) checkCanvasSize() //only in editor - } - } - - function startRenderLoop() { - if (running) return; - running = true; - - const loop = () => { - if (!running) return; - //RAPIER - if (physicsWorld && scene) { - physicsWorld.step(); - - scene.children.forEach((obj) => { - if (!obj.isMesh || !obj.physics) return; - if (obj.rigidBody) { - obj.position.copy(obj.rigidBody.translation()); - obj.quaternion.copy(obj.rigidBody.rotation()); - } - }); - } - if (scene && camera) { - if (controls) controls.update(); - - const delta = clock.getDelta(); - Object.values(models).forEach((model) => { - if (model) model.mixer.update(delta); - }); - - Object.values(lights).forEach((light) => updateShadowFrustum(light, camera.position)); - - //update custom effects time - customEffects.forEach((e) => { - if (e.uniforms.get("time")) { - e.uniforms.get("time").value += delta; - } - }); - Object.values(renderTargets).forEach((t) => { - if (t.camera.type == "PerspectiveCamera") { - t.camera.aspect = t.target.width / t.target.height; - t.camera.updateProjectionMatrix(); - } - // get meshes using the texture associated with this target - const displayMeshes = getMeshesUsingTexture(scene, t.target.texture); - - displayMeshes.forEach((mesh) => { - mesh.visible = false; - }); - - if (t.camera.type == "PerspectiveCamera") { - threeRenderer.setRenderTarget(t.target); - threeRenderer.clear(true, true, true); - threeRenderer.render(scene, t.camera); - } else { - t.target.clear(threeRenderer); - t.camera.update(threeRenderer, scene); //cubeCamera - } - - displayMeshes.forEach((mesh) => { - mesh.visible = true; - }); - }); - - camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height; - camera.updateProjectionMatrix(); - threeRenderer.setRenderTarget(null); - composer.render(delta); - } - - loopId = requestAnimationFrame(loop); - }; - - loopId = requestAnimationFrame(loop); - } - - function resize() { - const w = canvas.width; - const h = canvas.height; - - threeRenderer.setSize(w, h); - composer.setSize(w, h); - customEffects.forEach((e) => { - if (e.uniforms.get("resolution")) { - e.uniforms.get("resolution").value.set(w, h); - } - }); - - if (camera) { - camera.aspect = w / h; - camera.updateProjectionMatrix(); - } - } - //wait until all packages are loaded - Promise.resolve(load()).then(() => { - class threejsExtension { - getInfo() { - return { - id: "threejsExtension", - name: "Extra 3D", - color1: "#222222", - color2: "#222222", - color3: "#11cc99", - menuIconURI, - blockIconURI: menuIconURI, - - blocks: [{ - blockType: Scratch.BlockType.BUTTON, - text: "Show Docs", - func: "openDocs", - }, - { - blockType: Scratch.BlockType.BUTTON, - text: "Toggle Alerts", - func: "alerts", - }, - ], - menus: {}, - }; - } - openDocs() { - open("https://civ3ro.github.io/extensions/Documentation/"); - } - alerts() { - alerts = !alerts; - alerts ? alert("Alerts have been enabled!") : alert("Alerts have been disabled!"); - } - } - Scratch.extensions.register(new threejsExtension()); - - class ThreeRenderer { - getInfo() { - return { - id: "threeRenderer", - name: "Three Renderer", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "setRendererRatio", - blockType: Scratch.BlockType.COMMAND, - text: "set Pixel Ratio to [VALUE]", - arguments: { - VALUE: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "1", - }, - }, - }, - { - opcode: "eulerOrder", - blockType: Scratch.BlockType.COMMAND, - text: "set euler order to [VALUE]", - arguments: { - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "YXZ", - }, - }, - }, - ], - menus: {}, - }; - } - - setRendererRatio(args) { - threeRenderer.setPixelRatio(window.devicePixelRatio * args.VALUE); - } - eulerOrder(args) { - eulerOrder = args.VALUE; - console.log("euler order set to", eulerOrder); - } - } - Scratch.extensions.register(new ThreeRenderer()); - - class ThreeScene { - constructor() { - this.THREE = THREE; - this.scenes = {}; - } - - getInfo() { - return { - id: "threeScene", - name: "Three Scene", - color1: "#4638c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "newScene", - blockType: Scratch.BlockType.COMMAND, - text: "new Scene [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - }, - }, - - { - opcode: "setSceneProperty", - blockType: Scratch.BlockType.COMMAND, - text: "set Scene [PROPERTY] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "sceneProperties", - defaultValue: "background", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "new Color()", - exemptFromNormalization: true, - }, - }, - }, - "---", - { - opcode: "getSceneObjects", - blockType: Scratch.BlockType.REPORTER, - text: "get Scene [THING]", - arguments: { - THING: { - type: Scratch.ArgumentType.STRING, - menu: "sceneThings", - }, - }, - }, - { - opcode: "reset", - blockType: Scratch.BlockType.COMMAND, - text: "Reset Everything", - }, - ], - menus: { - sceneProperties: { - acceptReporters: false, - items: [{ - text: "Background", - value: "background", - }, - { - text: "Background Blurriness", - value: "backgroundBlurriness", - }, - { - text: "Background Intensity", - value: "backgroundIntensity", - }, - { - text: "Background Rotation", - value: "backgroundRotation", - }, - { - text: "Environment", - value: "environment", - }, - { - text: "Environment Intensity", - value: "environmentIntensity", - }, - { - text: "Environment Rotation", - value: "environmentRotation", - }, - { - text: "Fog", - value: "fog", - }, - ], - }, - sceneThings: { - acceptReporters: false, - items: ["Objects", "Materials", "Geometries", "Lights", "Scene Properties", "Other assets"], - }, - }, - }; - } - - newScene(args) { - scene = new THREE.Scene(); - scene.name = args.NAME; - scene.background = new THREE.Color("#222"); - //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - this.scenes = { - ...this.scenes, - ...scene, - }; - resetor(0); - } - - reset() { - resetor(1); - } - - async setSceneProperty(args) { - const property = args.PROPERTY; - const value = getAsset(args.VALUE); - - scene[property] = value; - } - getSceneObjects(args) { - const names = []; - if (args.THING === "Objects") { - scene.traverse((obj) => { - if (obj.name) names.push(obj.name); //if it has a name, add to list! - }); - } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); - else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); - else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); - else if (args.THING === "Scene Properties") { - console.log(scene); - return "check console"; - } else if (args.THING === "Other assets") return JSON.stringify(assets); - - return JSON.stringify(names); // if objects - } - } - Scratch.extensions.register(new ThreeScene()); - - class ThreeCameras { - getInfo() { - return { - id: "threeCameras", - name: "Three Cameras", - color1: "#38c59bff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "addCamera", - blockType: Scratch.BlockType.COMMAND, - text: "add camera [TYPE] [CAMERA] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "cameraTypes", - }, - }, - }, - { - opcode: "setCamera", - blockType: Scratch.BlockType.COMMAND, - text: "set camera [PROPERTY] of [CAMERA] to [VALUE]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "cameraProperties", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "0.1", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "getCamera", - blockType: Scratch.BlockType.REPORTER, - text: "get camera [PROPERTY] of [CAMERA]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "cameraProperties", - }, - }, - }, - "---", - { - opcode: "renderSceneCamera", - blockType: Scratch.BlockType.COMMAND, - text: "set rendering camera to [CAMERA]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - }, - }, - "---", - { - opcode: "cubeCamera", - blockType: Scratch.BlockType.COMMAND, - text: "add cube camera [CAMERA] to [GROUP] with RenderTarget [RT]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "cubeCamera", - }, - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - "---", - { - opcode: "renderTarget", - blockType: Scratch.BlockType.COMMAND, - text: "set a RenderTarget: [RT] for camera [CAMERA]", - arguments: { - CAMERA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myCamera", - }, - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - { - opcode: "sizeTarget", - blockType: Scratch.BlockType.COMMAND, - text: "set RenderTarget [RT] size to [W] [H]", - arguments: { - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - W: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 480, - }, - H: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 360, - }, - }, - }, - { - opcode: "getTarget", - blockType: Scratch.BlockType.REPORTER, - text: "get RenderTarget: [RT] texture", - arguments: { - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - { - opcode: "removeTarget", - blockType: Scratch.BlockType.COMMAND, - text: "remove RenderTarget: [RT]", - arguments: { - RT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myTarget", - }, - }, - }, - ], - menus: { - cameraTypes: { - acceptReporters: false, - items: [{ - text: "Perspective", - value: "PerspectiveCamera", - }, ], - }, - cameraProperties: { - acceptReporters: false, - items: [{ - text: "Near", - value: "near", - }, - { - text: "Far", - value: "far", - }, - { - text: "FOV", - value: "fov", - }, - { - text: "Focus (nothing...)", - value: "focus", - }, - { - text: "Zoom", - value: "zoom", - }, - ], - }, - }, - }; - } - addCamera(args) { - let v2 = new THREE.Vector2(); - threeRenderer.getSize(v2); - const object = new THREE.PerspectiveCamera(90, v2.x / v2.y); - object.position.z = 3; - - createObject(args.CAMERA, object, args.GROUP); - } - setCamera(args) { - let object = getObject(args.CAMERA); - object[args.PROPERTY] = args.VALUE; - object.updateProjectionMatrix(); - } - getCamera(args) { - let object = getObject(args.CAMERA); - const value = JSON.stringify(object[args.PROPERTY]); - return value; - } - renderSceneCamera(args) { - let object = getObject(args.CAMERA); - if (!object) return; - camera = object; - //reset composer, else it does not update. - composer.passes = []; - passes = {}; - customEffects = []; - updateComposers(); - } - - cubeCamera(args) { - // Create cube render target - const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, { - generateMipmaps: true, - }); - // Create cube camera - const cubeCamera = new THREE.CubeCamera(0.1, 500, cubeRenderTarget); - createObject(args.CAMERA, cubeCamera, args.GROUP); - - renderTargets[args.RT] = { - target: cubeRenderTarget, - camera: cubeCamera, - }; - assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture; - } - - renderTarget(args) { - let object = getObject(args.CAMERA); - const renderTarget = new THREE.WebGLRenderTarget(360, 360, { - generateMipmaps: false, - }); - - renderTargets[args.RT] = { - target: renderTarget, - camera: object, - }; - assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; - } - sizeTarget(args) { - renderTargets[args.RT].target.setSize(args.W, args.H); - } - getTarget(args) { - const t = renderTargets[args.RT].target.texture; - console.log(t, renderTargets[args.RT]); - return `renderTargets/${t.uuid}`; - } - removeTarget(args) { - delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; - renderTargets[args.RT].target.dispose(); - delete renderTargets[args.RT]; - } - } - Scratch.extensions.register(new ThreeCameras()); - - class ThreeObjects { - getInfo() { - return { - id: "threeObjects", - name: "Three Objects", - color1: "#38c567ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "addObject", - blockType: Scratch.BlockType.COMMAND, - text: "add object [OBJECT3D] [TYPE] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "objectTypes", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - { - opcode: "cloneObject", - blockType: Scratch.BlockType.COMMAND, - text: "clone object [OBJECT3D] as [NAME] & add to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myClone", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "setObject", - blockType: Scratch.BlockType.COMMAND, - text: "set [PROPERTY] of object [OBJECT3D] to [NAME]", - arguments: { - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectProperties", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - }, - }, - { - opcode: "getObject", - blockType: Scratch.BlockType.REPORTER, - text: "get [PROPERTY] of object [OBJECT3D]", - arguments: { - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectProperties", - }, - }, - }, - { - opcode: "objectE", - blockType: Scratch.BlockType.BOOLEAN, - text: "is there an object [NAME]?", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "removeObject", - blockType: Scratch.BlockType.COMMAND, - text: "remove object [OBJECT3D] from scene", - arguments: { - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: " ↳ Transforms", - }, - { - opcode: "setObjectV3", - extensions: ["colours_motion"], - blockType: Scratch.BlockType.COMMAND, - text: "set transform [PROPERTY] of [OBJECT3D] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectVector3", - defaultValue: "position", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - }, - }, - //{opcode: "changeObjectV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] of [OBJECT3D] by [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3", defaultValue: "position"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "[1,1,1]"}}}, - //{opcode: "changeObjectXV3",extensions: ["colours_motion"], blockType: Scratch.BlockType.COMMAND, text: "change transform [PROPERTY] [X] of [OBJECT3D] to [VALUE]", arguments: {PROPERTY: {type: Scratch.ArgumentType.STRING, menu: "objectVector3"},X: {type: Scratch.ArgumentType.STRING, menu: "XYZ"}, OBJECT3D: {type: Scratch.ArgumentType.STRING, defaultValue: "myObject"}, VALUE: {type: Scratch.ArgumentType.STRING, defaultValue: "1"}}}, - { - opcode: "getObjectV3", - extensions: ["colours_motion"], - blockType: Scratch.BlockType.REPORTER, - text: "get [PROPERTY] of [OBJECT3D]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "objectVector3", - defaultValue: "position", - }, - OBJECT3D: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: "↳ Materials", - }, - { - opcode: "newMaterial", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "new material [NAME] [TYPE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "materialTypes", - defaultValue: "MeshStandardMaterial", - }, - }, - }, - { - opcode: "materialE", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.BOOLEAN, - text: "is there a material [NAME]?", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - }, - }, - { - opcode: "removeMaterial", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "remove material [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - }, - }, - { - opcode: "setMaterial", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "set material [PROPERTY] of [NAME] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "materialProperties", - defaultValue: "color", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "new Color()", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "setBlending", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "set material [NAME] blending to [VALUE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - }, - }, - }, - { - opcode: "setDepth", - extensions: ["colours_looks"], - blockType: Scratch.BlockType.COMMAND, - text: "set material [NAME] depth to [VALUE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myMaterial", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - menu: "depthModes", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: "↳ Geometries", - }, - { - opcode: "newGeometry", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "new geometry [NAME] [TYPE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "geometryTypes", - defaultValue: "BoxGeometry", - }, - }, - }, - { - opcode: "geometryE", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.BOOLEAN, - text: "is there a geometry [NAME]?", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - }, - }, - { - opcode: "removeGeometry", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "remove geometry [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - }, - }, - "---", - { - opcode: "newGeo", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "new empty geometry [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[points]", - }, - }, - }, - { - opcode: "geoPoints", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "set geometry [NAME] vertex points to [POINTS]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[points]", - }, - }, - }, - { - opcode: "geoUVs", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "set geometry [NAME] UVs to [POINTS]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myGeometry", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[UVs]", - }, - }, - }, - "---", - { - opcode: "splines", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "create spline [NAME] from curve [CURVE]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySpline", - }, - CURVE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[curve]", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "splineModel", - extensions: ["colours_operators"], - blockType: Scratch.BlockType.COMMAND, - text: "create (geometry&material) spline [NAME] using model [MODEL] along curve [CURVE] with spacing [SPACING]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySpline", - }, - MODEL: { - type: Scratch.ArgumentType.STRING, - menu: "modelsList", - }, - CURVE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[curve]", - exemptFromNormalization: true, - }, - SPACING: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - "---", - { - blockType: Scratch.BlockType.BUTTON, - text: "Convert font to JSON", - func: "openConv", - }, - { - blockType: Scratch.BlockType.BUTTON, - text: "Load JSON font file", - func: "loadFont", - }, - { - opcode: "text", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.COMMAND, - text: "create text geometry [NAME] with text [TEXT] in font [FONT] size [S] depth [D] curvedSegments [CS]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myText", - }, - TEXT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "C-369", - }, - FONT: { - type: Scratch.ArgumentType.STRING, - menu: "fonts", - }, - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - D: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.1, - }, - CS: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 6, - }, - }, - }, - ], - menus: { - objectVector3: { - acceptReporters: false, - items: [{ - text: "Positon", - value: "position", - }, - { - text: "Rotation", - value: "rotation", - }, - { - text: "Scale", - value: "scale", - }, - { - text: "Facing Direction (.up)", - value: "up", - }, - ], - }, - objectProperties: { - acceptReporters: false, - items: [{ - text: "Geometry", - value: "geometry", - }, - { - text: "Material", - value: "material", - }, - { - text: "Visible (true/false)", - value: "visible", - }, - ], - }, - objectTypes: { - acceptReporters: false, - items: [{ - text: "Mesh", - value: "Mesh", - }, - { - text: "Sprite", - value: "Sprite", - }, - { - text: "Points", - value: "Points", - }, - { - text: "Line", - value: "Line", - }, - { - text: "Group", - value: "Group", - }, - ], - }, - XYZ: { - acceptReporters: false, - items: [{ - text: "X", - value: "x", - }, - { - text: "Y", - value: "y", - }, - { - text: "Z", - value: "z", - }, - ], - }, - materialProperties: { - acceptReporters: false, - items: [ - "|GENERAL| <-- not a property", - { - text: "Color", - value: "color", - }, - { - text: "Map", - value: "map", - }, - { - text: "Opacity", - value: "opacity", - }, - { - text: "Transparent", - value: "transparent", - }, - { - text: "Alpha Map", - value: "alphaMap", - }, - { - text: "Alpha Test", - value: "alphaTest", - }, - { - text: "Depth Test", - value: "depthTest", - }, - { - text: "Depth Write", - value: "depthWrite", - }, - { - text: "Color Write", - value: "colorWrite", - }, - { - text: "Side", - value: "side", - }, - { - text: "Visible", - value: "visible", - }, - /* - { text: "Blending", value: "blending" }, - { text: "Blend Src", value: "blendSrc" }, - { text: "Blend Dst", value: "blendDst" }, - { text: "Blend Equation", value: "blendEquation" }, - { text: "Blend Src Alpha", value: "blendSrcAlpha" }, - { text: "Blend Dst Alpha", value: "blendDstAlpha" }, - { text: "Blend Equation Alpha", value: "blendEquationAlpha" },*/ - { - text: "Blend Aplha", - value: "blendAplha", - }, - { - text: "Blend Color", - value: "blendColor", - }, - { - text: "Alpha Hash", - value: "alphaHash", - }, - { - text: "Premultiplied Alpha", - value: "premultipliedAlpha", - }, - - { - text: "Tone Mapped", - value: "toneMapped", - }, - { - text: "Fog", - value: "fog", - }, - { - text: "Flat Shading", - value: "flatShading", - }, - - "|MESH Standard / Physical| <-- not a property", - { - text: "Metalness", - value: "metalness", - }, - { - text: "Metalness Map", - value: "metalnessMap", - }, - { - text: "Roughness", - value: "roughness", - }, - { - text: "Reflectivity", - value: "reflectivity", - }, - { - text: "Roughness Map", - value: "roughnessMap", - }, - { - text: "Emissive", - value: "emissive", - }, - { - text: "Emissive Intensity", - value: "emissiveIntensity", - }, - { - text: "Emissive Map", - value: "emissiveMap", - }, - { - text: "Env Map", - value: "envMap", - }, - { - text: "Env Map Intensity", - value: "envMapIntensity", - }, - { - text: "Env Map Rotation", - value: "envMapRotation", - }, - { - text: "Ior", - value: "ior", - }, - { - text: "Refraction Ratio", - value: "refractionRatio", - }, - { - text: "Clearcoat", - value: "clearcoat", - }, - { - text: "Clearcoat Map", - value: "clearcoatMap", - }, - { - text: "Clearcoat Roughness", - value: "clearcoatRoughness", - }, - { - text: "Clearcoat Roughness Map", - value: "clearcoatRoughnessMap", - }, - { - text: "Dispersion", - value: "dispersion", - }, - { - text: "Sheen", - value: "sheen", - }, - { - text: "Sheen Color", - value: "sheenColor", - }, - { - text: "Sheen Color Map", - value: "sheenColorMap", - }, - { - text: "Sheen Roughness", - value: "sheenRoughness", - }, - { - text: "Sheen Roughness Map", - value: "sheenRoughnessMap", - }, - { - text: "Specular Color", - value: "specularColor", - }, - { - text: "Specular Color Map", - value: "specularColorMap", - }, - { - text: "Specular Intensity", - value: "specularIntensity", - }, - { - text: "Specular Intensity Map", - value: "specularIntensityMap", - }, - { - text: "Transmission", - value: "transmission", - }, - { - text: "Transmission Map", - value: "transmissionMap", - }, - { - text: "Thickness", - value: "thickness", - }, - { - text: "Thickness Map", - value: "thicknessMap", - }, - { - text: "Anisotropy", - value: "anisotropy", - }, - { - text: "Anisotropy Map", - value: "anisotropyMap", - }, - { - text: "Anisotropy Rotation", - value: "anisotropyRotation", - }, - { - text: "Attenuation Distance", - value: "attenuationDistance", - }, - { - text: "Attenuation Color", - value: "attenuationColor", - }, - { - text: "Thickness", - value: "thickness", - }, - { - text: "Iridescence", - value: "iridescence", - }, - { - text: "Iridescence Ior", - value: "iridescenceIOR", - }, - { - text: "Iridescence Map", - value: "iridescenceMap", - }, - { - text: "Iridescence Thickness Range", - value: "iridescenceThicknessRange", - }, - - "|MESH Displacement / Normal / Bump| <-- not a property", - { - text: "Displacement Map", - value: "displacementMap", - }, - { - text: "Displacement Scale", - value: "displacementScale", - }, - { - text: "Displacement Bias", - value: "displacementBias", - }, - { - text: "Bump Map", - value: "bumpMap", - }, - { - text: "Bump Scale", - value: "bumpScale", - }, - { - text: "Normal Map Type", - value: "normalMapType", - }, - - "|MESH Matcap / Toon / Phong / Lambert / Basic| <-- not a property", - { - text: "Shininess", - value: "shininess", - }, - - { - text: "Wireframe", - value: "wireframe", - }, - { - text: "Wireframe Linewidth", - value: "wireframeLinewidth", - }, - { - text: "Wireframe Linecap", - value: "wireframeLinecap", - }, - { - text: "Wireframe Linejoin", - value: "wireframeLinejoin", - }, - - "|POINTS| <-- not a property", - { - text: "Size", - value: "size", - }, - { - text: "Size Attenuation", - value: "sizeAttenuation", - }, - - "|LINES| <-- not a property", - { - text: "Scale", - value: "scale", - }, - { - text: "Dash Size", - value: "dashSize", - }, - { - text: "Gap Size", - value: "gapSize", - }, - - "|SPRITES| <-- not a property", - { - text: "Rotation", - value: "rotation", - }, - ], - }, - blendModes: { - acceptReporters: false, - items: [{ - text: "No Blending", - value: "NoBlending", - }, - { - text: "Normal Blending", - value: "NormalBlending", - }, - { - text: "Additive Blending", - value: "AdditiveBlending", - }, - { - text: "Subtractive Blending", - value: "SubtractiveBlending", - }, - { - text: "Multiply Blending", - value: "MultiplyBlending", - }, - { - text: "Custom Blending", - value: "CustomBlending", - }, - ], - }, - depthModes: { - acceptReporters: false, - items: [{ - text: "Never Depth", - value: "NeverDepth", - }, - { - text: "Always Depth", - value: "AlwaysDepth", - }, - { - text: "Equal Depth", - value: "EqualDepth", - }, - { - text: "Less Depth", - value: "LessDepth", - }, - { - text: "Less Equal Depth", - value: "LessEqualDepth", - }, - { - text: "Greater Equal Depth", - value: "GreaterEqualDepth", - }, - { - text: "Greater Depth", - value: "GreaterDepth", - }, - { - text: "Not Equal Depth", - value: "NotEqualDepth", - }, - ], - }, - materialTypes: { - acceptReporters: false, - items: [{ - text: "Mesh Basic Material", - value: "MeshBasicMaterial", - }, - { - text: "Mesh Standard Material", - value: "MeshStandardMaterial", - }, - { - text: "Mesh Physical Material", - value: "MeshPhysicalMaterial", - }, - { - text: "Mesh Lambert Material", - value: "MeshLambertMaterial", - }, - { - text: "Mesh Phong Material", - value: "MeshPhongMaterial", - }, - { - text: "Mesh Depth Material", - value: "MeshDepthMaterial", - }, - { - text: "Mesh Normal Material", - value: "MeshNormalMaterial", - }, - { - text: "Mesh Matcap Material", - value: "MeshMatcapMaterial", - }, - { - text: "Mesh Toon Material", - value: "MeshToonMaterial", - }, - { - text: "Line Basic Material", - value: "LineBasicMaterial", - }, - { - text: "Line Dashed Material", - value: "LineDashedMaterial", - }, - { - text: "Points Material", - value: "PointsMaterial", - }, - { - text: "Sprite Material", - value: "SpriteMaterial", - }, - { - text: "Shadow Material", - value: "ShadowMaterial", - }, - ], - }, - textureModes: { - acceptReporters: false, - items: ["Pixelate", "Blur"], - }, - textureStyles: { - acceptReporters: false, - items: ["Repeat", "Clamp"], - }, - geometryTypes: { - acceptReporters: false, - items: [{ - text: "Box Geometry", - value: "BoxGeometry", - }, - { - text: "Sphere Geometry", - value: "SphereGeometry", - }, - { - text: "Cylinder Geometry", - value: "CylinderGeometry", - }, - { - text: "Plane Geometry", - value: "PlaneGeometry", - }, - { - text: "Circle Geometry", - value: "CircleGeometry", - }, - { - text: "Torus Geometry", - value: "TorusGeometry", - }, - { - text: "Torus Knot Geometry", - value: "TorusKnotGeometry", - }, - ], - }, - modelsList: { - acceptReporters: false, - items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime - .getTargetForStage() - .getSounds() - .filter((e) => e.name && e.name.endsWith(".glb")); - if (models.length < 1) return [ - ["Load a model! (GLB Loader category)"] - ]; - - // @ts-ignore - return models.map((m) => [m.name]); - }, - }, - fonts: { - acceptReporters: false, - items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime - .getTargetForStage() - .getSounds() - .filter((e) => e.name && e.name.endsWith(".json")); - if (models.length < 1) return [ - ["Load a font!"] - ]; - - // @ts-ignore - return models.map((m) => [m.name]); - }, - }, - }, - }; - } - - addObject(args) { - const object = new THREE[args.TYPE](); - - object.castShadow = true; - object.receiveShadow = true; - - createObject(args.OBJECT3D, object, args.GROUP); - } - cloneObject(args) { - let object = getObject(args.OBJECT3D); - const clone = object.clone(true); - clone.name; - createObject(args.NAME, clone, args.GROUP); - } - setObjectV3(args) { - let object = getObject(args.OBJECT3D); - let values = JSON.parse(args.VALUE); - - function degToRad(deg) { - return (deg * Math.PI) / 180; - } - - if (object.rigidBody) { - const x = values[0]; - const y = values[1]; - const z = values[2]; - if (args.PROPERTY === "rotation") { - const euler = new THREE.Euler(degToRad(x), degToRad(y), degToRad(z), "YXZ"); - const quaternion = new THREE.Quaternion(); - quaternion.setFromEuler(euler); - - object.rigidBody.setRotation({ - x: quaternion.x, - y: quaternion.y, - z: quaternion.z, - w: quaternion.w, - }); - } else if (args.PROPERTY === "position") { - object.rigidBody.setTranslation({ - x: x, - y: y, - z: z, - }, - true - ); - } - return; - } - - if (object.isCamera == true && controls) {} - - if (args.PROPERTY === "rotation") { - values = values.map((v) => (v * Math.PI) / 180); - object.rotation.set(0, 0, 0); - } - if (object.isDirectionalLight == true) { - object.pos = new THREE.Vector3(...values); - console.log(true, values, object.pos); - return; - } - object[args.PROPERTY].set(...values); - - if (object.type == "CubeCamera") object.updateCoordinateSystem(); - } - /* - changeObjectV3(args) { - getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) - - if (args.PROPERTY === "rotation") { - values = values.map(v => v * Math.PI / 180); - object.rotation.x += values[0] - object.rotation.y += values[1] - object.rotation.z += values[2] - } - else { - object[args.PROPERTY].add(...values); - } - } - changeObjectXV3(args) { - getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "rotation") value = value * Math.PI / 180 - - object[args.PROPERTY][args.X] += value - } - */ - getObjectV3(args) { - let object = getObject(args.OBJECT3D); - if (!object) return; - let values = vector3ToString(object[args.PROPERTY]); - if (args.PROPERTY === "rotation") { - const toDeg = Math.PI / 180; - values = [values[0] / toDeg, values[1] / toDeg, values[2] / toDeg]; - } - - return JSON.stringify(values); - } - setObject(args) { - let object = getObject(args.OBJECT3D); - let value = args.VALUE; - if (args.PROPERTY === "material") { - const mat = materials[args.NAME]; - if (mat) value = mat; - else value = undefined; - } else if (args.PROPERTY === "geometry") { - const geo = geometries[args.NAME]; - if (geo) value = geo; - else value = undefined; - } else value = !!value; - - if (value == undefined) return; //invalid geo/mat - object[args.PROPERTY] = value; - } - getObject(args) { - let object = getObject(args.OBJECT3D); - if (!object) return; - let value; - if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; - else value = object.visible; - - return value; - } - removeObject(args) { - removeObject(args.OBJECT3D); - } - objectE(args) { - return scene.children.map((o) => o.name).includes(args.NAME); - } - - //defines - newMaterial(args) { - if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); - const mat = new THREE[args.TYPE](); - mat.name = args.NAME; - - materials[args.NAME] = mat; - } - async setMaterial(args) { - if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; - const mat = materials[args.NAME]; - - let value = args.VALUE; - - if (args.VALUE == "false") value = false; - - if (args.PROPERTY == "side") { - value = args.VALUE == "D" ? THREE.DoubleSide : args.VALUE == "B" ? THREE.BackSide : THREE.FrontSide; - } else if (args.PROPERTY === "normalScale") value = new THREE.Vector2(...JSON.parse(args.VALUE)); - else value = getAsset(value); - - console.log("o:", args.VALUE, typeof args.VALUE); - console.log("r:", value, typeof value); - - mat[args.PROPERTY] = await value; //await incase its a texture - mat.needsUpdate = true; - } - setBlending(args) { - const mat = materials[args.NAME]; - mat.blending = THREE[args.VALUE]; - mat.premultipliedAlpha = true; - mat.needsUpdate = true; - } - setDepth(args) { - const mat = materials[args.NAME]; - mat.depthFunc = THREE[args.VALUE]; - mat.needsUpdate = true; - } - removeMaterial(args) { - const mat = materials[args.NAME]; - mat.dispose(); - delete materials[args.NAME]; - } - materialE(args) { - return materials[args.NAME] ? true : false; - } - - newGeometry(args) { - if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); - const geo = new THREE[args.TYPE](); - geo.name = args.NAME; - - geometries[args.NAME] = geo; - } - setGeometry(args) { - const geo = geometries[args.NAME]; - geo[args.PROPERTY] = args.VALUE; - - geo.needsUpdate = true; - } - removeGeometry(args) { - const geo = geometries[args.NAME]; - geo.dispose(); - delete geometries[args.NAME]; - } - geometryE(args) { - return geometries[args.NAME] ? true : false; - } - - newGeo(args) { - const geometry = new THREE.BufferGeometry(); - geometry.name = args.NAME; - geometries[args.NAME] = geometry; - } - async geoPoints(args) { - const geometry = geometries[args.NAME]; - const positions = args.POINTS.split(" ") - .map((v) => JSON.parse(v)) - .flat(); //array of v3 of each vertex of each triangle - - geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3)); - geometry.computeVertexNormals(); - - geometry.needsUpdate = true; - } - geoUVs(args) { - const geometry = geometries[args.NAME]; - const UVs = args.POINTS.split(" ") - .map((v) => JSON.parse(v)) - .flat(); //array of v2 of each UV of each triangle - - geometry.setAttribute("uv", new THREE.BufferAttribute(new Float32Array(UVs), 2)); - geometry.needsUpdate = true; - } - - splines(args) { - const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); - geometry.name = args.NAME; - - geometries[args.NAME] = geometry; - } - - async splineModel(args) { - const model = await getModel(args.MODEL, args.NAME); - if (!model) return console.warn("Model not found:", args.MODEL); - - const curve = getAsset(args.CURVE); - const spacing = parseFloat(args.SPACING) || 1; - const curveLength = curve.getLength(); - const divisions = Math.floor(curveLength / spacing); - - const geomList = []; - const matList = []; - - for (let i = 0; i <= divisions; i++) { - const t = i / divisions; - const pos = curve.getPointAt(t); - const tangent = curve.getTangentAt(t); - - const temp = model.clone(true); - temp.position.copy(pos); - - const up = new THREE.Vector3(0, 0, 1); - const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()); - temp.quaternion.copy(quat); - - temp.updateMatrixWorld(true); - - temp.traverse((child) => { - if (child.isMesh && child.geometry) { - const geom = child.geometry.clone(); - geom.applyMatrix4(child.matrixWorld); - geomList.push(geom); - matList.push(child.material); //.clone() ? - } - }); - } - - const validGeoms = geomList.filter((g) => { - const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position; - if (!ok) console.warn("geometry skipped:", g); - return ok; - }); - - const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true); - merged.computeBoundingBox(); - merged.computeBoundingSphere(); - - merged.name = args.NAME; - geometries[args.NAME] = merged; - matList.name = args.NAME; - materials[args.NAME] = matList; - } - - async text(args) { - const fontFile = runtime - .getTargetForStage() - .getSounds() - .find((c) => c.name === args.FONT); - if (!fontFile) return; - - const json = new TextDecoder().decode(fontFile.asset.data.buffer); - const fontData = JSON.parse(json); - - const font = fontLoad.parse(fontData); - - const params = { - font: font, - size: JSON.parse(args.S), - height: JSON.parse(args.D), - curveSegments: JSON.parse(args.CS), - bevelEnabled: false, - }; - const geometry = new TextGeometry.TextGeometry(args.TEXT, params); - geometry.computeVertexNormals(); - geometry.center(); // optional, recenters the text - - geometry.name = args.NAME; - - geometries[args.NAME] = geometry; - } - - async loadFont() { - openFileExplorer(".json").then((files) => { - const file = files[0]; - const reader = new FileReader(); - - reader.onload = async (e) => { - const arrayBuffer = e.target.result; - - // From lily's assets - // // Thank you PenguinMod for providing this code. - - const targetId = runtime.getTargetForStage().id; //util.target.id not working! - const assetName = Cast.toString(file.name); - - const buffer = arrayBuffer; - - const storage = runtime.storage; - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ); - - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ); - alert("Font loaded successfully!"); - } catch (e) { - console.error(e); - alert("Error loading font."); - } - - // End of PenguinMod - }; - - reader.readAsArrayBuffer(file); - }); - } - openConv() { - { - open("https://gero3.github.io/facetype.js/"); - } - } - } - Scratch.extensions.register(new ThreeObjects()); - - class ThreeLights { - getInfo() { - return { - id: "threeLights", - name: "Three Lights", - color1: "#c7a22aff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "addLight", - blockType: Scratch.BlockType.COMMAND, - text: "add light [NAME] type [TYPE] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myLight", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "lightTypes", - }, - }, - }, - { - opcode: "setLight", - blockType: Scratch.BlockType.COMMAND, - text: "set light [NAME][PROPERTY] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "lightProperties", - defaultValue: "intensity", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myLight", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - exemptFromNormalization: true, - }, - }, - }, - ], - menus: { - lightTypes: { - acceptReporters: false, - items: [{ - text: "Ambient Light", - value: "AmbientLight", - }, - { - text: "Directional Light", - value: "DirectionalLight", - }, - { - text: "Point Light", - value: "PointLight", - }, - { - text: "Hemisphere Light", - value: "HemisphereLight", - }, - { - text: "Spot Light", - value: "SpotLight", - }, - ], - }, - lightProperties: { - acceptReporters: false, - items: [{ - text: "Color", - value: "color", - }, - { - text: "Intensity", - value: "intensity", - }, - { - text: "Cast Shadow?", - value: "castShadow", - }, - { - text: "Ground Color (HemisphereLight)", - value: "groundColor", - }, - { - text: "Map (SpotLight)", - value: "map", - }, - { - text: "Distance (SpotLight)", - value: "distance", - }, - { - text: "Decay (SpotLight)", - value: "decay", - }, - { - text: "Penumbra (SpotLight)", - value: "penumbra", - }, - { - text: "Angle/Size (SpotLight)", - value: "angle", - }, - { - text: "Power (SpotLight)", - value: "power", - }, - { - text: "Target Position (Directional/SpotLight)", - value: "target", - }, - ], - }, - }, - }; - } - - addLight(args) { - const light = new THREE[args.TYPE](0xffffff, 1); - - createObject(args.NAME, light, args.GROUP); - lights[args.NAME] = light; - if (light.type === "AmbientLight" || "HemisphereLight") return; - - light.castShadow = true; - if (light.type === "PointLight") return; - //Directional & Spot Light - light.target.position.set(0, 0, 0); - scene.add(light.target); - - light.pos = new THREE.Vector3(0, 0, 0); - - light.shadow.mapSize.width = 4096; - light.shadow.mapSize.height = 2048; - - if (light.type === "SpotLight") { - light.decay = 0; - light.shadow.camera.near = 500; - light.shadow.camera.far = 4000; - light.shadow.camera.fov = 30; - } - light.shadow.needsUpdate = true; - light.needsUpdate = true; - } - - setLight(args) { - const light = lights[args.NAME]; - if (!args.PROPERTY) return; - if (args.PROPERTY === "target") { - light.target.position.set(...JSON.parse(args.VALUE)); //vector3 - light.target.updateMatrixWorld(); - } else { - light[args.PROPERTY] = getAsset(args.VALUE); - } - light.needsUpdate = true; - - if (light.type === "AmbientLight" || "HemisphereLight") return; - - light.shadow.camera.updateProjectionMatrix(); - light.shadow.needsUpdate = true; - } - } - Scratch.extensions.register(new ThreeLights()); - - class ThreeUtilities { - getInfo() { - return { - id: "threeUtility", - name: "Three Utilities", - color1: "#3875c5ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "newVector2", - blockType: Scratch.BlockType.REPORTER, - text: "New Vector [X] [Y]", - arguments: { - X: { - type: Scratch.ArgumentType.NUMBER, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - }, - }, - }, - { - opcode: "newVector3", - blockType: Scratch.BlockType.REPORTER, - text: "New Vector [X] [Y] [Z]", - arguments: { - X: { - type: Scratch.ArgumentType.NUMBER, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - }, - Z: { - type: Scratch.ArgumentType.NUMBER, - }, - }, - }, - "---", - { - opcode: "operateV3", - blockType: Scratch.BlockType.REPORTER, - text: "do [V3] [O] [V32]", - arguments: { - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - O: { - type: Scratch.ArgumentType.STRING, - menu: "operators", - }, - V32: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[1,0,0]", - }, - }, - }, - { - opcode: "moveVector3", - blockType: Scratch.BlockType.REPORTER, - text: "move [S] steps in vector [V3] in direction [D3]", - arguments: { - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - D3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[1,0,0]", - }, - }, - }, - { - opcode: "directionTo", - blockType: Scratch.BlockType.REPORTER, - text: "direction from [V3] to [T3]", - arguments: { - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,3]", - }, - T3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - }, - }, - "---", - { - opcode: "newColor", - blockType: Scratch.BlockType.REPORTER, - text: "New Color [HEX]", - arguments: { - HEX: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#9966ff", - }, - }, - }, - { - opcode: "newFog", - blockType: Scratch.BlockType.REPORTER, - text: "New Fog [COLOR] [NEAR] [FAR]", - arguments: { - COLOR: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#9966ff", - exemptFromNormalization: true, - }, - NEAR: { - type: Scratch.ArgumentType.NUMBER, - }, - FAR: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 10, - }, - }, - }, - { - opcode: "newTexture", - blockType: Scratch.BlockType.REPORTER, - text: "New Texture [COSTUME] [MODE] [STYLE] repeat [X][Y]", - arguments: { - COSTUME: { - type: Scratch.ArgumentType.COSTUME, - }, - MODE: { - type: Scratch.ArgumentType.STRING, - menu: "textureModes", - }, - STYLE: { - type: Scratch.ArgumentType.STRING, - menu: "textureStyles", - }, - X: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - { - opcode: "newCubeTexture", - blockType: Scratch.BlockType.REPORTER, - text: "New Cube Texture X+[COSTUMEX0]X-[COSTUMEX1]Y+[COSTUMEY0]Y-[COSTUMEY1]Z+[COSTUMEZ0]Z-[COSTUMEZ1] [MODE] [STYLE] repeat [X][Y]", - arguments: { - COSTUMEX0: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEX1: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEY0: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEY1: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEZ0: { - type: Scratch.ArgumentType.COSTUME, - }, - COSTUMEZ1: { - type: Scratch.ArgumentType.COSTUME, - }, - MODE: { - type: Scratch.ArgumentType.STRING, - menu: "textureModes", - }, - STYLE: { - type: Scratch.ArgumentType.STRING, - menu: "textureStyles", - }, - X: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - { - opcode: "newEquirectangularTexture", - blockType: Scratch.BlockType.REPORTER, - text: "New Equirectangular Texture [COSTUME] [MODE]", - arguments: { - COSTUME: { - type: Scratch.ArgumentType.COSTUME, - }, - MODE: { - type: Scratch.ArgumentType.STRING, - menu: "textureModes", - }, - }, - }, - "---", - { - opcode: "curve", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.REPORTER, - text: "generate curve [TYPE] from points [POINTS], closed: [CLOSED]", - arguments: { - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "curveTypes", - }, - POINTS: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,3,0] [2.5,-1.5,0] [-2.5,-1.5,0]", - }, - CLOSED: { - type: Scratch.ArgumentType.STRING, - defaultValue: "true", - }, - }, - }, - "---", - { - opcode: "mouseDown", - extensions: ["colours_sensing"], - blockType: Scratch.BlockType.BOOLEAN, - text: "mouse [BUTTON] [action]?", - arguments: { - BUTTON: { - type: Scratch.ArgumentType.STRING, - menu: "mouseButtons", - }, - action: { - type: Scratch.ArgumentType.STRING, - menu: "mouseAction", - }, - }, - }, - { - opcode: "mousePos", - extensions: ["colours_sensing"], - blockType: Scratch.BlockType.REPORTER, - text: "mouse position", - arguments: {}, - }, - "---", - { - opcode: "getItem", - extensions: ["colours_data_lists"], - blockType: Scratch.BlockType.REPORTER, - text: "get item [ITEM] of [ARRAY]", - arguments: { - ITEM: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - }, - ARRAY: { - type: Scratch.ArgumentType.STRING, - defaultValue: `["myObject", "myLight"]`, - }, - }, - }, - { - blockType: Scratch.BlockType.LABEL, - text: "↳ Raycasting", - }, - { - opcode: "raycast", - blockType: Scratch.BlockType.COMMAND, - text: "Raycast from [V3] in direction [D3]", - arguments: { - V3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,3]", - }, - D3: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,1]", - }, - }, - }, - { - opcode: "getRaycast", - blockType: Scratch.BlockType.REPORTER, - text: "get raycast [PROPERTY]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "raycastProperties", - }, - }, - }, - ], - menus: { - materialProperties: { - acceptReporters: false, - items: [{ - text: "Color", - value: "color", - }, - { - text: "Map (texture)", - value: "map", - }, - { - text: "Alpha Map (texture)", - value: "alphaMap", - }, - { - text: "Alpha Test (0-1)", - value: "alphaTest", - }, - { - text: "Side (front/back/double)", - value: "side", - }, - { - text: "Bump Map (texture)", - value: "bumpMap", - }, - { - text: "Bump Scale", - value: "bumpScale", - }, - ], - }, - textureModes: { - acceptReporters: false, - items: ["Pixelate", "Blur"], - }, - textureStyles: { - acceptReporters: false, - items: ["Repeat", "Clamp"], - }, - raycastProperties: { - acceptReporters: false, - items: [{ - text: "Intersected Object Names", - value: "name", - }, - { - text: "Number of Objects", - value: "number", - }, - { - text: "Intersected Objects distances", - value: "distance", - }, - ], - }, - mouseButtons: { - acceptReporters: false, - items: ["left", "middle", "right"], - }, - mouseAction: { - acceptReporters: false, - items: ["Down", "Clicked"], - }, - curveTypes: { - acceptReporters: false, - items: ["CatmullRomCurve3"], - }, - operators: { - acceptReporters: false, - items: ["+", "-", "*", "/", "=", "max", "min", "dot", "cross", "distance to", "angle to", "apply euler"], - }, - }, - }; - } - mouseDown(args) { - if (args.action === "Down") return isMouseDown[args.BUTTON]; - if (args.action === "Clicked") { - if (isMouseDown[args.BUTTON] == prevMouse[args.BUTTON]) return false; - else prevMouse[args.BUTTON] = true; - return true; - } - } - mousePos(event) { - return JSON.stringify(mouseNDC); - } - newVector3(args) { - return JSON.stringify([args.X, args.Y, args.Z]); - } - operateV3(args) { - const v3 = new THREE.Vector3(...JSON.parse(args.V3)); - const v32 = new THREE.Vector3(...JSON.parse(args.V32)); - - let r; - if (args.O == "+") r = v3.add(v32); - else if (args.O == "-") r = v3.sub(v32); - else if (args.O == "*") r = v3.multiply(v32); - else if (args.O == "/") r = v3.divide(v32); - else if (args.O == "=") r = v3.equals(v32); - else if (args.O == "max") r = v3.max(v32); - else if (args.O == "min") r = v3.min(v32); - else if (args.O == "dot") r = v3.dot(v32); - else if (args.O == "cross") r = v3.cross(v32); - else if (args.O == "distance to") r = v3.distanceTo(v32); - else if (args.O == "angle to") r = v3.angleTo(v32); - else if (args.O == "apply euler") r = v3.applyEuler(new THREE.Euler(v32.x, v32.y, v32.z, eulerOrder)); - - if (typeof r == "object") return JSON.stringify([r.x, r.y, r.z]); - else return JSON.stringify(r); - } - - newVector2(args) { - return JSON.stringify([args.X, args.Y]); - } - - moveVector3(args) { - const currentPos = new THREE.Vector3(...JSON.parse(args.V3)); - const steps = Number(args.S); - - const [pitchInputDeg, yawInputDeg, rollInputDeg] = JSON.parse(args.D3).map(Number); - - const yaw = THREE.MathUtils.degToRad(yawInputDeg); - const pitch = THREE.MathUtils.degToRad(pitchInputDeg); - const roll = THREE.MathUtils.degToRad(rollInputDeg); - - const euler = new THREE.Euler(pitch, yaw, roll, eulerOrder); - - const forwardVector = new THREE.Vector3(0, 0, -1); - const direction = forwardVector.applyEuler(euler).normalize(); - - const newPos = currentPos.add(direction.multiplyScalar(steps)); - return JSON.stringify([newPos.x, newPos.y, newPos.z]); - } - - directionTo(args) { - const v3 = new THREE.Vector3(...JSON.parse(args.V3)); - const toV3 = new THREE.Vector3(...JSON.parse(args.T3)); - - const direction = toV3.clone().sub(v3).normalize(); - // Pitch (X) - const pitch = Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)); - // Yaw (Y) - const yaw = Math.atan2(direction.x, direction.z); - - // Roll always 0 - return JSON.stringify([180 + THREE.MathUtils.radToDeg(pitch), THREE.MathUtils.radToDeg(yaw), 0]); - } - - newColor(args) { - const color = new THREE.Color(args.HEX); - const uuid = crypto.randomUUID(); - assets.colors[uuid] = color; - return `colors/${uuid}`; - } - newFog(args) { - const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR); - const uuid = crypto.randomUUID(); - assets.fogs[uuid] = fog; - return `fogs/${uuid}`; - } - async newTexture(args) { - const textureURI = encodeCostume(args.COSTUME); - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); - assets.textures[texture.uuid] = texture; - return `textures/${texture.uuid}`; - } - async newCubeTexture(args) { - const uris = [ - encodeCostume(args.COSTUMEX0), - encodeCostume(args.COSTUMEX1), - encodeCostume(args.COSTUMEY0), - encodeCostume(args.COSTUMEY1), - encodeCostume(args.COSTUMEZ0), - encodeCostume(args.COSTUMEZ1), - ]; - const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); - const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); - - texture.name = "CubeTexture" + args.COSTUMEX0; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); - assets.textures[texture.uuid] = texture; - return `textures/${texture.uuid}`; - } - async newEquirectangularTexture(args) { - const textureURI = encodeCostume(args.COSTUME); - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME; - texture.mapping = THREE.EquirectangularReflectionMapping; - - setTexutre(texture, args.MODE); - assets.textures[texture.uuid] = texture; - return `textures/${texture.uuid}`; - } - - curve(args) { - function parsePoints(input) { - // Match all [x,y,z] groups - const matches = input.match(/\[([^\]]+)\]/g); - if (!matches) return []; - - return matches.map((str) => { - const nums = str - .replace(/[\[\]\s]/g, "") - .split(",") - .map(Number); - return new THREE.Vector3(nums[0] || 0, nums[1] || 0, nums[2] || 0); - }); - } - const points = parsePoints(args.POINTS); - const curve = new THREE[args.TYPE](points); - curve.closed = JSON.parse(args.CLOSED); - - const uuid = crypto.randomUUID(); - assets.curves[uuid] = curve; - return `curves/${uuid}`; - } - - getItem(args) { - const items = JSON.parse(args.ARRAY); - const item = items[args.ITEM - 1]; - if (!item) return "0"; - return item; - } - - raycast(args) { - const origin = new THREE.Vector3(...JSON.parse(args.V3)); - // rotation is in degrees => convert to radians first - const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); - - const euler = new THREE.Euler(rot[0], rot[1], rot[2], eulerOrder); - const direction = new THREE.Vector3(0, 0, -1).applyEuler(euler).normalize(); - - const raycaster = new THREE.Raycaster(); - //const camera = getObject(args.CAMERA) - raycaster.set(origin, direction); - - const intersects = raycaster.intersectObjects(scene.children, true); - - raycastResult = intersects; - } - getRaycast(args) { - if (args.PROPERTY === "number") return raycastResult.length; - if (args.PROPERTY === "distance") return JSON.stringify(raycastResult.map((i) => i.distance)); - return JSON.stringify(raycastResult.map((i) => i.object[args.PROPERTY])); - } - } - Scratch.extensions.register(new ThreeUtilities()); - - class ThreeGLB { - getInfo() { - return { - id: "threeGLB", - name: "Three GLB Loader", - color1: "#c53838ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - blockType: Scratch.BlockType.BUTTON, - text: "Load GLB File", - func: "loadModelFile", - }, - { - opcode: "addModel", - blockType: Scratch.BlockType.COMMAND, - text: "add [ITEM] as [NAME] to [GROUP]", - arguments: { - GROUP: { - type: Scratch.ArgumentType.STRING, - defaultValue: "scene", - }, - ITEM: { - type: Scratch.ArgumentType.STRING, - menu: "modelsList", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - }, - }, - { - opcode: "getModel", - blockType: Scratch.BlockType.REPORTER, - text: "get object [PROPERTY] of [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "modelProperties", - }, - }, - }, - { - opcode: "playAnimation", - blockType: Scratch.BlockType.COMMAND, - text: "play animation [ANAME] of [NAME], [TIMES] times", - arguments: { - TIMES: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "0", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - ANAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "walk", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "pauseAnimation", - blockType: Scratch.BlockType.COMMAND, - text: "set [TOGGLE] animation [ANAME] of [NAME]", - arguments: { - TOGGLE: { - type: Scratch.ArgumentType.NUMBER, - menu: "pauseUn", - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - ANAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "walk", - exemptFromNormalization: true, - }, - }, - }, - { - opcode: "stopAnimation", - blockType: Scratch.BlockType.COMMAND, - text: "stop animation [ANAME] of [NAME]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myModel", - }, - ANAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "walk", - exemptFromNormalization: true, - }, - }, - }, - ], - menus: { - modelProperties: { - acceptReporters: false, - items: [{ - text: "Animations", - value: "animations", - }, ], - }, - pauseUn: { - acceptReporters: true, - items: [{ - text: "Pause", - value: "true", - }, - { - text: "Unpasue", - value: "false", - }, - ], - }, - modelsList: { - acceptReporters: false, - items: () => { - const stage = runtime.getTargetForStage(); - if (!stage) return ["(loading...)"]; - - // @ts-ignore - const models = Scratch.vm.runtime - .getTargetForStage() - .getSounds() - .filter((e) => e.name && e.name.endsWith(".glb")); - if (models.length < 1) return [ - ["Load a model!"] - ]; - - // @ts-ignore - return models.map((m) => [m.name]); - }, - }, - }, - }; - } - - async loadModelFile() { - openFileExplorer(".glb").then((files) => { - const file = files[0]; - const reader = new FileReader(); - - reader.onload = async (e) => { - const arrayBuffer = e.target.result; - - { - // From lily's assets - - // Thank you PenguinMod for providing this code. - { - const targetId = runtime.getTargetForStage().id; //util.target.id not working! - const assetName = Cast.toString(file.name); - - //const res = await Scratch.fetch(args.URL); - //const buffer = await res.arrayBuffer(); - const buffer = arrayBuffer; - - const storage = runtime.storage; - const asset = storage.createAsset( - storage.AssetType.Sound, - storage.DataFormat.MP3, - // @ts-ignore - new Uint8Array(buffer), - null, - true - ); - - try { - await vm.addSound( - // @ts-ignore - { - asset, - md5: asset.assetId + "." + asset.dataFormat, - name: assetName, - }, - targetId - ); - alert("Model loaded successfully!"); - } catch (e) { - console.error(e); - alert("Error loading model."); - } - } - // End of PenguinMod - } - }; - - reader.readAsArrayBuffer(file); - }); - } - async addModel(args) { - const group = await getModel(args.ITEM, args.NAME); - - createObject(args.NAME, group, args.GROUP); - } - getModel(args) { - if (!models[args.NAME]) return; - return Object.keys(models[args.NAME].actions).toString(); - } - - playAnimation(args) { - const model = models[args.NAME]; - if (!model) { - console.log("no model!"); - return; - } - - const action = model.actions[args.ANAME]; //clones of models dont have a stored actions! - if (!action) { - console.log("no action!"); - return; - } - - args.TIMES > 0 ? action.setLoop(THREE.LoopRepeat, args.TIMES) : action.setLoop(THREE.LoopRepeat, Infinity); - - action.reset().play(); - } - stopAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.stop(); - } - pauseAnimation(args) { - const model = models[args.NAME]; - if (!model) return; - - const action = model.actions[args.ANAME]; - if (action) action.paused = args.TOGGLE; - } - } - Scratch.extensions.register(new ThreeGLB()); - - class ThreeAddons { - getInfo() { - return { - id: "threeAddons", - name: "Three Addons", - color1: "#c538a2ff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - blockType: Scratch.BlockType.LABEL, - text: "Orbit Control", - }, - { - opcode: "OrbitControl", - blockType: Scratch.BlockType.COMMAND, - text: "set addon Orbit Control [STATE]", - arguments: { - STATE: { - type: Scratch.ArgumentType.STRING, - menu: "onoff", - }, - }, - }, - - { - blockType: Scratch.BlockType.LABEL, - text: "Post Processing", - }, - { - opcode: "resetComposer", - blockType: Scratch.BlockType.COMMAND, - text: "reset composer", - }, - { - opcode: "bloom", - blockType: Scratch.BlockType.COMMAND, - text: "add bloom intensity:[I] smoothing:[S] threshold:[T] | blend: [BLEND] opacity:[OP]", - arguments: { - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - I: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.5, - }, - T: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.5, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "SCREEN", - }, - }, - }, - { - opcode: "godRays", - blockType: Scratch.BlockType.COMMAND, - text: "add god rays object:[NAME] density:[DENS] decay:[DEC] weight:[WEI] exposition:[EXP] | resolution:[RES] samples:[SAMP] | blend: [BLEND] opacity:[OP]", - arguments: { - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "SCREEN", - }, - DEC: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.95, - }, - DENS: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - EXP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.1, - }, - WEI: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.4, - }, - RES: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - SAMP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 64, - }, - }, - }, - { - opcode: "dots", - blockType: Scratch.BlockType.COMMAND, - text: "add dots scale:[S] angle:[A] | blend: [BLEND] opacity:[OP]", - arguments: { - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - S: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - A: { - type: Scratch.ArgumentType.ANGLE, - defaultValue: 0, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "SCREEN", - }, - }, - }, - { - opcode: "depth", - blockType: Scratch.BlockType.COMMAND, - text: "add depth of field focusDistance:[FD] focalLength:[FL] bokehScale:[BS] | height:[H] | blend: [BLEND] opacity:[OP]", - arguments: { - FD: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 3, - }, - FL: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0.001, - }, - BS: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 4, - }, - H: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 240, - }, - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "NORMAL", - }, - }, - }, - "---", - { - opcode: "custom", - blockType: Scratch.BlockType.COMMAND, - text: "add custom shader [NAME] with GLSL fragm [FRA] vert [VER] | blend: [BLEND] opacity:[OP]", - arguments: { - NAME: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myShader", - }, - FRA: { - type: Scratch.ArgumentType.STRING, - }, - VER: { - type: Scratch.ArgumentType.STRING, - }, - BLEND: { - type: Scratch.ArgumentType.STRING, - menu: "blendModes", - defaultValue: "NORMAL", - }, - OP: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - }, - }, - ], - menus: { - onoff: { - acceptReporters: true, - items: [{ - text: "enabled", - value: "1", - }, - { - text: "disabled", - value: "0", - }, - ], - }, - blendModes: { - acceptReporters: false, - items: [ - "SKIP", - "SET", - "ADD", - "ALPHA", - "AVERAGE", - "COLOR", - "COLOR_BURN", - "COLOR_DODGE", - "DARKEN", - "DIFFERENCE", - "DIVIDE", - "DST", - "EXCLUSION", - "HARD_LIGHT", - "HARD_MIX", - "HUE", - "INVERT", - "INVERT_RGB", - "LIGHTEN", - "LINEAR_BURN", - "LINEAR_DODGE", - "LINEAR_LIGHT", - "LUMINOSITY", - "MULTIPLY", - "NEGATION", - "NORMAL", - "OVERLAY", - "PIN_LIGHT", - "REFLECT", - "SCREEN", - "SRC", - "SATURATION", - "SOFT_LIGHT", - "SUBTRACT", - "VIVID_LIGHT", - ], - }, - }, - }; - } - - OrbitControl(args) { - if (controls) controls.dispose(); - - console.log("creating...", OrbitControls); - controls = new OrbitControls.OrbitControls(camera, threeRenderer.domElement); - controls.enableDamping = true; - - controls.enabled = !!args.STATE; - console.log(controls); - } - - resetComposer() { - composer.passes = []; - passes = {}; - customEffects = []; - updateComposers(); - } - - bloom(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - const bloomEffect = new BloomEffect({ - intensity: args.I, - luminanceThreshold: args.T, // ← correct key - luminanceSmoothing: args.S, - blendFunction: BlendFunction[args.BLEND], - }); - bloomEffect.blendMode.opacity.value = args.OP; - - const pass = new EffectPass(camera, bloomEffect); - - composer.addPass(pass); - } - - godRays(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - let object = getObject(args.NAME); - const sun = object; - - const godRays = new GodRaysEffect(camera, sun, { - resolutionScale: args.RES, - density: args.DENS, // ray density - decay: args.DEC, // fade out - weight: args.WEI, // brightness of rays - exposure: args.EXP, - samples: args.SAMP, - blendFunction: BlendFunction[args.BLEND], - }); - godRays.blendMode.opacity.value = args.OP; - const pass = new EffectPass(camera, godRays); - composer.addPass(pass); - } - - dots(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - const dot = new DotScreenEffect({ - angle: args.A, - scale: args.S, - blendFunction: BlendFunction[args.BLEND], - }); - dot.blendMode.opacity.value = args.OP; - const pass = new EffectPass(camera, dot); - composer.addPass(pass); - } - - depth(args) { - if (!camera || !scene) { - if (alerts) alert("set a camera!"); - return; - } - const dofEffect = new DepthOfFieldEffect(camera, { - focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) - focalLength: args.FL, // lens focal length in meters - bokehScale: args.BS, // strength/size of the blur circles - height: args.H, // resolution hint (affects quality/perf) - blendFunction: BlendFunction[args.BLEND], - }); - dofEffect.blendMode.opacity.value = args.OP; - - const dofPass = new EffectPass(camera, dofEffect); - composer.addPass(dofPass); - } - - async custom(args) { - function cleanGLSL(glslCode) { - //delete multilines comments - let cleanedCode = glslCode - .replace(/\/\*[\s\S]*?\*\//g, " ") - .replace(/ /g, "\n") - .replace(/\/\/.*$/gm, " ") - .replace(/; /g, ";\n"); - - return cleanedCode; - } - - let fs = cleanGLSL(` - ${args.FRA} - `); - if (!args.FRA.trim()) { - fs = `void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { outputColor = inputColor; }`; - } - const vs = cleanGLSL(` - ${args.VER} - `); - console.log(fs); - console.log(vs); - - const effect = new Effect("Custom", fs, { - blendFunction: BlendFunction[args.BLEND], - vertexShader: vs, - uniforms: new Map([ - //uniforms usually in shaders... open to more! - ["time", new THREE.Uniform(0.0)], - [ - "resolution", - new THREE.Uniform(new THREE.Vector2(threeRenderer.domElement.width, threeRenderer.domElement.height)), - ], - ]), - defines: new Map([ - ["USE_TIME", "1"], - ["USE_VERTEX_TRANSFORM", ""], - ]), - }); - - effect.blendMode.opacity.value = args.OP; - - const pass = new EffectPass(camera, effect); - composer.addPass(pass); - - customEffects.push(effect); - } - } - Scratch.extensions.register(new ThreeAddons()); - - class RapierPhysics { - getInfo() { - return { - id: "rapierPhysics", - name: "RAPIER Physics", - color1: "#222222", - color2: "#203024ff", - color3: "#78f07eff", - blocks: [{ - opcode: "createWorld", - blockType: Scratch.BlockType.COMMAND, - text: "create world | gravity:[G]", - arguments: { - G: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,-9.81,0]", - }, - }, - }, - { - opcode: "getWorld", - blockType: Scratch.BlockType.REPORTER, - text: "get world [PROPERTY]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "wProp", - }, - }, - }, - "---", - { - opcode: "objectPhysics", - blockType: Scratch.BlockType.COMMAND, - text: "enable physics for object [OBJECT] [state] | rigidBody [type] | collider [collider] mass [mass] density [density] friction [friction] sensor [state2]", - arguments: { - state2: { - type: Scratch.ArgumentType.STRING, - menu: "state2", - }, - state: { - type: Scratch.ArgumentType.STRING, - menu: "state", - defaultValue: "true", - }, - type: { - type: Scratch.ArgumentType.STRING, - menu: "objectTypes", - defaultValue: "dynamic", - }, - collider: { - type: Scratch.ArgumentType.STRING, - menu: "colliderTypes", - defaultValue: "cuboid", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - mass: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "1", - }, - density: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "1", - }, - friction: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: "0.5", - }, - }, - }, - "---", - { - blockType: Scratch.BlockType.LABEL, - text: "- RigidBody", - }, - { - opcode: "setRB", - blockType: Scratch.BlockType.COMMAND, - text: "set rigidbody [PROPERTY] of [OBJECT] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "rigidBodySets", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - }, - }, - }, - { - opcode: "getRB", - blockType: Scratch.BlockType.REPORTER, - text: "get rigidbody [PROPERTY] of [OBJECT]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "rigidBodyProperties", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "lockObjectAxis", - blockType: Scratch.BlockType.COMMAND, - text: "lock rigidbody [OBJECT] [PROPERTY] on x:[X] y:[Y] z:[Z]", - arguments: { - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "lockAxes", - }, - X: { - type: Scratch.ArgumentType.STRING, - menu: "tf", - }, - Y: { - type: Scratch.ArgumentType.STRING, - menu: "tf", - }, - Z: { - type: Scratch.ArgumentType.STRING, - menu: "tf", - }, - }, - }, - "---", - { - opcode: "addForce", - blockType: Scratch.BlockType.COMMAND, - text: "set [PROPERTY] of [OBJECT] to [VALUE] in [SPACE] space", - arguments: { - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,10,0]", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "forces", - defaultValue: "addForce", - }, - SPACE: { - type: Scratch.ArgumentType.STRING, - menu: "spaces", - defaultValue: "world", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - { - opcode: "resetForces", - blockType: Scratch.BlockType.COMMAND, - text: "reset [PROPERTY] of [OBJECT]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "resetF", - defaultValue: "resetForces", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "enableCCD", - blockType: Scratch.BlockType.COMMAND, - text: "enable Continuous Collision Detection for [OBJECT] [state]", - arguments: { - state: { - type: Scratch.ArgumentType.STRING, - menu: "state", - defaultValue: "true", - }, - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "oPropS", - defaultValue: "physics", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "fixedJoint", - blockType: Scratch.BlockType.COMMAND, - text: "create FIXED joint between [ObjA] & [ObjB] | anchor A: [VA] [RA] B: [VB] [RB]", - arguments: { - ObjA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - ObjB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObjectB", - }, - VA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - VB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,1,0]", - }, - RA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - RB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - }, - }, - { - opcode: "sphericalJoint", - blockType: Scratch.BlockType.COMMAND, - text: "create SPHERICAL joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB]", - arguments: { - ObjA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - ObjB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObjectB", - }, - VA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - VB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,1,0]", - }, - }, - }, - { - opcode: "revoluteJoint", - blockType: Scratch.BlockType.COMMAND, - text: "create REVOLUTE joint between [ObjA] & [ObjB] | anchor A: [VA] B: [VB] | axis: [X]", - arguments: { - ObjA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - ObjB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObjectB", - }, - VA: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,0,0]", - }, - VB: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[0,1,0]", - }, - X: { - type: Scratch.ArgumentType.STRING, - defaultValue: "[1,0,0]", - }, - }, - }, - "---", - { - blockType: Scratch.BlockType.LABEL, - text: "- Collider", - }, - { - opcode: "setC", - blockType: Scratch.BlockType.COMMAND, - text: "set collider [PROPERTY] of [OBJECT] to [VALUE]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "colliderSets", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - VALUE: { - type: Scratch.ArgumentType.STRING, - defaultValue: "1", - }, - }, - }, - { - opcode: "getC", - blockType: Scratch.BlockType.REPORTER, - text: "get collider [PROPERTY] of [OBJECT]", - arguments: { - PROPERTY: { - type: Scratch.ArgumentType.STRING, - menu: "colliderProperties", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - "---", - { - opcode: "sensorSingle", - blockType: Scratch.BlockType.BOOLEAN, - text: "is sensor [SENSOR] touching [OBJECT]?", - arguments: { - SENSOR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySensor", - }, - OBJECT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "myObject", - }, - }, - }, - { - opcode: "sensorAll", - blockType: Scratch.BlockType.REPORTER, - text: "objects touching sensor [SENSOR]", - arguments: { - SENSOR: { - type: Scratch.ArgumentType.STRING, - defaultValue: "mySensor", - }, - }, - }, - ], - menus: { - wProp: { - acceptReporters: false, - items: [{ - text: "Gravity", - value: "gravity", - }, - { - text: "log to console", - value: "log", - }, - ], - }, - tf: { - acceptReporters: true, - items: [{ - text: "false", - value: "false", - }, - { - text: "true", - value: "true", - }, - ], - }, - lockAxes: { - acceptReporters: false, - items: [{ - text: "Translation", - value: "setEnabledTranslations", - }, - { - text: "Rotation", - value: "setEnabledRotations", - }, - ], - }, - rigidBodyProperties: { - acceptReporters: false, - items: [{ - text: "Type", - value: "bodyType", - }, - { - text: "Linear Velocity", - value: "linvel", - }, - { - text: "Angular Velocity", - value: "angvel", - }, - { - text: "Translation (position)", - value: "translation", - }, - { - text: "Rotation (quaternion)", - value: "rotation", - }, - { - text: "Mass", - value: "mass", - }, - //{text: "Center of Mass", value: "centerOfMass"}, - { - text: "Linear Damping", - value: "linearDamping", - }, - { - text: "Angular Damping", - value: "angularDamping", - }, - { - text: "Is Sleeping?", - value: "isSleeping", - }, - //{text: "Can Sleep?", value: "isCanSleep"}, - { - text: "Gravity Scale", - value: "gravityScale", - }, - { - text: "Is Fixed?", - value: "isFixed", - }, - { - text: "Is Dynamic?", - value: "isDynamic", - }, - { - text: "Is Kinematic?", - value: "isKinematic", - }, - //{text: "Sleeping", value: "sleeping"} - ], - }, - rigidBodySets: { - acceptReporters: false, - items: [ - //{text: "Linear Velocity", value: "setLinvel"}, - //{text: "Angular Velocity", value: "setAngvel"}, - //{text: "Mass", value: "setMass"}, - { - text: "Gravity Scale", - value: "setGravityScale", - }, - //{text: "Can Sleep?", value: "setCanSleep"}, - //{text: "Sleeping", value: "sleeping"}, - { - text: "Linear Damping", - value: "setLinearDamping", - }, - { - text: "Angular Damping", - value: "setAngularDamping", - }, - { - text: "Is Fixed?", - value: "isFixed", - }, - { - text: "Is Dynamic?", - value: "isDynamic", - }, - { - text: "Is Kinematic?", - value: "isKinematic", - }, - ], - }, - colliderProperties: { - acceptReporters: false, - items: [ - //{text: "Collider Type", value: "type"}, - { - text: "Is Sensor?", - value: "isSensor", - }, - { - text: "Friction", - value: "friction", - }, - { - text: "Restitution", - value: "restitution", - }, - { - text: "Density", - value: "density", - }, - { - text: "Mass", - value: "mass", - }, - { - text: "Position", - value: "translation", - }, - { - text: "Rotation", - value: "rotation", - }, - //{text: "Area", value: "area"}, - { - text: "Volume", - value: "volume", - }, - { - text: "Collision Groups", - value: "collisionGroups", - }, - //{text: "Collision Mask", value: "collisionMask"}, - //{text: "Is Enabled?", value: "enabled"}, - //{text: "Contact Count", value: "contactCount"}, - //{text: "RigidBody Handle", value: "rigidBody"} - ], - }, - colliderSets: { - acceptReporters: false, - items: [{ - text: "Friction", - value: "setFriction", - }, - { - text: "Restitution", - value: "setRestitution", - }, - { - text: "Density", - value: "setDensity", - }, - { - text: "Is Sensor?", - value: "setSensor", - }, - { - text: "Collision Groups", - value: "setCollisionGroups", - }, - //{text: "Enabled", value: "enabled"}, // object.collider.enabled = bool - //{text: "Position Offset", value: "setTranslation"}, - //{text: "Rotation Offset", value: "setRotation"} - ], - }, - state: { - acceptReporters: true, - items: [{ - text: "on", - value: "true", - }, - { - text: "off", - value: "false", - }, - ], - }, - state2: { - acceptReporters: true, - items: [{ - text: "false", - value: "false", - }, - { - text: "true (must be fixed)", - value: "true", - }, - ], - }, - spaces: { - acceptReporters: false, - items: [{ - text: "World", - value: "world", - }, - { - text: "Local", - value: "local", - }, - ], - }, - objectTypes: { - acceptReporters: false, - items: [{ - text: "Dynamic", - value: "dynamic", - }, - { - text: "Fixed", - value: "fixed", - }, - { - text: "Kinematic Position Based", - value: "kinematicPositionBased", - }, - ], - }, - colliderTypes: { - acceptReporters: false, - items: [{ - text: "Box, Rectangle, cuboid", - value: "cuboid", - }, - { - text: "Sphere, ball", - value: "ball", - }, - { - text: "Custom, complex simple shapes, convexHull", - value: "convexHull", - }, - { - text: "Precision, TriMesh", - value: "trimesh", - }, - ], - }, - forces: { - acceptReporters: false, - items: [{ - text: "Force", - value: "addForce", - }, - { - text: "Torque (rotation)", - value: "addTorque", - }, - { - text: "Apply Impulse", - value: "applyImpulse", - }, - { - text: "Apply Torque Impulse (rotation)", - value: "applyTorqueImpulse", - }, - { - text: "Linear Velocity", - value: "setLinvel", - }, - { - text: "Angular Velocity", - value: "setAngvel", - }, - ], - }, - resetF: { - acceptReporters: false, - items: [{ - text: "Forces", - value: "resetForces", - }, - { - text: "Torques", - value: "resetTorques", - }, - ], - }, - }, - }; - } - joint(jointData, bodyA, bodyB) { - physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); - } - - fixedJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - let RA = JSON.parse(args.RA).map(Number); - let RB = JSON.parse(args.RB).map(Number); - - RA = new THREE.Quaternion().setFromEuler( - new THREE.Euler( - THREE.MathUtils.degToRad(RA[0]), - THREE.MathUtils.degToRad(RA[1]), - THREE.MathUtils.degToRad(RA[2]) - ) - ); - RB = new THREE.Quaternion().setFromEuler( - new THREE.Euler( - THREE.MathUtils.degToRad(RB[0]), - THREE.MathUtils.degToRad(RB[1]), - THREE.MathUtils.degToRad(RB[2]) - ) - ); - - const data = RAPIER.JointData.fixed({ - x: VA[0], - y: VA[1], - z: VA[2], - }, - RA, { - x: VB[0], - y: VB[1], - z: VB[2], - }, - RB - ); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - sphericalJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - - const data = RAPIER.JointData.spherical({ - x: VA[0], - y: VA[1], - z: VA[2], - }, { - x: VB[0], - y: VB[1], - z: VB[2], - }); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - revoluteJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - const x = JSON.parse(args.X).map(Number); - - const data = RAPIER.JointData.revolute({ - x: VA[0], - y: VA[1], - z: VA[2], - }, { - x: VB[0], - y: VB[1], - z: VB[2], - }, { - x: x[0], - y: x[1], - z: x[2], - }); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - prismaticJoint(args) { - const VA = JSON.parse(args.VA).map(Number); - const VB = JSON.parse(args.VB).map(Number); - const x = JSON.parse(args.X).map(Number); - - const data = RAPIER.JointData.prismatic({ - x: VA[0], - y: VA[1], - z: VA[2], - }, { - x: VB[0], - y: VB[1], - z: VB[2], - }, { - x: x[0], - y: x[1], - z: x[2], - }); - const objectA = getObject(args.ObjA); - let object = getObject(args.ObjB); - this.joint(data, objectA, object); - } - - createWorld(args) { - const v3 = JSON.parse(args.G).map(Number); - const gravity = { - x: v3[0], - y: v3[1], - z: v3[2], - }; - physicsWorld = new RAPIER.World(gravity); - - console.log(physicsWorld); - } - - getWorld(args) { - if (args.PROPERTY === "log") { - console.log(physicsWorld); - return "logged"; - } - return JSON.stringify(physicsWorld[args.PROPERTY]); - } - - setRB(args) { - let value = args.VALUE; - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; - let object = getObject(args.OBJECT); - object.rigidBody[args.PROPERTY](value); - } - setC(args) { - let value = args.VALUE; - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; - let object = getObject(args.OBJECT); - object.collider[args.PROPERTY](value); - } - - getRB(args) { - let object = getObject(args.OBJECT); - return JSON.stringify(object.rigidBody[args.PROPERTY]()); - } - getC(args) { - let object = getObject(args.OBJECT); - return JSON.stringify(object.collider[args.PROPERTY]()); - } - - lockObjectAxis(args) { - let object = getObject(args.OBJECT); - const x = !JSON.parse(args.X); - const y = !JSON.parse(args.Y); - const z = !JSON.parse(args.Z); - object.rigidBody[args.PROPERTY](x, y, z, true); //changes is xyz, wake up - } - - objectPhysics(args) { - let object = getObject(args.OBJECT); - object.physics = JSON.parse(args.state); - - if (JSON.parse(args.state)) { - //if already exists delete: - if (object.rigidBody) { - physicsWorld.removeRigidBody(object.rigidBody); - object.rigidBody = null; - object.collider = null; - } - /*asing a rigidbody and collider to object and add them to physicsWorld*/ - let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() - .setTranslation(object.position.x, object.position.y, object.position.z) - .setRotation({ - w: object.quaternion._w, - x: object.quaternion._x, - y: object.quaternion._y, - z: object.quaternion._z, - }); - - let colliderDesc; - switch (args.collider) { - case "cuboid": - colliderDesc = createCuboidCollider(object); - break; - case "ball": - colliderDesc = createBallCollider(object); - break; - case "convexHull": - colliderDesc = createConvexHullCollider(object); - break; - case "trimesh": - colliderDesc = TriMesh(object); - break; - } - colliderDesc - .setSensor(JSON.parse(args.state2)) - .setMass(args.mass) - .setDensity(args.density) - .setFriction(args.friction); - - let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc); - let collider = physicsWorld.createCollider(colliderDesc, rigidBody); - - object.rigidBody = rigidBody; - object.collider = collider; - } else { - /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ - physicsWorld.removeRigidBody(object.rigidBody); - object.rigidBody = null; - object.collider = null; - } - } - - enableCCD(args) { - let object = getObject(args.OBJECT); - if (object.physics) { - let rigidBody = object.rigidBody; - rigidBody.enableCcd(JSON.parse(args.state)); - } - } - - addForce(args) { - let object = getObject(args.OBJECT); - const vector = JSON.parse(args.VALUE).map(Number); - - let force = new THREE.Vector3(vector[0], vector[1], vector[2]); - if (args.SPACE === "local") { - force.applyQuaternion(object.quaternion); - } - - object.rigidBody[args.PROPERTY](force, true); - } - - resetForces(args) { - rigidBody[args.PROPERTY](true); - } - - sensorSingle(args) { - const sensor = getObject(args.SENSOR); - - let object = getObject(args.OBJECT); - - let touching = false; - physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { - if (otherCollider === object.collider) touching = true; - }); - - return touching; - } - - sensorAll(args) { - const sensor = getObject(args.SENSOR); - - const touchedObjects = []; - - // loop thruogh every collider touching sensor - physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { - // find owner of collider - const otherObject = scene.children.find((o) => o.collider === otherCollider); - console.log(otherCollider); - if (otherObject) touchedObjects.push(otherObject.name); - }); - - return JSON.stringify(touchedObjects); - } - } - Scratch.extensions.register(new RapierPhysics()); - - //Thanks to the PointerLock extension of Turbowarp - const mouse = vm.runtime.ioDevices.mouse; - let isLocked = false; - let isPointerLockEnabled = false; - - let rect = threeRenderer.domElement.getBoundingClientRect(); - document.addEventListener("resize", () => { - rect = threeRenderer.domElement.getBoundingClientRect(); - }); - - const postMouseData = (e, isDown) => { - const { - movementX, - movementY - } = e; - const { - width, - height - } = rect; - const x = mouse._clientX + movementX; - const y = mouse._clientY - movementY; - mouse._clientX = x; - mouse._scratchX = mouse.runtime.stageWidth * (x / width - 0.5); - mouse._clientY = y; - mouse._scratchY = mouse.runtime.stageHeight * (y / height - 0.5); - if (typeof isDown === "boolean") { - const data = { - button: e.button, - isDown, - }; - originalPostIOData(data); - } - }; - - const mouseDevice = vm.runtime.ioDevices.mouse; - const originalPostIOData = mouseDevice.postData.bind(mouseDevice); - mouseDevice.postData = (data) => { - if (!isPointerLockEnabled) { - return originalPostIOData(data); - } - }; - - document.addEventListener( - "mousedown", - (e) => { - // @ts-expect-error - if (threeRenderer.domElement.contains(e.target)) { - if (isLocked) { - postMouseData(e, true); - } else if (isPointerLockEnabled) { - threeRenderer.domElement.requestPointerLock(); - } - } - }, - true - ); - document.addEventListener( - "mouseup", - (e) => { - if (isLocked) { - postMouseData(e, false); - // @ts-expect-error - } else if (isPointerLockEnabled && threeRenderer.domElement.contains(e.target)) { - threeRenderer.domElement.requestPointerLock(); - } - }, - true - ); - document.addEventListener( - "mousemove", - (e) => { - if (isLocked) { - postMouseData(e); - } - }, - true - ); - - document.addEventListener("pointerlockchange", () => { - isLocked = document.pointerLockElement === threeRenderer.domElement; - }); - document.addEventListener("pointerlockerror", (e) => { - console.error("Pointer lock error", e); - }); - - const oldStep = vm.runtime._step; - vm.runtime._step = function(...args) { - const ret = oldStep.call(this, ...args); - if (isPointerLockEnabled) { - const { - width, - height - } = rect; - mouse._clientX = width / 2; - mouse._clientY = height / 2; - mouse._scratchX = 0; - mouse._scratchY = 0; - } - return ret; - }; - - vm.runtime.on("PROJECT_LOADED", () => { - isPointerLockEnabled = false; - if (isLocked) { - document.exitPointerLock(); - } - }); - - class Pointerlock { - getInfo() { - return { - id: "threepointerlockmod", - name: "Pointerlock for Extra 3D", - color1: "#8a8a8aff", - color2: "#222222", - color3: "#222222", - - blocks: [{ - opcode: "setLocked", - blockType: Scratch.BlockType.COMMAND, - text: "set pointer lock [enabled]", - arguments: { - enabled: { - type: Scratch.ArgumentType.STRING, - defaultValue: "true", - menu: "enabled", - }, - }, - }, - { - opcode: "isLocked", - blockType: Scratch.BlockType.BOOLEAN, - text: "pointer locked?", - }, - ], - menus: { - enabled: { - acceptReporters: true, - items: [{ - text: "enabled", - value: "true", - }, - { - text: "disabled", - value: "false", - }, - ], - }, - }, - }; - } - - setLocked(args) { - isPointerLockEnabled = Scratch.Cast.toBoolean(args.enabled) === true; - if (!isPointerLockEnabled && isLocked) { - document.exitPointerLock(); - } - } - - isLocked() { - return isLocked; - } - } - Scratch.extensions.register(new Pointerlock()); - }); -})(Scratch); From 7bd8d1257bdbc5d6fe1a3f2ec2acd1cd0930fad3 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:09:38 -0600 Subject: [PATCH 15/32] fix --- threejsD.js | 908 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 548 insertions(+), 360 deletions(-) diff --git a/threejsD.js b/threejsD.js index 9ea56ec..bc6db57 100644 --- a/threejsD.js +++ b/threejsD.js @@ -59,22 +59,23 @@ let RAPIER; let physicsWorld; - let threeRenderer - let scene - let camera - let eulerOrder = "YXZ" - - let composer - let passes = {} - let customEffects = [] - let renderTargets = {} - - let materials = {} - let geometries = {} - let lights = {} - let models = {} - - let assets = { //should i place materials, geometries; inside too? + let threeRenderer; + let scene; + let camera; + let eulerOrder = "YXZ"; + + let composer; + let passes = {}; + let customEffects = []; + let renderTargets = {}; + + let materials = {}; + let geometries = {}; + let lights = {}; + let models = {}; + + let assets = { + //should i place materials, geometries; inside too? textures: {}, colors: {}, fogs: {}, @@ -85,17 +86,17 @@ let raycastResult = []; function resetor(level) { - camera = undefined - composer.reset() + camera = undefined; + composer.reset(); - passes = {} - customEffects = [] - renderTargets = {} + passes = {}; + customEffects = []; + renderTargets = {}; - materials = {} - geometries = {} - lights = {} - models = {} + materials = {}; + geometries = {}; + lights = {}; + models = {}; if (level > 0) { assets = { @@ -128,42 +129,62 @@ return [x, y, z]; } - object.add(content) + //objects + function createObject(name, content, parentName) { + let object = getObject(name, true); + if (object) { + removeObject(name); + alerts ? alert(name + " already exsisted, will replace!") : null; } - function removeObject(name) { - let object = getObject(name) - if (!object) return + content.name = name; + content.rotation._order = eulerOrder; + parentName === scene.name ? (object = scene) : (object = getObject(parentName)); + content.physics = false; - scene.remove(object) + object.add(content); + } - if (object.rigidBody) { - physicsWorld.removeCollider(object.collider, true) - physicsWorld.removeRigidBody(object.rigidBody, true) - object.rigidBody = null - object.collider = null - } - if (object.isLight) { - delete(lights[name]) - } + function removeObject(name) { + let object = getObject(name); + if (!object) return; + + scene.remove(object); + + if (object.rigidBody) { + physicsWorld.removeCollider(object.collider, true); + physicsWorld.removeRigidBody(object.rigidBody, true); + object.rigidBody = null; + object.collider = null; } - function getObject(name, isNew) { - let object = null - if (!scene) { - alerts ? alert("Can not get " + name + ". Create a scene first!") : null; return;} - object = scene.getObjectByName(name) - if (!object && !isNew) {alerts ? alert(name + " does not exist! Add it to scene"):null; return;} - return object + if (object.isLight) { + delete lights[name]; } + } -//materials - function encodeCostume (name) { - if (name.startsWith("data:image/")) return name - return Scratch.vm.editingTarget.sprite.costumes.find(c => c.name === name).asset.encodeDataURI() + function getObject(name, isNew) { + let object = null; + if (!scene) { + alerts ? alert("Can not get " + name + ". Create a scene first!") : null; + return; + } + object = scene.getObjectByName(name); + if (!object && !isNew) { + alerts ? alert(name + " does not exist! Add it to scene") : null; + return; } - function setTexutre (texture, mode, style, x, y) { - texture.colorSpace = THREE.SRGBColorSpace + return object; + } + + //materials + function encodeCostume(name) { + if (name.startsWith("data:image/")) return name; + return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); + } - if (mode === "Pixelate") { + function setTexutre(texture, mode, style, x, y) { + texture.colorSpace = THREE.SRGBColorSpace; + + if (mode === "Pixelate") { texture.minFilter = THREE.NearestFilter; texture.magFilter = THREE.NearestFilter; } else { @@ -226,12 +247,12 @@ // Ensure matrices are updated. light.target.updateMatrixWorld(); - light.shadow.camera.updateProjectionMatrix() - light.shadow.needsUpdate = true; -} -//composer -function updateComposers() { - if (!camera || !scene) return; // nothing to do yet + light.shadow.camera.updateProjectionMatrix(); + light.shadow.needsUpdate = true; + } + //composer + function updateComposers() { + if (!camera || !scene) return; // nothing to do yet // always recreate the RenderPass to point to the current scene/camera passes["Render"] = new RenderPass(scene, camera); @@ -422,8 +443,11 @@ function updateComposers() { return JSON.parse(path); //boolean or number } - return JSON.parse(path) //boolean or number -} + let mouseNDC = [0, 0]; + //loops/init + function stopLoop() { + if (!running) return; + running = false; if (loopId) { cancelAnimationFrame(loopId); @@ -476,11 +500,11 @@ function updateComposers() { powerPreference: "high-performance", antialias: false, stencil: false, - depth: true - }) - threeRenderer.setPixelRatio(window.devicePixelRatio) - threeRenderer.outputColorSpace = THREE.SRGBColorSpace // correct colors - threeRenderer.toneMapping = THREE.ACESFilmicToneMapping // HDR look (test) + depth: true, + }); + threeRenderer.setPixelRatio(window.devicePixelRatio); + threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors + threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) //threeRenderer.toneMappingExposure = 1.0 //(test) threeRenderer.shadowMap.enabled = true; @@ -532,23 +556,26 @@ function updateComposers() { } } - const loop = () => { - if (!running) return - //RAPIER - if (physicsWorld && scene) { - physicsWorld.step() - - scene.children.forEach(obj => { - if (!(obj.isMesh) || !(obj.physics)) return - if (obj.rigidBody) { - obj.position.copy(obj.rigidBody.translation()) - obj.quaternion.copy(obj.rigidBody.rotation()) - } - }) - - } - if (scene && camera) { - if (controls) controls.update() + function startRenderLoop() { + if (running) return; + running = true; + + const loop = () => { + if (!running) return; + //RAPIER + if (physicsWorld && scene) { + physicsWorld.step(); + + scene.children.forEach((obj) => { + if (!obj.isMesh || !obj.physics) return; + if (obj.rigidBody) { + obj.position.copy(obj.rigidBody.translation()); + obj.quaternion.copy(obj.rigidBody.rotation()); + } + }); + } + if (scene && camera) { + if (controls) controls.update(); const delta = clock.getDelta(); Object.values(models).forEach((model) => { @@ -605,9 +632,13 @@ function updateComposers() { const w = canvas.width; const h = canvas.height; -function resize() { - const w = canvas.width - const h = canvas.height + threeRenderer.setSize(w, h); + composer.setSize(w, h); + customEffects.forEach((e) => { + if (e.uniforms.get("resolution")) { + e.uniforms.get("resolution").value.set(w, h); + } + }); if (camera) { camera.aspect = w / h; @@ -697,8 +728,11 @@ function resize() { } Scratch.extensions.register(new ThreeRenderer()); - } - Scratch.extensions.register(new ThreeRenderer()) + class ThreeScene { + constructor() { + this.THREE = THREE; + this.scenes = {}; + } getInfo() { return { @@ -800,37 +834,41 @@ function resize() { }; } - newScene(args) { - scene = new THREE.Scene(); - scene.name = args.NAME - scene.background = new THREE.Color("#222") - //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - - resetor(0) - } + newScene(args) { + scene = new THREE.Scene(); + scene.name = args.NAME; + scene.background = new THREE.Color("#222"); + //scene.add(new THREE.GridHelper(16, 16)) //future helper section? + this.scenes = { + ...this.scenes, + ...scene, + }; + resetor(0); + } - reset() { - resetor(1) - } + reset() { + resetor(1); + } - async setSceneProperty(args) { + async setSceneProperty(args) { const property = args.PROPERTY; const value = getAsset(args.VALUE); scene[property] = value; - } - getSceneObjects(args){ - const names = []; - if (args.THING === "Objects") { - scene.traverse(obj => { - if (obj.name) names.push(obj.name); //if it has a name, add to list! - }); - } - else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)) - else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)) - else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)) - else if (args.THING === "Scene Properties") {console.log(scene); return "check console"} - else if (args.THING === "Other assets") return JSON.stringify(assets) + } + getSceneObjects(args) { + const names = []; + if (args.THING === "Objects") { + scene.traverse((obj) => { + if (obj.name) names.push(obj.name); //if it has a name, add to list! + }); + } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); + else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); + else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); + else if (args.THING === "Scene Properties") { + console.log(scene); + return "check console"; + } else if (args.THING === "Other assets") return JSON.stringify(assets); return JSON.stringify(names); // if objects } @@ -1031,12 +1069,28 @@ function resize() { const object = new THREE.PerspectiveCamera(90, v2.x / v2.y); object.position.z = 3; - cubeCamera(args) { - // Create cube render target - const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true } ) - // Create cube camera - const cubeCamera = new THREE.CubeCamera( 0.1, 500, cubeRenderTarget ) - createObject(args.CAMERA, cubeCamera, args.GROUP) + createObject(args.CAMERA, object, args.GROUP); + } + setCamera(args) { + let object = getObject(args.CAMERA); + object[args.PROPERTY] = args.VALUE; + object.updateProjectionMatrix(); + } + getCamera(args) { + let object = getObject(args.CAMERA); + const value = JSON.stringify(object[args.PROPERTY]); + return value; + } + renderSceneCamera(args) { + let object = getObject(args.CAMERA); + if (!object) return; + camera = object; + //reset composer, else it does not update. + composer.passes = []; + passes = {}; + customEffects = []; + updateComposers(); + } cubeCamera(args) { // Create cube render target @@ -1054,8 +1108,31 @@ function resize() { assets.renderTargets[cubeRenderTarget.texture.uuid] = cubeRenderTarget.texture; } - renderTargets[args.RT] = {target: renderTarget, camera: object} - assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture + renderTarget(args) { + let object = getObject(args.CAMERA); + const renderTarget = new THREE.WebGLRenderTarget(360, 360, { + generateMipmaps: false, + }); + + renderTargets[args.RT] = { + target: renderTarget, + camera: object, + }; + assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; + } + sizeTarget(args) { + renderTargets[args.RT].target.setSize(args.W, args.H); + } + getTarget(args) { + const t = renderTargets[args.RT].target.texture; + console.log(t, renderTargets[args.RT]); + return `renderTargets/${t.uuid}`; + } + removeTarget(args) { + delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; + renderTargets[args.RT].target.dispose(); + delete renderTargets[args.RT]; + } } Scratch.extensions.register(new ThreeCameras()); @@ -2112,17 +2189,17 @@ function resize() { object.castShadow = true; object.receiveShadow = true; - createObject(args.OBJECT3D, object, args.GROUP) - } - cloneObject(args) { - let object = getObject(args.OBJECT3D) - const clone = object.clone(true) - clone.name - createObject(args.NAME, clone, args.GROUP) - } - setObjectV3(args) { - let object = getObject(args.OBJECT3D) - let values = JSON.parse(args.VALUE) + createObject(args.OBJECT3D, object, args.GROUP); + } + cloneObject(args) { + let object = getObject(args.OBJECT3D); + const clone = object.clone(true); + clone.name; + createObject(args.NAME, clone, args.GROUP); + } + setObjectV3(args) { + let object = getObject(args.OBJECT3D); + let values = JSON.parse(args.VALUE); function degToRad(deg) { return (deg * Math.PI) / 180; @@ -2190,57 +2267,63 @@ function resize() { let value = args.VALUE if (args.PROPERTY === "rotation") value = value * Math.PI / 180 - object[args.PROPERTY][args.X] += value - } - */ - getObjectV3(args) { - let object = getObject(args.OBJECT3D) - if (!object) return - let values = vector3ToString(object[args.PROPERTY]) + object[args.PROPERTY][args.X] += value + } + */ + getObjectV3(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let values = vector3ToString(object[args.PROPERTY]); if (args.PROPERTY === "rotation") { const toDeg = Math.PI / 180; values = [values[0] / toDeg, values[1] / toDeg, values[2] / toDeg]; } - return JSON.stringify(values) - } - setObject(args){ - let object = getObject(args.OBJECT3D) - let value = args.VALUE - if (args.PROPERTY === "material") {const mat = materials[args.NAME]; if (mat) value = mat; else value = undefined} - else if (args.PROPERTY === "geometry") {const geo = geometries[args.NAME]; if (geo) value = geo; else value = undefined} - else value = !!value - - if (value == undefined) return //invalid geo/mat - object[args.PROPERTY] = value - } - getObject(args){ - let object = getObject(args.OBJECT3D) - if (!object) return - let value - if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; - else value = object.visible; - - return value - } - removeObject(args) { - removeObject(args.OBJECT3D) - } - objectE(args) { - return scene.children.map(o => o.name).includes(args.NAME) - } + return JSON.stringify(values); + } + setObject(args) { + let object = getObject(args.OBJECT3D); + let value = args.VALUE; + if (args.PROPERTY === "material") { + const mat = materials[args.NAME]; + if (mat) value = mat; + else value = undefined; + } else if (args.PROPERTY === "geometry") { + const geo = geometries[args.NAME]; + if (geo) value = geo; + else value = undefined; + } else value = !!value; + + if (value == undefined) return; //invalid geo/mat + object[args.PROPERTY] = value; + } + getObject(args) { + let object = getObject(args.OBJECT3D); + if (!object) return; + let value; + if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; + else value = object.visible; + + return value; + } + removeObject(args) { + removeObject(args.OBJECT3D); + } + objectE(args) { + return scene.children.map((o) => o.name).includes(args.NAME); + } -//defines - newMaterial(args) { - if (materials[args.NAME] && alerts) alert ("material already exists! will replace...") - const mat = new THREE[args.TYPE](); - mat.name = args.NAME; + //defines + newMaterial(args) { + if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); + const mat = new THREE[args.TYPE](); + mat.name = args.NAME; - materials[args.NAME] = mat; - } - async setMaterial(args) { - if (typeof(args.VALUE) == "string" && args.VALUE.at(0) == "|") return - const mat = materials[args.NAME] + materials[args.NAME] = mat; + } + async setMaterial(args) { + if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; + const mat = materials[args.NAME]; let value = args.VALUE; @@ -2254,47 +2337,72 @@ function resize() { console.log("o:", args.VALUE, typeof args.VALUE); console.log("r:", value, typeof value); - geometries[args.NAME] = geo - } - setGeometry(args) { - const geo = geometries[args.NAME] - geo[args.PROPERTY] = (args.VALUE) + mat[args.PROPERTY] = await value; //await incase its a texture + mat.needsUpdate = true; + } + setBlending(args) { + const mat = materials[args.NAME]; + mat.blending = THREE[args.VALUE]; + mat.premultipliedAlpha = true; + mat.needsUpdate = true; + } + setDepth(args) { + const mat = materials[args.NAME]; + mat.depthFunc = THREE[args.VALUE]; + mat.needsUpdate = true; + } + removeMaterial(args) { + const mat = materials[args.NAME]; + mat.dispose(); + delete materials[args.NAME]; + } + materialE(args) { + return materials[args.NAME] ? true : false; + } - geo.needsUpdate = true; - } - removeGeometry(args){ - const geo = geometries[args.NAME] - geo.dispose() - delete(geometries[args.NAME]) - } - geometryE(args) { - return geometries[args.NAME] ? true : false - } + newGeometry(args) { + if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); + const geo = new THREE[args.TYPE](); + geo.name = args.NAME; - newGeo(args) { - const geometry = new THREE.BufferGeometry() - geometry.name = args.NAME - geometries[args.NAME] = geometry - } - async geoPoints(args) { - const geometry = geometries[args.NAME] - const positions = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v3 of each vertex of each triangle + geometries[args.NAME] = geo; + } + setGeometry(args) { + const geo = geometries[args.NAME]; + geo[args.PROPERTY] = args.VALUE; - geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3)) - geometry.computeVertexNormals() + geo.needsUpdate = true; + } + removeGeometry(args) { + const geo = geometries[args.NAME]; + geo.dispose(); + delete geometries[args.NAME]; + } + geometryE(args) { + return geometries[args.NAME] ? true : false; + } - geometry.needsUpdate = true - } - geoUVs(args) { - const geometry = geometries[args.NAME] - const UVs = args.POINTS.split(" ").map(v=>JSON.parse(v)).flat() //array of v2 of each UV of each triangle + newGeo(args) { + const geometry = new THREE.BufferGeometry(); + geometry.name = args.NAME; + geometries[args.NAME] = geometry; + } + async geoPoints(args) { + const geometry = geometries[args.NAME]; + const positions = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v3 of each vertex of each triangle geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3)); geometry.computeVertexNormals(); - splines(args) { - const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)) - geometry.name = args.NAME + geometry.needsUpdate = true; + } + geoUVs(args) { + const geometry = geometries[args.NAME]; + const UVs = args.POINTS.split(" ") + .map((v) => JSON.parse(v)) + .flat(); //array of v2 of each UV of each triangle geometry.setAttribute("uv", new THREE.BufferAttribute(new Float32Array(UVs), 2)); geometry.needsUpdate = true; @@ -2304,10 +2412,8 @@ function resize() { const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); geometry.name = args.NAME; - const curve = getAsset(args.CURVE) - const spacing = parseFloat(args.SPACING) || 1 - const curveLength = curve.getLength() - const divisions = Math.floor(curveLength / spacing) + geometries[args.NAME] = geometry; + } async splineModel(args) { const model = await getModel(args.MODEL, args.NAME); @@ -2329,12 +2435,36 @@ function resize() { const temp = model.clone(true); temp.position.copy(pos); - temp.traverse(child => { - if (child.isMesh && child.geometry) { - const geom = child.geometry.clone() - geom.applyMatrix4(child.matrixWorld) - geomList.push(geom) - matList.push(child.material) //.clone() ? + const up = new THREE.Vector3(0, 0, 1); + const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent.clone().normalize()); + temp.quaternion.copy(quat); + + temp.updateMatrixWorld(true); + + temp.traverse((child) => { + if (child.isMesh && child.geometry) { + const geom = child.geometry.clone(); + geom.applyMatrix4(child.matrixWorld); + geomList.push(geom); + matList.push(child.material); //.clone() ? + } + }); + } + + const validGeoms = geomList.filter((g) => { + const ok = g && g.isBufferGeometry && g.attributes && g.attributes.position; + if (!ok) console.warn("geometry skipped:", g); + return ok; + }); + + const merged = BufferGeometryUtils.mergeGeometries(validGeoms, true); + merged.computeBoundingBox(); + merged.computeBoundingSphere(); + + merged.name = args.NAME; + geometries[args.NAME] = merged; + matList.name = args.NAME; + materials[args.NAME] = matList; } async text(args) { @@ -2362,11 +2492,8 @@ function resize() { geometry.name = args.NAME; - const params = {font: font, size: JSON.parse(args.S), height: JSON.parse(args.D), curveSegments: JSON.parse(args.CS), bevelEnabled: false} - const geometry = new TextGeometry.TextGeometry(args.TEXT, params) - geometry.computeVertexNormals() - geometry.center() // optional, recenters the text - + geometries[args.NAME] = geometry; + } async loadFont() { openFileExplorer(".json").then((files) => { @@ -2558,18 +2685,11 @@ function resize() { lights[args.NAME] = light; if (light.type === "AmbientLight" || "HemisphereLight") return; - light.shadow.mapSize.width = 4096 - light.shadow.mapSize.height = 2048 - - if (light.type === "SpotLight") { - light.decay = 0 - light.shadow.camera.near = 500; - light.shadow.camera.far = 4000; - light.shadow.camera.fov = 30; - } - light.shadow.needsUpdate = true - light.needsUpdate = true - } + light.castShadow = true; + if (light.type === "PointLight") return; + //Directional & Spot Light + light.target.position.set(0, 0, 0); + scene.add(light.target); light.pos = new THREE.Vector3(0, 0, 0); @@ -2586,7 +2706,16 @@ function resize() { light.needsUpdate = true; } - if (light.type === "AmbientLight" || "HemisphereLight") return + setLight(args) { + const light = lights[args.NAME]; + if (!args.PROPERTY) return; + if (args.PROPERTY === "target") { + light.target.position.set(...JSON.parse(args.VALUE)); //vector3 + light.target.updateMatrixWorld(); + } else { + light[args.PROPERTY] = getAsset(args.VALUE); + } + light.needsUpdate = true; if (light.type === "AmbientLight" || "HemisphereLight") return; @@ -3042,34 +3171,50 @@ function resize() { return JSON.stringify([180 + THREE.MathUtils.radToDeg(pitch), THREE.MathUtils.radToDeg(yaw), 0]); } - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - async newCubeTexture(args) { - const uris = [encodeCostume(args.COSTUMEX0),encodeCostume(args.COSTUMEX1), encodeCostume(args.COSTUMEY0),encodeCostume(args.COSTUMEY1), encodeCostume(args.COSTUMEZ0),encodeCostume(args.COSTUMEZ1)] - const normalized = await Promise.all(uris.map(uri => resizeImageToSquare(uri, 256))); - const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); - - texture.name = "CubeTexture" + args.COSTUMEX0; - - setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y) - assets.textures[texture.uuid] = texture - return `textures/${texture.uuid}` - } - async newEquirectangularTexture(args) { - const textureURI = encodeCostume(args.COSTUME) - const texture = await new THREE.TextureLoader().loadAsync(textureURI); - texture.name = args.COSTUME - texture.mapping = THREE.EquirectangularReflectionMapping + newColor(args) { + const color = new THREE.Color(args.HEX); + const uuid = crypto.randomUUID(); + assets.colors[uuid] = color; + return `colors/${uuid}`; + } + newFog(args) { + const fog = new THREE.Fog(args.COLOR, args.NEAR, args.FAR); + const uuid = crypto.randomUUID(); + assets.fogs[uuid] = fog; + return `fogs/${uuid}`; + } + async newTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newCubeTexture(args) { + const uris = [ + encodeCostume(args.COSTUMEX0), + encodeCostume(args.COSTUMEX1), + encodeCostume(args.COSTUMEY0), + encodeCostume(args.COSTUMEY1), + encodeCostume(args.COSTUMEZ0), + encodeCostume(args.COSTUMEZ1), + ]; + const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); + const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); texture.name = "CubeTexture" + args.COSTUMEX0; - curve(args) { - function parsePoints(input) { - // Match all [x,y,z] groups - const matches = input.match(/\[([^\]]+)\]/g) - if (!matches) return [] + setTexutre(texture, args.MODE, args.STYLE, args.X, args.Y); + assets.textures[texture.uuid] = texture; + return `textures/${texture.uuid}`; + } + async newEquirectangularTexture(args) { + const textureURI = encodeCostume(args.COSTUME); + const texture = await new THREE.TextureLoader().loadAsync(textureURI); + texture.name = args.COSTUME; + texture.mapping = THREE.EquirectangularReflectionMapping; setTexutre(texture, args.MODE); assets.textures[texture.uuid] = texture; @@ -3094,8 +3239,20 @@ function resize() { const curve = new THREE[args.TYPE](points); curve.closed = JSON.parse(args.CLOSED); - raycast(args) { - const origin = new THREE.Vector3(...JSON.parse(args.V3)) + const uuid = crypto.randomUUID(); + assets.curves[uuid] = curve; + return `curves/${uuid}`; + } + + getItem(args) { + const items = JSON.parse(args.ARRAY); + const item = items[args.ITEM - 1]; + if (!item) return "0"; + return item; + } + + raycast(args) { + const origin = new THREE.Vector3(...JSON.parse(args.V3)); // rotation is in degrees => convert to radians first const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); @@ -3322,9 +3479,12 @@ function resize() { async addModel(args) { const group = await getModel(args.ITEM, args.NAME); - } - async addModel(args) { - const group = await getModel(args.ITEM, args.NAME) + createObject(args.NAME, group, args.GROUP); + } + getModel(args) { + if (!models[args.NAME]) return; + return Object.keys(models[args.NAME].actions).toString(); + } playAnimation(args) { const model = models[args.NAME]; @@ -3625,26 +3785,31 @@ function resize() { updateComposers(); } - const pass = new EffectPass(camera, bloomEffect) + bloom(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const bloomEffect = new BloomEffect({ + intensity: args.I, + luminanceThreshold: args.T, // ← correct key + luminanceSmoothing: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + bloomEffect.blendMode.opacity.value = args.OP; const pass = new EffectPass(camera, bloomEffect); composer.addPass(pass); } - const godRays = new GodRaysEffect(camera, sun, { - resolutionScale: args.RES, - density: args.DENS, // ray density - decay: args.DEC, // fade out - weight: args.WEI, // brightness of rays - exposure: args.EXP, - samples: args.SAMP, - blendFunction: BlendFunction[args.BLEND], - }) - godRays.blendMode.opacity.value = args.OP - const pass = new EffectPass(camera, godRays) - composer.addPass(pass) - } + godRays(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + let object = getObject(args.NAME); + const sun = object; const godRays = new GodRaysEffect(camera, sun, { resolutionScale: args.RES, @@ -3660,20 +3825,34 @@ function resize() { composer.addPass(pass); } - depth(args) { - if (!camera || !scene) {if (alerts) alert("set a camera!"); return} - const dofEffect = new DepthOfFieldEffect(camera, { - focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) - focalLength: args.FL, // lens focal length in meters - bokehScale: args.BS, // strength/size of the blur circles - height: args.H, // resolution hint (affects quality/perf) - blendFunction: BlendFunction[args.BLEND], - }) - dofEffect.blendMode.opacity.value = args.OP - - const dofPass = new EffectPass(camera, dofEffect) - composer.addPass(dofPass) - } + dots(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dot = new DotScreenEffect({ + angle: args.A, + scale: args.S, + blendFunction: BlendFunction[args.BLEND], + }); + dot.blendMode.opacity.value = args.OP; + const pass = new EffectPass(camera, dot); + composer.addPass(pass); + } + + depth(args) { + if (!camera || !scene) { + if (alerts) alert("set a camera!"); + return; + } + const dofEffect = new DepthOfFieldEffect(camera, { + focusDistance: (args.FD - camera.near) / (camera.far - camera.near), // how far from camera things are sharp (0 = near, 1 = far) + focalLength: args.FL, // lens focal length in meters + bokehScale: args.BS, // strength/size of the blur circles + height: args.H, // resolution hint (affects quality/perf) + blendFunction: BlendFunction[args.BLEND], + }); + dofEffect.blendMode.opacity.value = args.OP; const dofPass = new EffectPass(camera, dofEffect); composer.addPass(dofPass); @@ -4403,7 +4582,7 @@ function resize() { }; } joint(jointData, bodyA, bodyB) { - physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true) + physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); } fixedJoint(args) { @@ -4521,57 +4700,54 @@ function resize() { } getWorld(args) { - if (args.PROPERTY === "log") {console.log(physicsWorld); return "logged"} - return JSON.stringify(physicsWorld[args.PROPERTY]) + if (args.PROPERTY === "log") { + console.log(physicsWorld); + return "logged"; + } + return JSON.stringify(physicsWorld[args.PROPERTY]); } setRB(args) { - let value = args.VALUE - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE - let object = getObject(args.OBJECT) - object.rigidBody[args.PROPERTY](value) + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.rigidBody[args.PROPERTY](value); } setC(args) { - let value = args.VALUE - if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE - let object = getObject(args.OBJECT) - object.collider[args.PROPERTY](value) + let value = args.VALUE; + if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; + let object = getObject(args.OBJECT); + object.collider[args.PROPERTY](value); } getRB(args) { - let object = getObject(args.OBJECT) - return JSON.stringify(object.rigidBody[args.PROPERTY]()) + let object = getObject(args.OBJECT); + return JSON.stringify(object.rigidBody[args.PROPERTY]()); } getC(args) { - let object = getObject(args.OBJECT) - return JSON.stringify(object.collider[args.PROPERTY]()) + let object = getObject(args.OBJECT); + return JSON.stringify(object.collider[args.PROPERTY]()); } lockObjectAxis(args) { - let object = getObject(args.OBJECT) - const x = !JSON.parse(args.X) - const y = !JSON.parse(args.Y) - const z = !JSON.parse(args.Z) - object.rigidBody[args.PROPERTY](x,y,z,true) //changes is xyz, wake up + let object = getObject(args.OBJECT); + const x = !JSON.parse(args.X); + const y = !JSON.parse(args.Y); + const z = !JSON.parse(args.Z); + object.rigidBody[args.PROPERTY](x, y, z, true); //changes is xyz, wake up } objectPhysics(args) { - let object = getObject(args.OBJECT) - object.physics = JSON.parse(args.state) + let object = getObject(args.OBJECT); + object.physics = JSON.parse(args.state); if (JSON.parse(args.state)) { //if already exists delete: if (object.rigidBody) { - physicsWorld.removeRigidBody(object.rigidBody) - object.rigidBody = null - object.collider = null - } - - if (!physicsWorld) { - console.error("Physics world not created!"); - return; + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; } - /*asing a rigidbody and collider to object and add them to physicsWorld*/ let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() .setTranslation(object.position.x, object.position.y, object.position.z) @@ -4582,14 +4758,26 @@ function resize() { z: object.quaternion._z, }); - let colliderDesc - switch(args.collider) { - case "cuboid": colliderDesc = createCuboidCollider(object,); break - case "ball": colliderDesc = createBallCollider(object); break - case "convexHull": colliderDesc = createConvexHullCollider(object); break - case "trimesh": colliderDesc = TriMesh(object); break - } - colliderDesc.setSensor(JSON.parse(args.state2)).setMass(args.mass).setDensity(args.density).setFriction(args.friction) + let colliderDesc; + switch (args.collider) { + case "cuboid": + colliderDesc = createCuboidCollider(object); + break; + case "ball": + colliderDesc = createBallCollider(object); + break; + case "convexHull": + colliderDesc = createConvexHullCollider(object); + break; + case "trimesh": + colliderDesc = TriMesh(object); + break; + } + colliderDesc + .setSensor(JSON.parse(args.state2)) + .setMass(args.mass) + .setDensity(args.density) + .setFriction(args.friction); let rigidBody = physicsWorld.createRigidBody(rigidBodyDesc); let collider = physicsWorld.createCollider(colliderDesc, rigidBody); @@ -4598,25 +4786,25 @@ function resize() { object.collider = collider; } else { /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ - physicsWorld.removeRigidBody(object.rigidBody) - object.rigidBody = null - object.collider = null + physicsWorld.removeRigidBody(object.rigidBody); + object.rigidBody = null; + object.collider = null; } } enableCCD(args) { - let object = getObject(args.OBJECT) + let object = getObject(args.OBJECT); if (object.physics) { - let rigidBody = object.rigidBody - rigidBody.enableCcd(JSON.parse(args.state)) + let rigidBody = object.rigidBody; + rigidBody.enableCcd(JSON.parse(args.state)); } } - addForce(args) { - let object = getObject(args.OBJECT) - const vector = JSON.parse(args.VALUE).map(Number) - - let force = new THREE.Vector3(vector[0],vector[1],vector[2]) + addForce(args) { + let object = getObject(args.OBJECT); + const vector = JSON.parse(args.VALUE).map(Number); + + let force = new THREE.Vector3(vector[0], vector[1], vector[2]); if (args.SPACE === "local") { force.applyQuaternion(object.quaternion); } @@ -4624,14 +4812,14 @@ function resize() { object.rigidBody[args.PROPERTY](force, true); } - resetForces(args) { - rigidBody[args.PROPERTY](true) - } + resetForces(args) { + rigidBody[args.PROPERTY](true); + } - sensorSingle(args) { - const sensor = getObject(args.SENSOR) + sensorSingle(args) { + const sensor = getObject(args.SENSOR); - let object = getObject(args.OBJECT) + let object = getObject(args.OBJECT); let touching = false; physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { @@ -4641,18 +4829,18 @@ function resize() { return touching; } - sensorAll(args) { - const sensor = getObject(args.SENSOR) + sensorAll(args) { + const sensor = getObject(args.SENSOR); const touchedObjects = []; - // loop thruogh every collider touching sensor - physicsWorld.intersectionPairsWith(sensor.collider, otherCollider => { - // find owner of collider - const otherObject = scene.children.find(o => o.collider === otherCollider) - console.log(otherCollider) - if (otherObject) touchedObjects.push(otherObject.name) - }) + // loop thruogh every collider touching sensor + physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { + // find owner of collider + const otherObject = scene.children.find((o) => o.collider === otherCollider); + console.log(otherCollider); + if (otherObject) touchedObjects.push(otherObject.name); + }); return JSON.stringify(touchedObjects); } From b1b5084d3945846822adfe379127b93b96ee65f3 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:12:06 -0600 Subject: [PATCH 16/32] Update threejsD.js From 38725ca2b700a672b9239374114316d42ad7e8e4 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:16:50 -0600 Subject: [PATCH 17/32] fix again --- threejsD.js | 298 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 223 insertions(+), 75 deletions(-) diff --git a/threejsD.js b/threejsD.js index bc6db57..4237d0b 100644 --- a/threejsD.js +++ b/threejsD.js @@ -60,7 +60,6 @@ let physicsWorld; let threeRenderer; - let scene; let camera; let eulerOrder = "YXZ"; @@ -68,9 +67,8 @@ let passes = {}; let customEffects = []; let renderTargets = {}; - - let materials = {}; - let geometries = {}; + window.__THREE_MATERIALS__ = {}; + window.__THREE_GEOMETRIES__ = {} let lights = {}; let models = {}; @@ -87,14 +85,13 @@ function resetor(level) { camera = undefined; - composer.reset(); + if (composer) composer.reset(); passes = {}; customEffects = []; renderTargets = {}; - - materials = {}; - geometries = {}; + window.__THREE_MATERIALS__ = {}; + window.__THREE_GEOMETRIES__ = {}; lights = {}; models = {}; @@ -129,8 +126,36 @@ return [x, y, z]; } + // Helper function to get current scene + function getCurrentScene() { + const threeScene = runtime.ext_threeScene; + if (!threeScene) return null; + const currentSceneName = threeScene.currentSceneName; + return currentSceneName ? threeScene.scenes[currentSceneName] : null; + } + + // Helper function to get scene by name + function getSceneByName(sceneName) { + const threeScene = runtime.ext_threeScene; + if (!threeScene) return null; + return threeScene.scenes[sceneName]; + } + + // Helper function to set current scene + function setCurrentScene(sceneName) { + const threeScene = runtime.ext_threeScene; + if (!threeScene) return; + threeScene.currentSceneName = sceneName; + } + //objects function createObject(name, content, parentName) { + const scene = getCurrentScene(); + if (!scene) { + alerts ? alert("No active scene! Create a scene first!") : null; + return; + } + let object = getObject(name, true); if (object) { removeObject(name); @@ -138,13 +163,25 @@ } content.name = name; content.rotation._order = eulerOrder; - parentName === scene.name ? (object = scene) : (object = getObject(parentName)); + + let parentObject; + if (parentName === scene.name) { + parentObject = scene; + } else { + parentObject = getObject(parentName); + if (!parentObject) { + parentObject = scene; + } + } + content.physics = false; - - object.add(content); + parentObject.add(content); } function removeObject(name) { + const scene = getCurrentScene(); + if (!scene) return; + let object = getObject(name); if (!object) return; @@ -162,15 +199,16 @@ } function getObject(name, isNew) { - let object = null; + const scene = getCurrentScene(); if (!scene) { alerts ? alert("Can not get " + name + ". Create a scene first!") : null; - return; + return null; } - object = scene.getObjectByName(name); + + let object = scene.getObjectByName(name); if (!object && !isNew) { alerts ? alert(name + " does not exist! Add it to scene") : null; - return; + return null; } return object; } @@ -178,7 +216,10 @@ //materials function encodeCostume(name) { if (name.startsWith("data:image/")) return name; - return Scratch.vm.editingTarget.sprite.costumes.find((c) => c.name === name).asset.encodeDataURI(); + const editingTarget = vm.editingTarget; + if (!editingTarget) return null; + const costume = editingTarget.sprite.costumes.find((c) => c.name === name); + return costume ? costume.asset.encodeDataURI() : null; } function setTexutre(texture, mode, style, x, y) { @@ -252,6 +293,7 @@ } //composer function updateComposers() { + const scene = getCurrentScene(); if (!camera || !scene) return; // nothing to do yet // always recreate the RenderPass to point to the current scene/camera @@ -447,6 +489,7 @@ //loops/init function stopLoop() { if (!running) return; + vm.renderer.canvas.style.visibility="visible"; running = false; if (loopId) { @@ -458,8 +501,8 @@ async function load() { if (!THREE) { // @ts-ignore - THREE = await import("https://esm.sh/three@0.180.0") - window._THREE_ = THREE + THREE = await import("https://esm.sh/three@0.180.0"); + window._THREE_ = THREE; //Addons GLTFLoader = await import("https://esm.sh/three@0.180.0/examples/jsm/loaders/GLTFLoader.js"); OrbitControls = await import("https://esm.sh/three@0.180.0/examples/jsm/controls/OrbitControls.js"); @@ -502,6 +545,7 @@ stencil: false, depth: true, }); + window.__THREE_RENDERER__ = threeRenderer; threeRenderer.setPixelRatio(window.devicePixelRatio); threeRenderer.outputColorSpace = THREE.SRGBColorSpace; // correct colors threeRenderer.toneMapping = THREE.ACESFilmicToneMapping; // HDR look (test) @@ -548,22 +592,32 @@ running = false; load(); - startRenderLoop() - runtime.on('PROJECT_START', () => startRenderLoop()) - runtime.on('PROJECT_STOP_ALL', () => stopLoop()) - runtime.on('STAGE_SIZE_CHANGED', () => {requestAnimationFrame(() => resize())}) - checkCanvasSize() + startRenderLoop(); + runtime.on("PROJECT_START", () => startRenderLoop()); + runtime.on("PROJECT_STOP_ALL", () => stopLoop()); + runtime.on("STAGE_SIZE_CHANGED", () => { + requestAnimationFrame(() => resize()); + }); + checkCanvasSize(); } } function startRenderLoop() { if (running) return; + vm.renderer.canvas.style.visibility="hidden"; running = true; const loop = () => { if (!running) return; + + const scene = getCurrentScene(); + if (!scene) { + loopId = requestAnimationFrame(loop); + return; + } + //RAPIER - if (physicsWorld && scene) { + if (physicsWorld) { physicsWorld.step(); scene.children.forEach((obj) => { @@ -574,7 +628,7 @@ } }); } - if (scene && camera) { + if (camera) { if (controls) controls.update(); const delta = clock.getDelta(); @@ -633,7 +687,7 @@ const h = canvas.height; threeRenderer.setSize(w, h); - composer.setSize(w, h); + if (composer) composer.setSize(w, h); customEffects.forEach((e) => { if (e.uniforms.get("resolution")) { e.uniforms.get("resolution").value.set(w, h); @@ -730,8 +784,10 @@ class ThreeScene { constructor() { + // expose threejs and the scenes, so other extensions and javascript can do stuff manually this.THREE = THREE; this.scenes = {}; + this.currentSceneName = null; } getInfo() { @@ -835,36 +891,51 @@ } newScene(args) { - scene = new THREE.Scene(); - scene.name = args.NAME; + const scene = new THREE.Scene(); + const sceneName = Scratch.Cast.toString(args.NAME); + scene.name = sceneName; scene.background = new THREE.Color("#222"); - //scene.add(new THREE.GridHelper(16, 16)) //future helper section? - this.scenes = { - ...this.scenes, - ...scene, - }; + + this.scenes[sceneName] = scene; + this.currentSceneName = sceneName; // Set as current scene + resetor(0); } reset() { resetor(1); + this.scenes = {}; + this.currentSceneName = null; } async setSceneProperty(args) { + const scene = getCurrentScene(); + if (!scene) { + alerts ? alert("No active scene! Create a scene first!") : null; + return; + } + const property = args.PROPERTY; const value = getAsset(args.VALUE); scene[property] = value; } + getSceneObjects(args) { + const scene = getCurrentScene(); + if (!scene) { + alerts ? alert("No active scene! Create a scene first!") : null; + return "[]"; + } + const names = []; if (args.THING === "Objects") { scene.traverse((obj) => { if (obj.name) names.push(obj.name); //if it has a name, add to list! }); - } else if (args.THING === "Materials") return JSON.stringify(Object.keys(materials)); - else if (args.THING === "Geometries") return JSON.stringify(Object.keys(geometries)); - else if (args.THING === "Ligts") return JSON.stringify(Object.keys(lights)); + } else if (args.THING === "Materials") return JSON.stringify(Object.keys(window.__THREE_MATERIALS__)); + else if (args.THING === "Geometries") return JSON.stringify(Object.keys(window.__THREE_GEOMETRIES__)); + else if (args.THING === "Lights") return JSON.stringify(Object.keys(lights)); else if (args.THING === "Scene Properties") { console.log(scene); return "check console"; @@ -1073,11 +1144,13 @@ } setCamera(args) { let object = getObject(args.CAMERA); + if (!object) return; object[args.PROPERTY] = args.VALUE; object.updateProjectionMatrix(); } getCamera(args) { let object = getObject(args.CAMERA); + if (!object) return "null"; const value = JSON.stringify(object[args.PROPERTY]); return value; } @@ -1110,6 +1183,8 @@ renderTarget(args) { let object = getObject(args.CAMERA); + if (!object) return; + const renderTarget = new THREE.WebGLRenderTarget(360, 360, { generateMipmaps: false, }); @@ -1118,19 +1193,25 @@ target: renderTarget, camera: object, }; - assets.renderTargets[renderTarget.texture.uuid] == renderTarget.texture; + assets.renderTargets[renderTarget.texture.uuid] = renderTarget.texture; } sizeTarget(args) { - renderTargets[args.RT].target.setSize(args.W, args.H); + const target = renderTargets[args.RT]; + if (!target) return; + target.target.setSize(args.W, args.H); } getTarget(args) { - const t = renderTargets[args.RT].target.texture; + const target = renderTargets[args.RT]; + if (!target) return "null"; + const t = target.target.texture; console.log(t, renderTargets[args.RT]); return `renderTargets/${t.uuid}`; } removeTarget(args) { - delete assets.renderTargets[renderTargets[args.RT].target.texture.uuid]; - renderTargets[args.RT].target.dispose(); + const target = renderTargets[args.RT]; + if (!target) return; + delete assets.renderTargets[target.target.texture.uuid]; + target.target.dispose(); delete renderTargets[args.RT]; } } @@ -2193,12 +2274,15 @@ } cloneObject(args) { let object = getObject(args.OBJECT3D); + if (!object) return; const clone = object.clone(true); clone.name; createObject(args.NAME, clone, args.GROUP); } setObjectV3(args) { let object = getObject(args.OBJECT3D); + if (!object) return; + let values = JSON.parse(args.VALUE); function degToRad(deg) { @@ -2272,7 +2356,7 @@ */ getObjectV3(args) { let object = getObject(args.OBJECT3D); - if (!object) return; + if (!object) return "[0,0,0]"; let values = vector3ToString(object[args.PROPERTY]); if (args.PROPERTY === "rotation") { const toDeg = Math.PI / 180; @@ -2283,13 +2367,15 @@ } setObject(args) { let object = getObject(args.OBJECT3D); + if (!object) return; + let value = args.VALUE; if (args.PROPERTY === "material") { - const mat = materials[args.NAME]; + const mat = window.__THREE_MATERIALS__[args.NAME]; if (mat) value = mat; else value = undefined; } else if (args.PROPERTY === "geometry") { - const geo = geometries[args.NAME]; + const geo = window.__THREE_GEOMETRIES__[args.NAME]; if (geo) value = geo; else value = undefined; } else value = !!value; @@ -2299,7 +2385,7 @@ } getObject(args) { let object = getObject(args.OBJECT3D); - if (!object) return; + if (!object) return "null"; let value; if (args.PROPERTY != "visible") value = object[args.PROPERTY].name; else value = object.visible; @@ -2310,20 +2396,23 @@ removeObject(args.OBJECT3D); } objectE(args) { + const scene = getCurrentScene(); + if (!scene) return false; return scene.children.map((o) => o.name).includes(args.NAME); } //defines newMaterial(args) { - if (materials[args.NAME] && alerts) alert("material already exists! will replace..."); + if (window.__THREE_MATERIALS__[args.NAME] && alerts) alert("material already exists! will replace..."); const mat = new THREE[args.TYPE](); mat.name = args.NAME; - materials[args.NAME] = mat; + window.__THREE_MATERIALS__[args.NAME] = mat; } async setMaterial(args) { if (typeof args.VALUE == "string" && args.VALUE.at(0) == "|") return; - const mat = materials[args.NAME]; + const mat = window.__THREE_MATERIALS__[args.NAME]; + if (!mat) return; let value = args.VALUE; @@ -2341,54 +2430,60 @@ mat.needsUpdate = true; } setBlending(args) { - const mat = materials[args.NAME]; + const mat = window.__THREE_MATERIALS__[args.NAME]; + if (!mat) return; mat.blending = THREE[args.VALUE]; mat.premultipliedAlpha = true; mat.needsUpdate = true; } setDepth(args) { - const mat = materials[args.NAME]; + const mat = window.__THREE_MATERIALS__[args.NAME]; + if (!mat) return; mat.depthFunc = THREE[args.VALUE]; mat.needsUpdate = true; } removeMaterial(args) { - const mat = materials[args.NAME]; + const mat = window.__THREE_MATERIALS__[args.NAME]; + if (!mat) return; mat.dispose(); - delete materials[args.NAME]; + delete window.__THREE_MATERIALS__[args.NAME]; } materialE(args) { - return materials[args.NAME] ? true : false; + return window.__THREE_MATERIALS__[args.NAME] ? true : false; } newGeometry(args) { - if (geometries[args.NAME] && alerts) alert("geometry already exists! will replace..."); + if (window.__THREE_GEOMETRIES__[args.NAME] && alerts) alert("geometry already exists! will replace..."); const geo = new THREE[args.TYPE](); geo.name = args.NAME; - geometries[args.NAME] = geo; + window.__THREE_GEOMETRIES__[args.NAME] = geo; } setGeometry(args) { - const geo = geometries[args.NAME]; + const geo = window.__THREE_GEOMETRIES__[args.NAME]; + if (!geo) return; geo[args.PROPERTY] = args.VALUE; geo.needsUpdate = true; } removeGeometry(args) { - const geo = geometries[args.NAME]; + const geo = window.__THREE_GEOMETRIES__[args.NAME]; + if (!geo) return; geo.dispose(); - delete geometries[args.NAME]; + delete window.__THREE_GEOMETRIES__[args.NAME]; } geometryE(args) { - return geometries[args.NAME] ? true : false; + return window.__THREE_GEOMETRIES__[args.NAME] ? true : false; } newGeo(args) { const geometry = new THREE.BufferGeometry(); geometry.name = args.NAME; - geometries[args.NAME] = geometry; + window.__THREE_GEOMETRIES__[args.NAME] = geometry; } async geoPoints(args) { - const geometry = geometries[args.NAME]; + const geometry = window.__THREE_GEOMETRIES__[args.NAME]; + if (!geometry) return; const positions = args.POINTS.split(" ") .map((v) => JSON.parse(v)) .flat(); //array of v3 of each vertex of each triangle @@ -2399,7 +2494,8 @@ geometry.needsUpdate = true; } geoUVs(args) { - const geometry = geometries[args.NAME]; + const geometry = window.__THREE_GEOMETRIES__[args.NAME]; + if (!geometry) return; const UVs = args.POINTS.split(" ") .map((v) => JSON.parse(v)) .flat(); //array of v2 of each UV of each triangle @@ -2412,7 +2508,7 @@ const geometry = new THREE.TubeGeometry(getAsset(args.CURVE)); geometry.name = args.NAME; - geometries[args.NAME] = geometry; + window.__THREE_GEOMETRIES__[args.NAME] = geometry; } async splineModel(args) { @@ -2462,9 +2558,9 @@ merged.computeBoundingSphere(); merged.name = args.NAME; - geometries[args.NAME] = merged; + window.__THREE_GEOMETRIES__[args.NAME] = merged; matList.name = args.NAME; - materials[args.NAME] = matList; + window.__THREE_MATERIALS__[args.NAME] = matList; } async text(args) { @@ -2492,7 +2588,7 @@ geometry.name = args.NAME; - geometries[args.NAME] = geometry; + window.__THREE_GEOMETRIES__[args.NAME] = geometry; } async loadFont() { @@ -2689,7 +2785,9 @@ if (light.type === "PointLight") return; //Directional & Spot Light light.target.position.set(0, 0, 0); - scene.add(light.target); + + const scene = getCurrentScene(); + if (scene) scene.add(light.target); light.pos = new THREE.Vector3(0, 0, 0); @@ -2708,7 +2806,8 @@ setLight(args) { const light = lights[args.NAME]; - if (!args.PROPERTY) return; + if (!light || !args.PROPERTY) return; + if (args.PROPERTY === "target") { light.target.position.set(...JSON.parse(args.VALUE)); //vector3 light.target.updateMatrixWorld(); @@ -3185,6 +3284,7 @@ } async newTexture(args) { const textureURI = encodeCostume(args.COSTUME); + if (!textureURI) return "null"; const texture = await new THREE.TextureLoader().loadAsync(textureURI); texture.name = args.COSTUME; @@ -3201,6 +3301,9 @@ encodeCostume(args.COSTUMEZ0), encodeCostume(args.COSTUMEZ1), ]; + // Check if all URIs are valid + if (uris.some(uri => !uri)) return "null"; + const normalized = await Promise.all(uris.map((uri) => resizeImageToSquare(uri, 256))); const texture = await new THREE.CubeTextureLoader().loadAsync(normalized); @@ -3212,6 +3315,7 @@ } async newEquirectangularTexture(args) { const textureURI = encodeCostume(args.COSTUME); + if (!textureURI) return "null"; const texture = await new THREE.TextureLoader().loadAsync(textureURI); texture.name = args.COSTUME; texture.mapping = THREE.EquirectangularReflectionMapping; @@ -3252,6 +3356,9 @@ } raycast(args) { + const scene = getCurrentScene(); + if (!scene) return; + const origin = new THREE.Vector3(...JSON.parse(args.V3)); // rotation is in degrees => convert to radians first const rot = JSON.parse(args.D3).map((v) => (v * Math.PI) / 180); @@ -3482,7 +3589,7 @@ createObject(args.NAME, group, args.GROUP); } getModel(args) { - if (!models[args.NAME]) return; + if (!models[args.NAME]) return "null"; return Object.keys(models[args.NAME].actions).toString(); } @@ -3786,6 +3893,7 @@ } bloom(args) { + const scene = getCurrentScene(); if (!camera || !scene) { if (alerts) alert("set a camera!"); return; @@ -3804,11 +3912,13 @@ } godRays(args) { + const scene = getCurrentScene(); if (!camera || !scene) { if (alerts) alert("set a camera!"); return; } let object = getObject(args.NAME); + if (!object) return; const sun = object; const godRays = new GodRaysEffect(camera, sun, { @@ -3826,6 +3936,7 @@ } dots(args) { + const scene = getCurrentScene(); if (!camera || !scene) { if (alerts) alert("set a camera!"); return; @@ -3841,6 +3952,7 @@ } depth(args) { + const scene = getCurrentScene(); if (!camera || !scene) { if (alerts) alert("set a camera!"); return; @@ -4582,6 +4694,7 @@ }; } joint(jointData, bodyA, bodyB) { + if (!physicsWorld || !bodyA || !bodyB) return; physicsWorld.createImpulseJoint(jointData, bodyA.rigidBody, bodyB.rigidBody, true); } @@ -4700,6 +4813,7 @@ } getWorld(args) { + if (!physicsWorld) return "null"; if (args.PROPERTY === "log") { console.log(physicsWorld); return "logged"; @@ -4711,26 +4825,32 @@ let value = args.VALUE; if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; let object = getObject(args.OBJECT); + if (!object || !object.rigidBody) return; object.rigidBody[args.PROPERTY](value); } setC(args) { let value = args.VALUE; if (args.VALUE === "true" || args.VALUE === "false") value = !!args.VALUE; let object = getObject(args.OBJECT); + if (!object || !object.collider) return; object.collider[args.PROPERTY](value); } getRB(args) { let object = getObject(args.OBJECT); + if (!object || !object.rigidBody) return "null"; return JSON.stringify(object.rigidBody[args.PROPERTY]()); } getC(args) { let object = getObject(args.OBJECT); + if (!object || !object.collider) return "null"; return JSON.stringify(object.collider[args.PROPERTY]()); } lockObjectAxis(args) { let object = getObject(args.OBJECT); + if (!object || !object.rigidBody) return; + const x = !JSON.parse(args.X); const y = !JSON.parse(args.Y); const z = !JSON.parse(args.Z); @@ -4739,15 +4859,25 @@ objectPhysics(args) { let object = getObject(args.OBJECT); + if (!object) return; + object.physics = JSON.parse(args.state); if (JSON.parse(args.state)) { //if already exists delete: if (object.rigidBody) { - physicsWorld.removeRigidBody(object.rigidBody); + if (physicsWorld) { + physicsWorld.removeRigidBody(object.rigidBody); + } object.rigidBody = null; object.collider = null; } + + if (!physicsWorld) { + console.error("Physics world not created!"); + return; + } + /*asing a rigidbody and collider to object and add them to physicsWorld*/ let rigidBodyDesc = RAPIER.RigidBodyDesc[args.type]() .setTranslation(object.position.x, object.position.y, object.position.z) @@ -4772,7 +4902,13 @@ case "trimesh": colliderDesc = TriMesh(object); break; + default: + console.error("Unknown collider type:", args.collider); + return; } + + if (!colliderDesc) return; + colliderDesc .setSensor(JSON.parse(args.state2)) .setMass(args.mass) @@ -4786,7 +4922,9 @@ object.collider = collider; } else { /*if disabling physics, delete rigidbody and collider from physicsWorld and object*/ - physicsWorld.removeRigidBody(object.rigidBody); + if (physicsWorld && object.rigidBody) { + physicsWorld.removeRigidBody(object.rigidBody); + } object.rigidBody = null; object.collider = null; } @@ -4794,7 +4932,7 @@ enableCCD(args) { let object = getObject(args.OBJECT); - if (object.physics) { + if (object && object.physics && object.rigidBody) { let rigidBody = object.rigidBody; rigidBody.enableCcd(JSON.parse(args.state)); } @@ -4802,6 +4940,8 @@ addForce(args) { let object = getObject(args.OBJECT); + if (!object || !object.rigidBody) return; + const vector = JSON.parse(args.VALUE).map(Number); let force = new THREE.Vector3(vector[0], vector[1], vector[2]); @@ -4813,13 +4953,18 @@ } resetForces(args) { - rigidBody[args.PROPERTY](true); + let object = getObject(args.OBJECT); + if (!object || !object.rigidBody) return; + + object.rigidBody[args.PROPERTY](true); } sensorSingle(args) { const sensor = getObject(args.SENSOR); + if (!sensor || !sensor.collider || !physicsWorld) return false; let object = getObject(args.OBJECT); + if (!object || !object.collider) return false; let touching = false; physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { @@ -4831,14 +4976,17 @@ sensorAll(args) { const sensor = getObject(args.SENSOR); + if (!sensor || !sensor.collider || !physicsWorld) return "[]"; const touchedObjects = []; - // loop thruogh every collider touching sensor + // loop through every collider touching sensor physicsWorld.intersectionPairsWith(sensor.collider, (otherCollider) => { // find owner of collider + const scene = getCurrentScene(); + if (!scene) return; + const otherObject = scene.children.find((o) => o.collider === otherCollider); - console.log(otherCollider); if (otherObject) touchedObjects.push(otherObject.name); }); From b1ecda16140b7289d0a429a0398c745fb891b1d6 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:28:17 -0600 Subject: [PATCH 18/32] test --- threejsD.js | 147 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 84 insertions(+), 63 deletions(-) diff --git a/threejsD.js b/threejsD.js index 4237d0b..9ea3aeb 100644 --- a/threejsD.js +++ b/threejsD.js @@ -602,86 +602,107 @@ } } - function startRenderLoop() { - if (running) return; - vm.renderer.canvas.style.visibility="hidden"; - running = true; - - const loop = () => { - if (!running) return; - - const scene = getCurrentScene(); + async function startRenderLoop() { + if (running) return + vm.renderer.canvas.style.visibility = "hidden" + running = true + + const fixedDt = 1 / 60 + let accumulator = 0 + let lastTime = performance.now() + + const yieldToBrowser = () => new Promise(r => setTimeout(r, 0)) + + const loop = async (now) => { + if (!running) return + + const scene = getCurrentScene() if (!scene) { - loopId = requestAnimationFrame(loop); - return; + loopId = requestAnimationFrame(loop) + return } - - //RAPIER + + /* ---------- FIXED PHYSICS ---------- */ + const deltaTime = Math.min(0.1, (now - lastTime) / 1000) + lastTime = now + accumulator += deltaTime + if (physicsWorld) { - physicsWorld.step(); - + while (accumulator >= fixedDt) { + physicsWorld.step() + accumulator -= fixedDt + } + scene.children.forEach((obj) => { - if (!obj.isMesh || !obj.physics) return; - if (obj.rigidBody) { - obj.position.copy(obj.rigidBody.translation()); - obj.quaternion.copy(obj.rigidBody.rotation()); - } - }); + if (!obj.isMesh || !obj.physics || !obj.rigidBody) return + obj.position.copy(obj.rigidBody.translation()) + obj.quaternion.copy(obj.rigidBody.rotation()) + }) } + + /* ---------- ASYNC YIELD ---------- */ + await yieldToBrowser() + + /* ---------- NORMAL UPDATE ---------- */ if (camera) { - if (controls) controls.update(); - - const delta = clock.getDelta(); + if (controls) controls.update() + + const delta = clock.getDelta() + Object.values(models).forEach((model) => { - if (model) model.mixer.update(delta); - }); - - Object.values(lights).forEach((light) => updateShadowFrustum(light, camera.position)); - - //update custom effects time + if (model) model.mixer.update(delta) + }) + + Object.values(lights).forEach((light) => + updateShadowFrustum(light, camera.position) + ) + customEffects.forEach((e) => { if (e.uniforms.get("time")) { - e.uniforms.get("time").value += delta; + e.uniforms.get("time").value += delta } - }); + }) + Object.values(renderTargets).forEach((t) => { - if (t.camera.type == "PerspectiveCamera") { - t.camera.aspect = t.target.width / t.target.height; - t.camera.updateProjectionMatrix(); + if (t.camera.type === "PerspectiveCamera") { + t.camera.aspect = t.target.width / t.target.height + t.camera.updateProjectionMatrix() } - // get meshes using the texture associated with this target - const displayMeshes = getMeshesUsingTexture(scene, t.target.texture); - - displayMeshes.forEach((mesh) => { - mesh.visible = false; - }); - - if (t.camera.type == "PerspectiveCamera") { - threeRenderer.setRenderTarget(t.target); - threeRenderer.clear(true, true, true); - threeRenderer.render(scene, t.camera); + + // ✅ CACHE mesh lookup + if (!t._displayMeshes) { + t._displayMeshes = getMeshesUsingTexture(scene, t.target.texture) + } + + t._displayMeshes.forEach((mesh) => (mesh.visible = false)) + + if (t.camera.type === "PerspectiveCamera") { + threeRenderer.setRenderTarget(t.target) + threeRenderer.clear(true, true, true) + threeRenderer.render(scene, t.camera) } else { - t.target.clear(threeRenderer); - t.camera.update(threeRenderer, scene); //cubeCamera + t.target.clear(threeRenderer) + t.camera.update(threeRenderer, scene) } - - displayMeshes.forEach((mesh) => { - mesh.visible = true; - }); - }); - - camera.aspect = threeRenderer.domElement.width / threeRenderer.domElement.height; - camera.updateProjectionMatrix(); - threeRenderer.setRenderTarget(null); - composer.render(delta); + + t._displayMeshes.forEach((mesh) => (mesh.visible = true)) + }) + + camera.aspect = + threeRenderer.domElement.width / threeRenderer.domElement.height + camera.updateProjectionMatrix() + + threeRenderer.setRenderTarget(null) + composer.render(delta) } - - loopId = requestAnimationFrame(loop); - }; - - loopId = requestAnimationFrame(loop); + + loopId = requestAnimationFrame(loop) + } + + loopId = requestAnimationFrame(loop) } + function resize() { const w = canvas.width; const h = canvas.height; From e762febbca8c256b45566f3c982de83b78be39f6 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:50:31 -0600 Subject: [PATCH 19/32] Update threejsD.js --- threejsD.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/threejsD.js b/threejsD.js index 9ea3aeb..b1d7c1a 100644 --- a/threejsD.js +++ b/threejsD.js @@ -489,6 +489,7 @@ //loops/init function stopLoop() { if (!running) return; + checkCanvasSize(); vm.renderer.canvas.style.visibility="visible"; running = false; @@ -611,9 +612,7 @@ let accumulator = 0 let lastTime = performance.now() - const yieldToBrowser = () => new Promise(r => setTimeout(r, 0)) - - const loop = async (now) => { + const loop = (now) => { if (!running) return const scene = getCurrentScene() @@ -622,11 +621,11 @@ return } - /* ---------- FIXED PHYSICS ---------- */ - const deltaTime = Math.min(0.1, (now - lastTime) / 1000) + const deltaTime = Math.min(0.25, (now - lastTime) / 1000) lastTime = now accumulator += deltaTime + /* ---------- FIXED PHYSICS ---------- */ if (physicsWorld) { while (accumulator >= fixedDt) { physicsWorld.step() @@ -635,14 +634,16 @@ scene.children.forEach((obj) => { if (!obj.isMesh || !obj.physics || !obj.rigidBody) return + + // sync transform obj.position.copy(obj.rigidBody.translation()) obj.quaternion.copy(obj.rigidBody.rotation()) + + // allow this body to sleep + obj.rigidBody.setCanSleep(true) }) } - /* ---------- ASYNC YIELD ---------- */ - await yieldToBrowser() - /* ---------- NORMAL UPDATE ---------- */ if (camera) { if (controls) controls.update() @@ -650,7 +651,7 @@ const delta = clock.getDelta() Object.values(models).forEach((model) => { - if (model) model.mixer.update(delta) + model?.mixer?.update(delta) }) Object.values(lights).forEach((light) => @@ -669,7 +670,6 @@ t.camera.updateProjectionMatrix() } - // ✅ CACHE mesh lookup if (!t._displayMeshes) { t._displayMeshes = getMeshesUsingTexture(scene, t.target.texture) } @@ -702,7 +702,6 @@ loopId = requestAnimationFrame(loop) } - function resize() { const w = canvas.width; const h = canvas.height; From 04077a3892787b8f67f480c26c6345a7143a5521 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:57:01 -0600 Subject: [PATCH 20/32] Update threejsD.js --- threejsD.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/threejsD.js b/threejsD.js index b1d7c1a..a46fa1a 100644 --- a/threejsD.js +++ b/threejsD.js @@ -638,9 +638,6 @@ // sync transform obj.position.copy(obj.rigidBody.translation()) obj.quaternion.copy(obj.rigidBody.rotation()) - - // allow this body to sleep - obj.rigidBody.setCanSleep(true) }) } @@ -4906,7 +4903,8 @@ x: object.quaternion._x, y: object.quaternion._y, z: object.quaternion._z, - }); + }) + .setCanSleep(true); let colliderDesc; switch (args.collider) { From 9e97a4c6079b589aa385471205fc6573546a8de7 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:03:59 -0600 Subject: [PATCH 21/32] Update threejsD.js --- threejsD.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/threejsD.js b/threejsD.js index a46fa1a..e1eee1e 100644 --- a/threejsD.js +++ b/threejsD.js @@ -4904,7 +4904,7 @@ y: object.quaternion._y, z: object.quaternion._z, }) - .setCanSleep(true); + .setCanSleep(false); let colliderDesc; switch (args.collider) { From b3e62fdb2c9385716663611fb114bf29e1857ea7 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 08:54:06 -0600 Subject: [PATCH 22/32] hopefully fix tw --- threejsD.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/threejsD.js b/threejsD.js index e1eee1e..8aa6276 100644 --- a/threejsD.js +++ b/threejsD.js @@ -128,7 +128,7 @@ // Helper function to get current scene function getCurrentScene() { - const threeScene = runtime.ext_threeScene; + const threeScene = runtime.ext_threeScene || window.__threeScene__; if (!threeScene) return null; const currentSceneName = threeScene.currentSceneName; return currentSceneName ? threeScene.scenes[currentSceneName] : null; @@ -136,14 +136,14 @@ // Helper function to get scene by name function getSceneByName(sceneName) { - const threeScene = runtime.ext_threeScene; + const threeScene = runtime.ext_threeScene || window.__threeScene__; if (!threeScene) return null; return threeScene.scenes[sceneName]; } // Helper function to set current scene function setCurrentScene(sceneName) { - const threeScene = runtime.ext_threeScene; + const threeScene = runtime.ext_threeScene || window.__threeScene__; if (!threeScene) return; threeScene.currentSceneName = sceneName; } @@ -961,7 +961,8 @@ return JSON.stringify(names); // if objects } } - Scratch.extensions.register(new ThreeScene()); + window.__threeScene__ = new ThreeScene(); + Scratch.extensions.register(window.__threeScene__); class ThreeCameras { getInfo() { From ad2b1af54bcdbe1c3f54902801ff055e1c2033e3 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 08:58:29 -0600 Subject: [PATCH 23/32] Update threejsD.js --- threejsD.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/threejsD.js b/threejsD.js index 8aa6276..7c82609 100644 --- a/threejsD.js +++ b/threejsD.js @@ -491,6 +491,14 @@ if (!running) return; checkCanvasSize(); vm.renderer.canvas.style.visibility="visible"; + // borrowed from penguinmod's runtime core extension + RGB = "ffffff" + this.runtime.renderer.setBackgroundColor( + parseInt(RGB.slice(0, 2), 16) / 255, + parseInt(RGB.slice(2, 4), 16) / 255, + parseInt(RGB.slice(4, 6), 16) / 255, + RGB.length === 8 ? parseInt(RGB.slice(6, 8), 16) / 255 : 1 + ) running = false; if (loopId) { From 378bfb6d46697fff58e685ff42a725451a132d9e Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 08:59:34 -0600 Subject: [PATCH 24/32] Update threejsD.js --- threejsD.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/threejsD.js b/threejsD.js index 7c82609..973a3fe 100644 --- a/threejsD.js +++ b/threejsD.js @@ -491,14 +491,7 @@ if (!running) return; checkCanvasSize(); vm.renderer.canvas.style.visibility="visible"; - // borrowed from penguinmod's runtime core extension - RGB = "ffffff" - this.runtime.renderer.setBackgroundColor( - parseInt(RGB.slice(0, 2), 16) / 255, - parseInt(RGB.slice(2, 4), 16) / 255, - parseInt(RGB.slice(4, 6), 16) / 255, - RGB.length === 8 ? parseInt(RGB.slice(6, 8), 16) / 255 : 1 - ) + renderer.setBackgroundColor(255, 255, 255) running = false; if (loopId) { From ab4f1c27528c794c57590b49fee62f89d96a77a5 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 09:01:37 -0600 Subject: [PATCH 25/32] Update threejsD.js --- threejsD.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/threejsD.js b/threejsD.js index 973a3fe..a024f3f 100644 --- a/threejsD.js +++ b/threejsD.js @@ -491,7 +491,6 @@ if (!running) return; checkCanvasSize(); vm.renderer.canvas.style.visibility="visible"; - renderer.setBackgroundColor(255, 255, 255) running = false; if (loopId) { @@ -567,7 +566,7 @@ renderer.addOverlay(threeRenderer.domElement, "manual"); renderer.addOverlay(canvas, "manual"); - renderer.setBackgroundColor(1, 1, 1, 0); + renderer.setBackgroundColor(1, 1, 1); resize(); From 984e920aaef0060108a80428f0ebf58ae07ac3a9 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:23:47 -0600 Subject: [PATCH 26/32] Update threejsD.js --- threejsD.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/threejsD.js b/threejsD.js index a024f3f..ba9b857 100644 --- a/threejsD.js +++ b/threejsD.js @@ -599,6 +599,9 @@ runtime.on("STAGE_SIZE_CHANGED", () => { requestAnimationFrame(() => resize()); }); + new ResizeObserver(() => { + if (running) vm.renderer.canvas.style.visibility = "hidden"; + }).observe(canvas); checkCanvasSize(); } } From e15243409c1514123b73c4203d255f90dc0ed445 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:33:57 -0600 Subject: [PATCH 27/32] Update threejsD.js --- threejsD.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/threejsD.js b/threejsD.js index ba9b857..cc59747 100644 --- a/threejsD.js +++ b/threejsD.js @@ -599,9 +599,6 @@ runtime.on("STAGE_SIZE_CHANGED", () => { requestAnimationFrame(() => resize()); }); - new ResizeObserver(() => { - if (running) vm.renderer.canvas.style.visibility = "hidden"; - }).observe(canvas); checkCanvasSize(); } } @@ -609,6 +606,7 @@ async function startRenderLoop() { if (running) return vm.renderer.canvas.style.visibility = "hidden" + checkCanvasSize(); running = true const fixedDt = 1 / 60 From 2152f9ea61694894356a8522086f34179c3c4926 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:36:32 -0600 Subject: [PATCH 28/32] Update threejsD.js --- threejsD.js | 1 + 1 file changed, 1 insertion(+) diff --git a/threejsD.js b/threejsD.js index cc59747..b3c525e 100644 --- a/threejsD.js +++ b/threejsD.js @@ -716,6 +716,7 @@ camera.aspect = w / h; camera.updateProjectionMatrix(); } + if (running) vm.renderer.canvas.style.visibility = "hidden"; } //wait until all packages are loaded Promise.resolve(load()).then(() => { From 23a4588cab5ef941ca24e2b592d71c1604b031c7 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:38:17 -0600 Subject: [PATCH 29/32] Update threejsD.js --- threejsD.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/threejsD.js b/threejsD.js index b3c525e..a763e40 100644 --- a/threejsD.js +++ b/threejsD.js @@ -716,7 +716,11 @@ camera.aspect = w / h; camera.updateProjectionMatrix(); } - if (running) vm.renderer.canvas.style.visibility = "hidden"; + if (running) { + vm.renderer.canvas.style.visibility = "hidden" + } else { + vm.renderer.canvas.style.visibility = "visible" + } } //wait until all packages are loaded Promise.resolve(load()).then(() => { From 657698fd94900083c4376882160a7dbff27bdb70 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:53:22 -0600 Subject: [PATCH 30/32] Update threejsD.js --- threejsD.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/threejsD.js b/threejsD.js index a763e40..59fdcaa 100644 --- a/threejsD.js +++ b/threejsD.js @@ -490,7 +490,8 @@ function stopLoop() { if (!running) return; checkCanvasSize(); - vm.renderer.canvas.style.visibility="visible"; + //vm.renderer.canvas.style.visibility="visible"; + renderer.setBackgroundColor(1, 1, 1); running = false; if (loopId) { @@ -566,7 +567,7 @@ renderer.addOverlay(threeRenderer.domElement, "manual"); renderer.addOverlay(canvas, "manual"); - renderer.setBackgroundColor(1, 1, 1); + //renderer.setBackgroundColor(1, 1, 1, 0); resize(); @@ -600,12 +601,14 @@ requestAnimationFrame(() => resize()); }); checkCanvasSize(); + renderer.setBackgroundColor(1, 1, 1); } } async function startRenderLoop() { if (running) return - vm.renderer.canvas.style.visibility = "hidden" + //vm.renderer.canvas.style.visibility = "hidden" + renderer.setBackgroundColor(1, 1, 1, 0); checkCanvasSize(); running = true @@ -717,9 +720,9 @@ camera.updateProjectionMatrix(); } if (running) { - vm.renderer.canvas.style.visibility = "hidden" + //vm.renderer.canvas.style.visibility = "hidden" } else { - vm.renderer.canvas.style.visibility = "visible" + //vm.renderer.canvas.style.visibility = "visible" } } //wait until all packages are loaded From 929657c41f9b601d1938ad89922df3a9224ab371 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:31:02 -0600 Subject: [PATCH 31/32] Update threejsD.js --- threejsD.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/threejsD.js b/threejsD.js index 59fdcaa..0b3f6e4 100644 --- a/threejsD.js +++ b/threejsD.js @@ -489,9 +489,10 @@ //loops/init function stopLoop() { if (!running) return; - checkCanvasSize(); + //checkCanvasSize(); //vm.renderer.canvas.style.visibility="visible"; renderer.setBackgroundColor(1, 1, 1); + threeRenderer.domElement.style.visibility="hidden" running = false; if (loopId) { @@ -609,7 +610,8 @@ if (running) return //vm.renderer.canvas.style.visibility = "hidden" renderer.setBackgroundColor(1, 1, 1, 0); - checkCanvasSize(); + threeRenderer.domElement.style.visibility="visible" + //checkCanvasSize(); running = true const fixedDt = 1 / 60 From 0b2040c73dd5c828704d7cbad8772490419919c5 Mon Sep 17 00:00:00 2001 From: FreshPenguin112 <93781766+FreshPenguin112@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:42:45 -0600 Subject: [PATCH 32/32] Update threejsD.js --- threejsD.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/threejsD.js b/threejsD.js index 0b3f6e4..e068eb7 100644 --- a/threejsD.js +++ b/threejsD.js @@ -80,7 +80,8 @@ curves: {}, renderTargets: {}, //not the same as the global one! this one only stores textures }; - + let rect; + let raycastResult = []; function resetor(level) { @@ -256,7 +257,7 @@ ctx.drawImage(img, 0, 0, size, size); resolve(canvas.toDataURL()); // return normalized Data URI - //delete canvas? + canvas.remove(); }; img.src = uri; }); @@ -727,6 +728,8 @@ //vm.renderer.canvas.style.visibility = "visible" } } + rect = threeRenderer.domElement.getBoundingClientRect() + //wait until all packages are loaded Promise.resolve(load()).then(() => { class threejsExtension { @@ -5030,11 +5033,6 @@ let isLocked = false; let isPointerLockEnabled = false; - let rect = threeRenderer.domElement.getBoundingClientRect(); - document.addEventListener("resize", () => { - rect = threeRenderer.domElement.getBoundingClientRect(); - }); - const postMouseData = (e, isDown) => { const { movementX,