Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions v0.5/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ <h3>Config</h3>
<script src="src/params.js"></script>
<script src="src/geometry/paths.js"></script>
<script src="src/geometry/crossSections.js"></script>
<script src="src/geometry/primitives.js"></script>
<script src="src/geometry/modifiers.js"></script>
<script src="src/geometry/tube.js"></script>
<script src="src/geometry/innerTube.js"></script>
Expand Down
222 changes: 222 additions & 0 deletions v0.5/src/geometry/primitives.js
Original file line number Diff line number Diff line change
@@ -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;
}

5 changes: 5 additions & 0 deletions v0.5/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
29 changes: 26 additions & 3 deletions v0.5/src/params.js
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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)
Expand Down
83 changes: 83 additions & 0 deletions v0.5/src/rendering/mesh.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading