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;