Skip to content

Commit 8989a96

Browse files
authored
feat(voxel-renderer): Allow to copy (clone) an existing layer (#269)
1 parent 4d08bcb commit 8989a96

File tree

8 files changed

+108
-5
lines changed

8 files changed

+108
-5
lines changed

.changeset/fruity-phones-pump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@jolly-pixel/voxel.renderer": minor
3+
---
4+
5+
feat(voxel-renderer): Allow to copy (clone) an existing layer

packages/voxel-renderer/src/VoxelRenderer.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import type { TilesetLoader } from "./tileset/TilesetLoader.ts";
4141
import { VoxelWorld } from "./world/VoxelWorld.ts";
4242
import {
4343
VoxelLayer,
44-
type VoxelLayerConfigurableOptions
44+
type VoxelLayerConfigurableOptions,
45+
type VoxelLayerOptions
4546
} from "./world/VoxelLayer.ts";
4647
import { VoxelChunk } from "./world/VoxelChunk.ts";
4748
import type { VoxelEntry, VoxelCoord } from "./world/types.ts";
@@ -51,7 +52,7 @@ import type {
5152
VoxelLayerHookListener,
5253
VoxelLayerHookEvent
5354
} from "./hooks.ts";
54-
import type { VoxelSetOptions, VoxelRemoveOptions } from "./types.ts";
55+
import type { VoxelSetOptions, VoxelRemoveOptions, PartialExcept } from "./types.ts";
5556

5657
export type { VoxelSetOptions, VoxelRemoveOptions };
5758

@@ -561,6 +562,21 @@ export class VoxelRenderer extends ActorComponent {
561562
return this.world.getLayer(name);
562563
}
563564

565+
cloneLayer(name: string, options: PartialExcept<VoxelLayerOptions, "name">): VoxelLayer | undefined {
566+
const clone = this.world.cloneLayer(name, options);
567+
if (!clone) {
568+
return undefined;
569+
}
570+
571+
this.#emitHook({
572+
action: "cloned",
573+
layerName: name,
574+
metadata: { options }
575+
});
576+
577+
return clone;
578+
}
579+
564580
addLayer(
565581
name: string,
566582
options: VoxelLayerConfigurableOptions = {}

packages/voxel-renderer/src/hooks.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
import type { Vector3Like } from "three";
33

44
// Import Internal Dependencies
5-
import type { VoxelLayerConfigurableOptions } from "./world/VoxelLayer.ts";
5+
import type { VoxelLayerConfigurableOptions, VoxelLayerOptions } from "./world/VoxelLayer.ts";
66
import type { VoxelCoord } from "./world/types.ts";
77
import type {
88
VoxelObjectLayerJSON,
99
VoxelObjectJSON
1010
} from "./serialization/VoxelSerializer.ts";
11-
import type { VoxelSetOptions, VoxelRemoveOptions } from "./types.ts";
11+
import type { VoxelSetOptions, VoxelRemoveOptions, PartialExcept } from "./types.ts";
1212

1313
export type VoxelLayerHookEvent =
1414
| {
@@ -30,6 +30,13 @@ export type VoxelLayerHookEvent =
3030
options: Partial<VoxelLayerConfigurableOptions>;
3131
};
3232
}
33+
| {
34+
action: "cloned";
35+
layerName: string;
36+
metadata: {
37+
options: PartialExcept<VoxelLayerOptions, "name">;
38+
};
39+
}
3340
| {
3441
action: "offset-updated";
3542
layerName: string;

packages/voxel-renderer/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ export interface VoxelSetOptions {
1717
export interface VoxelRemoveOptions {
1818
position: Vector3Like;
1919
}
20+
21+
export type PartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>;

packages/voxel-renderer/src/world/VoxelLayer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,12 @@ export class VoxelLayer {
368368
voxels: this.#exportVoxels()
369369
};
370370
}
371+
372+
clone(opts: Partial<VoxelLayerOptions> = {}): VoxelLayer {
373+
return new VoxelLayer({
374+
chunkSize: this.#chunkSize,
375+
...this.toJSON(),
376+
...opts
377+
});
378+
}
371379
}

packages/voxel-renderer/src/world/VoxelWorld.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { Vector3Like } from "three";
44
// Import Internal Dependencies
55
import {
66
VoxelLayer,
7-
type VoxelLayerConfigurableOptions
7+
type VoxelLayerConfigurableOptions,
8+
type VoxelLayerOptions
89
} from "./VoxelLayer.ts";
910
import { VoxelChunk, DEFAULT_CHUNK_SIZE } from "./VoxelChunk.ts";
1011
import type { VoxelEntry, VoxelCoord } from "./types.ts";
@@ -14,6 +15,7 @@ import type {
1415
VoxelObjectJSON,
1516
VoxelObjectLayerJSON
1617
} from "../serialization/VoxelSerializer.ts";
18+
import type { PartialExcept } from "../types.ts";
1719

1820
// CONSTANTS
1921
let kLayerIdCounter = 0;
@@ -185,6 +187,18 @@ export class VoxelWorld {
185187
);
186188
}
187189

190+
cloneLayer(name: string, options: PartialExcept<VoxelLayerOptions, "name">): VoxelLayer | undefined {
191+
const layer = this.getLayer(name);
192+
if (!layer) {
193+
return undefined;
194+
}
195+
196+
const clone = layer.clone({ ...options, id: `${layer.id}_${kLayerIdCounter++}` });
197+
this.#layers.push(clone);
198+
199+
return clone;
200+
}
201+
188202
// --- Object layer management --- //
189203

190204
addObjectLayer(

packages/voxel-renderer/test/world/VoxelLayer.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,20 @@ describe("getChunks", () => {
199199
assert.equal(chunks.length, 2);
200200
});
201201
});
202+
203+
describe("clone", () => {
204+
it("should clone a layer", () => {
205+
const layer = makeLayer({ chunkSize: 4 });
206+
const clone = layer.clone();
207+
assert.deepEqual(clone.toJSON(), layer.toJSON());
208+
assert.notEqual(clone, layer);
209+
});
210+
211+
it("should be able to overide or add value on the fly", () => {
212+
const layer = makeLayer({ chunkSize: 4 });
213+
const clone = layer.clone({ visible: false, name: "Cloned" });
214+
assert.deepEqual(clone.toJSON(), {
215+
...layer.toJSON(), visible: false, name: "Cloned"
216+
});
217+
});
218+
});

packages/voxel-renderer/test/world/VoxelWorld.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import assert from "node:assert/strict";
44

55
// Import Internal Dependencies
66
import { VoxelWorld } from "../../src/world/VoxelWorld.ts";
7+
import { VoxelLayer } from "../../src/world/VoxelLayer.ts";
78
import { FACE } from "../../src/utils/math.ts";
89

910
function makeEntry(blockId = 1, transform = 0) {
@@ -298,3 +299,36 @@ describe("VoxelWorld chunkSize", () => {
298299
assert.equal(world.chunkSize, 8);
299300
});
300301
});
302+
303+
describe("VoxelWorld clone", () => {
304+
it("shouldn't clone a layer that doesn't exist", () => {
305+
const world = new VoxelWorld(8);
306+
assert.equal(world.cloneLayer("A", { name: "A_1" }), undefined);
307+
assert.equal(world.getLayer("A"), undefined);
308+
});
309+
it("should clone a layer", () => {
310+
const world = new VoxelWorld(8);
311+
world.addLayer("A");
312+
const layer = removeId(world.getLayer("A")!);
313+
const expected = { ...layer, name: "A_1" };
314+
const clone = removeId(world.cloneLayer("A", { name: "A_1" })!);
315+
assert.deepEqual(clone, expected);
316+
assert.deepEqual(removeId(world.getLayer("A_1")!), expected);
317+
});
318+
319+
it("should be able to override or add properties", () => {
320+
const world = new VoxelWorld(8);
321+
world.addLayer("A");
322+
const layer = removeId(world.getLayer("A")!);
323+
const expected = { ...layer, name: "A_1", visible: false };
324+
const clone = removeId(world.cloneLayer("A", { name: "A_1", visible: false })!);
325+
assert.deepEqual(clone, expected);
326+
assert.deepEqual(removeId(world.getLayer("A_1")!), expected);
327+
});
328+
329+
function removeId(layer: VoxelLayer) {
330+
const { id, ...rest } = layer.toJSON();
331+
332+
return rest;
333+
}
334+
});

0 commit comments

Comments
 (0)