|
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") |
|
|
|
|
|
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> |
|
""" |
|
|
|
|
|
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") |