Skip to content

Commit

Permalink
feat(core): Add Mesh.clone() (#872)
Browse files Browse the repository at this point in the history
  • Loading branch information
jespertheend authored Feb 20, 2024
1 parent 1c0f2c4 commit 5c80527
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 103 deletions.
36 changes: 26 additions & 10 deletions src/core/Mesh.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import {MeshAttributeBuffer} from "./MeshAttributeBuffer.js";
/** @typedef {() => void} OnIndexBufferChangeCallback */

export class Mesh {
/** @type {MeshAttributeBuffer[]} */
#buffers = [];
/** @type {Map<number, MeshAttributeBuffer>} */
#unusedBuffers = new Map();

constructor() {
/** @private @type {MeshAttributeBuffer[]} */
this._buffers = [];
/** @private @type {Map<number, MeshAttributeBuffer>} */
this._unusedBuffers = new Map();
/** @private @type {import("../rendering/VertexState.js").VertexState?} */
this._vertexState = null;
this.indexBuffer = new ArrayBuffer(0);
Expand Down Expand Up @@ -312,7 +313,7 @@ export class Mesh {
isUnused: true,
});
unusedBuffer.setVertexCount(this.vertexCount);
this._unusedBuffers.set(attributeType, unusedBuffer);
this.#unusedBuffers.set(attributeType, unusedBuffer);
return unusedBuffer;
}

Expand All @@ -321,11 +322,11 @@ export class Mesh {
* @returns {Generator<MeshAttributeBuffer>}
*/
*getBuffers(includeUnused = true) {
for (const buffer of this._buffers) {
for (const buffer of this.#buffers) {
yield buffer;
}
if (includeUnused) {
for (const buffer of this._unusedBuffers.values()) {
for (const buffer of this.#unusedBuffers.values()) {
yield buffer;
}
}
Expand All @@ -342,8 +343,8 @@ export class Mesh {
this._vertexState = vertexState;

const oldBuffers = Array.from(this.getBuffers());
this._buffers = [];
this._unusedBuffers.clear();
this.#buffers = [];
this.#unusedBuffers.clear();

if (vertexState) {
for (const bufferDescriptor of vertexState.buffers) {
Expand All @@ -362,7 +363,7 @@ export class Mesh {
attributes,
});
if (this.vertexCount) buffer.setVertexCount(this.vertexCount);
this._buffers.push(buffer);
this.#buffers.push(buffer);
}
}

Expand All @@ -371,6 +372,21 @@ export class Mesh {
}
}

clone() {
const newMesh = new Mesh();
newMesh.setVertexState(this._vertexState);
newMesh.setVertexCount(this.vertexCount);
for (const [i, buffer] of this.#buffers.entries()) {
newMesh.#buffers[i] = buffer.clone();
}
for (const [attributeType, buffer] of this.#unusedBuffers) {
newMesh.#unusedBuffers.set(attributeType, buffer.clone());
}
newMesh.setIndexFormat(this.indexFormat);
newMesh.setIndexData(this.indexBuffer);
return newMesh;
}

/**
* @param {OnIndexBufferChangeCallback} cb
*/
Expand Down
12 changes: 12 additions & 0 deletions src/core/MeshAttributeBuffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export class MeshAttributeBuffer {
}

/**
* Sets the array stride of the attributes in the buffer.
* Set to `null` to infer an array stride from the currently provided attributes.
* @param {number?} arrayStride
*/
setArrayStride(arrayStride) {
Expand Down Expand Up @@ -311,6 +313,16 @@ export class MeshAttributeBuffer {
}
}

clone() {
const newBuffer = new MeshAttributeBuffer(this.mesh, {
arrayStride: this.arrayStride,
attributes: structuredClone(this.attributes),
arrayBuffer: structuredClone(this.buffer),
isUnused: this.isUnused,
});
return newBuffer;
}

/**
* @param {OnBufferChangedCallback} cb
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {assertEquals, assertNotStrictEquals, assertStrictEquals, assertThrows} from "std/testing/asserts.ts";
import {Mesh, Vec3} from "../../../../src/mod.js";
import {Mesh, Vec3} from "../../../../../src/mod.js";
import {mockVertexStateSingleAttribute, mockVertexStateTwoAttributes} from "./shared.js";

Deno.test({
name: "Mesh should have an index format of UINT16 by default",
Expand Down Expand Up @@ -261,54 +262,6 @@ Deno.test({
},
});

class FakeVertexState {
/**
* @param {unknown[]} buffers
*/
constructor(buffers) {
this.buffers = buffers;
}
}
const mockVertexStateSingleAttribute = /** @type {import("../../../../src/mod.js").VertexState} */ (new FakeVertexState([
{
attributes: new Map([
[
Mesh.AttributeType.POSITION,
{
attributeType: Mesh.AttributeType.POSITION,
offset: 0,
format: Mesh.AttributeFormat.FLOAT32,
componentCount: 3,
},
],
]),
},
]));
const mockVertexStateTwoAttributes = /** @type {import("../../../../src/mod.js").VertexState} */ (new FakeVertexState([
{
attributes: new Map([
[
Mesh.AttributeType.POSITION,
{
attributeType: Mesh.AttributeType.POSITION,
offset: 0,
format: Mesh.AttributeFormat.FLOAT32,
componentCount: 3,
},
],
[
Mesh.AttributeType.NORMAL,
{
attributeType: Mesh.AttributeType.NORMAL,
offset: 12,
format: Mesh.AttributeFormat.FLOAT32,
componentCount: 3,
},
],
]),
},
]));

Deno.test({
name: "setVertexCount() with attributes from VertexState",
fn() {
Expand Down
88 changes: 88 additions & 0 deletions test/unit/src/core/Mesh/clone.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {assertEquals, assertStrictEquals} from "std/testing/asserts.ts";
import {Mesh, Vec3} from "../../../../../src/mod.js";
import {FakeVertexState, mockVertexStateSingleAttribute} from "./shared.js";
import {assertVecAlmostEquals} from "../../../shared/asserts.js";

Deno.test({
name: "maintains the same vertexState instance",
fn() {
const mesh = new Mesh();
const vertexState = /** @type {import("../../../../../src/mod.js").VertexState} */ (new FakeVertexState([]));
mesh.setVertexState(vertexState);

const clone = mesh.clone();
assertStrictEquals(clone.vertexState, vertexState);
},
});

Deno.test({
name: "clones used buffers",
fn() {
const mesh = new Mesh();
mesh.setVertexState(mockVertexStateSingleAttribute);
mesh.setVertexCount(3);
mesh.setVertexData(Mesh.AttributeType.POSITION, [new Vec3(1, 2, 3)]);

const clone = mesh.clone();
const positionBuffer1 = clone.getBufferForAttributeType(Mesh.AttributeType.POSITION);
const vertexData1 = Array.from(positionBuffer1.getVertexData(Mesh.AttributeType.POSITION));
assertVecAlmostEquals(vertexData1[0], [1, 2, 3]);

mesh.setVertexData(Mesh.AttributeType.POSITION, [new Vec3(4, 5, 6)]);
const positionBuffer2 = clone.getBufferForAttributeType(Mesh.AttributeType.POSITION);
const vertexData2 = Array.from(positionBuffer2.getVertexData(Mesh.AttributeType.POSITION));
assertVecAlmostEquals(vertexData2[0], [1, 2, 3]);
},
});

Deno.test({
name: "clones unused buffers",
fn() {
const mesh = new Mesh();
mesh.setVertexCount(3);
mesh.setVertexData(Mesh.AttributeType.POSITION, [new Vec3(1, 2, 3)]);

const clone = mesh.clone();
const buffers1 = Array.from(clone.getBuffers());
assertEquals(buffers1.length, 1);
const vertexData1 = Array.from(buffers1[0].getVertexData(Mesh.AttributeType.POSITION));
assertVecAlmostEquals(vertexData1[0], [1, 2, 3]);

mesh.setVertexData(Mesh.AttributeType.POSITION, [new Vec3(4, 5, 6)]);
const buffers2 = Array.from(clone.getBuffers());
assertEquals(buffers2.length, 1);
const vertexData2 = Array.from(buffers2[0].getVertexData(Mesh.AttributeType.POSITION));
assertVecAlmostEquals(vertexData2[0], [1, 2, 3]);
},
});

Deno.test({
name: "clones the index buffer",
fn() {
const mesh = new Mesh();
mesh.setIndexFormat(Mesh.IndexFormat.UINT_32);
mesh.setIndexData([1, 2, 3]);

const clone = mesh.clone();
assertEquals(Array.from(clone.getIndexData()), [1, 2, 3]);
assertEquals(clone.indexFormat, Mesh.IndexFormat.UINT_32);
assertEquals(clone.indexCount, 3);

mesh.setIndexData([4, 5, 6]);
assertEquals(Array.from(clone.getIndexData()), [1, 2, 3]);
},
});

Deno.test({
name: "clones the vertex count",
fn() {
const mesh = new Mesh();
mesh.setVertexCount(3);

const clone = mesh.clone();
assertEquals(clone.vertexCount, 3);

mesh.setVertexCount(4);
assertEquals(clone.vertexCount, 3);
},
});
50 changes: 50 additions & 0 deletions test/unit/src/core/Mesh/shared.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {Mesh} from "../../../../../src/mod.js";

export class FakeVertexState {
/**
* @param {unknown[]} buffers
*/
constructor(buffers) {
this.buffers = buffers;
}
}

export const mockVertexStateSingleAttribute = /** @type {import("../../../../../src/mod.js").VertexState} */ (new FakeVertexState([
{
attributes: new Map([
[
Mesh.AttributeType.POSITION,
{
attributeType: Mesh.AttributeType.POSITION,
offset: 0,
format: Mesh.AttributeFormat.FLOAT32,
componentCount: 3,
},
],
]),
},
]));
export const mockVertexStateTwoAttributes = /** @type {import("../../../../../src/mod.js").VertexState} */ (new FakeVertexState([
{
attributes: new Map([
[
Mesh.AttributeType.POSITION,
{
attributeType: Mesh.AttributeType.POSITION,
offset: 0,
format: Mesh.AttributeFormat.FLOAT32,
componentCount: 3,
},
],
[
Mesh.AttributeType.NORMAL,
{
attributeType: Mesh.AttributeType.NORMAL,
offset: 12,
format: Mesh.AttributeFormat.FLOAT32,
componentCount: 3,
},
],
]),
},
]));
Loading

0 comments on commit 5c80527

Please sign in to comment.