Compare commits

..

2 Commits

Author SHA1 Message Date
azykov@mail.ru bc9e8e8e4e
object rotation and scaling in editor 2026-06-02 13:49:06 +03:00
azykov@mail.ru 742e841ce0
default voxel colors and transparency fixed 2026-06-02 13:29:08 +03:00
7 changed files with 102 additions and 29 deletions

View File

@ -4,6 +4,7 @@ import { useEffect, useRef, type RefObject } from "react";
import type { Group, Mesh } from "three"; import type { Group, Mesh } from "three";
import { Edges, TransformControls } from "@react-three/drei"; import { Edges, TransformControls } from "@react-three/drei";
import { state } from "../state"; import { state } from "../state";
import { nextSelectionEditMode } from "../state/worldEditor";
type ObjectViewProps = { type ObjectViewProps = {
object: ObjectInstance; object: ObjectInstance;
@ -18,6 +19,9 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) {
return null; return null;
const isSelected = state.worldEditor.isEnabled && const isSelected = state.worldEditor.isEnabled &&
state.worldEditor.selectedObjectId === object.id; state.worldEditor.selectedObjectId === object.id;
const selectionMode = isSelected
? state.worldEditor.selectedObjectMode
: undefined;
useEffect(() => { useEffect(() => {
if (!isSelected) if (!isSelected)
@ -38,7 +42,7 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) {
return; return;
e.stopPropagation(); e.stopPropagation();
state.worldEditor.setSelectedObjectId(object.id); state.worldEditor.setSelectedObject(object.id, nextSelectionEditMode(selectionMode));
}; };
const handleObjectChange = () => { const handleObjectChange = () => {
@ -46,20 +50,43 @@ export const ObjectView = observer(function ({ object }: ObjectViewProps) {
if (!group) if (!group)
return; return;
const { x, y, z } = group.position; state.worldEditor.setObjectTransform(
state.worldEditor.setObjectPosition(object.id, [x, y, z]); object.id,
group.position.toArray(),
group.rotation.toArray().slice(0, 3), // chop EulerOrder off array
group.scale.toArray(),
);
}; };
return (<> return (<>
{(isSelected && groupRef.current) && <TransformControls object={groupRef as RefObject<Group>} onChange={handleObjectChange} mode="translate" />} {
<group ref={groupRef} position={object.position} rotation={object.rotation} onClick={handleClick}> (isSelected && (selectionMode !== undefined) && groupRef.current) &&
<TransformControls
object={groupRef as RefObject<Group>}
onChange={handleObjectChange}
mode={selectionMode}
/>
}
<group
ref={groupRef}
position={object.position}
rotation={object.rotation}
scale={object.scale}
onClick={handleClick}
>
{ {
objectType.voxels.map((v, idx) => { objectType.voxels.map((v, idx) => {
const vt = state.world.getVoxelTypeById(v.typeId); const vt = state.world.getVoxelTypeById(v.typeId);
const color = (v.color ?? vt?.color) ?? 'white';
const opacity = (v.opacity ?? vt?.opacity) ?? 1;
return ( return (
<mesh key={idx} position={v.position}> <mesh key={idx} position={v.position}>
<boxGeometry args={[1, 1, 1]} /> <boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={(v.color ?? vt?.color) ?? 'white'} opacity={(v.opacity ?? vt?.opacity) ?? 1} /> <meshStandardMaterial
color={color}
opacity={opacity}
transparent={opacity < 1}
/>
{isSelected && <Edges color="white" lineWidth={3} stencilWrite={false} />} {isSelected && <Edges color="white" lineWidth={3} stencilWrite={false} />}
</mesh> </mesh>
); );

View File

@ -8,7 +8,7 @@ export const ThreeView = observer(function () {
<div style={{ width: '800px', height: '600px', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden' }}> <div style={{ width: '800px', height: '600px', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden' }}>
<Canvas <Canvas
camera={state.world.character.camera} camera={state.world.character.camera}
onPointerMissed={() => state.worldEditor.setSelectedObjectId(undefined)} onPointerMissed={() => state.worldEditor.resetSelectedObject()}
> >
<WorldView /> <WorldView />
</Canvas> </Canvas>

View File

@ -13,7 +13,7 @@ const dirt: VoxelType = {
name: 'Dirt', name: 'Dirt',
opacity: 1, opacity: 1,
collidable: true, collidable: true,
color: '302520', color: '#302520',
}; };
const water: VoxelType = { const water: VoxelType = {
@ -21,15 +21,15 @@ const water: VoxelType = {
name: 'Stone', name: 'Stone',
opacity: 0.7, opacity: 0.7,
collidable: false, collidable: false,
color: 'c0d0ff', color: '#80a0f0',
}; };
const glass: VoxelType = { const glass: VoxelType = {
id: 'glass', id: 'glass',
name: 'Glass', name: 'Glass',
opacity: 0.4, opacity: 0.3,
collidable: true, collidable: true,
color: 'f0f8ff', color: '#b0d8e8',
}; };
export const DEFAULT_VOXEL_TYPES = { export const DEFAULT_VOXEL_TYPES = {

View File

@ -19,3 +19,5 @@ export class RootState {
} }
export const state = new RootState(); export const state = new RootState();
state.world.load();

View File

@ -2,8 +2,8 @@ import { makeAutoObservable } from "mobx";
import { WorldFactory } from "../model/worldFactory"; import { WorldFactory } from "../model/worldFactory";
import type { ObjectType, RunningGameState, Scene, World } from "../types"; import type { ObjectType, RunningGameState, Scene, World } from "../types";
import { CharacterState } from "./character"; import { CharacterState } from "./character";
import type { Pos3, V3 } from "../types/3d"; import type { Pos3 } from "../types/3d";
import { clone, randomId } from "../utils"; import { clone } from "../utils";
import type { VoxelType } from "../types/voxel"; import type { VoxelType } from "../types/voxel";
import { state } from "./root"; import { state } from "./root";
import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes"; import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes";
@ -14,19 +14,19 @@ export class WorldState {
public character = new CharacterState(this); public character = new CharacterState(this);
constructor() { constructor() {
this.load();
makeAutoObservable(this); makeAutoObservable(this);
} }
public reset() { public reset() {
state.worldEditor.resetSelectedObject();
this.data = WorldFactory.create(); this.data = WorldFactory.create();
} }
public loadMock() { public loadMock() {
const objTypeId = 'test1'; const objTypeId = 'test1';
const voxelTypeId = 'water'; const voxelTypeId = 'glass';
const objectId = 'object1'; const objectId1 = 'object1';
const objectId2 = 'object2';
this.data = { this.data = {
objectTypes: { objectTypes: {
@ -52,11 +52,19 @@ export class WorldState {
look: [0, 0, 0], look: [0, 0, 0],
}, },
objects: { objects: {
[objectId]: { [objectId1]: {
id: objectId, id: objectId1,
typeId: objTypeId, typeId: objTypeId,
position: [0, 0, 0], position: [0, 0, 0],
rotation: [0, 0, 0], rotation: [0, 0, 0],
scale: [1, 1, 1],
},
[objectId2]: {
id: objectId2,
typeId: objTypeId,
position: [1, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
}, },
}, },
}, },
@ -67,10 +75,13 @@ export class WorldState {
playing: false, playing: false,
}, },
}; };
state.worldEditor.resetSelectedObject();
this.save();
} }
public load() { public load() {
this.data = WorldFactory.load() ?? WorldFactory.create(); this.data = WorldFactory.load() ?? WorldFactory.create();
state.worldEditor.resetSelectedObject();
} }
public save(): void { public save(): void {
@ -114,8 +125,7 @@ export class WorldState {
scene: clone(this.data.initialScene), scene: clone(this.data.initialScene),
} }
} }
state.worldEditor.resetSelectedObject();
state.worldEditor.setSelectedObjectId(undefined);
} }
public pause(): void { public pause(): void {

View File

@ -3,12 +3,29 @@ import type { WorldState } from "./world";
import type { ObjectInstance, Scene, World } from "../types"; import type { ObjectInstance, Scene, World } from "../types";
import { createObjectInstance } from "../utils/object"; import { createObjectInstance } from "../utils/object";
import { randomId } from "../utils"; import { randomId } from "../utils";
import type { Pos3, V3 } from "../types/3d"; import type { Pos3, R3, V3 } from "../types/3d";
export const SelectionEditModeEnum = [
'translate',
'rotate',
'scale',
] as const;
type SelectionEditModeTuple = typeof SelectionEditModeEnum;
export type SelectionEditMode = SelectionEditModeTuple[number];
export function nextSelectionEditMode(mode: SelectionEditMode | undefined): SelectionEditMode {
if (mode === undefined)
return 'translate';
const idx = SelectionEditModeEnum.indexOf(mode);
return SelectionEditModeEnum[(idx + 1) % SelectionEditModeEnum.length];
}
export class WorldEditorState { export class WorldEditorState {
private readonly world: WorldState; private readonly world: WorldState;
public selectedObjectId: string | undefined; public selectedObjectId: string | undefined;
public selectedObjectMode: SelectionEditMode | undefined;
public isDragging: boolean = false; public isDragging: boolean = false;
constructor(world: WorldState) { constructor(world: WorldState) {
@ -25,8 +42,14 @@ export class WorldEditorState {
this.world.save(); this.world.save();
} }
public setSelectedObjectId(value: string | undefined): void { public setSelectedObject(id: string, mode: SelectionEditMode): void {
this.selectedObjectId = value; this.selectedObjectId = id;
this.selectedObjectMode = mode;
}
public resetSelectedObject(): void {
this.selectedObjectId = undefined;
this.selectedObjectMode = undefined;
} }
public setIsDragging(value: boolean): void { public setIsDragging(value: boolean): void {
@ -67,13 +90,22 @@ export class WorldEditorState {
}); });
} }
public setObjectPosition(id: string, pos: V3): void { public setObjectTransform(
id: string,
position: V3,
rotation: R3,
scale: V3,
): void {
const obj = this.scene.objects[id]; const obj = this.scene.objects[id];
if (!obj) if (!obj)
return; return;
obj.position = pos; this.mutateObject({
this.mutateObject(obj); ...obj,
position,
rotation,
scale,
});
} }
public addObjectCloneAtRandomPosition(typeId: string): ObjectInstance { public addObjectCloneAtRandomPosition(typeId: string): ObjectInstance {

View File

@ -1,3 +1,4 @@
import type { R3, V3 } from "./3d";
import type { Voxel } from "./voxel"; import type { Voxel } from "./voxel";
export type ObjectType = { export type ObjectType = {
@ -9,6 +10,7 @@ export type ObjectType = {
export type ObjectInstance = { export type ObjectInstance = {
id: string; id: string;
typeId: string; typeId: string;
position: [number, number, number]; position: V3;
rotation: [number, number, number]; rotation: R3;
scale: V3;
} }