From ff5528008b30aed5ec58951b129ca050079e058d Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Fri, 5 Jun 2026 23:12:46 +0300 Subject: [PATCH] object type creation and deletion --- src/components/MenuView.scss | 13 ++++--- src/model/defaultVoxelTypes.ts | 4 ++- src/state/menuState.tsx | 62 +++++++++++++++++++++------------- src/state/worldEditorState.ts | 34 +++++++++++++++++-- 4 files changed, 82 insertions(+), 31 deletions(-) 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]) } }