Parametric relationship: Walls, Floors

Does anyone know how to create interactive walls in Three.js that automatically adjust when a connected wall is manipulated? I have a room modeled as a GLB file, comprising four walls and a floor. For example, if I stretch one wall, the adjacent wall, the opposite wall, and the floor should adjust accordingly to maintain the room’s shape. Is it feasible to implement this functionality in Three.js?

Hard coded the side but I am trying to do this automatically and the adjacent side do not move correctly when the wall is stretched

import "./style.css";
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { Pane } from "tweakpane";
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';


var scene, camera, renderer, controls, panel, cubeTexture, raycaster, mouse;
var model, selectedObject;


setupThreeScene();
loadModel();


function loadModel() {
  var loader = new GLTFLoader();
  loader.load('simpleBox.glb', function (gltf) {
    model = gltf.scene;
    console.log("Model Loaded:", model);


    // Iterate through the children of the model and set materials
    model.traverse(function (child) {
      if (child.isMesh) {
        child.material = new THREE.MeshStandardMaterial({
          color: "white",
          map: getTexture(),
          envMap: cubeTexture,
          roughness: 0.2,
          metalness: 0.7,
          side: THREE.DoubleSide // Ensure both sides of the material are rendered
        });
        child.name = child.name || 'Unnamed Mesh'; // Ensure each mesh has a name
      }
    });


    // Add the model to the scene
    scene.add(model);


    // Adjust model position and scale if necessary
    model.position.set(0, 0, 0);
    model.scale.set(1, 1, 1);
    console.log("Model Position:", model.position);
    console.log("Model Scale:", model.scale);
  }, undefined, function (error) {
    console.error('An error happened:', error);
  });
}


function setupThreeScene() {
  scene = new THREE.Scene();
  scene.background = new THREE.Color("gainsboro");
  camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );
  camera.position.z = 5;
  camera.position.y = 2;
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setAnimationLoop(update);
  document.body.appendChild(renderer.domElement);


  // Add a grid helper
  var gridHelper = new THREE.GridHelper(10, 20);
  gridHelper.position.y = -1;
  scene.add(gridHelper);


  // Controls
  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;


  var light = new THREE.DirectionalLight(0xFFFFFF);
  light.position.set(0, 2.0, 1);
  scene.add(light, new THREE.AmbientLight(0xffffff, 0.9));


  // Reflection map
  var loader = new THREE.CubeTextureLoader();
  loader.setCrossOrigin("");
  loader.setPath('https://threejs.org/examples/textures/cube/pisa/');


  cubeTexture = loader.load([
    'px.png', 'nx.png',
    'py.png', 'ny.png',
    'pz.png', 'nz.png'
  ]);


  window.addEventListener("resize", (event) => {
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
  });


  raycaster = new THREE.Raycaster();
  mouse = new THREE.Vector2();


  window.addEventListener('click', onClick, false);


  panel = new Pane();
}


function onClick(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;


  raycaster.setFromCamera(mouse, camera);


  const intersects = raycaster.intersectObjects(scene.children, true);


  if (intersects.length > 0) {
    selectedObject = intersects[0].object;
    console.log('Clicked on:', selectedObject.name);


    // Get connected sides
    const connectedSides = getConnectedSides(selectedObject.name);
    console.log('Connected sides:', connectedSides);


    // Clear previous panel contents
    panel.children.forEach(child => panel.remove(child));


    // Add controls for the selected object
    panel.addBinding(selectedObject.scale, 'x', { min: 0, max: 10, step: 0.1 }).on('change', (value) => {
      extrudeGeometry(selectedObject, 'x', value.value, connectedSides);
    });
    panel.addBinding(selectedObject.scale, 'y', { min: 0, max: 10, step: 0.1 }).on('change', (value) => {
      extrudeGeometry(selectedObject, 'y', value.value, connectedSides);
    });
    panel.addBinding(selectedObject.scale, 'z', { min: 0, max: 10, step: 0.1 }).on('change', (value) => {
      extrudeGeometry(selectedObject, 'z', value.value, connectedSides);
    });
    panel.addBinding(selectedObject.material, 'wireframe');
  }
}


function extrudeGeometry(object, axis, value, connectedSides) {
  const previousValue = object.scale[axis];
  const delta = value - previousValue;
console.log(object)

  // Update the selected object
  updateObjectGeometry(object, axis, value);


  // Calculate the new length and divide it by 2
  const newLength = object.scale[axis];
  const halfLength = newLength / 2;


  // Update the parallel side
  const parallelSideName = connectedSides.parallel;
  const parallelSide = model.getObjectByName(parallelSideName);
  if (parallelSide) {
    updateObjectGeometry(parallelSide, axis, value);
  }

  const floorSide = model.getObjectByName('Cube005');
  if (floorSide && object.name !== 'Cube005' && object.type!=='GridHelper') {
    updateObjectGeometry(floorSide, axis, value);
  }


  // Update the other sides that need to move
  connectedSides.moving.forEach((sideName, index) => {
    const side = model.getObjectByName(sideName);
    if (side) {
      side.position[axis] = (index % 2 === 0) ? halfLength : -halfLength;
    }
  });
}


function updateObjectGeometry(object, axis, value) {
  object.scale[axis] = value;
  object.geometry.attributes.position.needsUpdate = true;
  object.geometry.computeBoundingBox();
  object.geometry.computeBoundingSphere();
}


function getConnectedSides(name) {
  // Define the connections for each side of the box
  const connections = {
    Cube001: { parallel: 'Cube003', moving: ['Cube004', 'Cube002'] },
    Cube002: { parallel: 'Cube004', moving: ['Cube001', 'Cube003'] },
    Cube003: { parallel: 'Cube001', moving: ['Cube002', 'Cube004'] },
    Cube004: { parallel: 'Cube002', moving: ['Cube001', 'Cube003'] },
    Cube005: { parallel: '', moving: ['Cube001', 'Cube002', 'Cube003', 'Cube004'] },
  };
  return connections[name] || { parallel: '', moving: [] };
}


function update() {
  controls.update();
  renderer.render(scene, camera);
}


function getTexture() {
  var texSize = 32;
  var canvas = document.createElement("CANVAS");
  canvas.width = texSize;
  canvas.height = texSize;


  // Procedurally defining of a black square
  var context = canvas.getContext("2d");
  context.fillStyle = "linen";
  context.fillRect(0, 0, texSize, texSize);
  context.strokeStyle = "black";
  context.lineWidth = 1;
  context.strokeRect(3, 3, 27, 27);


  var texture = new THREE.CanvasTexture(canvas);
  texture.wrapS = THREE.RepeatWrapping;
  texture.wrapT = THREE.RepeatWrapping;
  texture.repeat.x = texture.repeat.y = 5;


  return texture;
}

Yes, as long as you know how to code it. Three is just rendering - you’re responsible for the logic.

Do you mean Sims-like wall building? If yes, first of all, don’t mix dynamic and static walls. Either have all walls hard-modelled in a GLB, or have all walls defined in data and then rendered. Don’t stretch walls from a prefab room model.

I’d personally remove all the walls / floors / dynamic content from the GLB and use some placeholders / data structure to define them in code. That’ll allow you to manipulate and connect / disconnect the walls easier.