diff --git a/README.md b/README.md
index e2677c8..bb912ef 100644
--- a/README.md
+++ b/README.md
@@ -38,5 +38,5 @@ python -m http.server 8000
## Project Structure
-- `v0.5/` - Current version (plain HTML/JS, no build step required)
+- `v0.5/` - Current version (plain HTML/JS, no build step required). Includes the new geometry mode toggle so you can switch between parametric tubes (spirals, helices, etc.) and editable primitive shapes such as spheres, cubes, pentagonal/decagonal prisms, discs, and dodecahedrons. Primitive shapes support real-time twist, taper, bend, noise, and inflate deformations directly in the UI.
- `v0.1/`, `v0.2/` - Previous versions with build systems
diff --git a/v0.5/index.html b/v0.5/index.html
index 80232b8..8386e50 100644
--- a/v0.5/index.html
+++ b/v0.5/index.html
@@ -105,6 +105,7 @@
Config
+
diff --git a/v0.5/src/geometry/primitives.js b/v0.5/src/geometry/primitives.js
new file mode 100644
index 0000000..6bc6f46
--- /dev/null
+++ b/v0.5/src/geometry/primitives.js
@@ -0,0 +1,222 @@
+// Primitive geometry generators and deformation helpers
+
+const primitiveGenerators = {
+ 'circle-disc': {
+ name: 'Circle / Disc',
+ buildGeometry: (params) => {
+ const radius = params.primitiveRadius || 1.5;
+ const segments = Math.max(3, Math.floor(params.primitiveSegments || 48));
+ return new THREE.CircleGeometry(radius, segments);
+ },
+ params: {
+ primitiveRadius: { type: 'range', min: 0.2, max: 4, step: 0.01, label: 'Radius' },
+ primitiveSegments: { type: 'range', min: 8, max: 160, step: 1, label: 'Segments' }
+ }
+ },
+ 'sphere': {
+ name: 'Sphere',
+ buildGeometry: (params) => {
+ const radius = params.primitiveRadius || 1.8;
+ const widthSegs = Math.max(8, Math.floor(params.primitiveWidthSegments || 32));
+ const heightSegs = Math.max(6, Math.floor(params.primitiveHeightSegments || 24));
+ return new THREE.SphereGeometry(radius, widthSegs, heightSegs);
+ },
+ params: {
+ primitiveRadius: { type: 'range', min: 0.2, max: 4, step: 0.01, label: 'Radius' },
+ primitiveWidthSegments: { type: 'range', min: 8, max: 160, step: 1, label: 'Horizontal Segments' },
+ primitiveHeightSegments: { type: 'range', min: 6, max: 160, step: 1, label: 'Vertical Segments' }
+ }
+ },
+ 'cube': {
+ name: 'Cube',
+ buildGeometry: (params) => {
+ const size = params.primitiveBoxSize || 2;
+ const subdivisions = Math.max(1, Math.floor(params.primitiveBoxSubdivisions || 1));
+ return new THREE.BoxGeometry(size, size, size, subdivisions, subdivisions, subdivisions);
+ },
+ params: {
+ primitiveBoxSize: { type: 'range', min: 0.2, max: 4, step: 0.01, label: 'Size' },
+ primitiveBoxSubdivisions: { type: 'range', min: 1, max: 12, step: 1, label: 'Subdivisions' }
+ }
+ },
+ 'pentagonal-prism': {
+ name: 'Pentagonal Prism',
+ buildGeometry: (params) => {
+ const radius = params.primitiveRadius || 1.6;
+ const height = params.primitiveHeight || 2.5;
+ const stacks = Math.max(1, Math.floor(params.primitiveStackSegments || 1));
+ return new THREE.CylinderGeometry(radius, radius, height, 5, stacks, false);
+ },
+ params: {
+ primitiveRadius: { type: 'range', min: 0.2, max: 4, step: 0.01, label: 'Radius' },
+ primitiveHeight: { type: 'range', min: 0.2, max: 5, step: 0.01, label: 'Height' },
+ primitiveStackSegments: { type: 'range', min: 1, max: 20, step: 1, label: 'Height Segments' }
+ }
+ },
+ 'decagonal-prism': {
+ name: 'Decagonal Prism (Decahedron)',
+ buildGeometry: (params) => {
+ const radiusTop = params.primitiveTopRadius || params.primitiveRadius || 1.4;
+ const radiusBottom = params.primitiveBottomRadius || params.primitiveRadius || 1.4;
+ const height = params.primitiveHeight || 2.2;
+ const stacks = Math.max(1, Math.floor(params.primitiveStackSegments || 1));
+ return new THREE.CylinderGeometry(radiusTop, radiusBottom, height, 10, stacks, false);
+ },
+ params: {
+ primitiveTopRadius: { type: 'range', min: 0.2, max: 4, step: 0.01, label: 'Top Radius' },
+ primitiveBottomRadius: { type: 'range', min: 0.2, max: 4, step: 0.01, label: 'Bottom Radius' },
+ primitiveHeight: { type: 'range', min: 0.2, max: 5, step: 0.01, label: 'Height' },
+ primitiveStackSegments: { type: 'range', min: 1, max: 20, step: 1, label: 'Height Segments' }
+ }
+ },
+ 'dodecahedron': {
+ name: 'Dodecahedron',
+ buildGeometry: (params) => {
+ const radius = params.primitiveRadius || 1.8;
+ const detail = Math.max(0, Math.floor(params.primitiveDetail || 0));
+ return new THREE.DodecahedronGeometry(radius, detail);
+ },
+ params: {
+ primitiveRadius: { type: 'range', min: 0.2, max: 4, step: 0.01, label: 'Radius' },
+ primitiveDetail: { type: 'range', min: 0, max: 4, step: 1, label: 'Detail' }
+ }
+ }
+};
+
+const primitiveDeformers = {
+ primitiveTwistAmount: { type: 'range', min: -4, max: 4, step: 0.01, label: 'Twist (turns)' },
+ primitiveTaperAmount: { type: 'range', min: -1, max: 1, step: 0.01, label: 'Taper' },
+ primitiveBendAmount: { type: 'range', min: -Math.PI, max: Math.PI, step: 0.01, label: 'Bend (radians)' },
+ primitiveNoiseAmplitude: { type: 'range', min: 0, max: 1, step: 0.01, label: 'Noise Amplitude' },
+ primitiveNoiseFrequency: { type: 'range', min: 0.5, max: 8, step: 0.1, label: 'Noise Frequency' },
+ primitiveInflate: { type: 'range', min: -0.8, max: 1.5, step: 0.01, label: 'Inflate' }
+};
+
+function getPrimitiveGenerator(type) {
+ return primitiveGenerators[type] || primitiveGenerators['sphere'];
+}
+
+function needsPrimitiveDeformation(params) {
+ return (
+ (params.primitiveTwistAmount && Math.abs(params.primitiveTwistAmount) > 0.0001) ||
+ (params.primitiveTaperAmount && Math.abs(params.primitiveTaperAmount) > 0.0001) ||
+ (params.primitiveBendAmount && Math.abs(params.primitiveBendAmount) > 0.0001) ||
+ (params.primitiveNoiseAmplitude && Math.abs(params.primitiveNoiseAmplitude) > 0.0001) ||
+ (params.primitiveInflate && Math.abs(params.primitiveInflate) > 0.0001)
+ );
+}
+
+function pseudoNoise3(x, y, z, frequency) {
+ const f = frequency;
+ return Math.sin(f * x + 0.53) * Math.cos(f * y - 1.21) * Math.sin(f * z + 2.17);
+}
+
+function applyPrimitiveDeformations(geometry, params) {
+ if (!geometry || !geometry.attributes || !geometry.attributes.position || !needsPrimitiveDeformation(params)) {
+ return;
+ }
+
+ geometry.computeBoundingBox();
+ const bbox = geometry.boundingBox || new THREE.Box3(
+ new THREE.Vector3(-1, -1, -1),
+ new THREE.Vector3(1, 1, 1)
+ );
+ const center = new THREE.Vector3();
+ bbox.getCenter(center);
+ const size = new THREE.Vector3();
+ bbox.getSize(size);
+ const halfSize = size.clone().multiplyScalar(0.5);
+
+ const positions = geometry.attributes.position;
+ const vertex = new THREE.Vector3();
+ const radial = new THREE.Vector3();
+
+ const twistTurns = params.primitiveTwistAmount || 0;
+ const twistRadians = twistTurns * Math.PI * 2;
+ const taperAmount = params.primitiveTaperAmount || 0;
+ const bendAmount = params.primitiveBendAmount || 0;
+ const noiseAmplitude = params.primitiveNoiseAmplitude || 0;
+ const noiseFrequency = params.primitiveNoiseFrequency || 0;
+ const inflateAmount = params.primitiveInflate || 0;
+
+ const halfY = halfSize.y || 1;
+ const halfX = halfSize.x || 1;
+
+ for (let i = 0; i < positions.count; i++) {
+ vertex.set(positions.getX(i), positions.getY(i), positions.getZ(i));
+ vertex.sub(center);
+
+ if (inflateAmount !== 0) {
+ vertex.multiplyScalar(1 + inflateAmount);
+ }
+
+ if (taperAmount !== 0) {
+ const normalizedY = halfY === 0 ? 0 : ((vertex.y / halfY) + 1) * 0.5; // 0..1
+ const taperScale = 1 + taperAmount * (normalizedY - 0.5);
+ vertex.x *= taperScale;
+ vertex.z *= taperScale;
+ }
+
+ if (twistRadians !== 0) {
+ const normalizedY = halfY === 0 ? 0.5 : ((vertex.y / halfY) + 1) * 0.5;
+ const angle = twistRadians * (normalizedY - 0.5);
+ const cos = Math.cos(angle);
+ const sin = Math.sin(angle);
+ const x = vertex.x * cos - vertex.z * sin;
+ const z = vertex.x * sin + vertex.z * cos;
+ vertex.x = x;
+ vertex.z = z;
+ }
+
+ if (bendAmount !== 0) {
+ const normalizedX = halfX === 0 ? 0.5 : ((vertex.x / halfX) + 1) * 0.5;
+ const angle = bendAmount * (normalizedX - 0.5);
+ const cos = Math.cos(angle);
+ const sin = Math.sin(angle);
+ const y = vertex.y * cos - vertex.z * sin;
+ const z = vertex.y * sin + vertex.z * cos;
+ vertex.y = y;
+ vertex.z = z;
+ }
+
+ if (noiseAmplitude !== 0 && noiseFrequency !== 0) {
+ const noise = pseudoNoise3(vertex.x, vertex.y, vertex.z, noiseFrequency) * noiseAmplitude;
+ radial.copy(vertex);
+ const len = radial.length();
+ if (len > 0.0001) {
+ radial.multiplyScalar(1 / len);
+ } else {
+ radial.set(0, 1, 0);
+ }
+ vertex.addScaledVector(radial, noise);
+ }
+
+ vertex.add(center);
+ positions.setXYZ(i, vertex.x, vertex.y, vertex.z);
+ }
+
+ positions.needsUpdate = true;
+ geometry.computeVertexNormals();
+ geometry.computeBoundingSphere();
+ geometry.computeBoundingBox();
+}
+
+function buildPrimitiveGeometry(params) {
+ const generator = getPrimitiveGenerator(params.primitiveType || 'sphere');
+ if (!generator || typeof generator.buildGeometry !== 'function') {
+ return new THREE.BufferGeometry();
+ }
+
+ let geometry = generator.buildGeometry(params);
+ if (!geometry) {
+ geometry = new THREE.BufferGeometry();
+ }
+
+ if (geometry && !geometry.isBufferGeometry && geometry.toBufferGeometry) {
+ geometry = geometry.toBufferGeometry();
+ }
+
+ applyPrimitiveDeformations(geometry, params);
+ return geometry;
+}
+
diff --git a/v0.5/src/main.js b/v0.5/src/main.js
index 2861694..eee88d9 100644
--- a/v0.5/src/main.js
+++ b/v0.5/src/main.js
@@ -7,6 +7,11 @@ const renderer = createRenderer(canvas);
// Initialize with default parameters
let currentParams = { ...defaultParams };
+// Ensure geometry mode defaults exist
+if (!currentParams.geometryMode) {
+ currentParams.geometryMode = 'tube';
+}
+
// Ensure pathType and crossSectionType are set
if (!currentParams.pathType) {
currentParams.pathType = 'spiral';
diff --git a/v0.5/src/params.js b/v0.5/src/params.js
index 667bc22..267ddcf 100644
--- a/v0.5/src/params.js
+++ b/v0.5/src/params.js
@@ -1,7 +1,9 @@
const defaultParams = {
- // Base geometry type
- pathType: 'spiral', // Path generator type
- crossSectionType: 'circle', // Cross-section generator type
+ // Base geometry setup
+ geometryMode: 'tube', // 'tube' or 'primitive'
+ pathType: 'spiral', // Path generator type (tube mode)
+ crossSectionType: 'circle', // Cross-section generator type (tube mode)
+ primitiveType: 'sphere', // Primitive generator type (primitive mode)
// Initialize default path and cross-section types if not set
// (will be set by dynamic controls)
@@ -60,6 +62,27 @@ const defaultParams = {
noiseStrength: 0.3, // Noise strength (for deformed-sphere-noise)
noiseScale: 3.0, // Noise scale (for deformed-sphere-noise)
+ // Primitive parameters (dynamically loaded based on primitiveType)
+ primitiveRadius: 1.8, // Shared radius default
+ primitiveSegments: 64, // Disc segments
+ primitiveWidthSegments: 32, // Sphere width segments
+ primitiveHeightSegments: 24, // Sphere height segments
+ primitiveBoxSize: 2.0, // Cube size
+ primitiveBoxSubdivisions: 1, // Cube subdivisions
+ primitiveHeight: 2.5, // Prism height
+ primitiveStackSegments: 1, // Prism stack segments
+ primitiveTopRadius: 1.4, // Decagonal top radius
+ primitiveBottomRadius: 1.4, // Decagonal bottom radius
+ primitiveDetail: 0, // Dodecahedron detail level
+
+ // Primitive deformation controls
+ primitiveTwistAmount: 0.0, // Twist in turns
+ primitiveTaperAmount: 0.0, // Linear taper
+ primitiveBendAmount: 0.0, // Bend angle in radians
+ primitiveNoiseAmplitude: 0.0, // Noise displacement
+ primitiveNoiseFrequency: 2.5, // Noise frequency
+ primitiveInflate: 0.0, // Inflate/deflate amount
+
// Modifiers
decayMode: 'Exponential', // 'None', 'Exponential', or 'Linear'
k: 0.18, // Decay constant (for exponential)
diff --git a/v0.5/src/rendering/mesh.js b/v0.5/src/rendering/mesh.js
index 55d573e..5b08f69 100644
--- a/v0.5/src/rendering/mesh.js
+++ b/v0.5/src/rendering/mesh.js
@@ -160,10 +160,93 @@ function buildWireframeLines(params) {
// Outline creation is now in outline.js - using createOutlineLines from there
+function buildPrimitiveWireframeLines(geometry, params) {
+ const edgesGeometry = new THREE.EdgesGeometry(geometry);
+ const material = new THREE.LineBasicMaterial({
+ color: new THREE.Color(params.outerColor),
+ linewidth: params.lineWidth,
+ transparent: false
+ });
+ return new THREE.LineSegments(edgesGeometry, material);
+}
+
+function updatePrimitiveMesh(meshGroup, params, camera) {
+ const geometry = buildPrimitiveGeometry(params);
+ if (!geometry) return;
+
+ if (params.renderStyle === 'Solid') {
+ const material = new THREE.MeshStandardMaterial({
+ color: params.outerColor,
+ side: THREE.DoubleSide,
+ metalness: 0.35,
+ roughness: 0.55
+ });
+ const mesh = new THREE.Mesh(geometry, material);
+ meshGroup.add(mesh);
+ return;
+ }
+
+ const lineSegments = buildPrimitiveWireframeLines(geometry, params);
+ const allLines = [lineSegments];
+
+ if (params.renderStyle === 'Wireframe') {
+ if (params.occludeInner) {
+ const depthMaterial = new THREE.MeshBasicMaterial({
+ colorWrite: false,
+ depthWrite: true,
+ depthTest: true,
+ side: THREE.DoubleSide
+ });
+ const depthMesh = new THREE.Mesh(geometry, depthMaterial);
+ depthMesh.renderOrder = 0;
+ meshGroup.add(depthMesh);
+
+ lineSegments.material.depthTest = true;
+ lineSegments.material.depthWrite = false;
+ lineSegments.renderOrder = 1;
+ }
+
+ meshGroup.add(lineSegments);
+
+ if (params.showOutline && camera) {
+ const outlineLines = createOutlineLines(meshGroup, allLines, camera, params);
+ outlineLines.forEach(line => meshGroup.add(line));
+ }
+ return;
+ }
+
+ if (params.renderStyle === 'Hidden-line') {
+ const depthMaterial = new THREE.MeshBasicMaterial({
+ colorWrite: false,
+ depthWrite: true,
+ depthTest: true,
+ side: THREE.DoubleSide
+ });
+ const depthMesh = new THREE.Mesh(geometry, depthMaterial);
+ depthMesh.renderOrder = 0;
+ meshGroup.add(depthMesh);
+
+ lineSegments.material.depthTest = true;
+ lineSegments.material.depthWrite = false;
+ lineSegments.renderOrder = 1;
+ meshGroup.add(lineSegments);
+
+ if (params.showOutline && camera) {
+ const outlineLines = createOutlineLines(meshGroup, allLines, camera, params);
+ outlineLines.forEach(line => meshGroup.add(line));
+ }
+ }
+}
+
function updateMesh(meshGroup, params, camera = null) {
// Clear existing mesh
meshGroup.clear();
+ if ((params.geometryMode || 'tube') === 'primitive') {
+ updatePrimitiveMesh(meshGroup, params, camera);
+ return;
+ }
+
if (params.renderStyle === 'Solid') {
// Solid rendering: use geometry meshes
const outerGeometry = buildOuterTubeGeometry(params);
diff --git a/v0.5/src/ui/dynamicControls.js b/v0.5/src/ui/dynamicControls.js
index 96c6c07..06a6e28 100644
--- a/v0.5/src/ui/dynamicControls.js
+++ b/v0.5/src/ui/dynamicControls.js
@@ -120,6 +120,39 @@ function generateGeometryControls(params, onUpdate) {
container.innerHTML = '';
+ // Geometry mode selector
+ const modeSelectContainer = document.createElement('div');
+ modeSelectContainer.className = 'control-group';
+ const modeLabel = document.createElement('label');
+ modeLabel.setAttribute('for', 'geometryMode');
+ modeLabel.textContent = 'Geometry Mode';
+ modeSelectContainer.appendChild(modeLabel);
+
+ const modeSelect = document.createElement('select');
+ modeSelect.id = 'geometryMode';
+ [
+ { value: 'tube', label: 'Parametric Tube' },
+ { value: 'primitive', label: 'Primitive Shape' }
+ ].forEach(option => {
+ const optionEl = document.createElement('option');
+ optionEl.value = option.value;
+ optionEl.textContent = option.label;
+ modeSelect.appendChild(optionEl);
+ });
+ modeSelect.value = params.geometryMode || 'tube';
+ modeSelect.addEventListener('change', (e) => {
+ params.geometryMode = e.target.value;
+ generateGeometryControls(params, onUpdate);
+ onUpdate();
+ });
+ modeSelectContainer.appendChild(modeSelect);
+ container.appendChild(modeSelectContainer);
+
+ if ((params.geometryMode || 'tube') === 'primitive') {
+ buildPrimitiveControls(container, params, onUpdate);
+ return;
+ }
+
// Path type selector
const pathSelectContainer = document.createElement('div');
pathSelectContainer.className = 'control-group';
@@ -337,3 +370,49 @@ function generateGeometryControls(params, onUpdate) {
container.appendChild(vDivControl);
}
+function buildPrimitiveControls(container, params, onUpdate) {
+ // Primitive type selector
+ const primitiveSelectContainer = document.createElement('div');
+ primitiveSelectContainer.className = 'control-group';
+ const primitiveLabel = document.createElement('label');
+ primitiveLabel.setAttribute('for', 'primitiveType');
+ primitiveLabel.textContent = 'Primitive Type';
+ primitiveSelectContainer.appendChild(primitiveLabel);
+
+ const primitiveSelect = document.createElement('select');
+ primitiveSelect.id = 'primitiveType';
+ Object.keys(primitiveGenerators).forEach(key => {
+ const option = document.createElement('option');
+ option.value = key;
+ option.textContent = primitiveGenerators[key].name;
+ primitiveSelect.appendChild(option);
+ });
+ primitiveSelect.value = params.primitiveType || 'sphere';
+ primitiveSelect.addEventListener('change', (e) => {
+ params.primitiveType = e.target.value;
+ generateGeometryControls(params, onUpdate);
+ onUpdate();
+ });
+ primitiveSelectContainer.appendChild(primitiveSelect);
+ container.appendChild(primitiveSelectContainer);
+
+ // Primitive-specific parameter controls
+ const primitiveGen = getPrimitiveGenerator(params.primitiveType || 'sphere');
+ if (primitiveGen && primitiveGen.params) {
+ Object.entries(primitiveGen.params).forEach(([key, config]) => {
+ addControl(container, key, config, params, onUpdate);
+ });
+ }
+
+ // Deformation controls
+ const deformHeader = document.createElement('h4');
+ deformHeader.textContent = 'Deformations';
+ deformHeader.style.marginTop = '20px';
+ deformHeader.style.marginBottom = '10px';
+ container.appendChild(deformHeader);
+
+ Object.entries(primitiveDeformers).forEach(([key, config]) => {
+ addControl(container, key, config, params, onUpdate);
+ });
+}
+