VoxelGrid and VoxelManager
This commit is contained in:
parent
29d2006e61
commit
1c3411e7de
10 changed files with 1442 additions and 62 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
/node_modules
|
||||
/node_modules
|
||||
/dist
|
||||
25
build.js
Normal file
25
build.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { build } from 'esbuild';
|
||||
import { copyFileSync, mkdirSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Ensure dist directory exists
|
||||
mkdirSync('dist', { recursive: true });
|
||||
|
||||
// Build JavaScript
|
||||
await build({
|
||||
entryPoints: ['src/game-viewport.js'],
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
outfile: 'dist/game-viewport.js',
|
||||
platform: 'browser',
|
||||
});
|
||||
|
||||
// Copy HTML file
|
||||
copyFileSync('src/index.html', 'dist/index.html');
|
||||
|
||||
console.log('Build complete!');
|
||||
|
||||
935
package-lock.json
generated
935
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
|
@ -5,9 +5,10 @@
|
|||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "web-dev-server --node-resolve --watch --root-dir src",
|
||||
"test": "web-test-runner \"test/**/*.test.js\" --node-resolve",
|
||||
"test:watch": "web-test-runner \"test/**/*.test.js\" --node-resolve --watch"
|
||||
"build": "node build.js",
|
||||
"start": "web-dev-server --node-resolve --watch --root-dir dist",
|
||||
"test": "web-test-runner \"test/**/*.test.js\" --node-resolve --puppeteer",
|
||||
"test:watch": "web-test-runner \"test/**/*.test.js\" --node-resolve --watch --puppeteer"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -18,11 +19,14 @@
|
|||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@esm-bundle/chai": "^4.3.4-fix.0",
|
||||
"@web/test-runner": "^0.20.2",
|
||||
"@web/dev-server": "^0.4.6",
|
||||
"@web/test-runner": "^0.20.2",
|
||||
"@web/test-runner-puppeteer": "^0.18.0",
|
||||
"esbuild": "^0.27.1",
|
||||
"sinon": "^21.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lit": "^3.3.1",
|
||||
"three": "^0.182.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import {
|
||||
LitElement,
|
||||
html,
|
||||
css,
|
||||
} from "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js";
|
||||
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.182.0/build/three.module.js";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
import { VoxelGrid } from "./grid/VoxelGrid.js";
|
||||
import { VoxelManager } from "./grid/VoxelManager.js";
|
||||
|
||||
// --- 1. Define the Game Viewport Component using Lit ---
|
||||
class GameViewport extends LitElement {
|
||||
export class GameViewport extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
|
@ -14,66 +12,114 @@ class GameViewport extends LitElement {
|
|||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
#canvas-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.renderer = null;
|
||||
this.voxelGrid = null;
|
||||
this.voxelManager = null;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
// Initialize Three.js scene after the component renders
|
||||
this.initThreeJS();
|
||||
this.initGameWorld();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
initThreeJS() {
|
||||
const container = this.shadowRoot.getElementById("canvas-container");
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x0a0b10); // Void Black
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
// Scene Setup
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x0a0b10);
|
||||
|
||||
// Lighting (Essential for LambertMaterial)
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 1.5); // Soft white light
|
||||
this.scene.add(ambientLight);
|
||||
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
dirLight.position.set(10, 20, 10);
|
||||
this.scene.add(dirLight);
|
||||
|
||||
// Camera
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
45,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true }); // Enable AA for smoother edges
|
||||
this.camera.position.set(20, 20, 20);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
|
||||
// Set initial size
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
container.appendChild(renderer.domElement);
|
||||
// Renderer
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
container.appendChild(this.renderer.domElement);
|
||||
|
||||
// Add a rotating Voxel (Cube)
|
||||
const geometry = new THREE.BoxGeometry();
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0x00f0ff,
|
||||
wireframe: true,
|
||||
});
|
||||
const cube = new THREE.Mesh(geometry, material);
|
||||
scene.add(cube);
|
||||
|
||||
camera.position.z = 5;
|
||||
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate);
|
||||
cube.rotation.x += 0.01;
|
||||
cube.rotation.y += 0.01;
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
animate();
|
||||
// Controls
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
|
||||
// Handle Resize
|
||||
window.addEventListener("resize", () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
window.addEventListener("resize", this.onWindowResize.bind(this));
|
||||
}
|
||||
|
||||
initGameWorld() {
|
||||
// 1. Create Data Grid
|
||||
this.voxelGrid = new VoxelGrid(16, 8, 16);
|
||||
|
||||
// 2. Generate Test Terrain (Simple Flat Floor + Random Pillars)
|
||||
for (let x = 0; x < 16; x++) {
|
||||
for (let z = 0; z < 16; z++) {
|
||||
// Base Floor (Stone)
|
||||
this.voxelGrid.setVoxel(x, 0, z, 1);
|
||||
|
||||
// Random Details (Dirt/Grass)
|
||||
if (Math.random() > 0.8) {
|
||||
this.voxelGrid.setVoxel(x, 1, z, 2); // Dirt Mound
|
||||
}
|
||||
|
||||
// Aether Crystal Pillar
|
||||
if (x === 8 && z === 8) {
|
||||
this.voxelGrid.setVoxel(x, 1, z, 4);
|
||||
this.voxelGrid.setVoxel(x, 2, z, 4);
|
||||
this.voxelGrid.setVoxel(x, 3, z, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Initialize Visual Manager
|
||||
this.voxelManager = new VoxelManager(this.voxelGrid, this.scene);
|
||||
}
|
||||
|
||||
animate() {
|
||||
requestAnimationFrame(this.animate.bind(this));
|
||||
|
||||
this.controls.update();
|
||||
|
||||
// Update Voxels if dirty
|
||||
if (this.voxelManager) this.voxelManager.update();
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
onWindowResize() {
|
||||
if (!this.camera || !this.renderer) return;
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <div id="canvas-container"></div> `;
|
||||
return html`<div id="canvas-container"></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Register the web component
|
||||
customElements.define("game-viewport", GameViewport);
|
||||
|
|
|
|||
72
src/grid/VoxelGrid.js
Normal file
72
src/grid/VoxelGrid.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* VoxelGrid.js
|
||||
* The pure data representation of the game world.
|
||||
* Uses a flat Uint8Array for high-performance memory access.
|
||||
*/
|
||||
export class VoxelGrid {
|
||||
constructor(width, height, depth) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.depth = depth;
|
||||
|
||||
// 0 = Air, 1+ = Solid Blocks
|
||||
this.cells = new Uint8Array(width * height * depth);
|
||||
this.dirty = true; // Flag to tell Renderer to update
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts 3D coordinates to a flat array index.
|
||||
*/
|
||||
getIndex(x, y, z) {
|
||||
return y * this.width * this.depth + z * this.width + x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if coordinates are inside the grid dimensions.
|
||||
*/
|
||||
isValidBounds(x, y, z) {
|
||||
return (
|
||||
x >= 0 &&
|
||||
x < this.width &&
|
||||
y >= 0 &&
|
||||
y < this.height &&
|
||||
z >= 0 &&
|
||||
z < this.depth
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a voxel ID at the specified position.
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} z
|
||||
* @param {number} typeId - 0 for Air, integers for material types
|
||||
*/
|
||||
setVoxel(x, y, z, typeId) {
|
||||
if (!this.isValidBounds(x, y, z)) return;
|
||||
|
||||
const index = this.getIndex(x, y, z);
|
||||
|
||||
// Only update if changed to save render cycles
|
||||
if (this.cells[index] !== typeId) {
|
||||
this.cells[index] = typeId;
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the voxel ID at the specified position.
|
||||
* Returns 0 (Air) if out of bounds.
|
||||
*/
|
||||
getVoxel(x, y, z) {
|
||||
if (!this.isValidBounds(x, y, z)) return 0;
|
||||
return this.cells[this.getIndex(x, y, z)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Checks if a voxel is solid (non-air).
|
||||
*/
|
||||
isSolid(x, y, z) {
|
||||
return this.getVoxel(x, y, z) !== 0;
|
||||
}
|
||||
}
|
||||
92
src/grid/VoxelManager.js
Normal file
92
src/grid/VoxelManager.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import * as THREE from "three";
|
||||
|
||||
/**
|
||||
* VoxelManager.js
|
||||
* Handles the Three.js rendering of the VoxelGrid.
|
||||
* Uses InstancedMesh for performance (1 draw call for 10,000 blocks).
|
||||
*/
|
||||
export class VoxelManager {
|
||||
constructor(grid, scene) {
|
||||
this.grid = grid;
|
||||
this.scene = scene;
|
||||
this.mesh = null;
|
||||
|
||||
// Define Material Palette (ID -> Color)
|
||||
this.palette = [
|
||||
null, // 0: Air (Invisible)
|
||||
new THREE.Color(0x888888), // 1: Stone (Grey)
|
||||
new THREE.Color(0x8b4513), // 2: Dirt (Brown)
|
||||
new THREE.Color(0x228b22), // 3: Grass (Green)
|
||||
new THREE.Color(0x00f0ff), // 4: Aether Crystal (Cyan)
|
||||
];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Create geometry once
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
||||
|
||||
// Basic material allows for coloring individual instances
|
||||
const material = new THREE.MeshLambertMaterial({ color: 0xffffff });
|
||||
|
||||
// Calculate max capacity based on grid size
|
||||
const count = this.grid.width * this.grid.height * this.grid.depth;
|
||||
|
||||
// Create the InstancedMesh
|
||||
this.mesh = new THREE.InstancedMesh(geometry, material, count);
|
||||
this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
||||
|
||||
// Add to scene
|
||||
this.scene.add(this.mesh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the visual mesh based on the grid data.
|
||||
* Call this in the game loop if grid.dirty is true.
|
||||
*/
|
||||
update() {
|
||||
if (!this.grid.dirty) return;
|
||||
|
||||
let instanceId = 0;
|
||||
const dummy = new THREE.Object3D();
|
||||
|
||||
for (let y = 0; y < this.grid.height; y++) {
|
||||
for (let z = 0; z < this.grid.depth; z++) {
|
||||
for (let x = 0; x < this.grid.width; x++) {
|
||||
const typeId = this.grid.getVoxel(x, y, z);
|
||||
|
||||
if (typeId !== 0) {
|
||||
// Position the dummy object
|
||||
dummy.position.set(x, y, z);
|
||||
dummy.updateMatrix();
|
||||
|
||||
// Update the instance matrix
|
||||
this.mesh.setMatrixAt(instanceId, dummy.matrix);
|
||||
|
||||
// Update the instance color based on ID
|
||||
const color = this.palette[typeId] || new THREE.Color(0xff00ff); // Magenta = Error
|
||||
this.mesh.setColorAt(instanceId, color);
|
||||
|
||||
instanceId++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide unused instances by scaling them to zero (or moving them to infinity)
|
||||
// For simplicity in this prototype, we define count as max possible,
|
||||
// but in production, we would manage the count property more dynamically.
|
||||
this.mesh.count = instanceId;
|
||||
|
||||
this.mesh.instanceMatrix.needsUpdate = true;
|
||||
|
||||
// instanceColor is lazy-created by Three.js only when setColorAt is called.
|
||||
// If the grid is empty, instanceColor might be null.
|
||||
if (this.mesh.instanceColor) {
|
||||
this.mesh.instanceColor.needsUpdate = true;
|
||||
}
|
||||
|
||||
this.grid.dirty = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -13,17 +13,6 @@
|
|||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- IMPORT MAP: Defines module locations -->
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
||||
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
|
||||
"lit": "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
/* Palette Definition */
|
||||
|
|
|
|||
96
test/grid/VoxelGrid.test.js
Normal file
96
test/grid/VoxelGrid.test.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { expect } from "@esm-bundle/chai";
|
||||
import { VoxelGrid } from "../../src/grid/VoxelGrid.js";
|
||||
|
||||
describe("Phase 1: VoxelGrid Data Structure", () => {
|
||||
let grid;
|
||||
const width = 10;
|
||||
const height = 10;
|
||||
const depth = 10;
|
||||
|
||||
beforeEach(() => {
|
||||
grid = new VoxelGrid(width, height, depth);
|
||||
});
|
||||
|
||||
it("CoA 1: Should initialize with correct dimensions and empty cells", () => {
|
||||
expect(grid.width).to.equal(width);
|
||||
expect(grid.height).to.equal(height);
|
||||
expect(grid.depth).to.equal(depth);
|
||||
expect(grid.cells).to.be.instanceOf(Uint8Array);
|
||||
expect(grid.cells.length).to.equal(width * height * depth);
|
||||
|
||||
// Verify all are 0 (Air)
|
||||
const isAllZero = grid.cells.every((val) => val === 0);
|
||||
expect(isAllZero).to.be.true;
|
||||
});
|
||||
|
||||
it("CoA 2: Should correctly validate bounds", () => {
|
||||
// Inside bounds
|
||||
expect(grid.isValidBounds(0, 0, 0)).to.be.true;
|
||||
expect(grid.isValidBounds(5, 5, 5)).to.be.true;
|
||||
expect(grid.isValidBounds(width - 1, height - 1, depth - 1)).to.be.true;
|
||||
|
||||
// Outside bounds (Negative)
|
||||
expect(grid.isValidBounds(-1, 0, 0)).to.be.false;
|
||||
expect(grid.isValidBounds(0, -1, 0)).to.be.false;
|
||||
expect(grid.isValidBounds(0, 0, -1)).to.be.false;
|
||||
|
||||
// Outside bounds (Overflow)
|
||||
expect(grid.isValidBounds(width, 0, 0)).to.be.false;
|
||||
expect(grid.isValidBounds(0, height, 0)).to.be.false;
|
||||
expect(grid.isValidBounds(0, 0, depth)).to.be.false;
|
||||
});
|
||||
|
||||
it("CoA 3: Should store and retrieve voxel IDs", () => {
|
||||
const x = 2,
|
||||
y = 3,
|
||||
z = 4;
|
||||
const typeId = 5; // Arbitrary material ID
|
||||
|
||||
grid.setVoxel(x, y, z, typeId);
|
||||
|
||||
const result = grid.getVoxel(x, y, z);
|
||||
expect(result).to.equal(typeId);
|
||||
});
|
||||
|
||||
it("CoA 4: Should return 0 (Air) for out-of-bounds getVoxel", () => {
|
||||
expect(grid.getVoxel(-5, 0, 0)).to.equal(0);
|
||||
expect(grid.getVoxel(100, 100, 100)).to.equal(0);
|
||||
});
|
||||
|
||||
it("CoA 5: Should handle isSolid checks", () => {
|
||||
grid.setVoxel(1, 1, 1, 1); // Solid
|
||||
grid.setVoxel(2, 2, 2, 0); // Air
|
||||
|
||||
expect(grid.isSolid(1, 1, 1)).to.be.true;
|
||||
expect(grid.isSolid(2, 2, 2)).to.be.false;
|
||||
expect(grid.isSolid(-1, -1, -1)).to.be.false; // Out of bounds is not solid
|
||||
});
|
||||
|
||||
it("CoA 6: Should manage the dirty flag correctly", () => {
|
||||
// Initial state
|
||||
expect(grid.dirty).to.be.true;
|
||||
|
||||
// Reset flag manually (simulating a render cycle completion)
|
||||
grid.dirty = false;
|
||||
|
||||
// Set voxel to SAME value
|
||||
grid.setVoxel(0, 0, 0, 0);
|
||||
expect(grid.dirty).to.be.false; // Should not dirty if value didn't change
|
||||
|
||||
// Set voxel to NEW value
|
||||
grid.setVoxel(0, 0, 0, 1);
|
||||
expect(grid.dirty).to.be.true; // Should be dirty now
|
||||
});
|
||||
|
||||
it("CoA 7: Should calculate flat array index correctly", () => {
|
||||
// Based on logic: (y * width * depth) + (z * width) + x
|
||||
// x=1, y=0, z=0 -> 1
|
||||
expect(grid.getIndex(1, 0, 0)).to.equal(1);
|
||||
|
||||
// x=0, y=0, z=1 -> 1 * 10 = 10
|
||||
expect(grid.getIndex(0, 0, 1)).to.equal(10);
|
||||
|
||||
// x=0, y=1, z=0 -> 1 * 10 * 10 = 100
|
||||
expect(grid.getIndex(0, 1, 0)).to.equal(100);
|
||||
});
|
||||
});
|
||||
124
test/grid/VoxelManager.test.js
Normal file
124
test/grid/VoxelManager.test.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { expect } from "@esm-bundle/chai";
|
||||
import * as THREE from "three";
|
||||
import sinon from "sinon";
|
||||
import { VoxelManager } from "../../src/grid/VoxelManager.js";
|
||||
import { VoxelGrid } from "../../src/grid/VoxelGrid.js";
|
||||
|
||||
describe("Phase 1: VoxelManager (Renderer)", () => {
|
||||
let grid;
|
||||
let scene;
|
||||
let manager;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup a basic scene and grid
|
||||
scene = new THREE.Scene();
|
||||
grid = new VoxelGrid(4, 4, 4); // Small 4x4x4 grid
|
||||
manager = new VoxelManager(grid, scene);
|
||||
});
|
||||
|
||||
it("CoA 1: Should initialize and add InstancedMesh to the scene", () => {
|
||||
// Check if something was added to the scene
|
||||
expect(scene.children.length).to.equal(1);
|
||||
|
||||
const mesh = scene.children[0];
|
||||
expect(mesh).to.be.instanceOf(THREE.InstancedMesh);
|
||||
|
||||
// Max capacity should match grid size
|
||||
expect(mesh.count).to.equal(4 * 4 * 4);
|
||||
});
|
||||
|
||||
it("CoA 2: Should update visual instances based on grid data", () => {
|
||||
// 1. Setup Data: Place 3 voxels
|
||||
grid.setVoxel(0, 0, 0, 1); // Stone
|
||||
grid.setVoxel(1, 0, 0, 2); // Dirt
|
||||
grid.setVoxel(0, 1, 0, 3); // Grass
|
||||
|
||||
// 2. Act: Trigger Update
|
||||
// (VoxelGrid sets dirty=true automatically on setVoxel)
|
||||
manager.update();
|
||||
|
||||
// 3. Assert: Mesh count should represent only ACTIVE voxels
|
||||
// Note: In the implementation, we update `mesh.count` to the number of visible instances
|
||||
const mesh = manager.mesh;
|
||||
expect(mesh.count).to.equal(3);
|
||||
});
|
||||
|
||||
it("CoA 3: Should respect the Dirty Flag (Performance)", () => {
|
||||
// Setup: Add a voxel so instanceColor buffer is initialized by Three.js
|
||||
grid.setVoxel(0, 0, 0, 1);
|
||||
|
||||
const mesh = manager.mesh;
|
||||
|
||||
// 1. First Update (Dirty is true by default)
|
||||
manager.update();
|
||||
expect(grid.dirty).to.be.false;
|
||||
|
||||
// Capture current versions. Three.js increments these when needsUpdate = true.
|
||||
const matrixVersion = mesh.instanceMatrix.version;
|
||||
// instanceColor might be null if no color set logic ran, but manager.update() ensures it if voxels exist.
|
||||
const colorVersion = mesh.instanceColor ? mesh.instanceColor.version : -1;
|
||||
|
||||
// 2. Call Update again (Dirty is false)
|
||||
manager.update();
|
||||
|
||||
// 3. Assert: Versions should NOT have incremented
|
||||
expect(mesh.instanceMatrix.version).to.equal(matrixVersion);
|
||||
if (mesh.instanceColor) {
|
||||
expect(mesh.instanceColor.version).to.equal(colorVersion);
|
||||
}
|
||||
|
||||
// 4. Change data -> Dirty becomes true
|
||||
grid.setVoxel(3, 3, 3, 1);
|
||||
expect(grid.dirty).to.be.true;
|
||||
|
||||
// 5. Update again
|
||||
manager.update();
|
||||
|
||||
// 6. Assert: Versions SHOULD increment
|
||||
expect(mesh.instanceMatrix.version).to.be.greaterThan(matrixVersion);
|
||||
if (mesh.instanceColor) {
|
||||
expect(mesh.instanceColor.version).to.be.greaterThan(colorVersion);
|
||||
}
|
||||
});
|
||||
|
||||
it("CoA 4: Should apply correct colors from palette", () => {
|
||||
// ID 4 is Cyan (Aether Crystal) based on Manager code
|
||||
grid.setVoxel(0, 0, 0, 4);
|
||||
manager.update();
|
||||
|
||||
const mesh = manager.mesh;
|
||||
const color = new THREE.Color();
|
||||
|
||||
// Get color of the first instance (index 0)
|
||||
mesh.getColorAt(0, color);
|
||||
|
||||
// Check against the palette definition in VoxelManager
|
||||
const expectedColor = new THREE.Color(0x00f0ff); // Cyan
|
||||
|
||||
expect(color.r).to.be.closeTo(expectedColor.r, 0.01);
|
||||
expect(color.g).to.be.closeTo(expectedColor.g, 0.01);
|
||||
expect(color.b).to.be.closeTo(expectedColor.b, 0.01);
|
||||
});
|
||||
|
||||
it("CoA 5: Should position instances correctly in 3D space", () => {
|
||||
const targetX = 2;
|
||||
const targetY = 3;
|
||||
const targetZ = 1;
|
||||
|
||||
grid.setVoxel(targetX, targetY, targetZ, 1);
|
||||
manager.update();
|
||||
|
||||
const mesh = manager.mesh;
|
||||
const matrix = new THREE.Matrix4();
|
||||
|
||||
// Get matrix of first instance
|
||||
mesh.getMatrixAt(0, matrix);
|
||||
|
||||
const position = new THREE.Vector3();
|
||||
position.setFromMatrixPosition(matrix);
|
||||
|
||||
expect(position.x).to.equal(targetX);
|
||||
expect(position.y).to.equal(targetY);
|
||||
expect(position.z).to.equal(targetZ);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue