I have developed a code that generates various roof types, consisting of over 250,000 characters and featuring more than a dozen distinct designs. I have attached a portion of this code to establish a basic foundation to gain some support for this project.
I’m trying to find a way to convert the outlines of these shapes into a Complex Mesh, so the entire structure can be represented as a single mesh rather than just separate lines. Additionally, I’ve noticed an issue with one side of the Dutch Gable, where the intersection calculations aren’t working correctly.
While I can use a lathe to shape some parts by applying a scaling factor and adjusting the number of sides, I prefer not to use that method. Instead, I want to keep the underlying skeleton to accurately place custom boxes that will act as rafters at the line locations.
<!DOCTYPE html>
<html lang="en">c
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Building Model</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128/examples/js/controls/OrbitControls.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100vh;
background-color: #1a1a1a;
}
#container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
#controls {
padding: 5px;
background-color: #333;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
bottom: 10px;
width: 100%;
border-radius: 10px;
}
label {
margin: 0 5px;
font-size: 0.9em;
}
input {
margin-left: 5px;
width: 50px; /* Increased width for better visibility */
}
select {
margin-left: 5px;
}
button {
padding: 5px 10px;
background-color: #555;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #777;
}
.input-container {
display: none; /* Hide all containers by default */
}
.active {
display: flex; /* Show active container */
}
</style>
</head>
<body>
<div id="container"></div>
<div id="controls">
<label for="roofType">Roof Type:</label>
<select id="roofType">
<option value="gable">Gable</option>
<option value="dutchGable">Dutch Gable</option>
</select>
<label for="rise">Roof Slope (rise/run):</label>
<input type="number" id="rise" value="9" step="0.0625" min="0">/
<input type="number" id="run" value="12" step="1" min="1">
<!-- Gable Input Container -->
<div id="gableInputs" class="input-container">
<label for="length">Length:</label>
<input type="number" id="length" value="480" step="1" min="3" max="1200">
<label for="width">Width:</label>
<input type="number" id="width" value="240" step="1" min="3" max="600">
<label for="materialThickness">Material Thickness:</label>
<input type="number" id="materialThickness" value="1.5" step="0.0625" min="0.5" max="5.5">
</div>
<!-- Dutch Gable Input Container -->
<div id="dutchGableInputs" class="input-container">
<label for="length7">Length:</label>
<input type="number" id="length7" value="480" step="1" min="3" max="1200">
<label for="width7">Width:</label>
<input type="number" id="width7" value="240" step="1" min="3" max="1200">
<label for="materialThickness7">Material Thickness:</label>
<input type="number" id="materialThickness7" value="1.5" step="0.0625" min="0.5" max="5.5">
</div>
<button id="update">Update</button>
</div>
<script>
// Scene, camera, renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 5000); // Increased far plane from 1000 to 5000
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight * 0.9);
document.getElementById('container').appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// Set the camera position further back
camera.position.z = 300; // You can adjust this value based on your scene size
const animate = function () {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
</script>
<script>
function createGable(rise, run, length, width, materialThickness) {
const spacing = 24; // Spacing set inside the function
const totalRun = width / 2 - materialThickness / 2;
const angleInRadians = Math.atan(rise / run);
const totalRise = Math.tan(angleInRadians) * totalRun;
const height = totalRise;
const bottomVertices = [
new THREE.Vector3(-length / 2, 0, width / 2),
new THREE.Vector3(length / 2, 0, width / 2),
new THREE.Vector3(length / 2, 0, -width / 2),
new THREE.Vector3(-length / 2, 0, -width / 2)
];
const topVertices = [
new THREE.Vector3(-length / 2, height, materialThickness / 2),
new THREE.Vector3(length / 2, height, materialThickness / 2),
new THREE.Vector3(length / 2, height, -materialThickness / 2),
new THREE.Vector3(-length / 2, height, -materialThickness / 2)
];
// Add bottom rectangle
const bottomGeometry = new THREE.BufferGeometry().setFromPoints(bottomVertices);
const bottomMaterial = new THREE.LineBasicMaterial({ color: 0x00ff00 });
const bottomRectangle = new THREE.LineLoop(bottomGeometry, bottomMaterial);
scene.add(bottomRectangle);
// Add top rectangle
const topGeometry = new THREE.BufferGeometry().setFromPoints(topVertices);
const topMaterial = new THREE.LineBasicMaterial({ color: 0x0000ff });
const topRectangle = new THREE.LineLoop(topGeometry, topMaterial);
scene.add(topRectangle);
// Calculate the hypotenuse for the gable structure
const hypotenuse = Math.sqrt(rise * rise + run * run);
const connectingLinesMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
// Create connecting lines from bottom to top vertices (extended at the bottom)
for (let i = 0; i < bottomVertices.length; i++) {
const bottomPoint = bottomVertices[i];
const topPoint = topVertices[i];
const direction = new THREE.Vector3().subVectors(topPoint, bottomPoint).normalize();
const extendedStart = bottomPoint.clone().sub(direction.clone().multiplyScalar(hypotenuse));
const extendedEnd = topPoint.clone();
const lineGeometry = new THREE.BufferGeometry();
const points = [extendedStart, extendedEnd];
lineGeometry.setFromPoints(points);
const connectingLine = new THREE.Line(lineGeometry, connectingLinesMaterial);
scene.add(connectingLine);
}
// Create the additional array of connecting lines every 24 units OC
const numDivisions = Math.floor(length / spacing); // Total divisions based on spacing
const step = length / numDivisions; // Step size for placement along the length
for (let i = 0; i < bottomVertices.length; i++) {
const bottomPoint = bottomVertices[i];
const topPoint = topVertices[i];
// Create connecting lines spaced every 24 units along the length
for (let j = 1; j < numDivisions; j++) { // Start at 1 to avoid the endpoints
const offset = step * j - length / 2; // Calculate position based on step size
const newBottomPoint = new THREE.Vector3(offset, 0, bottomPoint.z);
const newTopPoint = new THREE.Vector3(offset, height, topPoint.z);
// Extend the bottom point for the array connecting lines
const direction = new THREE.Vector3().subVectors(newTopPoint, newBottomPoint).normalize();
const extendedBottomPoint = newBottomPoint.clone().sub(direction.clone().multiplyScalar(hypotenuse));
const lineGeometry = new THREE.BufferGeometry();
const points = [extendedBottomPoint, newTopPoint];
lineGeometry.setFromPoints(points);
const arrayConnectingLine = new THREE.Line(lineGeometry, connectingLinesMaterial);
scene.add(arrayConnectingLine);
}
}
}
function createDutchGable(rise, run, length, width, materialThickness) {
const midWidth = width / 2;
const totalRun = width / 2-midWidth/2 - materialThickness / 2;
const angleInRadians = Math.atan(rise / run);
const totalRise = Math.tan(angleInRadians) * totalRun;
const totalRun2 = midWidth/2 - materialThickness / 2;
const angleInRadians2 = Math.atan(rise / run);
const totalRise2 = Math.tan(angleInRadians2) * totalRun2;
const height2 = totalRise2;
const height = totalRise+totalRise2;
const ridgeLength = length-(width-midWidth);
const middleVertices = [
new THREE.Vector3(-ridgeLength / 2, totalRise2, -midWidth / 2),
new THREE.Vector3(ridgeLength / 2, totalRise2, -midWidth / 2),
new THREE.Vector3(ridgeLength / 2, totalRise2, midWidth / 2),
new THREE.Vector3(-ridgeLength / 2, totalRise2, midWidth / 2),
];
const topVertices = [
new THREE.Vector3(-ridgeLength / 2, height, -materialThickness / 2),
new THREE.Vector3(ridgeLength / 2, height, -materialThickness / 2),
new THREE.Vector3(ridgeLength / 2, height, materialThickness / 2),
new THREE.Vector3(-ridgeLength / 2, height, materialThickness / 2),
];
const bottomVertices = [];
const spacing = 24; // Spacing for the lines
const connectingLinesMaterial = new THREE.LineBasicMaterial({ color: 0x00ff00 });
const baseRectMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
const topRectMaterial = new THREE.LineBasicMaterial({ color: 0x0000ff }); // Blue for the top rectangle
const pinkMaterial = new THREE.LineBasicMaterial({ color: 0xff00ff }); // Pink for the connecting lines
// Create vertices for the rectangular base
bottomVertices.push(new THREE.Vector3(-length / 2, 0, -width / 2)); // Bottom-left
bottomVertices.push(new THREE.Vector3(length / 2, 0, -width / 2)); // Bottom-right
bottomVertices.push(new THREE.Vector3(length / 2, 0, width / 2)); // Top-right
bottomVertices.push(new THREE.Vector3(-length / 2, 0, width / 2)); // Top-left
const middleGeometry = new THREE.BufferGeometry().setFromPoints(middleVertices);
const middleMaterial = new THREE.LineBasicMaterial({ color: 0x00ff00 });
const middleRectangle = new THREE.LineLoop(middleGeometry, middleMaterial);
scene.add(middleRectangle);
const hypotenuse = Math.sqrt(rise * rise + run * run);
for (let i = 0; i < middleVertices.length; i++) {
const lineGeometry = new THREE.BufferGeometry();
const bottomPoint = middleVertices[i];
const topPoint = topVertices[i];
const direction = new THREE.Vector3().subVectors(topPoint, bottomPoint).normalize();
const extendedStart = bottomPoint.clone();
const extendedEnd = topPoint.clone();
const points = [extendedStart, extendedEnd];
lineGeometry.setFromPoints(points);
const connectingLine1 = new THREE.Line(lineGeometry, connectingLinesMaterial);
scene.add(connectingLine1);
}
// Draw the rectangular base
const baseRectGeometry = new THREE.BufferGeometry().setFromPoints([...bottomVertices, bottomVertices[0]]);
const baseRect = new THREE.LineLoop(baseRectGeometry, baseRectMaterial);
scene.add(baseRect);
// Draw the top rectangle
const topRectGeometry = new THREE.BufferGeometry().setFromPoints([...topVertices, topVertices[0]]);
const topRect = new THREE.LineLoop(topRectGeometry, topRectMaterial);
scene.add(topRect);
// Calculate bottom points for base edges
const baseMidPoints = [
new THREE.Vector3((bottomVertices[0].x + bottomVertices[1].x) / 2, bottomVertices[0].y, (bottomVertices[0].z + bottomVertices[1].z) / 2),
new THREE.Vector3((bottomVertices[1].x + bottomVertices[2].x) / 2, bottomVertices[1].y, (bottomVertices[1].z + bottomVertices[2].z) / 2),
new THREE.Vector3((bottomVertices[2].x + bottomVertices[3].x) / 2, bottomVertices[2].y, (bottomVertices[2].z + bottomVertices[3].z) / 2),
new THREE.Vector3((bottomVertices[3].x + bottomVertices[0].x) / 2, bottomVertices[3].y, (bottomVertices[3].z + bottomVertices[0].z) / 2)
];
// Calculate midpoints for middle edges
const middleMidPoints = [
new THREE.Vector3((middleVertices[0].x + middleVertices[1].x) / 2, middleVertices[0].y, (middleVertices[0].z + middleVertices[1].z) / 2),
new THREE.Vector3((middleVertices[1].x + middleVertices[2].x) / 2, middleVertices[1].y, (middleVertices[1].z + middleVertices[2].z) / 2),
new THREE.Vector3((middleVertices[2].x + middleVertices[3].x) / 2, middleVertices[2].y, (middleVertices[2].z + middleVertices[3].z) / 2),
new THREE.Vector3((middleVertices[3].x + middleVertices[0].x) / 2, middleVertices[3].y, (middleVertices[3].z + middleVertices[0].z) / 2)
];
// Draw center lines from base midpoints to top midpoints
baseMidPoints.forEach((baseMidPoint, index) => {
const centerLineGeometry = new THREE.BufferGeometry();
const centerLinePoints = [baseMidPoint, middleMidPoints[index]]; // Connecting base midpoint to top midpoint
centerLineGeometry.setFromPoints(centerLinePoints);
const centerLine = new THREE.Line(centerLineGeometry, connectingLinesMaterial);
scene.add(centerLine);
});
// Create the additional array of connecting lines every 24 units OC
const numDivisions1 = Math.floor(ridgeLength / spacing); // Total divisions based on spacing
const step1 = ridgeLength / numDivisions1; // Step size for placement along the length
for (let i = 0; i < middleVertices.length; i++) {
const bottomPoint = middleVertices[i];
const topPoint = topVertices[i];
// Create connecting lines spaced every 24 units along the length
for (let j = 1; j < numDivisions1; j++) { // Start at 1 to avoid the endpoints
const offset = step1 * j - ridgeLength / 2; // Calculate position based on step size
const newBottomPoint = new THREE.Vector3(offset, height2, bottomPoint.z);
const newTopPoint = new THREE.Vector3(offset, height, topPoint.z);
// Extend the bottom point for the array connecting lines
const direction = new THREE.Vector3().subVectors(newTopPoint, newBottomPoint).normalize();
const extendedBottomPoint = newBottomPoint.clone();
const lineGeometry = new THREE.BufferGeometry();
const points = [extendedBottomPoint, newTopPoint];
lineGeometry.setFromPoints(points);
const arrayConnectingLine = new THREE.Line(lineGeometry, connectingLinesMaterial);
scene.add(arrayConnectingLine);
}
}
// For each side of the base, connect it to the corresponding side of the top rectangle
for (let i = 0; i < bottomVertices.length; i++) {
const bottomPoint1 = bottomVertices[i];
const bottomPoint2 = bottomVertices[(i + 1) % bottomVertices.length];
// Get the top points corresponding to this side
const topPoint1 = middleVertices[i];
const topPoint2 = middleVertices[(i + 1) % middleVertices.length];
// Draw the connecting lines between the bottom vertices and the top rectangle vertices with pink color
const mainEdgeGeometry1 = new THREE.BufferGeometry().setFromPoints([bottomPoint1, topPoint1]);
const mainEdgeLine1 = new THREE.Line(mainEdgeGeometry1, pinkMaterial); // Use pink material
scene.add(mainEdgeLine1);
const mainEdgeGeometry2 = new THREE.BufferGeometry().setFromPoints([bottomPoint2, topPoint2]);
const mainEdgeLine2 = new THREE.Line(mainEdgeGeometry2, pinkMaterial); // Use pink material
scene.add(mainEdgeLine2);
// Calculate the edge length of the base side
const edgeLength = bottomPoint1.distanceTo(bottomPoint2);
// Calculate the number of divisions along this edge based on the spacing
const numDivisions = Math.floor(edgeLength / spacing);
const stepX = (bottomPoint2.x - bottomPoint1.x) / numDivisions;
const stepZ = (bottomPoint2.z - bottomPoint1.z) / numDivisions;
// Determine the midpoint of the edge
const midPoint = new THREE.Vector3().addVectors(bottomPoint1, bottomPoint2).multiplyScalar(0.5);
// Store the original positions of the arrayed lines
const originalLines = [];
// Draw arrayed lines for half the edge
for (let j = 1; j <= Math.floor(numDivisions / 2); j++) { // Only draw half
// Calculate the new bottom point along the base edge
const newBottomPoint = new THREE.Vector3(
bottomPoint1.x + stepX * j,
0,
bottomPoint1.z + stepZ * j
);
// Determine the direction of the line by averaging the corresponding midpoints
const midPoint1 = baseMidPoints[i];
const midPoint2 = middleMidPoints[i];
const direction = new THREE.Vector3().subVectors(midPoint2, midPoint1).normalize(); // Get the direction from base to top
// Calculate the initial parallel point above the new bottom point using the direction
let parallelTopPoint = newBottomPoint.clone().add(direction.clone().multiplyScalar(height2)); // Move up by the height
// Check for intersections with the top rectangle
const intersectsTopRect = checkLineIntersection(newBottomPoint, parallelTopPoint, middleVertices);
// Check for intersections with the pink lines (the pink edges)
const intersectsPinkLines = checkLineIntersection(newBottomPoint, parallelTopPoint, [bottomPoint1, topPoint1, bottomPoint2, topPoint2]);
// If it intersects the top rectangle or connecting lines, set the parallelTopPoint to the intersection point
if (intersectsTopRect) {
parallelTopPoint = intersectsTopRect;
} else if (intersectsPinkLines) {
parallelTopPoint = intersectsPinkLines; // Stop at the intersection with the pink lines
}
// Create geometry for the connecting line
const lineGeometry = new THREE.BufferGeometry();
const points = [newBottomPoint, parallelTopPoint];
lineGeometry.setFromPoints(points);
// Create and add the connecting line to the scene
const connectingLine = new THREE.Line(lineGeometry, connectingLinesMaterial);
scene.add(connectingLine);
// Store the original bottom and top points for mirroring later
originalLines.push({ bottomPoint: newBottomPoint.clone(), topPoint: parallelTopPoint.clone(), direction });
}
// Draw mirrored lines for the other half of the edge
for (const line of originalLines) {
// Get the original points
const originalBottomPoint = line.bottomPoint;
const originalTopPoint = line.topPoint;
const direction = line.direction;
// Calculate the length of the line segment
const length = originalTopPoint.distanceTo(originalBottomPoint);
// Mirror the bottom point about the midpoint
const mirroredBottomPoint = new THREE.Vector3(
midPoint.x + (midPoint.x - originalBottomPoint.x),
0,
midPoint.z + (midPoint.z - originalBottomPoint.z)
);
// Calculate the mirrored top point based on the direction and length
const mirroredTopPoint = mirroredBottomPoint.clone().add(direction.clone().multiplyScalar(length));
// Create geometry for the mirrored connecting line
const mirroredLineGeometry = new THREE.BufferGeometry();
const mirroredPoints = [mirroredBottomPoint, mirroredTopPoint];
mirroredLineGeometry.setFromPoints(mirroredPoints);
// Create and add the mirrored connecting line to the scene
const mirroredConnectingLine = new THREE.Line(mirroredLineGeometry, connectingLinesMaterial);
scene.add(mirroredConnectingLine);
}
}
}
// Function to check for intersections with lines defined by vertices
function checkLineIntersection(startPoint, endPoint, vertices) {
const raycaster = new THREE.Raycaster(startPoint, new THREE.Vector3().subVectors(endPoint, startPoint).normalize());
const intersects = raycaster.intersectObject(new THREE.LineLoop(new THREE.BufferGeometry().setFromPoints(vertices)));
if (intersects.length > 0) {
return intersects[0].point; // Return the intersection point
}
return null; // No intersection
}
function updateComponents() {
while (scene.children.length > 0) {
scene.remove(scene.children[0]);
}
const rise = parseFloat(document.getElementById('rise').value);
const run = parseFloat(document.getElementById('run').value);
const length = parseFloat(document.getElementById('length').value);
const width = parseFloat(document.getElementById('width').value);
const materialThickness = parseFloat(document.getElementById('materialThickness').value);
const length7 = parseFloat(document.getElementById('length7').value);
const width7 = parseFloat(document.getElementById('width7').value);
const materialThickness7 = parseFloat(document.getElementById('materialThickness7').value);
const roofType = document.getElementById('roofType').value;
if (roofType === 'gable') {
createGable(rise, run, length, width, materialThickness);
} else if (roofType === 'dutchGable') {
createDutchGable(rise, run, length7, width7, materialThickness7);
}
}
document.getElementById('roofType').addEventListener('change', function() {
const roofType = this.value;
const gableInputs = document.getElementById('gableInputs');
const dutchGableInputs = document.getElementById('dutchGableInputs');
// Reset all inputs to inactive
gableInputs.classList.remove('active');
dutchGableInputs.classList.remove('active');
// Activate the corresponding inputs based on selected roof type
if (roofType === 'dutchGable') {
dutchGableInputs.classList.add('active');
} else {
gableInputs.classList.add('active'); // Default to gable
}
updateComponents(); // Update components when changing roof type
});
document.getElementById('update').addEventListener('click', updateComponents);
</script>
</body>
</html>