Skip to content

Commit 7659f64

Browse files
authored
feat(voxel-renderer): implement APIs for object layers (#232)
1 parent 0dc63ec commit 7659f64

File tree

9 files changed

+369
-17
lines changed

9 files changed

+369
-17
lines changed

.changeset/sour-ghosts-feel.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+
Implement new APIs to manage object layers

packages/voxel-renderer/docs/Hooks.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,21 @@ const vr = new VoxelRenderer({
2626

2727
```ts
2828
export type VoxelLayerHookAction =
29-
| "added"
30-
| "removed"
31-
| "updated"
32-
| "offset-updated"
33-
| "voxel-set"
34-
| "voxel-removed"
35-
| "reordered";
29+
// Voxel-layer actions
30+
| "added" // a voxel layer was created
31+
| "removed" // a voxel layer was deleted
32+
| "updated" // layer properties (visibility, …) changed
33+
| "offset-updated" // layer world offset changed
34+
| "voxel-set" // a voxel was placed in a layer
35+
| "voxel-removed" // a voxel was removed from a layer
36+
| "reordered" // layer render order changed
37+
// Object-layer actions
38+
| "object-layer-added" // a new object layer was created
39+
| "object-layer-removed" // an object layer was deleted
40+
| "object-layer-updated" // object layer properties (e.g. visibility) changed
41+
| "object-added" // an object was added to an object layer
42+
| "object-removed" // an object was removed from an object layer
43+
| "object-updated"; // an object's properties were patched
3644

3745
// Describes a change related to a layer.
3846
export interface VoxelLayerHookEvent {

packages/voxel-renderer/docs/Serialization.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,21 @@ interface VoxelLayerJSON {
4646
* and are loaded as if offset is {0,0,0} — identical to the previous behaviour.
4747
*/
4848

49+
/**
50+
* Flat key/value bag for custom object properties.
51+
* Only primitive scalars (string, number, boolean) survive the round-trip.
52+
*/
53+
type VoxelObjectProperties = Record<string, string | number | boolean>;
54+
55+
/**
56+
* A single named object placed in the world (spawn point, trigger zone, …).
57+
* Coordinates are in voxel/tile space; floats are allowed for sub-tile precision.
58+
* `y` is 0 for maps imported from a flat 2-D source.
59+
*/
4960
interface VoxelObjectJSON {
50-
id: number;
61+
id: string;
5162
name: string;
63+
/** Optional semantic type tag (e.g. "SpawnPoint", "Trigger"). */
5264
type?: string;
5365
x: number;
5466
y: number;
@@ -60,16 +72,15 @@ interface VoxelObjectJSON {
6072
properties?: VoxelObjectProperties;
6173
}
6274

75+
/** A named layer that holds placed objects rather than voxel data. */
6376
interface VoxelObjectLayerJSON {
64-
id: number;
77+
id: string;
6578
name: string;
6679
visible: boolean;
6780
order: number;
6881
objects: VoxelObjectJSON[];
6982
}
7083

71-
type VoxelObjectProperties = Record<string, string | number | boolean>;
72-
7384
interface VoxelWorldJSON {
7485
version: 1;
7586
chunkSize: number;
@@ -79,7 +90,11 @@ interface VoxelWorldJSON {
7990
* Auto-registered on load.
8091
**/
8192
blocks?: BlockDefinition[];
82-
/** Object layers produced by converters from Tiled object layers. */
93+
/**
94+
* Named object layers (spawn points, triggers, etc.).
95+
* Present in converter output and in files saved after object layers
96+
* were added at runtime via VoxelRenderer.addObjectLayer().
97+
*/
8398
objectLayers?: VoxelObjectLayerJSON[];
8499
}
85100
```
@@ -95,4 +110,5 @@ Converts the world and tileset metadata to a plain JSON-serialisable object.
95110

96111
#### `deserialize(data: VoxelWorldJSON, world: VoxelWorld): void`
97112

98-
Clears `world` and restores it from a snapshot. Throws if `data.version !== 1`.
113+
Clears `world` and restores it from a snapshot. Voxel layers and object layers are
114+
both restored. Throws if `data.version !== 1`.

packages/voxel-renderer/docs/VoxelRenderer.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,49 @@ referenced tilesets that are not already loaded.
271271

272272
Mark all the chunks as dirty and rebuild them in the next frame
273273

274+
### Object Layer API
275+
276+
Object layers hold placed objects (spawn points, trigger zones, etc.) rather than voxel
277+
data. Each mutating method fires a `VoxelLayerHookEvent` so external systems stay in sync.
278+
279+
#### `addObjectLayer(name: string, options?: { visible?: boolean; order?: number }): VoxelObjectLayerJSON`
280+
281+
Creates a new object layer in the world and fires `"object-layer-added"`.
282+
Returns the new layer descriptor.
283+
284+
#### `removeObjectLayer(name: string): boolean`
285+
286+
Removes an object layer from the world. Fires `"object-layer-removed"` on success.
287+
Returns `false` if not found.
288+
289+
#### `getObjectLayer(name: string): VoxelObjectLayerJSON | undefined`
290+
291+
Returns the layer descriptor for `name`, or `undefined` if it does not exist.
292+
293+
#### `getObjectLayers(): readonly VoxelObjectLayerJSON[]`
294+
295+
Returns a snapshot array of all object layers in insertion order.
296+
297+
#### `updateObjectLayer(name: string, patch: { visible?: boolean }): boolean`
298+
299+
Applies a partial patch to a named object layer and fires `"object-layer-updated"`.
300+
Returns `false` if not found.
301+
302+
#### `addObject(layerName: string, object: VoxelObjectJSON): boolean`
303+
304+
Appends an object to the named layer and fires `"object-added"`.
305+
Returns `false` if the layer does not exist.
306+
307+
#### `removeObject(layerName: string, objectId: string): boolean`
308+
309+
Removes the object with the given `id` from the layer and fires `"object-removed"`.
310+
Returns `false` if the layer or object is not found.
311+
312+
#### `updateObject(layerName: string, objectId: string, patch: Partial<VoxelObjectJSON>): boolean`
313+
314+
Merges `patch` into the matching object and fires `"object-updated"`.
315+
Returns `false` if the layer or object is not found.
316+
274317
### Hooks
275318

276319
See [Hooks](./Hooks.md) for more information

packages/voxel-renderer/docs/World.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,5 +110,46 @@ Iterates over chunks whose `dirty` flag is set.
110110

111111
#### `clear(): void`
112112

113-
Removes all layers.
113+
Removes all voxel layers and object layers.
114+
115+
### Object Layer Management
116+
117+
Object layers hold placed objects (spawn points, trigger zones, etc.) rather than
118+
voxel data. They are stored by name and serialised as part of `VoxelWorldJSON`.
119+
120+
#### `addObjectLayer(name: string, options?: { visible?: boolean; order?: number }): VoxelObjectLayerJSON`
121+
122+
Creates a new object layer. `order` defaults to the current layer count (appended last).
123+
Returns the new layer descriptor.
124+
125+
#### `removeObjectLayer(name: string): boolean`
126+
127+
Deletes an object layer by name. Returns `false` if not found.
128+
129+
#### `getObjectLayer(name: string): VoxelObjectLayerJSON | undefined`
130+
131+
Returns the layer descriptor for `name`, or `undefined` if it does not exist.
132+
133+
#### `getObjectLayers(): readonly VoxelObjectLayerJSON[]`
134+
135+
Returns a snapshot array of all object layers in insertion order.
136+
137+
#### `updateObjectLayer(name: string, patch: { visible?: boolean }): boolean`
138+
139+
Applies a partial patch to a named object layer. Returns `false` if not found.
140+
141+
#### `addObjectToLayer(layerName: string, object: VoxelObjectJSON): boolean`
142+
143+
Appends an object to the named layer's `objects` array. Returns `false` if the layer
144+
does not exist.
145+
146+
#### `removeObjectFromLayer(layerName: string, objectId: string): boolean`
147+
148+
Removes the object with the given `id` from the layer. Returns `false` if the layer or
149+
object is not found.
150+
151+
#### `updateObjectInLayer(layerName: string, objectId: string, patch: Partial<VoxelObjectJSON>): boolean`
152+
153+
Merges `patch` into the matching object. Returns `false` if the layer or object is not
154+
found.
114155

packages/voxel-renderer/src/VoxelRenderer.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ import {
2828
import { VoxelMeshBuilder } from "./mesh/VoxelMeshBuilder.ts";
2929
import {
3030
VoxelSerializer,
31-
type VoxelWorldJSON
31+
type VoxelWorldJSON,
32+
type VoxelObjectLayerJSON,
33+
type VoxelObjectJSON
3234
} from "./serialization/VoxelSerializer.ts";
3335
import {
3436
TilesetManager,
@@ -477,6 +479,112 @@ export class VoxelRenderer extends ActorComponent {
477479
return layer.centerToWorld();
478480
}
479481

482+
// --- Object Layer API --- //
483+
484+
addObjectLayer(
485+
name: string,
486+
options?: Partial<Pick<VoxelObjectLayerJSON, "visible" | "order">>
487+
): VoxelObjectLayerJSON {
488+
const layer = this.world.addObjectLayer(name, options);
489+
this.#onLayerUpdated?.({
490+
action: "object-layer-added",
491+
layerName: name,
492+
metadata: {}
493+
});
494+
495+
return layer;
496+
}
497+
498+
removeObjectLayer(
499+
name: string
500+
): boolean {
501+
const result = this.world.removeObjectLayer(name);
502+
if (result) {
503+
this.#onLayerUpdated?.({
504+
action: "object-layer-removed",
505+
layerName: name,
506+
metadata: {}
507+
});
508+
}
509+
510+
return result;
511+
}
512+
513+
getObjectLayer(
514+
name: string
515+
): VoxelObjectLayerJSON | undefined {
516+
return this.world.getObjectLayer(name);
517+
}
518+
519+
getObjectLayers(): readonly VoxelObjectLayerJSON[] {
520+
return this.world.getObjectLayers();
521+
}
522+
523+
updateObjectLayer(
524+
name: string,
525+
patch: Partial<Pick<VoxelObjectLayerJSON, "visible">>
526+
): boolean {
527+
const result = this.world.updateObjectLayer(name, patch);
528+
if (result) {
529+
this.#onLayerUpdated?.({
530+
action: "object-layer-updated",
531+
layerName: name,
532+
metadata: { patch }
533+
});
534+
}
535+
536+
return result;
537+
}
538+
539+
addObject(
540+
layerName: string,
541+
object: VoxelObjectJSON
542+
): boolean {
543+
const result = this.world.addObjectToLayer(layerName, object);
544+
if (result) {
545+
this.#onLayerUpdated?.({
546+
action: "object-added",
547+
layerName,
548+
metadata: { objectId: object.id }
549+
});
550+
}
551+
552+
return result;
553+
}
554+
555+
removeObject(
556+
layerName: string,
557+
objectId: string
558+
): boolean {
559+
const result = this.world.removeObjectFromLayer(layerName, objectId);
560+
if (result) {
561+
this.#onLayerUpdated?.({
562+
action: "object-removed",
563+
layerName,
564+
metadata: { objectId }
565+
});
566+
}
567+
568+
return result;
569+
}
570+
571+
updateObject(
572+
layerName: string,
573+
objectId: string,
574+
patch: Partial<VoxelObjectJSON>
575+
): boolean {
576+
const result = this.world.updateObjectInLayer(layerName, objectId, patch);
577+
if (result) {
578+
this.#onLayerUpdated?.({
579+
action: "object-updated",
580+
layerName,
581+
metadata: { objectId, patch }
582+
});
583+
}
584+
585+
return result;
586+
}
587+
480588
async loadTileset(
481589
def: TilesetDefinition
482590
): Promise<void> {

packages/voxel-renderer/src/hooks.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ export type VoxelLayerHookAction =
55
| "offset-updated"
66
| "voxel-set"
77
| "voxel-removed"
8-
| "reordered";
8+
| "reordered"
9+
| "object-layer-added"
10+
| "object-layer-removed"
11+
| "object-layer-updated"
12+
| "object-added"
13+
| "object-removed"
14+
| "object-updated";
915

1016
export interface VoxelLayerHookEvent {
1117
layerName: string;

packages/voxel-renderer/src/serialization/VoxelSerializer.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ export class VoxelSerializer {
7777
tilesets: tilesetManager.getDefinitions(),
7878
layers: world
7979
.getLayers()
80-
.map((layer) => layer.toJSON())
80+
.map((layer) => layer.toJSON()),
81+
objectLayers: [...world.getObjectLayers()]
8182
};
8283
}
8384

@@ -133,5 +134,15 @@ export class VoxelSerializer {
133134
layer.setVoxelAt({ x, y, z }, entry);
134135
}
135136
}
137+
138+
for (const layerJSON of data.objectLayers ?? []) {
139+
const layer = world.addObjectLayer(layerJSON.name, {
140+
visible: layerJSON.visible,
141+
order: layerJSON.order
142+
});
143+
144+
layer.id = layerJSON.id;
145+
layer.objects = [...layerJSON.objects];
146+
}
136147
}
137148
}

0 commit comments

Comments
 (0)