Creating Obj from Web and Bugfixing


In this second part of our journey, we focused on enhancing the functionality and user experience of the Tetrimino Tool by introducing advanced features such as auto-filling, saving/loading canvas states, and even exporting the canvas as a 3D .OBJ file for use in Blender. Let’s go through each step and the challenges we overcame to achieve this.

1. Auto-filling the Canvas with Random Shapes

The first major feature we implemented was the ability to auto-fill the canvas with random Tetrimino shapes. This involved generating shapes at random positions on the canvas, ensuring they fit within the grid and don’t overlap with each other or extend outside the boundaries.

const fillEmptySpaces = () => {
    const types = Object.keys(tetriminos).filter(type => type !== 'b'); // Excluding 'b'
    for (let y = 0; y < canvas.height; y += gridSize) {
        for (let x = 0; x < canvas.width; x += gridSize) {
            const randomType = types[Math.floor(Math.random() * types.length)];
            if (!isOutsideCanvas(tetriminos[randomType].shape, x, y) && !isPositionOccupied(tetriminos[randomType].shape, x, y)) {
                addTetriminoAtPosition(randomType, x, y);
            }
        }
    }
};

The challenge was to ensure that the shapes don’t overlap with existing ones and stay within the canvas limits. We solved this by writing helper functions like isPositionOccupied() and isOutsideCanvas() that validate each position before placing a shape.

2. Undoing Auto-fill Action

Alongside auto-filling, we also implemented an undo function, which allows the user to revert the canvas to its previous state before auto-fill was applied. This required storing the previous state of the canvas and re-applying it when necessary.

const undoLastFill = () => {
    if (lastFillWasEmptyCanvas) {
        clearCanvas();
    } else {
        tetriminoObjects = previousState;
        Object.keys(tetriminoCounts).forEach(type => {
            tetriminoCounts[type] = previousState.filter(obj => obj.shape === tetriminos[type].shape).length;
            document.getElementById(`count-${type}`).textContent = tetriminoCounts[type];
        });
        updateCanvas();
    }
};

This feature required us to track the state of the canvas, which involved saving the array of shapes and their positions, and updating it every time the user auto-fills the canvas.

3. Exporting Canvas to 3D .OBJ File

One of the most exciting features added to the tool was the ability to export the canvas as a 3D .OBJ file, which can then be imported into Blender for further 3D modeling. This was particularly useful for turning our 2D canvas into a 3D grid filled with Tetrimino-shaped cubes.

We achieved this by generating the vertex and face data for each cube and then writing this data into a .OBJ file format. This was a complex process as it required understanding the 3D coordinate system and properly organizing the vertices to create 3D cubes.

function generateAndDownloadObjFile() {
    let objFileContent = "# 21x21 grid with boundary and user-drawn 'b' cubes
";
    let vertices = [];
    let faces = [];
    let vertOffset = 1;
    // Generate boundary cubes
    for (let i = 0; i < 21; i++) {
        for (let j = 0; j < 21; j++) {
            if (i === 0 || i === 20 || j === 0 || j === 20) { // Boundary cubes
                let cubeVertices = generateCube(i, 0, j);
                vertices.push(...cubeVertices);
                faces.push([vertOffset, vertOffset + 1, vertOffset + 2, vertOffset + 3]); // bottom face
                vertOffset += 8;
            }
        }
    }
    // Add user-drawn cubes
    tetriminoObjects.forEach(obj => {
        if (obj.shape === tetriminos['b'].shape) { // Only 'b' type cubes
            let cubeVertices = generateCube(obj.x / gridSize, 0, obj.y / gridSize);
            vertices.push(...cubeVertices);
            faces.push([vertOffset, vertOffset + 1, vertOffset + 2, vertOffset + 3]); // bottom face
            vertOffset += 8;
        }
    });
    // Output .OBJ file format
    vertices.forEach(v => objFileContent += `v ${v.x} ${v.y} ${v.z}
`);
    faces.forEach(f => objFileContent += `f ${f.join(' ')}
`);
    downloadFile("canvas_with_cubes.obj", objFileContent);
}

This feature opened up new possibilities for creating more complex designs and using the tool beyond just 2D shapes. The exported .OBJ files were tested successfully in Blender, where the cubes appeared in the correct positions and scale.

Example of Smiling Poo emoji recreated in tool

4. Saving and Loading Canvas States

One of the most requested features was the ability to save and load canvas states. This allowed users to export the current state of the canvas to a JSON format and reload it later, preserving all Tetrimino positions.

const generateCanvasState = () => {
    const state = tetriminoObjects.map(obj => ({
        type: Object.keys(tetriminos).find(t => JSON.stringify(tetriminos[t].shape) === JSON.stringify(obj.shape)),
        x: obj.x,
        y: obj.y
    }));
    document.getElementById('canvasStateInput').value = JSON.stringify(state);
};
const applyCanvasState = () => {
    const state = JSON.parse(document.getElementById('canvasStateInput').value);
    tetriminoObjects = state.map(obj => ({
        shape: tetriminos[obj.type].shape,
        color: tetriminos[obj.type].color,
        x: obj.x,
        y: obj.y
    }));
    updateCanvas();
};

The challenge was ensuring that the shapes restored from the JSON state appeared exactly as they were before, in the correct position, and that the counters updated accordingly. This involved tweaking the logic to store the type of each shape and recreating it upon loading.

Is it the end?

Throughout this part of the project, we introduced exciting new features that extended the tool's functionality and made it more versatile. We faced challenges in managing the canvas state, preventing overlaps, and ensuring accurate 3D exports, but each problem was solved eventually.