Compare commits
No commits in common. "830c2bdde621cf8a73183c1c5d3cd0a8a75f42f1" and "ff5528008b30aed5ec58951b129ca050079e058d" have entirely different histories.
830c2bdde6
...
ff5528008b
36
src/App.tsx
36
src/App.tsx
|
|
@ -1,34 +1,25 @@
|
|||
import { BrowserRouter, Link, Outlet, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { VoxelEditorPage } from './components/voxelEditor/VoxelEditorPage';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { BrowserRouter, Link, Outlet, Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { reaction } from 'mobx';
|
||||
import { ThreeView } from './components/ThreeView';
|
||||
import { state } from './state';
|
||||
import './App.scss';
|
||||
import { LeftPanel } from './components/LeftPanel';
|
||||
import { Panels } from './components/Panels';
|
||||
|
||||
function StateToUrlSync() {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const pathnameRef = useRef(pathname);
|
||||
pathnameRef.current = pathname;
|
||||
|
||||
useEffect(() => reaction(
|
||||
() => ({
|
||||
isGame: !!state.game,
|
||||
isGame: state.isGamePlaying,
|
||||
selectedObjectId: state.worldEditor.selection?.type === 'object' && state.worldEditor.selection.id,
|
||||
selectedObjectTypeId: state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection.id,
|
||||
}),
|
||||
({ isGame, selectedObjectId, selectedObjectTypeId }) => {
|
||||
let target: string;
|
||||
if (isGame) target = '/game';
|
||||
else if (selectedObjectId) target = `/editor/object/${selectedObjectId}`;
|
||||
else if (selectedObjectTypeId) target = `/editor/objectType/${selectedObjectTypeId}`;
|
||||
else target = '/editor';
|
||||
|
||||
const current = pathnameRef.current;
|
||||
if (current === target || current.startsWith(target + '/')) return;
|
||||
navigate(target);
|
||||
if (isGame) navigate('/game');
|
||||
else if (selectedObjectId) navigate(`/editor/object/${selectedObjectId}`);
|
||||
else if (selectedObjectTypeId) navigate(`/editor/objectType/${selectedObjectTypeId}`);
|
||||
else navigate('/editor');
|
||||
},
|
||||
), []);
|
||||
return null;
|
||||
|
|
@ -36,7 +27,7 @@ function StateToUrlSync() {
|
|||
|
||||
function EditorRoute() {
|
||||
useEffect(() => {
|
||||
if (!!state.game) state.stopGame();
|
||||
if (state.isGamePlaying) state.stopGame();
|
||||
state.worldEditor.resetSelection();
|
||||
}, []);
|
||||
return null;
|
||||
|
|
@ -46,8 +37,7 @@ function EditorObjectRoute() {
|
|||
const { id } = useParams<{ id: string }>();
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
if (!!state.game)
|
||||
state.stopGame();
|
||||
if (state.isGamePlaying) state.stopGame();
|
||||
const editMode = state.worldEditor.selection?.type === 'object' && state.worldEditor.selection.id === id
|
||||
? state.worldEditor.selection.editMode ?? 'translate'
|
||||
: 'translate';
|
||||
|
|
@ -60,7 +50,7 @@ function EditorObjectTypeRoute() {
|
|||
const { id } = useParams<{ id: string }>();
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
if (!!state.game) state.stopGame();
|
||||
if (state.isGamePlaying) state.stopGame();
|
||||
const editMode = state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection.id === id
|
||||
? state.worldEditor.selection.editMode ?? 'scripts'
|
||||
: 'scripts';
|
||||
|
|
@ -71,8 +61,7 @@ function EditorObjectTypeRoute() {
|
|||
|
||||
function GameRoute() {
|
||||
useEffect(() => {
|
||||
if (!state.game)
|
||||
state.startGame();
|
||||
if (!state.isGamePlaying) state.startGame();
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -97,7 +86,6 @@ export const App = function () {
|
|||
<StateToUrlSync />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/editor/objectType/:id/voxels" element={<VoxelEditorPage />} />
|
||||
<Route element={<EditorLayout />}>
|
||||
<Route path="/editor" element={<EditorRoute />} />
|
||||
<Route path="/editor/object/:id" element={<EditorObjectRoute />} />
|
||||
|
|
|
|||
|
|
@ -3,11 +3,9 @@ import { useEffect, useRef } from 'react';
|
|||
import { state } from '../state';
|
||||
import './MenuView.scss';
|
||||
import type { MenuNode } from '../state/menuState';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const MenuNodeView = observer(function ({ node }: { node: MenuNode }) {
|
||||
const forceOpen = state.menu.nodeContainsSelected(node);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const ref = useRef<HTMLDetailsElement>(null);
|
||||
|
||||
|
|
@ -34,13 +32,7 @@ export const MenuNodeView = observer(function ({ node }: { node: MenuNode }) {
|
|||
<div
|
||||
key={action.id}
|
||||
title={action.tooltip}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
action.onClick?.();
|
||||
if (action.link)
|
||||
navigate(action.link);
|
||||
}}
|
||||
onClick={(e) => { e.stopPropagation(); e.preventDefault(); action.onClick(); }}
|
||||
>
|
||||
{action.content}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -59,9 +59,6 @@ export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectE
|
|||
if (!objectType)
|
||||
return null;
|
||||
|
||||
// Track cache so observer re-renders when refreshObjectTypeCaches is called
|
||||
void object.cache.updatedAt;
|
||||
|
||||
function handleClick() {
|
||||
if (state.worldEditor.isEnabled) {
|
||||
// Reading selection state inside an event handler: not tracked by observer.
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
|||
>
|
||||
{
|
||||
object.cache.voxelGroups.map((vg) =>
|
||||
<Instances key={`${vg.id}-${object.cache.updatedAt}`} limit={vg.positions.length}>
|
||||
<Instances key={vg.id} limit={vg.positions.length}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color={vg.color} opacity={vg.opacity} transparent={vg.opacity < 1} />
|
||||
{vg.positions.map((pos, i) => <Instance key={i} position={pos} />)}
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
import { Canvas } from '@react-three/fiber';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { state } from '../../state';
|
||||
import type { Voxel, V3 } from '../../types';
|
||||
import { VoxelEditorScene } from './VoxelEditorScene';
|
||||
import { IconArrowLeft, IconArrowBackUp, IconArrowForwardUp, IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
|
||||
const PALETTE = [
|
||||
'#ffffff', '#cccccc', '#999999', '#555555', '#222222',
|
||||
'#e05555', '#e08844', '#e0cc44', '#55cc55', '#55cccc',
|
||||
'#5588e0', '#aa55e0', '#a0522d', '#228b22', '#c06858',
|
||||
'#e8b820', '#484040', '#d0c8b8', '#808080', '#181414',
|
||||
];
|
||||
|
||||
type HistoryEntry =
|
||||
| { type: 'add'; voxel: Voxel }
|
||||
| { type: 'remove'; voxel: Voxel };
|
||||
|
||||
export const VoxelEditorPage = observer(function () {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [mode, setMode] = useState<'add' | 'remove'>('add');
|
||||
const [color, setColor] = useState('#808080');
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const history = useRef<HistoryEntry[]>([]);
|
||||
const future = useRef<HistoryEntry[]>([]);
|
||||
|
||||
const objectType = id ? state.world.getObjectTypeById(id) : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (id) state.worldEditor.refreshObjectTypeCaches(id);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
function pushHistory(entry: HistoryEntry) {
|
||||
history.current.push(entry);
|
||||
future.current = [];
|
||||
setCanUndo(true);
|
||||
setCanRedo(false);
|
||||
}
|
||||
|
||||
const undo = useCallback(() => {
|
||||
const entry = history.current.pop();
|
||||
if (!entry || !id)
|
||||
return;
|
||||
if (entry.type === 'add') {
|
||||
state.worldEditor.removeVoxelFromObjectType(id, entry.voxel.position);
|
||||
} else {
|
||||
state.worldEditor.addVoxelToObjectType(id, entry.voxel);
|
||||
}
|
||||
future.current.push(entry);
|
||||
setCanUndo(history.current.length > 0);
|
||||
setCanRedo(true);
|
||||
}, [id]);
|
||||
|
||||
const redo = useCallback(() => {
|
||||
const entry = future.current.pop();
|
||||
if (!entry || !id)
|
||||
return;
|
||||
if (entry.type === 'add') {
|
||||
state.worldEditor.addVoxelToObjectType(id, entry.voxel);
|
||||
} else {
|
||||
state.worldEditor.removeVoxelFromObjectType(id, entry.voxel.position);
|
||||
}
|
||||
history.current.push(entry);
|
||||
setCanUndo(true);
|
||||
setCanRedo(future.current.length > 0);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (!e.ctrlKey)
|
||||
return;
|
||||
if (e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo(); }
|
||||
if (e.key === 'y' || (e.key === 'z' && e.shiftKey)) { e.preventDefault(); redo(); }
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [undo, redo]);
|
||||
|
||||
function handleAdd(voxel: Voxel) {
|
||||
if (!id)
|
||||
return;
|
||||
state.worldEditor.addVoxelToObjectType(id, voxel);
|
||||
pushHistory({ type: 'add', voxel });
|
||||
}
|
||||
|
||||
function handleRemove(position: V3) {
|
||||
if (!id || !objectType)
|
||||
return;
|
||||
const voxel = objectType.voxels.find(v =>
|
||||
v.position[0] === position[0] &&
|
||||
v.position[1] === position[1] &&
|
||||
v.position[2] === position[2]
|
||||
);
|
||||
if (!voxel)
|
||||
return;
|
||||
state.worldEditor.removeVoxelFromObjectType(id, position);
|
||||
pushHistory({ type: 'remove', voxel });
|
||||
}
|
||||
|
||||
if (!objectType) {
|
||||
return (
|
||||
<div style={{ color: 'white', padding: 24 }}>
|
||||
Object type not found. <Link to="/editor" style={{ color: '#88aaff' }}>Back to editor</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, background: '#1a1a2e', display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Top bar */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', background: '#11111f', flexShrink: 0 }}>
|
||||
<button onClick={() => navigate(`/editor/objectType/${id}`)} style={btnStyle}>
|
||||
<IconArrowLeft size={20} />
|
||||
</button>
|
||||
<span style={{ color: '#ccc', fontWeight: 600 }}>{objectType.name}</span>
|
||||
<span style={{ color: '#666', fontSize: 13 }}>{objectType.voxels.length} voxels</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button onClick={undo} disabled={!canUndo} style={{ ...btnStyle, opacity: canUndo ? 1 : 0.35 }}>
|
||||
<IconArrowBackUp size={20} />
|
||||
</button>
|
||||
<button onClick={redo} disabled={!canRedo} style={{ ...btnStyle, opacity: canRedo ? 1 : 0.35 }}>
|
||||
<IconArrowForwardUp size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 3D canvas */}
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
<Canvas camera={{ position: [12, 10, 12], fov: 50 }}>
|
||||
<VoxelEditorScene
|
||||
voxels={objectType.voxels}
|
||||
mode={mode}
|
||||
color={color}
|
||||
typeId="stone"
|
||||
onAdd={handleAdd}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div style={{ background: '#11111f', padding: '10px 12px', flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{/* Color palette */}
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{PALETTE.map(c => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => { setColor(c); setMode('add'); }}
|
||||
style={{
|
||||
width: 36, height: 36,
|
||||
background: c,
|
||||
borderRadius: 6,
|
||||
border: c === color ? '3px solid white' : '3px solid #333',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mode toggle */}
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button
|
||||
onClick={() => setMode('add')}
|
||||
style={{ ...btnStyle, flex: 1, padding: '12px 0', gap: 6, background: mode === 'add' ? '#2d6a2d' : '#2a2a3a', border: mode === 'add' ? '2px solid #4CAF50' : '2px solid #444' }}
|
||||
>
|
||||
<IconPlus size={18} /> Add
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('remove')}
|
||||
style={{ ...btnStyle, flex: 1, padding: '12px 0', gap: 6, background: mode === 'remove' ? '#6a2d2d' : '#2a2a3a', border: mode === 'remove' ? '2px solid #f44336' : '2px solid #444' }}
|
||||
>
|
||||
<IconTrash size={18} /> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const btnStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#2a2a3a',
|
||||
color: '#ccc',
|
||||
border: '2px solid #444',
|
||||
borderRadius: 8,
|
||||
padding: '6px 10px',
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
gap: 4,
|
||||
};
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import { OrbitControls } from '@react-three/drei';
|
||||
import { useState } from 'react';
|
||||
import type { ThreeEvent } from '@react-three/fiber';
|
||||
import type { Voxel, V3 } from '../../types';
|
||||
|
||||
type Props = {
|
||||
voxels: Voxel[];
|
||||
mode: 'add' | 'remove';
|
||||
color: string;
|
||||
typeId: string;
|
||||
onAdd: (voxel: Voxel) => void;
|
||||
onRemove: (position: V3) => void;
|
||||
};
|
||||
|
||||
const FLOOR_Y = 0;
|
||||
|
||||
function adjPosition(pos: V3, nx: number, ny: number, nz: number): V3 {
|
||||
return [pos[0] + Math.round(nx), pos[1] + Math.round(ny), pos[2] + Math.round(nz)];
|
||||
}
|
||||
|
||||
function posEq(a: V3, b: V3) {
|
||||
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
||||
}
|
||||
|
||||
const DRAG_THRESHOLD = 5;
|
||||
|
||||
export function VoxelEditorScene({ voxels, mode, color, typeId, onAdd, onRemove }: Props) {
|
||||
const [ghost, setGhost] = useState<V3 | null>(null);
|
||||
|
||||
function handleVoxelClick(e: ThreeEvent<MouseEvent>, voxel: Voxel) {
|
||||
if (e.delta > DRAG_THRESHOLD)
|
||||
return;
|
||||
e.stopPropagation();
|
||||
if (mode === 'remove') {
|
||||
onRemove(voxel.position);
|
||||
setGhost(null);
|
||||
} else {
|
||||
if (!e.face)
|
||||
return;
|
||||
const { x, y, z } = e.face.normal;
|
||||
const pos = adjPosition(voxel.position, x, y, z);
|
||||
onAdd({ typeId, position: pos, color });
|
||||
}
|
||||
}
|
||||
|
||||
function handleVoxelMove(e: ThreeEvent<PointerEvent>, voxel: Voxel) {
|
||||
e.stopPropagation();
|
||||
if (mode === 'remove') {
|
||||
setGhost(voxel.position);
|
||||
} else {
|
||||
setGhost(null);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFloorClick(e: ThreeEvent<MouseEvent>) {
|
||||
if (e.delta > DRAG_THRESHOLD || mode !== 'add')
|
||||
return;
|
||||
e.stopPropagation();
|
||||
const pos: V3 = [Math.floor(e.point.x), 0, Math.floor(e.point.z)];
|
||||
onAdd({ typeId, position: pos, color });
|
||||
}
|
||||
|
||||
function handleFloorMove(e: ThreeEvent<PointerEvent>) {
|
||||
if (mode !== 'add')
|
||||
return;
|
||||
setGhost([Math.floor(e.point.x), 0, Math.floor(e.point.z)]);
|
||||
}
|
||||
|
||||
const ghostOccupied = ghost && voxels.some(v => posEq(v.position, ghost));
|
||||
|
||||
return (
|
||||
<>
|
||||
<ambientLight intensity={0.7} />
|
||||
<directionalLight position={[8, 12, 6]} intensity={1} castShadow />
|
||||
|
||||
<OrbitControls makeDefault />
|
||||
|
||||
{/* Floor plane */}
|
||||
<mesh
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
position={[0, FLOOR_Y, 0]}
|
||||
onClick={handleFloorClick}
|
||||
onPointerMove={handleFloorMove}
|
||||
onPointerLeave={() => setGhost(null)}
|
||||
>
|
||||
<planeGeometry args={[40, 40]} />
|
||||
<meshStandardMaterial color="#2a2a3a" />
|
||||
</mesh>
|
||||
|
||||
<gridHelper args={[40, 40, '#555', '#333']} position={[0, FLOOR_Y, 0]} />
|
||||
|
||||
{/* Voxels */}
|
||||
{voxels.map((v, i) => {
|
||||
const isHovered = !!ghost && mode === 'remove' && posEq(v.position, ghost);
|
||||
return (
|
||||
<mesh
|
||||
key={i}
|
||||
position={[v.position[0] + 0.5, v.position[1] + 0.5, v.position[2] + 0.5]}
|
||||
onClick={(e) => handleVoxelClick(e, v)}
|
||||
onPointerMove={(e) => handleVoxelMove(e, v)}
|
||||
onPointerLeave={() => setGhost(null)}
|
||||
>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial
|
||||
color={v.color ?? '#888888'}
|
||||
opacity={isHovered ? 0.4 : 1}
|
||||
transparent={isHovered}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Ghost voxel preview */}
|
||||
{ghost && mode === 'add' && !ghostOccupied && (
|
||||
<mesh
|
||||
position={[ghost[0] + 0.5, ghost[1] + 0.5, ghost[2] + 0.5]}
|
||||
raycast={() => null}
|
||||
>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial color={color} opacity={0.45} transparent />
|
||||
</mesh>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
import { state } from "./rootState";
|
||||
import type { ReactNode } from "react";
|
||||
import { IconRun, IconTrash, IconPlus, IconCubePlus, IconCubeOff, IconEdit } from '@tabler/icons-react';
|
||||
import { IconRun, IconTrash, IconPlus, IconCubePlus, IconCubeOff } from '@tabler/icons-react';
|
||||
|
||||
export type MenuNodeAction = {
|
||||
id: string;
|
||||
content: ReactNode;
|
||||
tooltip: string;
|
||||
onClick?: () => void;
|
||||
link?: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export type MenuNode = {
|
||||
|
|
@ -76,12 +75,6 @@ export class MenuState {
|
|||
tooltip: 'Create new object instance',
|
||||
onClick: () => { editor.addObjectInstanceAtRandomPosition(ot.id); },
|
||||
},
|
||||
{
|
||||
id: 'edit-object-type-voxels',
|
||||
content: <IconEdit size="1em" />,
|
||||
tooltip: 'Edit object type voxels',
|
||||
link: `/editor/objectType/${ot.id}/voxels`,
|
||||
},
|
||||
{
|
||||
id: 'delete-object-type',
|
||||
content: <IconCubeOff size="1em" />,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import type { WorldState } from "./worldState";
|
||||
import { type ObjectInstance, type Pos3, type R3, type V3, type RuntimeScene, type ObjectType, type Voxel } 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, computeBoundingBox } from "../utils/runtime";
|
||||
import { populateRuntimeObject } from "../utils/runtime";
|
||||
import { DEFAULT_VOXEL_TYPE } from "../model/defaultVoxelTypes";
|
||||
import { getObjectVoxelGroups } from "../utils/graphics/voxelGroup";
|
||||
import { buildObjectTrimesh } from "../utils/graphics/mesh";
|
||||
|
||||
export const ObjectEditModeEnum = [
|
||||
'translate',
|
||||
|
|
@ -163,42 +161,6 @@ export class WorldEditorState {
|
|||
return objectType;
|
||||
}
|
||||
|
||||
public addVoxelToObjectType(typeId: string, voxel: Voxel): void {
|
||||
const objectType = this.world.getObjectTypeById(typeId);
|
||||
if (!objectType) return;
|
||||
const occupied = objectType.voxels.some(v =>
|
||||
v.position[0] === voxel.position[0] &&
|
||||
v.position[1] === voxel.position[1] &&
|
||||
v.position[2] === voxel.position[2]
|
||||
);
|
||||
if (!occupied) objectType.voxels.push(voxel);
|
||||
}
|
||||
|
||||
public removeVoxelFromObjectType(typeId: string, position: V3): void {
|
||||
const objectType = this.world.getObjectTypeById(typeId);
|
||||
if (!objectType) return;
|
||||
const idx = objectType.voxels.findIndex(v =>
|
||||
v.position[0] === position[0] &&
|
||||
v.position[1] === position[1] &&
|
||||
v.position[2] === position[2]
|
||||
);
|
||||
if (idx !== -1) objectType.voxels.splice(idx, 1);
|
||||
}
|
||||
|
||||
public refreshObjectTypeCaches(typeId: string): void {
|
||||
const objectType = this.world.getObjectTypeById(typeId);
|
||||
if (!objectType) return;
|
||||
const world = this.world.data;
|
||||
const now = Date.now();
|
||||
for (const obj of Object.values(this.scene.objects)) {
|
||||
if (obj.typeId !== typeId) continue;
|
||||
obj.cache.voxelGroups = getObjectVoxelGroups(objectType, world.voxelTypes);
|
||||
obj.cache.colliderMesh = buildObjectTrimesh(objectType, world.voxelTypes);
|
||||
obj.cache.boundingBox = computeBoundingBox(objectType);
|
||||
obj.cache.updatedAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
public deleteObjectType(typeId: string) {
|
||||
if (this.selection?.type === 'objectType' && this.selection.id === typeId)
|
||||
this.resetSelection();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export type ObjectInstanceRuntimeData = {
|
|||
voxelGroups: VoxelGroup[];
|
||||
colliderMesh: [Float32Array, Uint32Array] | null;
|
||||
boundingBox: { min: V3; max: V3 };
|
||||
updatedAt: number;
|
||||
};
|
||||
pendingActions: {
|
||||
impulse?: { direction: V3, amplitude: number };
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export function createObjectInstance(id: string, typeId: string): ObjectInstance
|
|||
return {
|
||||
id,
|
||||
typeId: typeId,
|
||||
physics: true,
|
||||
physics: false,
|
||||
gravityScale: 1,
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { getObjectVoxelGroups } from "../graphics/voxelGroup";
|
|||
import type { ObjectInstance, ObjectType, RuntimeObjectInstance, V3, World } from "../../types";
|
||||
import { buildObjectTrimesh } from "../graphics/mesh";
|
||||
|
||||
export function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } {
|
||||
function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } {
|
||||
if (!objectType.voxels.length)
|
||||
return { min: [0, 0, 0], max: [1, 1, 1] };
|
||||
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
||||
|
|
@ -27,7 +27,6 @@ export function populateRuntimeObject(object: ObjectInstance, world: World): Run
|
|||
voxelGroups: getObjectVoxelGroups(objectType, world.voxelTypes),
|
||||
colliderMesh: buildObjectTrimesh(objectType, world.voxelTypes),
|
||||
boundingBox: computeBoundingBox(objectType),
|
||||
updatedAt: 0,
|
||||
},
|
||||
pendingActions: {},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue