From c593606f126e641761f64cdae7735909bcacc511 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 20 Nov 2025 04:17:20 +0000 Subject: [PATCH] feat: Add custom mesh loading and presets Introduces support for loading custom OBJ files and selecting from a list of psychedelic mesh presets. Updates UI to accommodate new mesh source options. Includes new rendering logic for external meshes and animations. Co-authored-by: steinbergisaac --- v0.5/index.html | 42 +++- v0.5/src/params.js | 2 + v0.5/src/rendering/customMeshes.js | 321 +++++++++++++++++++++++++++++ v0.5/src/rendering/mesh.js | 119 +++++++++++ v0.5/src/rendering/renderer.js | 14 +- v0.5/src/ui/controls.js | 184 ++++++++++++++--- v0.5/src/ui/styles.css | 27 +++ 7 files changed, 667 insertions(+), 42 deletions(-) create mode 100644 v0.5/src/rendering/customMeshes.js diff --git a/v0.5/index.html b/v0.5/index.html index 80232b8..061f6d5 100644 --- a/v0.5/index.html +++ b/v0.5/index.html @@ -14,7 +14,31 @@

Controls

-
+
+

Mesh Source

+
+ + +
+ +
+ + +
Ready-made neon geometries with animated gradients.
+
+ +
+ + +
No mesh uploaded yet.
+
+
+ +

Geometry

@@ -97,9 +121,10 @@

Config

- - - + + + + @@ -110,10 +135,11 @@

Config

- - - - + + + + + diff --git a/v0.5/src/params.js b/v0.5/src/params.js index 667bc22..4160606 100644 --- a/v0.5/src/params.js +++ b/v0.5/src/params.js @@ -1,5 +1,7 @@ const defaultParams = { // Base geometry type + meshSource: 'procedural', // 'procedural', 'preset', or 'custom' + presetMeshId: 'neon-knot', // Default psychedelic preset pathType: 'spiral', // Path generator type crossSectionType: 'circle', // Cross-section generator type diff --git a/v0.5/src/rendering/customMeshes.js b/v0.5/src/rendering/customMeshes.js new file mode 100644 index 0000000..96dddbe --- /dev/null +++ b/v0.5/src/rendering/customMeshes.js @@ -0,0 +1,321 @@ +// Psychedelic mesh presets and custom upload helpers + +(function () { + if (typeof THREE === 'undefined') { + console.warn('THREE.js not found - custom mesh helpers disabled.'); + return; + } + + const tempColor = new THREE.Color(); + const tempVec = new THREE.Vector3(); + const palettes = { + neon: ['#ff0080', '#ff8c00', '#ffe600', '#00ffd5', '#5400ff'], + aurora: ['#00ffaa', '#00aaff', '#7c00ff', '#ff008c'], + solar: ['#ff5f6d', '#ffc371', '#5d00ff', '#00f7ff'], + cosmic: ['#00ffd2', '#8a2be2', '#ff7aff', '#2bffea'] + }; + + function samplePalette(palette, t) { + if (!palette || palette.length === 0) { + tempColor.setHSL(t, 0.8, 0.5); + return tempColor.clone(); + } + const scaled = t * (palette.length - 1); + const idx = Math.floor(scaled); + const frac = scaled - idx; + const c1 = new THREE.Color(palette[idx]); + const c2 = new THREE.Color(palette[Math.min(idx + 1, palette.length - 1)]); + return c1.lerp(c2, frac); + } + + function applyGradient(geometry, palette, axis = 'y') { + if (!geometry || !geometry.attributes || !geometry.attributes.position) return; + const position = geometry.attributes.position; + const axisValues = []; + let min = Infinity; + let max = -Infinity; + + for (let i = 0; i < position.count; i++) { + tempVec.fromBufferAttribute(position, i); + let value = tempVec[axis] || tempVec.y; + if (axis === 'radius') { + value = Math.sqrt(tempVec.x * tempVec.x + tempVec.z * tempVec.z); + } + axisValues.push(value); + if (value < min) min = value; + if (value > max) max = value; + } + + const range = Math.max(0.0001, max - min); + const colors = new Float32Array(position.count * 3); + + for (let i = 0; i < position.count; i++) { + const t = (axisValues[i] - min) / range; + const color = samplePalette(palette, t); + colors[i * 3] = color.r; + colors[i * 3 + 1] = color.g; + colors[i * 3 + 2] = color.b; + } + + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + geometry.attributes.position.needsUpdate = true; + } + + function applyRadialNoise(geometry, amplitude = 0.2, frequency = 2.0) { + if (!geometry || !geometry.attributes || !geometry.attributes.position) return; + const position = geometry.attributes.position; + for (let i = 0; i < position.count; i++) { + tempVec.fromBufferAttribute(position, i); + const distortion = + Math.sin(tempVec.x * frequency) + + Math.sin(tempVec.y * frequency * 1.3) + + Math.sin(tempVec.z * frequency * 0.7); + const factor = 1 + (distortion / 3) * amplitude; + tempVec.multiplyScalar(factor); + position.setXYZ(i, tempVec.x, tempVec.y, tempVec.z); + } + position.needsUpdate = true; + geometry.computeVertexNormals(); + } + + function normalizeGroup(root) { + const box = new THREE.Box3().setFromObject(root); + if (!isFinite(box.min.x) || !isFinite(box.max.x)) return; + const center = box.getCenter(new THREE.Vector3()); + const size = box.getSize(new THREE.Vector3()); + const largest = Math.max(size.x, size.y, size.z) || 1; + + root.traverse((child) => { + if (child.geometry && child.geometry.isBufferGeometry) { + child.geometry.translate(-center.x, -center.y, -center.z); + } + }); + + const scale = 2.6 / largest; + root.scale.setScalar(scale); + } + + function buildNeonKnot() { + const group = new THREE.Group(); + const knotGeometry = new THREE.TorusKnotGeometry(1.2, 0.35, 512, 64, 2, 3); + applyGradient(knotGeometry, palettes.neon, 'y'); + const knotMaterial = new THREE.MeshStandardMaterial({ + vertexColors: true, + metalness: 0.2, + roughness: 0.3, + emissive: new THREE.Color('#8c00ff'), + emissiveIntensity: 0.45, + transparent: true, + opacity: 0.95 + }); + const knotMesh = new THREE.Mesh(knotGeometry, knotMaterial); + group.add(knotMesh); + + const haloGeometry = new THREE.TorusGeometry(2.0, 0.015, 16, 256); + const haloMaterial = new THREE.MeshBasicMaterial({ + color: '#ffffff', + transparent: true, + opacity: 0.25, + depthWrite: false + }); + const halo = new THREE.Mesh(haloGeometry, haloMaterial); + group.add(halo); + + return { + group, + animations: [ + (time) => { + knotMesh.rotation.y = time * 0.4; + knotMesh.rotation.x = Math.sin(time * 0.3) * 0.3; + knotMaterial.emissiveIntensity = 0.45 + 0.15 * Math.sin(time * 2.0); + halo.rotation.x = time * 0.2; + } + ] + }; + } + + function buildAuroraBloom() { + const group = new THREE.Group(); + const shellGeometry = new THREE.IcosahedronGeometry(1.35, 3); + applyRadialNoise(shellGeometry, 0.25, 3.2); + applyGradient(shellGeometry, palettes.aurora, 'radius'); + const shellMaterial = new THREE.MeshStandardMaterial({ + vertexColors: true, + metalness: 0.15, + roughness: 0.35, + emissive: new THREE.Color('#00f7ff'), + emissiveIntensity: 0.35, + transparent: true, + opacity: 0.9 + }); + const shell = new THREE.Mesh(shellGeometry, shellMaterial); + group.add(shell); + + const innerGeometry = new THREE.OctahedronGeometry(0.65, 2); + applyGradient(innerGeometry, palettes.solar, 'y'); + const innerMaterial = new THREE.MeshStandardMaterial({ + vertexColors: true, + metalness: 0.4, + roughness: 0.2, + emissive: new THREE.Color('#ff00c8'), + emissiveIntensity: 0.4 + }); + const inner = new THREE.Mesh(innerGeometry, innerMaterial); + group.add(inner); + + return { + group, + animations: [ + (time) => { + shell.rotation.y = time * 0.18; + shell.rotation.x = Math.sin(time * 0.4) * 0.2; + inner.rotation.y = -time * 0.35; + inner.rotation.x = Math.cos(time * 0.5) * 0.25; + innerMaterial.emissiveIntensity = 0.4 + 0.15 * Math.sin(time * 3.0); + } + ] + }; + } + + function buildSolarRibbon() { + const group = new THREE.Group(); + const pathPoints = []; + const turns = 5; + for (let i = 0; i <= 600; i++) { + const t = (i / 600) * Math.PI * 2 * turns; + const radius = 0.8 + 0.3 * Math.sin(t * 0.5); + const x = radius * Math.cos(t); + const y = 0.5 * Math.sin(t * 0.3); + const z = radius * Math.sin(t); + pathPoints.push(new THREE.Vector3(x, y, z)); + } + const curve = new THREE.CatmullRomCurve3(pathPoints); + const ribbonGeometry = new THREE.TubeGeometry(curve, 800, 0.18, 24, false); + applyGradient(ribbonGeometry, palettes.solar, 'y'); + const ribbonMaterial = new THREE.MeshStandardMaterial({ + vertexColors: true, + metalness: 0.3, + roughness: 0.25, + emissive: new THREE.Color('#ff7b00'), + emissiveIntensity: 0.5, + transparent: true, + opacity: 0.93 + }); + const ribbon = new THREE.Mesh(ribbonGeometry, ribbonMaterial); + group.add(ribbon); + + return { + group, + animations: [ + (time) => { + ribbon.rotation.y = time * 0.25; + ribbon.rotation.z = Math.sin(time * 0.3) * 0.2; + ribbonMaterial.emissiveIntensity = 0.45 + 0.2 * Math.sin(time * 4.0); + } + ] + }; + } + + const psychedelicMeshPresets = [ + { id: 'neon-knot', name: 'Neon Torus Knot', build: buildNeonKnot }, + { id: 'aurora-bloom', name: 'Aurora Bloom', build: buildAuroraBloom }, + { id: 'solar-ribbon', name: 'Solar Ribbon', build: buildSolarRibbon } + ]; + + const customMeshState = { + group: null, + name: null, + stats: null + }; + + function computeStats(group) { + let vertices = 0; + group.traverse((child) => { + if (child.isMesh && child.geometry && child.geometry.attributes && child.geometry.attributes.position) { + vertices += child.geometry.attributes.position.count; + } + }); + return { + vertices, + faces: Math.max(1, Math.round(vertices / 3)) + }; + } + + function prepareCustomGroup(sourceGroup) { + const clone = sourceGroup.clone(true); + clone.traverse((child) => { + if (child.isMesh) { + let geom = child.geometry; + if (!geom.isBufferGeometry) { + geom = new THREE.BufferGeometry().fromGeometry(geom); + } else { + geom = geom.clone(); + } + geom.computeVertexNormals(); + applyGradient(geom, palettes.cosmic, 'radius'); + child.geometry = geom; + child.material = new THREE.MeshStandardMaterial({ + vertexColors: true, + metalness: 0.2, + roughness: 0.35, + emissive: new THREE.Color('#00ffe0'), + emissiveIntensity: 0.3, + transparent: true, + opacity: 0.9 + }); + } + }); + normalizeGroup(clone); + return clone; + } + + function createPresetMeshInstance(presetId) { + const preset = psychedelicMeshPresets.find((p) => p.id === presetId) || psychedelicMeshPresets[0]; + if (!preset) return null; + return preset.build(); + } + + function setUploadedCustomMesh(sourceGroup, name = 'custom.obj') { + if (!sourceGroup) return; + const prepared = prepareCustomGroup(sourceGroup); + customMeshState.group = prepared; + customMeshState.name = name; + customMeshState.stats = computeStats(prepared); + } + + function getUploadedCustomMeshInstance() { + if (!customMeshState.group) return null; + const group = customMeshState.group.clone(true); + return { + group, + animations: [ + (time) => { + group.rotation.y = time * 0.15; + group.rotation.x = Math.sin(time * 0.2) * 0.2; + } + ], + metadata: { + name: customMeshState.name, + stats: customMeshState.stats + } + }; + } + + function getUploadedMeshStatus() { + if (!customMeshState.group) { + return 'No mesh uploaded yet.'; + } + const stats = customMeshState.stats; + if (stats) { + const faceText = stats.faces.toLocaleString(); + return `${customMeshState.name} • ${faceText} faces`; + } + return `${customMeshState.name} ready`; + } + + window.psychedelicMeshPresets = psychedelicMeshPresets; + window.createPresetMeshInstance = createPresetMeshInstance; + window.setUploadedCustomMesh = setUploadedCustomMesh; + window.getUploadedCustomMeshInstance = getUploadedCustomMeshInstance; + window.getUploadedMeshStatus = getUploadedMeshStatus; +})(); diff --git a/v0.5/src/rendering/mesh.js b/v0.5/src/rendering/mesh.js index 55d573e..179f27c 100644 --- a/v0.5/src/rendering/mesh.js +++ b/v0.5/src/rendering/mesh.js @@ -158,11 +158,130 @@ function buildWireframeLines(params) { return { outerLines, innerLines, depthLines }; } +function adjustExternalSurfaceOpacity(group, renderStyle) { + group.traverse((child) => { + if (child.isMesh && child.material) { + const mat = child.material; + if (!mat.userData) mat.userData = {}; + if (mat.userData.baseOpacity === undefined) { + mat.userData.baseOpacity = mat.opacity !== undefined ? mat.opacity : 1; + } + const base = mat.userData.baseOpacity; + if (renderStyle === 'Wireframe') { + mat.transparent = true; + mat.opacity = Math.min(base, 0.3); + } else if (renderStyle === 'Hidden-line') { + mat.transparent = true; + mat.opacity = 0.0; + } else { + mat.opacity = base; + mat.transparent = base < 1; + } + } + }); +} + +function attachEdgeOverlay(group, params, opacity = 0.9) { + group.traverse((child) => { + if (child.isMesh && child.geometry) { + const edgesGeom = new THREE.EdgesGeometry(child.geometry, 18); + const lineMat = new THREE.LineBasicMaterial({ + color: params.outerColor || '#ffffff', + linewidth: params.lineWidth, + transparent: true, + opacity, + depthTest: true, + depthWrite: false + }); + const edges = new THREE.LineSegments(edgesGeom, lineMat); + edges.renderOrder = 2; + edges.position.set(0, 0, 0); + edges.rotation.set(0, 0, 0); + edges.scale.set(1, 1, 1); + child.add(edges); + } + }); +} + +function attachDepthOccluders(group) { + group.traverse((child) => { + if (child.isMesh && child.geometry) { + const depthMaterial = new THREE.MeshBasicMaterial({ + colorWrite: false, + depthWrite: true, + depthTest: true, + side: THREE.DoubleSide + }); + const depthMesh = new THREE.Mesh(child.geometry, depthMaterial); + depthMesh.renderOrder = 0; + depthMesh.position.set(0, 0, 0); + depthMesh.rotation.set(0, 0, 0); + depthMesh.scale.set(1, 1, 1); + child.add(depthMesh); + } + }); +} + +function createPlaceholderMesh() { + const group = new THREE.Group(); + const ico = new THREE.IcosahedronGeometry(0.8, 0); + const material = new THREE.MeshBasicMaterial({ + color: 0xff0080, + wireframe: true + }); + const mesh = new THREE.Mesh(ico, material); + group.add(mesh); + const axes = new THREE.AxesHelper(1.5); + group.add(axes); + return { + group, + animations: [ + (time) => { + mesh.rotation.y = time * 0.4; + mesh.rotation.x = Math.sin(time * 0.7) * 0.2; + } + ] + }; +} + +function renderExternalMesh(meshGroup, params) { + let instance = null; + if (params.meshSource === 'preset' && typeof createPresetMeshInstance === 'function') { + instance = createPresetMeshInstance(params.presetMeshId); + } else if (params.meshSource === 'custom' && typeof getUploadedCustomMeshInstance === 'function') { + instance = getUploadedCustomMeshInstance(); + } + if (!instance || !instance.group) { + const placeholder = createPlaceholderMesh(); + meshGroup.add(placeholder.group); + meshGroup.userData.animations = placeholder.animations; + return; + } + + const root = instance.group; + adjustExternalSurfaceOpacity(root, params.renderStyle); + if (params.renderStyle === 'Wireframe' || params.renderStyle === 'Hidden-line') { + if (params.renderStyle === 'Hidden-line') { + attachDepthOccluders(root); + } + attachEdgeOverlay(root, params, params.renderStyle === 'Wireframe' ? 0.9 : 1.0); + } + + meshGroup.add(root); + meshGroup.userData.animations = instance.animations || []; +} + // Outline creation is now in outline.js - using createOutlineLines from there function updateMesh(meshGroup, params, camera = null) { // Clear existing mesh meshGroup.clear(); + meshGroup.userData.animations = []; + + if (params.meshSource && params.meshSource !== 'procedural') { + renderExternalMesh(meshGroup, params); + return; + } if (params.renderStyle === 'Solid') { // Solid rendering: use geometry meshes diff --git a/v0.5/src/rendering/renderer.js b/v0.5/src/rendering/renderer.js index 6218135..1305389 100644 --- a/v0.5/src/rendering/renderer.js +++ b/v0.5/src/rendering/renderer.js @@ -38,10 +38,22 @@ function setupAnimationLoop(renderer, scene, sceneState, params, updateMesh) { } } - function animate() { + function animate(time = 0) { requestAnimationFrame(animate); + const seconds = time / 1000; sceneState.orbitControls.update(); + const animations = (sceneState.meshGroup.userData && sceneState.meshGroup.userData.animations) || []; + if (animations.length) { + animations.forEach((fn) => { + try { + fn(seconds); + } catch (error) { + console.warn('Mesh animation error:', error); + } + }); + } + // Update outline if camera changed and outline is enabled (throttled) checkCameraChange(); if (needsOutlineUpdate) { diff --git a/v0.5/src/ui/controls.js b/v0.5/src/ui/controls.js index 5a771a6..f45f073 100644 --- a/v0.5/src/ui/controls.js +++ b/v0.5/src/ui/controls.js @@ -5,11 +5,17 @@ function setupUI(params, sceneState, scene, gridHelper, updateMesh) { const updateMeshWithCamera = () => { updateMesh(sceneState.meshGroup, params, sceneState.currentCamera); }; - - // Generate dynamic geometry controls - generateGeometryControls(params, updateMeshWithCamera); - - // Get rendering UI elements + + // Shared DOM references + const geometryContainer = document.getElementById('geometry-controls'); + const geometrySection = document.getElementById('geometry-section'); + const meshSourceSelect = document.getElementById('meshSourceSelect'); + const presetMeshControls = document.getElementById('presetMeshControls'); + const presetMeshSelect = document.getElementById('presetMeshSelect'); + const customMeshControls = document.getElementById('customMeshControls'); + const customMeshInput = document.getElementById('customMeshInput'); + const customMeshStatus = document.getElementById('customMeshStatus'); + const renderStyleSelect = document.getElementById('renderStyleSelect'); const projectionSelect = document.getElementById('projectionSelect'); const showInnerSurfaceCheck = document.getElementById('showInnerSurfaceCheck'); @@ -25,14 +31,71 @@ function setupUI(params, sceneState, scene, gridHelper, updateMesh) { const saveConfigBtn = document.getElementById('saveConfigBtn'); const loadConfigBtn = document.getElementById('loadConfigBtn'); const loadFileInput = document.getElementById('loadFileInput'); - + + if (presetMeshSelect && Array.isArray(psychedelicMeshPresets)) { + presetMeshSelect.innerHTML = ''; + psychedelicMeshPresets.forEach((preset, index) => { + const option = document.createElement('option'); + option.value = preset.id; + option.textContent = preset.name; + presetMeshSelect.appendChild(option); + if (!params.presetMeshId && index === 0) { + params.presetMeshId = preset.id; + } + }); + if (params.presetMeshId) { + presetMeshSelect.value = params.presetMeshId; + } + } + // Helper to update value displays function updateValueDisplay(element, value, decimals = 2) { if (element) { element.textContent = value.toFixed(decimals); } } - + + function refreshGeometryControls() { + if (!geometryContainer) return; + if (params.meshSource === 'procedural') { + generateGeometryControls(params, updateMeshWithCamera); + } else { + geometryContainer.innerHTML = '
Switch to Procedural source to edit spiral geometry.
'; + } + if (geometrySection) { + geometrySection.classList.toggle('section-collapsed', params.meshSource !== 'procedural'); + } + } + + function updateCustomMeshStatus(message) { + if (!customMeshStatus) return; + if (message) { + customMeshStatus.textContent = message; + return; + } + if (typeof getUploadedMeshStatus === 'function') { + customMeshStatus.textContent = getUploadedMeshStatus(); + } else { + customMeshStatus.textContent = 'No mesh uploaded yet.'; + } + } + + function updateMeshSourceUI() { + if (!params.meshSource) { + params.meshSource = 'procedural'; + } + if (meshSourceSelect) { + meshSourceSelect.value = params.meshSource; + } + if (presetMeshControls) { + presetMeshControls.style.display = params.meshSource === 'preset' ? 'block' : 'none'; + } + if (customMeshControls) { + customMeshControls.style.display = params.meshSource === 'custom' ? 'block' : 'none'; + } + refreshGeometryControls(); + } + // Initialize UI values function syncUI() { if (renderStyleSelect) renderStyleSelect.value = params.renderStyle; @@ -47,55 +110,111 @@ function setupUI(params, sceneState, scene, gridHelper, updateMesh) { if (outerColorPicker) outerColorPicker.value = params.outerColor; if (innerColorPicker) innerColorPicker.value = params.innerColor; if (backgroundColorPicker) backgroundColorPicker.value = params.backgroundColor; + if (meshSourceSelect) meshSourceSelect.value = params.meshSource || 'procedural'; + if (presetMeshSelect && params.presetMeshId) { + presetMeshSelect.value = params.presetMeshId; + } + updateMeshSourceUI(); + updateCustomMeshStatus(); + } + + // Mesh source controls + if (meshSourceSelect) { + meshSourceSelect.addEventListener('change', (e) => { + params.meshSource = e.target.value; + updateMeshSourceUI(); + updateMeshWithCamera(); + }); } - + + if (presetMeshSelect) { + presetMeshSelect.addEventListener('change', (e) => { + params.presetMeshId = e.target.value; + if (params.meshSource === 'preset') { + updateMeshWithCamera(); + } + }); + } + + if (customMeshInput && typeof THREE !== 'undefined' && typeof THREE.OBJLoader !== 'undefined') { + customMeshInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (!file) return; + updateCustomMeshStatus(`Loading ${file.name}...`); + const reader = new FileReader(); + reader.onload = (event) => { + try { + const loader = new THREE.OBJLoader(); + const objGroup = loader.parse(event.target.result); + if (typeof setUploadedCustomMesh === 'function') { + setUploadedCustomMesh(objGroup, file.name); + } + updateCustomMeshStatus(); + if (params.meshSource === 'custom') { + updateMeshWithCamera(); + } + } catch (error) { + console.error('OBJ load error:', error); + updateCustomMeshStatus('Error loading mesh: ' + error.message); + } finally { + customMeshInput.value = ''; + } + }; + reader.onerror = () => { + updateCustomMeshStatus('Failed to read file.'); + customMeshInput.value = ''; + }; + reader.readAsText(file); + }); + } + // Rendering controls renderStyleSelect.addEventListener('change', (e) => { params.renderStyle = e.target.value; updateMeshWithCamera(); }); - + projectionSelect.addEventListener('change', (e) => { params.projection = e.target.value; updateCameraProjection(sceneState, params.projection); }); - + showInnerSurfaceCheck.addEventListener('change', (e) => { params.showInnerSurface = e.target.checked; updateMeshWithCamera(); }); - + showOutlineCheck.addEventListener('change', (e) => { params.showOutline = e.target.checked; updateMeshWithCamera(); }); - + outlineMethodSelect.addEventListener('change', (e) => { params.outlineMethod = e.target.value; updateMeshWithCamera(); }); - + lineWidthSlider.addEventListener('input', (e) => { params.lineWidth = parseFloat(e.target.value); updateValueDisplay(lineWidthValue, params.lineWidth, 1); updateMeshWithCamera(); }); - + outerColorPicker.addEventListener('change', (e) => { params.outerColor = e.target.value; updateMeshWithCamera(); }); - + innerColorPicker.addEventListener('change', (e) => { params.innerColor = e.target.value; updateMeshWithCamera(); }); - + backgroundColorPicker.addEventListener('change', (e) => { params.backgroundColor = e.target.value; scene.background = new THREE.Color(params.backgroundColor); }); - + // Export controls exportSVGBtn.addEventListener('click', () => { exportSVGBtn.disabled = true; @@ -113,7 +232,7 @@ function setupUI(params, sceneState, scene, gridHelper, updateMesh) { exportSVGBtn.textContent = 'Export SVG'; }, 1000); }); - + exportOBJBtn.addEventListener('click', () => { exportOBJBtn.disabled = true; exportOBJBtn.textContent = 'Exporting...'; @@ -123,7 +242,7 @@ function setupUI(params, sceneState, scene, gridHelper, updateMesh) { exportOBJBtn.textContent = 'Export OBJ'; }, 1000); }); - + // Config controls saveConfigBtn.addEventListener('click', () => { const config = { @@ -142,7 +261,7 @@ function setupUI(params, sceneState, scene, gridHelper, updateMesh) { projection: params.projection } }; - + const json = JSON.stringify(config, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); @@ -154,31 +273,30 @@ function setupUI(params, sceneState, scene, gridHelper, updateMesh) { document.body.removeChild(a); URL.revokeObjectURL(url); }); - + loadConfigBtn.addEventListener('click', () => { loadFileInput.click(); }); - + loadFileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; - + const reader = new FileReader(); reader.onload = (event) => { try { const config = JSON.parse(event.target.result); - + // Load geometry parameters if (config.geometry) { Object.assign(params, config.geometry); scene.background = new THREE.Color(params.backgroundColor); gridHelper.visible = params.showGrid; - // Regenerate geometry controls with new params - generateGeometryControls(params, updateMeshWithCamera); + updateMeshSourceUI(); updateMeshWithCamera(); syncUI(); } - + // Load camera state if (config.camera) { if (config.camera.projection) { @@ -186,7 +304,7 @@ function setupUI(params, sceneState, scene, gridHelper, updateMesh) { updateCameraProjection(sceneState, params.projection); projectionSelect.value = params.projection; } - + if (config.camera.position) { sceneState.currentCamera.position.set( config.camera.position.x, @@ -194,7 +312,7 @@ function setupUI(params, sceneState, scene, gridHelper, updateMesh) { config.camera.position.z ); } - + if (config.camera.target) { sceneState.orbitControls.target.set( config.camera.target.x, @@ -202,20 +320,20 @@ function setupUI(params, sceneState, scene, gridHelper, updateMesh) { config.camera.target.z ); } - + sceneState.orbitControls.update(); } } catch (error) { alert('Error loading config: ' + error.message); console.error('Load error:', error); } - + loadFileInput.value = ''; }; - + reader.readAsText(file); }); - + // Initialize UI syncUI(); } diff --git a/v0.5/src/ui/styles.css b/v0.5/src/ui/styles.css index 7efaa83..d0325ad 100644 --- a/v0.5/src/ui/styles.css +++ b/v0.5/src/ui/styles.css @@ -181,6 +181,33 @@ body { background: #e8e8e8; } +.hint-text { + font-size: 12px; + color: #777; + line-height: 1.4; +} + +#customMeshControls input[type="file"] { + width: 100%; + padding: 8px; + border: 1px dashed #bbb; + border-radius: 6px; + background: #fafafa; + cursor: pointer; +} + +.muted-message { + font-size: 13px; + color: #999; + font-style: italic; + margin: 10px 0; +} + +.section-collapsed { + opacity: 0.45; + pointer-events: none; +} + /* Scrollbar styling */ #ui-panel::-webkit-scrollbar { width: 6px;