object type creation and deletion

This commit is contained in:
azykov@mail.ru 2026-06-05 23:12:46 +03:00
parent dee28ba5b2
commit ff5528008b
No known key found for this signature in database
4 changed files with 82 additions and 31 deletions

View File

@ -11,8 +11,6 @@
} }
display: flex; display: flex;
padding: 8px 8px;
gap: 8px;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
white-space: nowrap; white-space: nowrap;
@ -31,6 +29,7 @@
} }
&>.title { &>.title {
padding: 8px;
flex: 1; flex: 1;
overflow-x: hidden; overflow-x: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -38,12 +37,18 @@
&>.actions { &>.actions {
display: flex; display: flex;
gap: 8px;
&>* {
padding: 8px;
}
&>*:hover {
background: rgba(255, 255, 255, 0.08);
}
} }
& svg { & svg {
vertical-align: middle; vertical-align: middle;
margin: 2px;
box-sizing: border-box; box-sizing: border-box;
} }

View File

@ -38,3 +38,5 @@ export const DEFAULT_VOXEL_TYPES = {
[water.id]: water, [water.id]: water,
[glass.id]: glass, [glass.id]: glass,
} }
export const DEFAULT_VOXEL_TYPE = stone;

View File

@ -1,7 +1,7 @@
import { makeAutoObservable } from "mobx"; import { makeAutoObservable } from "mobx";
import { state } from "./rootState"; import { state } from "./rootState";
import type { ReactNode } from "react"; 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 = { export type MenuNodeAction = {
id: string; id: string;
@ -30,34 +30,22 @@ export class MenuState {
const scene = editor.scene; const scene = editor.scene;
return Object.values(state.world.data.objectTypes) return Object.values(state.world.data.objectTypes)
.map((ot) => ({ .map((ot) => {
id: `ot-${ot.id}`, const children = Object.values(scene.objects)
title: ot.name,
actions: [
{
id: 'create-instance',
content: <IconPlus size="1em" />,
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)
.filter((o) => o.typeId === ot.id) .filter((o) => o.typeId === ot.id)
.map((o) => { .map((o, idx) => {
const isPlayer = scene.playerObjectId === o.id; const isPlayer = scene.playerObjectId === o.id;
const actions: MenuNodeAction[] = []; const actions: MenuNodeAction[] = [];
if (!isPlayer) { if (!isPlayer) {
actions.push({ actions.push({
id: 'delete', id: 'delete-object-instance',
content: <IconTrash size="1em" />, content: <IconTrash size="1em" />,
tooltip: 'Delete the object', tooltip: 'Delete the object',
onClick: () => { editor.deleteObject(o.id); }, onClick: () => { editor.deleteObject(o.id); },
}); });
actions.push({ actions.push({
id: 'control-by-player', id: 'control-object-instance-by-player',
content: <IconRun size="1em" />, content: <IconRun size="1em" />,
tooltip: 'Mark as player', tooltip: 'Mark as player',
onClick: () => { scene.playerObjectId = o.id; }, onClick: () => { scene.playerObjectId = o.id; },
@ -68,25 +56,53 @@ export class MenuState {
id: `o-${o.id}`, id: `o-${o.id}`,
title: <> title: <>
{isPlayer && <IconRun size="1em" />} {isPlayer && <IconRun size="1em" />}
{o.id} {`${ot.name} ${idx + 1}`}
</>, </>,
className: isPlayer ? 'player-controlled-object' : undefined, className: isPlayer ? 'player-controlled-object' : undefined,
actions, actions,
onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }) }, onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }) },
selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id, selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id,
} as MenuNode; } 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: <IconPlus size="1em" />,
tooltip: 'Create new object instance',
onClick: () => { editor.addObjectInstanceAtRandomPosition(ot.id); },
},
{
id: 'delete-object-type',
content: <IconCubeOff size="1em" />,
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[] { private get editorMenu(): MenuNode[] {
return [ return [
{ {
id: 'editor-objects-menu', id: 'editor-scene-menu',
title: 'Objects', title: 'Scene',
onClick: () => { state.worldEditor.resetSelection() }, onClick: () => { state.worldEditor.resetSelection() },
selected: () => !state.worldEditor.selection, selected: () => !state.worldEditor.selection,
children: this.editorObjectTypesMenu, children: this.editorObjectTypesMenu,
actions: [{
id: 'create-object-type',
content: <IconCubePlus size="1em" />,
tooltip: 'Create new object type',
onClick: () => { state.worldEditor.addObjectType(); },
}],
} }
] ]
} }

View File

@ -1,10 +1,11 @@
import { makeAutoObservable, runInAction } from "mobx"; import { makeAutoObservable, runInAction } from "mobx";
import type { WorldState } from "./worldState"; 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 { createObjectInstance } from "../utils/object";
import { randomId } from "../utils"; import { randomId } from "../utils";
import { state } from "./rootState"; import { state } from "./rootState";
import { populateRuntimeObject } from "../utils/runtime"; import { populateRuntimeObject } from "../utils/runtime";
import { DEFAULT_VOXEL_TYPE } from "../model/defaultVoxelTypes";
export const ObjectEditModeEnum = [ export const ObjectEditModeEnum = [
'translate', 'translate',
@ -143,5 +144,32 @@ export class WorldEditorState {
this.resetSelection(); 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])
}
} }