I have developed a code that generates outlines for both rectangular and L-shaped floor plans. However, when attempting to apply openings, a noticeable discrepancy arises between the two shapes in terms of the applied Constructive Solid Geometry (CSG) subtraction.
I am seeking insights into the underlying reasons for this behavior and potential solutions to address it.
Despite my efforts to reverse the order of the vertices and recalculate the vertex normals, the issue persists. Additionally, I observed that increasing the thickness of the opening box causes it to remain visible in areas where it does not intersect with the L-shaped outline, but this does not occur with the rectangular outline.
function createRectangular(sizeLabel, position, extrusionLength, wallThickness) {
const [width, length] = rectangularSizes[sizeLabel];
// Outer shape
const outerShape = new THREE.Shape();
outerShape.moveTo(-width / 2, -length / 2);
outerShape.lineTo(width / 2, -length / 2);
outerShape.lineTo(width / 2, length / 2);
outerShape.lineTo(-width / 2, length / 2);
outerShape.lineTo(-width / 2, -length / 2);
// Inner shape as a hole (smaller by wall thickness)
const innerShape = new THREE.Path();
innerShape.moveTo(-width / 2 + wallThickness, -length / 2 + wallThickness);
innerShape.lineTo(width / 2 - wallThickness, -length / 2 + wallThickness);
innerShape.lineTo(width / 2 - wallThickness, length / 2 - wallThickness);
innerShape.lineTo(-width / 2 + wallThickness, length / 2 - wallThickness);
innerShape.lineTo(-width / 2 + wallThickness, -length / 2 + wallThickness);
// Add inner shape as a hole in the outer shape
outerShape.holes.push(innerShape);
// Extrude only the area between the inner and outer shapes
const geometry = new THREE.ExtrudeGeometry(outerShape, { depth: extrusionLength, bevelEnabled: false });
const material = new THREE.MeshStandardMaterial({
color: 0x0077ff,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(...position);
return mesh;
}
function createLShaped(sizeLabel, position, extrusionLength, wallThickness) {
const [length1, length2, width] = lShapedSizes[sizeLabel];
// Outer L-shape - defined in clockwise order
const outerShape = new THREE.Shape();
outerShape.moveTo(-length1 / 2, -length2 / 2); // Start from bottom left
outerShape.lineTo(-length1 / 2, length2 / 2); // Go up to top left
outerShape.lineTo(length1 / 2, length2 / 2); // Go to top right
outerShape.lineTo(length1 / 2, length2 / 2 - width); // Go down to right arm top
outerShape.lineTo(-length1 / 2 + width, length2 / 2 - width); // Go to right arm bottom
outerShape.lineTo(-length1 / 2 + width, -length2 / 2); // Go down to bottom right
outerShape.lineTo(-length1 / 2, -length2 / 2); // Go to bottom left
// Inner L-shape as a hole - defined in clockwise order
const innerShape = new THREE.Path();
innerShape.moveTo(-length1 / 2 + wallThickness, -length2 / 2 + wallThickness); // Start from bottom left of hole
innerShape.lineTo(-length1 / 2 + wallThickness, length2 / 2 - wallThickness); // Go up to top left of hole
innerShape.lineTo(length1 / 2 - wallThickness, length2 / 2 - wallThickness); // Go to top right of hole
innerShape.lineTo(length1 / 2 - wallThickness, length2 / 2 - width + wallThickness); // Go down to right arm top of hole
innerShape.lineTo(-length1 / 2 + width - wallThickness, length2 / 2 - width + wallThickness); // Go to right arm bottom of hole
innerShape.lineTo(-length1 / 2 + width - wallThickness, -length2 / 2 + wallThickness); // Go down to bottom right of hole
innerShape.lineTo(-length1 / 2 + wallThickness, -length2 / 2 + wallThickness); // Go to bottom left of hole
// Add the inner shape as a hole
outerShape.holes.push(innerShape);
// Extrude the shape with depth and double-sided material
const geometry = new THREE.ExtrudeGeometry(outerShape, { depth: extrusionLength, bevelEnabled: false });
geometry.computeVertexNormals(); // Ensure normals are correct for lighting
const material = new THREE.MeshStandardMaterial({
color: 0x0077ff,
side: THREE.DoubleSide // Show both sides of the geometry
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(...position);
return mesh;
}
function updateFloorplanMesh() {
const floorplanSelection = document.getElementById('floorPlan').value;
const sizeSelection = document.getElementById('floorPlanSize').value;
if (floorplanMesh) {
scene.remove(floorplanMesh);
}
const extrusionLength = parseFloat(document.getElementById('extrusionLength').value) || 96;
const wallThickness = parseFloat(document.getElementById('wallThickness').value) || 4;
const position = [0, 0, 0];
if (floorplanSelection === 'rectangular') {
floorplanMesh = createRectangular(sizeSelection, position, extrusionLength, wallThickness);
} else if (floorplanSelection === 'lShaped') {
floorplanMesh = createLShaped(sizeSelection, position, extrusionLength, wallThickness);
}
// Rotate the floorplan mesh 90 degrees in the X direction
floorplanMesh.rotation.x = -Math.PI / 2; // 90 degrees in radians
scene.add(floorplanMesh);
}
let openingCount = 0; // Ensure this is declared outside the function
const openings = []; // Ensure openings is declared to store opening data
function addOpening() {
openingCount++; // Increment the opening count
// Create a box (opening)
const geometry = new THREE.BoxGeometry(4, 80, 36);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const box = new THREE.Mesh(geometry, material);
// Position the box in the scene
box.position.set(0, 0, 40); // Adjust the position if necessary
// Store box details in openings array
const openingData = {
id: `opening${openingCount}`,
box: box,
position: { x: 0, y: 0, z: 40 },
dimensions: { length: 4, width: 80, height: 36 },
material: material // Store the material here
};
openings.push(openingData); // Add to openings array
// Log the material for the opening
console.log(`Material for ${openingData.id}:`, material);
// Update the dropdown
const openingSelect = document.getElementById('openingSelection');
const option = document.createElement('option');
option.value = openingData.id;
option.text = `Opening ${openingCount}`;
openingSelect.appendChild(option); // Add option to the dropdown
}
// Function to update input fields when an opening is selected
function updateInputFields() {
const selectedOpeningId = document.getElementById('openingSelection').value;
const opening = openings.find(o => o.id === selectedOpeningId);
if (opening) {
document.getElementById('positionX').value = opening.position.x;
document.getElementById('positionY').value = opening.position.y;
document.getElementById('positionZ').value = opening.position.z;
document.getElementById('length').value = opening.dimensions.length;
document.getElementById('width').value = opening.dimensions.width;
document.getElementById('height').value = opening.dimensions.height;
document.getElementById('rotationX').value = THREE.MathUtils.radToDeg(opening.box.rotation.x);
document.getElementById('rotationY').value = THREE.MathUtils.radToDeg(opening.box.rotation.y);
document.getElementById('rotationZ').value = THREE.MathUtils.radToDeg(opening.box.rotation.z);
}
}
// Function to update the box in the scene according to input values
function updateOpeningParameters() {
const selectedOpeningId = document.getElementById('openingSelection').value;
const opening = openings.find(o => o.id === selectedOpeningId);
if (opening) {
// Update position
opening.position.x = parseFloat(document.getElementById('positionX').value);
opening.position.y = parseFloat(document.getElementById('positionY').value);
opening.position.z = parseFloat(document.getElementById('positionZ').value);
opening.box.position.set(opening.position.x, opening.position.y, opening.position.z);
// Update dimensions
const length = parseFloat(document.getElementById('length').value);
const width = parseFloat(document.getElementById('width').value);
const height = parseFloat(document.getElementById('height').value);
opening.box.geometry.dispose(); // Dispose the old geometry
opening.box.geometry = new THREE.BoxGeometry(length, height, width); // Create new geometry
// Update rotation
opening.box.rotation.x = THREE.MathUtils.degToRad(parseFloat(document.getElementById('rotationX').value));
opening.box.rotation.y = THREE.MathUtils.degToRad(parseFloat(document.getElementById('rotationY').value));
opening.box.rotation.z = THREE.MathUtils.degToRad(parseFloat(document.getElementById('rotationZ').value));
// Update the opening mesh's matrix before subtraction
opening.box.updateMatrix(); // Ensure local matrix is updated
opening.box.updateMatrixWorld(); // Ensure world matrix is updated
}
}
// Event listener for dropdown selection
document.getElementById('openingSelection').addEventListener('change', updateInputFields);
// Add event listeners for the position, rotation, and dimension inputs
const inputFields = document.querySelectorAll('.position-input, .rotation-input, .dimension-input');
inputFields.forEach(input => {
input.addEventListener('input', updateOpeningParameters);
});
document.getElementById('addOpeningButton').addEventListener('click', addOpening);
// Add event listener for the 'Apply Openings' button
document.getElementById('applyOpeningsButton').addEventListener('click', () => {
applyCSGSubtraction(); // Perform the CSG subtraction when button is clicked
});
function applyCSGSubtraction() {
updateFloorplanMesh();
// Check if the floorplan mesh exists
if (!floorplanMesh) {
console.error('Floorplan mesh is not defined. Please create a floorplan first.');
return;
}
// Convert the floorplan mesh to CSG format
let floorplanCSG = CSG.fromMesh(floorplanMesh, 0);
// Create a red material for openings
const openingMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
side: THREE.DoubleSide
});
// Iterate over each opening for CSG subtraction
openings.forEach(openingData => {
const openingMesh = openingData.box;
// Check if openingMesh is valid
if (openingMesh) {
// Compute vertex normals for the opening mesh
openingMesh.geometry.computeVertexNormals();
// Update the opening mesh's matrix before conversion
openingMesh.updateMatrix(); // Ensure local matrix is updated
openingMesh.updateMatrixWorld(); // Ensure world matrix is updated
// Convert the box to CSG and apply subtraction
const openingCSG = CSG.fromMesh(openingMesh, 1);
floorplanCSG = floorplanCSG.subtract(openingCSG);
// Hide the opening box mesh after subtraction
openingMesh.visible = false; // Optionally remove from scene
} else {
console.warn('Opening mesh is invalid or not defined for opening data:', openingData);
}
});
// Create the resulting mesh from the CSG operation with an array of materials
const resultMesh = CSG.toMesh(floorplanCSG, floorplanMesh.matrix, [floorplanMesh.material, openingMaterial]);
// Rotate the resulting mesh 90 degrees in the X direction
resultMesh.rotation.x = -Math.PI / 2;
// Ensure proper normals and transformations
resultMesh.geometry.computeVertexNormals();
resultMesh.updateMatrix();
resultMesh.updateMatrixWorld();
// Replace the original floorplan mesh with the new one
scene.remove(floorplanMesh);
scene.add(resultMesh);
floorplanMesh = resultMesh; // Update floorplanMesh reference
// Optionally update the scene matrix
scene.updateMatrixWorld();
}