diff --git a/README.md b/README.md index f308ced..ab24e73 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,28 @@ -# Rocksi -Rocksi is the R**obot** Bl**ock**s **Si**mulator. Acronyms are strange :) +# Rocksi (OBJ Loader Fork) -Rocksi is a robot simulator that runs entirely in (modern) web browsers with absolutely no installation. It is thus platform independent and won't care if students are working on Android tablets, iPads or laptops. The robot is programmed using the popular [Blockly](https://developers.google.com/blockly/ library, which is also used by Scratch, Niryo, and a bunch of other projects, and a custom execution routine. +> **This is a fork of [Rocksi by ndahn](https://github.com/ndahn/Rocksi); a free, browser-based robot arm simulator for educational use. All credit for the original simulator goes to the original author. This fork adds native OBJ + MTL + texture loader support as a contribution back to the educational community. -A running version can be found on **[ndahn.github.io/](https://ndahn.github.io/)** and the source code is available at **[github.com/ndahn/Rocksi](https://github.com/ndahn/Rocksi)**! Unfortunately, the version at rocksi.net is outdated and I have no control over the URL anymore. +## About the Original Rocksi +Rocksi is an incredible piece of educational software built by [ndahn](https://github.com/ndahn) for the Robokind Foundation in Germany. It runs entirely in a web browser and lets students program a 3D robot arm visually — no installation, no registration, completely free. -## License -Rocksi is distributed under the very permissive MIT license, which basically states that you can do with it whatever you want! Check out the LICENSE file for further details. However, please be aware that some of the robot models included may use different licenses. The relevant companies have permitted the use in Rocksi. +- 🌐 Official version: https://ndahn.github.io/ +- 📖 Source: https://github.com/ndahn/Rocksi +- 📜 License: MIT +## What This Fork Adds -## Building -You will need [npm](https://www.npmjs.com/) or any other package manager that can handle `package.json` files to build this project. First install the dependencies by running the following command in the project's root directory: -``` -npm install -``` +Only one focused change: the "Custom object..." loader now supports `.obj` + `.mtl` + texture images in addition to `.stl`. Everything else is identical to upstream. -Afterwards you can build the project in development or build mode by running -``` -npm run [dev|build] -``` +### Why This Fork Exists -This will also start a local parcel webserver serving Rocksi. Especially when running in `build` mode, check the `scripts` section in package.json as it may contain some settings that influence the build process. +After noticing that the custom object loader only supported STL files (which lack color/texture data), I extended it to support OBJ with full material support, then submitted the change upstream as a pull request. +**If the upstream PR is merged, this version url will be removed.** -## Navigating the code -You can find the main entry point in `src/index.js`. From there, anything related to the 3d-side (e.g. viewport, robot model, inverse kinematics, etc.) can be found in `src/simulation/`, and the entrypoint for that directory is `scene.js`. +## Accessibility +🔗 https://YOUR-USERNAME.github.io/Rocksi/ -If you are interested in the robot's programming side, you should have a look at `src/editor/blockly.js`. The custom commands for the robot can be found in `src/editor/blocks/` and the functions for turning them into runnable code in `src/editor/generators/`. - -Finally, if you want to see how the robot executes the commands it receives, you should look at `src/simulation/simulator.js`. - -Feel free to drop me a message if you need further help! :) - - -## Motivation -Robots are one of the corner stones of future industries and technological development. The recent years' advancements in control, intuivity and sensitivity has made these beautiful yet complex machines much more accessible. Modern robots manage to hide much of their complexity, to the degree that even non-specialists and children can handle them with ease. - -Despite these major advancements, to the average person robots stay elusive and incomrpehensible, almost mystical. This is for two reasons: -* they are expensive and hard to come by -* the knowledge and tech that makes them move is non-trivial and inaccessible - -To this end many schools throughout the world have started adding robotics to their curricula. However, their expensive nature gave rise to a new problem: at classes of 20-40 students, how can every student work with the robot if the school can't afford to spend hundreds of thousands of euros? This is where Rocksi comes in! - - -## Further Reading -If you speak German and want to learn more about robotics, I want to highlight the free **Roboterführerschein** (robots driving license) on [robotikschulungen.de](https://robotikschulungen.de), which I co-developed. Hint: the Niryo courses are by me as well :) +## Credits +- **Original simulator**: ndahn and contributors (MIT License) +- **OBJ loader extens \ No newline at end of file diff --git a/src/simulator/objects/createObjects.js b/src/simulator/objects/createObjects.js index a9f5e09..dfa5c7e 100644 --- a/src/simulator/objects/createObjects.js +++ b/src/simulator/objects/createObjects.js @@ -1,5 +1,9 @@ /** This script handeles the 3D object addition + +MODIFIED: Added native OBJ + MTL loading support alongside existing STL loader. + Original STL behavior preserved. New OBJ loader uses Three.js's + OBJLoader and MTLLoader for full geometry + material support. **/ import { BoxBufferGeometry, MeshPhongMaterial, @@ -18,6 +22,9 @@ import { Vec3 } from 'cannon-es'; import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'; import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; +// === NEW: OBJ + MTL loader imports === +import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; +import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'; import * as Blockly from 'blockly/core'; @@ -117,7 +124,9 @@ export function addGeometry(simObject) { break; case 'custom': - loadUserSTL(simObject); //Body creation etc in event callback + // === MODIFIED: Use the new universal user file loader === + // Accepts .stl, .obj (with optional .mtl + textures) + loadUserFile(simObject); break; default: @@ -158,22 +167,42 @@ function loadAssetSTL(simObject, assetPath, shape) { }); } -//Loads a 3D object added by the user. -function loadUserSTL(simObject) { +// ============================================================================= +// === NEW: Universal user file loader - routes .stl / .obj to correct loader == +// ============================================================================= +// Supports multi-file selection so users can pick .obj + .mtl + textures +// together. File extension is used to determine which loader to dispatch. +function loadUserFile(simObject) { const upload = document.createElement('input'); - const reader = new FileReader(); - - reader.addEventListener('load', (event) => { - const data = event.target.result; - loadSTL(simObject, data); - }); - upload.setAttribute('type', 'file'); - upload.setAttribute('accept', '.stl'); + upload.setAttribute('accept', '.stl,.obj,.mtl,.png,.jpg,.jpeg,.bmp'); + upload.setAttribute('multiple', 'true'); // allow .obj + .mtl + textures + upload.onchange = (fileSelectedEvent) => { try { - const file = fileSelectedEvent.target.files[0]; - reader.readAsArrayBuffer(file); + const files = Array.from(fileSelectedEvent.target.files); + if (files.length === 0) return; + + // Find the primary geometry file (.stl or .obj) + const stlFile = files.find(f => f.name.toLowerCase().endsWith('.stl')); + const objFile = files.find(f => f.name.toLowerCase().endsWith('.obj')); + + if (stlFile) { + console.log('[Rocksi-OBJ-Extension] Loading STL:', stlFile.name); + const reader = new FileReader(); + reader.addEventListener('load', (event) => { + loadSTL(simObject, event.target.result); + }); + reader.readAsArrayBuffer(stlFile); + } + else if (objFile) { + console.log('[Rocksi-OBJ-Extension] Loading OBJ:', objFile.name); + loadUserOBJ(simObject, files, objFile); + } + else { + console.error('[Rocksi-OBJ-Extension] No .stl or .obj file selected.'); + alert('Please select an .stl or .obj file (you can also include .mtl and texture files).'); + } } catch (e) { console.log(e); } } @@ -182,39 +211,130 @@ function loadUserSTL(simObject) { document.body.removeChild(upload); } -//Loads a stl into a simObject -function loadSTL(simObject, data){ - const geometry = new STLLoader().parse( data ); - const material = new MeshPhongMaterial({color: simObject.colour}); - const mesh = new Mesh(); - const size = new Vector3(); - - mesh.geometry = geometry; - mesh.material = material; +function loadUserOBJ(simObject, allFiles, objFile) { + // Find optional .mtl file and any image textures the user uploaded + const mtlFile = allFiles.find(f => f.name.toLowerCase().endsWith('.mtl')); + const textureFiles = allFiles.filter(f => + /\.(png|jpe?g|bmp)$/i.test(f.name) + ); + const fileMap = {}; + allFiles.forEach(f => { + fileMap[f.name] = URL.createObjectURL(f); + }); - const sf = simObject.scaleFactor; - mesh.scale.set(sf, sf, sf); + const manager = new LoadingManager(); + manager.setURLModifier((url) => { + const filename = url.split('/').pop().split('\\').pop(); + if (fileMap[filename]) { + return fileMap[filename]; + } + return url; + }); - mesh.geometry.computeBoundingBox(); - mesh.geometry.center(); + const objReader = new FileReader(); + objReader.addEventListener('load', (event) => { + const objText = event.target.result; + + if (mtlFile) { + console.log('[Rocksi-OBJ-Extension] Loading MTL:', mtlFile.name); + const mtlReader = new FileReader(); + mtlReader.addEventListener('load', (mtlEvent) => { + const mtlText = mtlEvent.target.result; + const mtlLoader = new MTLLoader(manager); + const materials = mtlLoader.parse(mtlText); + materials.preload(); + + const objLoader = new OBJLoader(manager); + objLoader.setMaterials(materials); + const obj = objLoader.parse(objText); + attachOBJToSimObject(simObject, obj); + }); + mtlReader.readAsText(mtlFile); + } + else { + console.log('[Rocksi-OBJ-Extension] No MTL provided, using default material.'); + const objLoader = new OBJLoader(manager); + const obj = objLoader.parse(objText); + + const defaultMaterial = new MeshPhongMaterial({ color: simObject.color }); + obj.traverse((child) => { + if (child.isMesh) { + child.material = defaultMaterial; + } + }); + attachOBJToSimObject(simObject, obj); + } + }); + objReader.readAsText(objFile); +} - const tmpBox = new Box3().setFromObject(mesh); - tmpBox.getSize(size); +function attachOBJToSimObject(simObject, obj) { + const size = new Vector3(); + + obj.traverse((child) => { + if (child.isMesh) { + const convertMaterial = (mat) => { + if (mat && mat.isMeshPhongMaterial) return mat; + return new MeshPhongMaterial({ + color: (mat && mat.color) ? mat.color : 0xcccccc, + map: (mat && mat.map) ? mat.map : null, + normalMap: (mat && mat.normalMap) ? mat.normalMap : null, + specularMap: (mat && mat.specularMap) ? mat.specularMap : null, + side: 2, // DoubleSide + }); + }; + + if (Array.isArray(child.material)) { + child.material = child.material.map(convertMaterial); + } else { + child.material = convertMaterial(child.material); + } + child.visible = true; + child.frustumCulled = false; + } + }); + + const rawBox = new Box3().setFromObject(obj); + rawBox.getSize(size); + console.log('[Rocksi-OBJ-Extension v3] Raw size:', size.toArray().map(n => n.toFixed(3))); + + const TARGET_SIZE = 1.0; + const maxDim = Math.max(size.x, size.y, size.z); + + if (maxDim > 0 && isFinite(maxDim)) { + const sf = TARGET_SIZE / maxDim; + obj.scale.set(sf, sf, sf); + simObject.scaleFactor = sf; + console.log('[Rocksi-OBJ-Extension v3] Scale factor:', sf.toFixed(4)); + } else { + simObject.scaleFactor = 1; + console.warn('[Rocksi-OBJ-Extension v3] Could not scale - maxDim was', maxDim); + } + + obj.updateMatrixWorld(true); + const scaledBox = new Box3().setFromObject(obj); + const scaledCenter = new Vector3(); + scaledBox.getCenter(scaledCenter); + scaledBox.getSize(size); + + obj.position.x -= scaledCenter.x; + obj.position.y -= scaledCenter.y; + obj.position.z -= scaledBox.min.z; + + console.log('[Rocksi-OBJ-Extension v3] Final size:', size.toArray().map(n => n.toFixed(3))); + console.log('[Rocksi-OBJ-Extension v3] Position offset applied'); + simObject.size.copy(size); - - simObject.add(mesh); - + + simObject.add(obj); simObject.bodyShape = 'box'; - simObject.createBody(5, 2, 0.1); - + simObject.createBody(5, 2, 0.1); simObject.setGrippable(); simObject.setGripAxes(); - simObject.render(); } -//Create a new SimObject and add a 3D model to the simObject export function addSimObject(blockUUID, fieldValues, color, shape, scale) { let simObject = new SimObject; @@ -461,4 +581,4 @@ export function randomColour() { colour += hexDigits[Math.floor(Math.random() * 16)]; } return colour; -} +} \ No newline at end of file