Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| st.set_page_config(page_title="L-Grammar 3D Assembly Game", layout="wide") | |
| st.title("L-Grammar 3D Assembly Game") | |
| st.write("An interactive 3D game using L-Grammar to assemble primitive components") | |
| # Create a custom HTML component to embed Three.js | |
| html_code = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>L-Grammar 3D Assemblies</title> | |
| <style> | |
| body { margin: 0; overflow: hidden; } | |
| canvas { display: block; } | |
| .ui-panel { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| background: rgba(0,0,0,0.6); | |
| padding: 10px; | |
| border-radius: 5px; | |
| color: white; | |
| font-family: Arial, sans-serif; | |
| } | |
| .ui-panel button { | |
| margin: 5px; | |
| padding: 5px 10px; | |
| background: #555; | |
| color: white; | |
| border: none; | |
| border-radius: 3px; | |
| cursor: pointer; | |
| } | |
| .ui-panel button:hover { | |
| background: #777; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="ui-panel"> | |
| <h3>L-Grammar Controls</h3> | |
| <div id="rules"></div> | |
| <button id="generate">Generate New Assembly</button> | |
| <button id="reset">Reset View</button> | |
| <div id="stats"> | |
| <p>Parts: <span id="parts-count">0</span></p> | |
| <p>Complexity: <span id="complexity">0</span></p> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script> | |
| <script> | |
| // L-Grammar System for 3D Assemblies | |
| class LGrammarSystem { | |
| constructor() { | |
| this.axiom = "F"; | |
| this.rules = { | |
| "F": ["F[+F]F", "F[-F]F", "F[+F][-F]F", "FF"], | |
| "+": ["+", "++"], | |
| "-": ["-", "--"], | |
| "[": ["["], | |
| "]": ["]"] | |
| }; | |
| this.angle = Math.PI / 6; | |
| this.iterations = 3; | |
| this.currentString = this.axiom; | |
| } | |
| generate() { | |
| let result = this.axiom; | |
| for (let i = 0; i < this.iterations; i++) { | |
| let newString = ""; | |
| for (let j = 0; j < result.length; j++) { | |
| const char = result[j]; | |
| if (this.rules[char]) { | |
| const possibleRules = this.rules[char]; | |
| const selectedRule = possibleRules[Math.floor(Math.random() * possibleRules.length)]; | |
| newString += selectedRule; | |
| } else { | |
| newString += char; | |
| } | |
| } | |
| result = newString; | |
| } | |
| this.currentString = result; | |
| return result; | |
| } | |
| interpret(scene) { | |
| const stack = []; | |
| let position = new THREE.Vector3(0, 0, 0); | |
| let direction = new THREE.Vector3(0, 1, 0); | |
| let right = new THREE.Vector3(1, 0, 0); | |
| let up = new THREE.Vector3(0, 0, 1); | |
| // Clear previous objects | |
| while(scene.children.length > 0) { | |
| const object = scene.children[0]; | |
| if (object.type === "DirectionalLight" || | |
| object.type === "AmbientLight" || | |
| object.type === "PointLight") { | |
| scene.children.shift(); | |
| } else { | |
| scene.remove(object); | |
| } | |
| } | |
| let partCount = 0; | |
| for (let i = 0; i < this.currentString.length; i++) { | |
| const char = this.currentString[i]; | |
| switch(char) { | |
| case 'F': | |
| // Create a part (cylinder or box) | |
| const partType = Math.random() > 0.5 ? 'cylinder' : 'box'; | |
| const length = 2 + Math.random() * 3; | |
| const width = 0.3 + Math.random() * 0.5; | |
| let geometry, material, part; | |
| if (partType === 'cylinder') { | |
| geometry = new THREE.CylinderGeometry(width, width, length, 8); | |
| material = new THREE.MeshPhongMaterial({ | |
| color: new THREE.Color(Math.random(), Math.random(), Math.random()), | |
| shininess: 30 | |
| }); | |
| part = new THREE.Mesh(geometry, material); | |
| } else { | |
| geometry = new THREE.BoxGeometry(width, length, width); | |
| material = new THREE.MeshPhongMaterial({ | |
| color: new THREE.Color(Math.random(), Math.random(), Math.random()), | |
| shininess: 30 | |
| }); | |
| part = new THREE.Mesh(geometry, material); | |
| } | |
| // Position and orient the part | |
| const midPoint = position.clone().add(direction.clone().multiplyScalar(length/2)); | |
| part.position.copy(midPoint); | |
| // Calculate the rotation to align with direction | |
| const defaultDir = new THREE.Vector3(0, 1, 0); | |
| part.quaternion.setFromUnitVectors(defaultDir, direction.clone().normalize()); | |
| scene.add(part); | |
| partCount++; | |
| // Move forward | |
| position.add(direction.clone().multiplyScalar(length)); | |
| break; | |
| case '+': | |
| // Rotate right around the up vector | |
| const rotationMatrixPlus = new THREE.Matrix4().makeRotationAxis(up, this.angle); | |
| direction.applyMatrix4(rotationMatrixPlus); | |
| right.applyMatrix4(rotationMatrixPlus); | |
| break; | |
| case '-': | |
| // Rotate left around the up vector | |
| const rotationMatrixMinus = new THREE.Matrix4().makeRotationAxis(up, -this.angle); | |
| direction.applyMatrix4(rotationMatrixMinus); | |
| right.applyMatrix4(rotationMatrixMinus); | |
| break; | |
| case '&': | |
| // Pitch down around the right vector | |
| const rotationMatrixPitchDown = new THREE.Matrix4().makeRotationAxis(right, this.angle); | |
| direction.applyMatrix4(rotationMatrixPitchDown); | |
| up.applyMatrix4(rotationMatrixPitchDown); | |
| break; | |
| case '^': | |
| // Pitch up around the right vector | |
| const rotationMatrixPitchUp = new THREE.Matrix4().makeRotationAxis(right, -this.angle); | |
| direction.applyMatrix4(rotationMatrixPitchUp); | |
| up.applyMatrix4(rotationMatrixPitchUp); | |
| break; | |
| case '\\': | |
| // Roll clockwise around forward vector | |
| const rotationMatrixRollCW = new THREE.Matrix4().makeRotationAxis(direction, this.angle); | |
| right.applyMatrix4(rotationMatrixRollCW); | |
| up.applyMatrix4(rotationMatrixRollCW); | |
| break; | |
| case '/': | |
| // Roll counter-clockwise around forward vector | |
| const rotationMatrixRollCCW = new THREE.Matrix4().makeRotationAxis(direction, -this.angle); | |
| right.applyMatrix4(rotationMatrixRollCCW); | |
| up.applyMatrix4(rotationMatrixRollCCW); | |
| break; | |
| case '[': | |
| // Push current state onto stack | |
| stack.push({ | |
| position: position.clone(), | |
| direction: direction.clone(), | |
| right: right.clone(), | |
| up: up.clone() | |
| }); | |
| break; | |
| case ']': | |
| // Pop state from stack | |
| if (stack.length > 0) { | |
| const state = stack.pop(); | |
| position = state.position; | |
| direction = state.direction; | |
| right = state.right; | |
| up = state.up; | |
| } | |
| break; | |
| } | |
| } | |
| // Create a connector at each joint | |
| for (let i = 0; i < stack.length; i++) { | |
| const jointPosition = stack[i].position; | |
| const jointGeometry = new THREE.SphereGeometry(0.5, 8, 8); | |
| const jointMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xFFD700, | |
| shininess: 50 | |
| }); | |
| const joint = new THREE.Mesh(jointGeometry, jointMaterial); | |
| joint.position.copy(jointPosition); | |
| scene.add(joint); | |
| partCount++; | |
| } | |
| document.getElementById('parts-count').textContent = partCount; | |
| document.getElementById('complexity').textContent = this.currentString.length; | |
| return partCount; | |
| } | |
| } | |
| // Three.js setup | |
| let scene, camera, renderer; | |
| let lgrammar; | |
| let controls; | |
| function init() { | |
| // Scene setup | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x111122); | |
| // Camera setup | |
| const width = window.innerWidth; | |
| const height = window.innerHeight; | |
| camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); | |
| camera.position.set(0, 0, 30); | |
| camera.lookAt(0, 0, 0); | |
| // Renderer setup | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(width, height); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| document.body.appendChild(renderer.domElement); | |
| // Lighting | |
| const ambientLight = new THREE.AmbientLight(0x404040); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(1, 1, 1); | |
| scene.add(directionalLight); | |
| const pointLight = new THREE.PointLight(0xffffff, 0.5); | |
| pointLight.position.set(-10, 10, 10); | |
| scene.add(pointLight); | |
| // OrbitControls | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| // Initialize L-Grammar system | |
| lgrammar = new LGrammarSystem(); | |
| generateNewAssembly(); | |
| // Event listeners | |
| window.addEventListener('resize', onWindowResize); | |
| document.getElementById('generate').addEventListener('click', generateNewAssembly); | |
| document.getElementById('reset').addEventListener('click', resetView); | |
| // Start animation loop | |
| animate(); | |
| } | |
| function generateNewAssembly() { | |
| lgrammar.iterations = Math.floor(2 + Math.random() * 3); | |
| lgrammar.angle = (Math.PI / 8) + (Math.random() * Math.PI / 4); | |
| lgrammar.generate(); | |
| lgrammar.interpret(scene); | |
| resetView(); | |
| } | |
| function resetView() { | |
| camera.position.set(0, 0, 30); | |
| camera.lookAt(0, 0, 0); | |
| controls.reset(); | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| // Add OrbitControls (simplified version) | |
| THREE.OrbitControls = function(camera, domElement) { | |
| this.camera = camera; | |
| this.domElement = domElement; | |
| this.enableDamping = false; | |
| this.dampingFactor = 0.05; | |
| // API | |
| this.target = new THREE.Vector3(); | |
| // Internal state | |
| this.rotateStart = new THREE.Vector2(); | |
| this.rotateEnd = new THREE.Vector2(); | |
| this.rotateDelta = new THREE.Vector2(); | |
| this.panStart = new THREE.Vector2(); | |
| this.panEnd = new THREE.Vector2(); | |
| this.panDelta = new THREE.Vector2(); | |
| this.dollyStart = new THREE.Vector2(); | |
| this.dollyEnd = new THREE.Vector2(); | |
| this.dollyDelta = new THREE.Vector2(); | |
| this.state = { | |
| NONE: -1, | |
| ROTATE: 0, | |
| DOLLY: 1, | |
| PAN: 2 | |
| }; | |
| this.currentState = this.state.NONE; | |
| // Set up event listeners | |
| this.domElement.addEventListener('mousedown', onMouseDown.bind(this)); | |
| this.domElement.addEventListener('mousemove', onMouseMove.bind(this)); | |
| this.domElement.addEventListener('mouseup', onMouseUp.bind(this)); | |
| this.domElement.addEventListener('wheel', onMouseWheel.bind(this)); | |
| function onMouseDown(event) { | |
| event.preventDefault(); | |
| if (event.button === 0) { | |
| this.currentState = this.state.ROTATE; | |
| this.rotateStart.set(event.clientX, event.clientY); | |
| } else if (event.button === 1) { | |
| this.currentState = this.state.DOLLY; | |
| this.dollyStart.set(event.clientX, event.clientY); | |
| } else if (event.button === 2) { | |
| this.currentState = this.state.PAN; | |
| this.panStart.set(event.clientX, event.clientY); | |
| } | |
| } | |
| function onMouseMove(event) { | |
| event.preventDefault(); | |
| if (this.currentState === this.state.ROTATE) { | |
| this.rotateEnd.set(event.clientX, event.clientY); | |
| this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart); | |
| const element = this.domElement; | |
| // Rotate | |
| const rotSpeed = 0.002; | |
| const thetaX = 2 * Math.PI * this.rotateDelta.x / element.clientWidth * rotSpeed; | |
| const thetaY = 2 * Math.PI * this.rotateDelta.y / element.clientHeight * rotSpeed; | |
| // Calculate camera position relative to target | |
| const offset = new THREE.Vector3().subVectors(this.camera.position, this.target); | |
| // Rotate around target | |
| const qx = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), thetaX); | |
| offset.applyQuaternion(qx); | |
| const qy = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), thetaY); | |
| offset.applyQuaternion(qy); | |
| // Update camera position | |
| this.camera.position.copy(this.target).add(offset); | |
| this.camera.lookAt(this.target); | |
| this.rotateStart.copy(this.rotateEnd); | |
| } else if (this.currentState === this.state.DOLLY) { | |
| this.dollyEnd.set(event.clientX, event.clientY); | |
| this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart); | |
| // Zoom speed | |
| const zoomSpeed = 0.01; | |
| // Calculate zoom factor | |
| const factor = 1.0 + this.dollyDelta.y * zoomSpeed; | |
| // Apply zoom | |
| const offset = new THREE.Vector3().subVectors(this.camera.position, this.target); | |
| offset.multiplyScalar(factor); | |
| this.camera.position.copy(this.target).add(offset); | |
| this.dollyStart.copy(this.dollyEnd); | |
| } else if (this.currentState === this.state.PAN) { | |
| this.panEnd.set(event.clientX, event.clientY); | |
| this.panDelta.subVectors(this.panEnd, this.panStart); | |
| // Pan speed | |
| const panSpeed = 0.001; | |
| // Calculate pan offset | |
| const distance = this.camera.position.distanceTo(this.target); | |
| const panX = -this.panDelta.x * distance * panSpeed; | |
| const panY = this.panDelta.y * distance * panSpeed; | |
| // Pan camera | |
| const v = new THREE.Vector3(); | |
| v.copy(this.camera.position).sub(this.target); | |
| v.cross(this.camera.up).normalize().multiplyScalar(panX); | |
| const vpan = new THREE.Vector3().copy(this.camera.up).normalize().multiplyScalar(panY); | |
| v.add(vpan); | |
| this.camera.position.add(v); | |
| this.target.add(v); | |
| this.panStart.copy(this.panEnd); | |
| } | |
| } | |
| function onMouseUp(event) { | |
| event.preventDefault(); | |
| this.currentState = this.state.NONE; | |
| } | |
| function onMouseWheel(event) { | |
| event.preventDefault(); | |
| // Zoom speed | |
| const zoomSpeed = 0.05; | |
| // Calculate zoom factor (based on scroll direction) | |
| const delta = Math.sign(event.deltaY); | |
| const factor = 1.0 - delta * zoomSpeed; | |
| // Apply zoom | |
| const offset = new THREE.Vector3().subVectors(this.camera.position, this.target); | |
| offset.multiplyScalar(factor); | |
| this.camera.position.copy(this.target).add(offset); | |
| } | |
| this.update = function() { | |
| // For damping, not implemented in this simplified version | |
| }; | |
| this.reset = function() { | |
| this.target.set(0, 0, 0); | |
| this.camera.position.set(0, 0, 30); | |
| this.camera.lookAt(this.target); | |
| }; | |
| }; | |
| // Initialize the application | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # Display the Three.js application in an iframe | |
| components.html(html_code, height=800) | |
| st.sidebar.title("Game Instructions") | |
| st.sidebar.write(""" | |
| ## L-Grammar 3D Assembly Game | |
| This game uses L-system grammars to procedurally generate 3D assemblies of parts. | |
| ### How it works: | |
| 1. The system starts with a simple axiom and applies transformation rules iteratively | |
| 2. The resulting string of characters defines the 3D structure | |
| 3. Parts are created and connected based on these rules | |
| ### Controls: | |
| - **Left-click + drag**: Rotate the view | |
| - **Right-click + drag**: Pan the view | |
| - **Mouse wheel**: Zoom in/out | |
| - **Generate New Assembly**: Creates a new random structure | |
| - **Reset View**: Returns to the default camera position | |
| ### L-Grammar Commands: | |
| - F: Move forward and create a part | |
| - +/-: Rotate left/right | |
| - [: Push current state onto stack (branch) | |
| - ]: Pop state from stack (end branch) | |
| Have fun exploring the procedurally generated 3D structures! | |
| """) | |
| st.sidebar.markdown("---") | |
| st.sidebar.write("Made with Three.js and Streamlit") |