I am building an app in three.js, and basically the point is to have rows and columns of individual blocks that also extend upwards. When you click on a block it will change colour, so I have implemented clipping along all three axes, so that the middle blocks can be reached as well. I have also added a Gui using lil-gui, and that is where the problem comes in. I have controls for the amount of rows, columns and the height, as well as for how much it should clip in each direction. When initialized originally, the rows, columns and height are all set to 1, and if you increase any of them from there, the amount of blocks will correctly update in the specified direction.
My problem comes in when I load a previous save. The previous state is saved to localStorage, in a function that runs every few minutes, and saving also occurs with ctrl + s. It converts my current state, a 3 dimensional array, into a much simpler object, which is also a 3 dimensional object. It saves correctly, I have logged the output multiple times. When it loads, I convert the string into a JSON object, which I then convert into a state object, and I then render that state object. The strangest thing is that it only displays roughly half of what it should in all 3 directions, E.G A 3x3x3 grid is rendered as a 2x2x2 grid, but as soon as I click on rows, columns or height in my GUI, and click out again, it magically renders properly. What is also odd is that although all of my GUI fields have a min, a max and a step, they are unconstrained in the gui.
I have simplified my code so that it only deals with rows, as an mcve but the problem scales up as well.
The lil-gui panel code
function setUpPanel(){
const panel = new GUI( {width:310} );
const folder1 = panel.addFolder( "Size" );
const folder2 = panel.addFolder( "X Clipping" );
settings = {
'rows' : rows,
planeX: {
blocks: rows,
},
};
rowsObj = folder1.add(settings, 'rows').min(1).step(1).max(50).setValue( rows ).onFinishChange( function ( size ) {
rowsElement.max(size);
rowsElement.setValue(size);
rowsElement.updateDisplay();
rows = size;
redraw();
} );
rowsElement = folder2.add(settings.planeX, 'blocks',1, rows, 1).onChange( setClippingPlaneRows );
}
The code called when the app is started
import * as THREE from './js/three.module.js';
import { GUI } from './js/lil-gui.module.min.js';
import { OrbitControls } from './js/OrbitControls.js';
const blockOffset = 0.1;
const blockSize = 1;
const BlockStates = {
Empty: Symbol('e'),
}
const clippingPlanes = [
new THREE.Plane( new THREE.Vector3( -1, 0, 0 ), 0, {name: "plane"} ),
];
//gui object
let rowsObj
//more gui object
let rowsElement
const emptyMaterial = new THREE.MeshStandardMaterial( {color: 0xffffff, opacity: 0.5, transparent: true} );
emptyMaterial.clippingPlanes = clippingPlanes;
let rows=1;
let state = [];
let boxes = new THREE.Group();
let settings;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight , 0.1, 1000 );
camera.position.set(-1.5,2.5,3);
const renderer = new THREE.WebGLRenderer();
renderer.localClippingEnabled = true;
renderer.setSize( window.innerWidth, window.innerHeight);
document.body.appendChild( renderer.domElement );
var autoSaveID = setInterval(function() {
localStorage.lastSave = JSON.stringify(createJsonObjectFromState());
localStorage.rows = rows;
}, 300000);
if(localStorage.lastSave){
let loadSave = confirm("There is a previous save detected. Would you like to load it?");
if(loadSave){
const loadedObj = createStateObjectFromJsonString(localStorage.lastSave);
state = loadedObj.state;
boxes = loadedObj.boxes;
rows = localStorage.rows;
boxes.position.set(-(rows * (blockSize + blockOffset)- blockSize - blockOffset)/2,0, 0);
}
}
const light = new THREE.AmbientLight( 0x404040, 10);
const controls = new OrbitControls(camera, renderer.domElement );
setUpPanel();
drawBoxes();
boxes.position.set(0,0,0);
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 's') {
// Prevent the Save dialog from opening
e.preventDefault();
saveCurrent();
}
});
scene.add( boxes );
scene.add( light );
renderer.render(scene, camera);
The save function
function saveCurrent(){
let stringVersion = JSON.stringify(createJsonObjectFromState());
//save to local storage
if(localStorage.lastSave){
let writeSave = confirm("There is a previous save detected. Would you like to overwrite it?");
if(writeSave){
localStorage.lastSave = stringVersion;
localStorage.rows = rows;
}
}
else{
localStorage.lastSave = stringVersion;
localStorage.rows = rows;
}
}
createStateObjectFromJsonString
function createStateObjectFromJsonString(jsonString){
let newObj =[];
let newBoxes=new THREE.Group();
let jsonObj = JSON.parse(jsonString);
for(let i = 0; i < jsonObj.length; i++){
let box;
switch(jsonObj[i]){
case "E":
box = drawBox(BlockStates.Empty);
box.position.set(i*(blockSize + blockOffset ), 0, 0);
newBoxes.add(box);
newObj.push({state: BlockStates.Empty, item:box});
break;
}
}
return {state: newObj, boxes: newBoxes};
}
createJsonObjectFromState
function createJsonObjectFromState(){
const JSONObj = [];
for(let i = 0; i < rows; i++){
switch(state[i].state){
case BlockStates.Empty:
JSONObj.push("E");
break;
}
}
return JSONObj;
}
The function to set the clipping to a specific amount of blocks
function setClippingPlaneRows(amt){
clippingPlanes[0].constant = rows%2 === 0 ? (blockSize + blockOffset) * (amt - Math.floor(rows/2) ) : (blockSize + blockOffset) *( amt - Math.floor(rows/2) - 0.5);
}
The redraw function
function redraw(){
drawBoxes();
}
The drawBox function
function drawBox(state){
const geometry = new THREE.BoxGeometry(blockSize, blockSize, blockSize);
let material = emptyMaterial;
return new THREE.Mesh( geometry, material );
}
The drawBoxes function
function drawBoxes(){
if(state.length === 0)
{
boxes.clear();
state = [];
for(let i = 0; i < rows; i++){
const box = drawBox(BlockStates.Empty);
box.position.set(i*(blockSize + blockOffset ), 0, 0);
boxes.add(box);
state.push({state : BlockStates.Empty, item: box});
}
}
else{
if(state.length <= rows){
//just draw in the extra rows
for(let i = state.length; i < rows; i++){
const box = drawBox(BlockStates.Empty);
box.position.set(i*(blockSize + blockOffset ), 0, 0);
boxes.add(box);
state.push({state : BlockStates.Empty, item: box});
}
}
else{
//give a warning, and if accepted remove neccesary rows
let accepted = confirm("Making the rows smaller could remove some of the work you have done. Are you sure?");
if(accepted){
for(let i = 0; i < state.length - rows; i++){
const box = state[state.length - 1].item;
boxes.remove(box);
state.pop();
}
}
}
}
//plane initial positions
boxes.position.set(-(rows * (blockSize + blockOffset)- blockSize - blockOffset)/2, 0, 0);
setClippingPlaneRows(rows);
}
The animation part
function animate() {
let frame = requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();
I apologize for posting so much code, I just have no idea where the problem is stemming from. I have tried many things, such as checking the size of the rows, columns, etc at many points, but they are always correct. I have tried using setValue() to force the gui to call the onChange function, which hasn't done anything. I have tried many other variations too, but no luck.
Edit
I have discovered while playing around that if I set rows to a number other than 1 in the code itself, such as 7, and don't load a saved file, a very similar problem occurs. Only about half of the blocks are displaying, but this time the gui behaves itself and remains constrained.