diff --git a/src/components/MenuView.scss b/src/components/MenuView.scss
index 02a9327..83c90aa 100644
--- a/src/components/MenuView.scss
+++ b/src/components/MenuView.scss
@@ -11,8 +11,6 @@
}
display: flex;
- padding: 8px 8px;
- gap: 8px;
cursor: pointer;
border-radius: 4px;
white-space: nowrap;
@@ -31,6 +29,7 @@
}
&>.title {
+ padding: 8px;
flex: 1;
overflow-x: hidden;
text-overflow: ellipsis;
@@ -38,12 +37,18 @@
&>.actions {
display: flex;
- gap: 8px;
+
+ &>* {
+ padding: 8px;
+ }
+
+ &>*:hover {
+ background: rgba(255, 255, 255, 0.08);
+ }
}
& svg {
vertical-align: middle;
- margin: 2px;
box-sizing: border-box;
}
diff --git a/src/model/defaultVoxelTypes.ts b/src/model/defaultVoxelTypes.ts
index 5f9f7e1..ac87511 100644
--- a/src/model/defaultVoxelTypes.ts
+++ b/src/model/defaultVoxelTypes.ts
@@ -37,4 +37,6 @@ export const DEFAULT_VOXEL_TYPES = {
[dirt.id]: dirt,
[water.id]: water,
[glass.id]: glass,
-}
\ No newline at end of file
+}
+
+export const DEFAULT_VOXEL_TYPE = stone;
diff --git a/src/state/menuState.tsx b/src/state/menuState.tsx
index 44bcda7..3be0fef 100644
--- a/src/state/menuState.tsx
+++ b/src/state/menuState.tsx
@@ -1,7 +1,7 @@
import { makeAutoObservable } from "mobx";
import { state } from "./rootState";
import type { ReactNode } from "react";
-import { IconRun, IconTrash, IconCubePlus, IconPlus } from '@tabler/icons-react';
+import { IconRun, IconTrash, IconPlus, IconCubePlus, IconCubeOff } from '@tabler/icons-react';
export type MenuNodeAction = {
id: string;
@@ -30,34 +30,22 @@ export class MenuState {
const scene = editor.scene;
return Object.values(state.world.data.objectTypes)
- .map((ot) => ({
- id: `ot-${ot.id}`,
- title: ot.name,
- actions: [
- {
- id: 'create-instance',
- content: ,
- tooltip: 'Create new object instance',
- onClick: () => { editor.addObjectInstanceAtRandomPosition(ot.id); },
- },
- ],
- onClick: () => { editor.setSelectedObjectType({ id: ot.id, editMode: 'scripts' }) },
- selected: () => editor.selection?.type === 'objectType' && editor.selection?.id === ot.id,
- children: Object.values(scene.objects)
+ .map((ot) => {
+ const children = Object.values(scene.objects)
.filter((o) => o.typeId === ot.id)
- .map((o) => {
+ .map((o, idx) => {
const isPlayer = scene.playerObjectId === o.id;
const actions: MenuNodeAction[] = [];
if (!isPlayer) {
actions.push({
- id: 'delete',
+ id: 'delete-object-instance',
content: ,
tooltip: 'Delete the object',
onClick: () => { editor.deleteObject(o.id); },
});
actions.push({
- id: 'control-by-player',
+ id: 'control-object-instance-by-player',
content: ,
tooltip: 'Mark as player',
onClick: () => { scene.playerObjectId = o.id; },
@@ -68,25 +56,53 @@ export class MenuState {
id: `o-${o.id}`,
title: <>
{isPlayer && }
- {o.id}
+ {`${ot.name} ${idx + 1}`}
>,
className: isPlayer ? 'player-controlled-object' : undefined,
actions,
onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }) },
selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id,
} as MenuNode;
- })
- } as MenuNode));
+ });
+
+ return {
+ id: `ot-${ot.id}`,
+ title: `${ot.name} (${children.length ? children.length : 'no'} instance${children.length != 1 ? 's' : ''})`,
+ actions: [
+ {
+ id: 'create-object-instance',
+ content: ,
+ tooltip: 'Create new object instance',
+ onClick: () => { editor.addObjectInstanceAtRandomPosition(ot.id); },
+ },
+ {
+ id: 'delete-object-type',
+ content: ,
+ tooltip: 'Delete object type',
+ onClick: () => { editor.deleteObjectType(ot.id); },
+ },
+ ],
+ onClick: () => { editor.setSelectedObjectType({ id: ot.id, editMode: 'scripts' }) },
+ selected: () => editor.selection?.type === 'objectType' && editor.selection?.id === ot.id,
+ children,
+ } as MenuNode
+ });
}
private get editorMenu(): MenuNode[] {
return [
{
- id: 'editor-objects-menu',
- title: 'Objects',
+ id: 'editor-scene-menu',
+ title: 'Scene',
onClick: () => { state.worldEditor.resetSelection() },
selected: () => !state.worldEditor.selection,
children: this.editorObjectTypesMenu,
+ actions: [{
+ id: 'create-object-type',
+ content: ,
+ tooltip: 'Create new object type',
+ onClick: () => { state.worldEditor.addObjectType(); },
+ }],
}
]
}
diff --git a/src/state/worldEditorState.ts b/src/state/worldEditorState.ts
index e4d0ba0..e4e14c1 100644
--- a/src/state/worldEditorState.ts
+++ b/src/state/worldEditorState.ts
@@ -1,10 +1,11 @@
import { makeAutoObservable, runInAction } from "mobx";
import type { WorldState } from "./worldState";
-import { type ObjectInstance, type Pos3, type R3, type V3, type RuntimeScene } from "../types";
+import { type ObjectInstance, type Pos3, type R3, type V3, type RuntimeScene, type ObjectType } from "../types";
import { createObjectInstance } from "../utils/object";
import { randomId } from "../utils";
import { state } from "./rootState";
import { populateRuntimeObject } from "../utils/runtime";
+import { DEFAULT_VOXEL_TYPE } from "../model/defaultVoxelTypes";
export const ObjectEditModeEnum = [
'translate',
@@ -139,9 +140,36 @@ export class WorldEditorState {
}
public deleteObject(objectId: string) {
- if (this.selection?.type ==='object' && this.selection.id === objectId)
+ if (this.selection?.type === 'object' && this.selection.id === objectId)
this.resetSelection();
- delete (this.scene.objects[objectId]);
+ delete (this.scene.objects[objectId]);
+ }
+
+ public addObjectType(): ObjectType {
+ const objectType: ObjectType = {
+ id: randomId(),
+ name: 'Unnamed',
+ voxels: [
+ {
+ typeId: DEFAULT_VOXEL_TYPE.id,
+ position: [0, 0, 0],
+ }
+ ],
+ };
+ this.world.data.objectTypes[objectType.id] = objectType;
+ this.setSelectedObjectType({ id: objectType.id, editMode: 'scripts' });
+ return objectType;
+ }
+
+ public deleteObjectType(typeId: string) {
+ if (this.selection?.type === 'objectType' && this.selection.id === typeId)
+ this.resetSelection();
+
+ for (const id in this.scene.objects)
+ if (this.scene.objects[id].typeId === typeId)
+ this.deleteObject(id);
+
+ delete (this.world.data.objectTypes[typeId])
}
}