object move blocks

debug floaters
This commit is contained in:
azykov@mail.ru
2026-06-12 21:11:35 +03:00
parent 20a4edad5a
commit 209ff3f710
31 changed files with 527 additions and 53 deletions
+2 -2
View File
@@ -47,8 +47,8 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
lookAtY: centerY,
};
}, [object.cache.boundingBox]);
const yawRef = useRef(0);
const yawRef = useRef(object.rotation[1]);
const pitchRef = useRef(0);
const mouseRef = useRef({ x: 0, y: 0 });
const jumpPressedRef = useRef(false);
+67
View File
@@ -0,0 +1,67 @@
import { Html } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { useRef } from "react";
import * as THREE from "three";
import type { RuntimeObjectInstance } from "../types";
import { debugLabelsStore } from "../state/debugLabelsStore";
import { state } from "../state";
import { observer } from "mobx-react-lite";
export const DebugLabel = observer(function ({ obj }: { obj: RuntimeObjectInstance; }) {
const groupRef = useRef<THREE.Group>(null);
const rb = obj.rb();
const texts = [
`${state.world.getObjectTypeById(obj.typeId)?.name} ${obj.id}`,
`position: ${obj.position}`,
`rotation: ${obj.rotation}`,
...Object.entries(debugLabelsStore.getAll(obj.id))
.map(([k, v]) => `${k}: ${v}`),
];
if (rb) {
try {
const t = rb.translation();
texts.push(...[
`rb position: ${[t.x, t.y, t.z]}`,
]);
} catch {
texts.push('rb: stale handle');
}
}
useFrame(() => {
if (!groupRef.current)
return;
const [px, py, pz] = obj.position;
// const bb = obj.cache.boundingBox;
// const halfHeight = bb ? (bb.max[1] - bb.min[1]) / 2 : 1;
// groupRef.current.position.set(px, py + halfHeight + 0.5, pz);
groupRef.current.position.set(px, py, pz);
});
const text = texts.join('\n');
if (!text)
return null;
return (
<group ref={groupRef}>
<Html center sprite>
<div style={{
background: 'rgba(0,0,0,0.65)',
color: '#fff',
padding: '2px 7px',
borderRadius: 4,
fontSize: 12,
fontFamily: 'monospace',
whiteSpace: 'pre',
pointerEvents: 'none',
userSelect: 'none',
}}>
{text}
</div>
</Html>
</group>
);
});
@@ -0,0 +1,25 @@
import { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { debugLabelsStore } from "../state/debugLabelsStore";
import { DebugLabel } from "./DebugLabel";
import type { RuntimeScene } from "../types";
export const DebugLabelsView = observer(function ({ scene }: { scene: RuntimeScene }) {
useEffect(
() => {
return () => { debugLabelsStore.clear(); };
},
[],
);
return <>
{
Object.entries(scene.objects)
.map(([objectId, obj]) => {
return <DebugLabel key={objectId} obj={obj} />;
})
}
</>;
});
+1
View File
@@ -1,5 +1,6 @@
import { observer } from "mobx-react-lite";
import { SceneView } from "./SceneView";
import { DebugLabelsView } from "./DebugLabelsView";
import { state } from "../state";
import { useFrame } from "@react-three/fiber";
import { Suspense } from "react";
+29 -8
View File
@@ -1,7 +1,9 @@
import { observer } from "mobx-react-lite";
import { runInAction } from "mobx";
import type { ObjectInstance, R3, Runtime } from "../types";
import { useMemo, useRef, type RefObject } from "react";
import type { Group } from "three";
import { Euler, Quaternion, Vector3 } from "three";
import { TransformControls, useHelper } from "@react-three/drei";
import { BoxHelper } from "three";
import { state } from "../state";
@@ -16,12 +18,13 @@ type ObjectEditorViewProps = {
type SelectionOverlayProps = {
objectId: string;
ref: RefObject<ObjectViewInternalHandle | null>;
onTransformChange: () => void;
onTransformEnd: () => void;
}
// Separate observer so only the 2 affected instances (selected/deselected)
// re-render on selection change, not all N objects in the scene.
const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: SelectionOverlayProps) {
const SelectionOverlay = observer(function ({ objectId, ref, onTransformChange, onTransformEnd }: SelectionOverlayProps) {
const isSelected = state.worldEditor.isEnabled &&
state.worldEditor.selection?.type === 'object' &&
state.worldEditor.selection.id === objectId;
@@ -45,6 +48,7 @@ const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: S
<TransformControls
object={groupRef as RefObject<Group>}
mode={selectionMode}
onChange={onTransformChange}
onMouseUp={onTransformEnd}
/>
);
@@ -72,21 +76,38 @@ export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectE
}
}
function handleTransformChange() {
const group = dataRef.current?.group;
if (!group) return;
const pos = group.getWorldPosition(new Vector3());
const quat = group.getWorldQuaternion(new Quaternion());
const euler = new Euler().setFromQuaternion(quat);
runInAction(() => {
object.position = pos.toArray() as R3;
object.rotation = [euler.x, euler.y, euler.z];
});
}
function handleTransformEnd() {
const group = dataRef.current?.group;
if (group)
state.worldEditor.setObjectTransform(
object.id,
group.position.toArray(),
group.rotation.toArray().slice(0, 3) as R3,
group.scale.toArray(),
);
if (!group) return;
const pos = group.getWorldPosition(new Vector3());
const quat = group.getWorldQuaternion(new Quaternion());
const euler = new Euler().setFromQuaternion(quat);
const scale = group.getWorldScale(new Vector3());
state.worldEditor.setObjectTransform(
object.id,
pos.toArray(),
[euler.x, euler.y, euler.z],
scale.toArray(),
);
}
return (<>
<SelectionOverlay
objectId={object.id}
ref={dataRef}
onTransformChange={handleTransformChange}
onTransformEnd={handleTransformEnd}
/>
<ObjectViewInternal
+4
View File
@@ -2,9 +2,13 @@ import { observer } from "mobx-react-lite";
import { LeftPanel } from "./LeftPanel";
import { MainPanel } from "./MainPanel";
import './Panels.scss';
import { state } from "../state";
export const Panels = observer(function () {
if (state.game)
return null;
return <div className="overlay-panels">
<LeftPanel />
<MainPanel />
+6 -4
View File
@@ -1,7 +1,8 @@
import { observer } from "mobx-react-lite";
import type { RuntimeScene } from "../types";
import type { RuntimeGameObjectInstance, RuntimeScene } from "../types";
import { GameObjectView } from "./GameObjectView";
import { ObjectEditorView } from "./ObjectEditorView";
import { DebugLabelsView } from "./DebugLabelsView";
type SceneViewProps = {
scene: RuntimeScene,
@@ -14,10 +15,11 @@ export const SceneView = observer(function (props: SceneViewProps) {
<directionalLight position={[5, 5, 5]} intensity={1} />
{
Object.values(props.scene.objects).map((obj) => (
props.editMode
? <ObjectEditorView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
: <GameObjectView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
!props.editMode
? <GameObjectView key={obj.id} object={obj as RuntimeGameObjectInstance} isPlayer={props.scene.playerObjectId === obj.id} />
: <ObjectEditorView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
))
}
<DebugLabelsView scene={props.scene} />
</>);
});
+5 -2
View File
@@ -7,7 +7,7 @@ import { GameView } from './GameView';
import { SceneEditorView } from './SceneEditorView';
import { JoystickView } from './JoystickView';
import type { RefObject } from 'react';
import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled } from '@tabler/icons-react';
import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled, IconPlayerSkipForwardFilled } from '@tabler/icons-react';
import { RenderInfoUpdater } from './renderInfo';
export const ThreeView = observer(function () {
@@ -37,7 +37,10 @@ export const ThreeView = observer(function () {
<button onClick={() => state.stopGame()}><IconPlayerStopFilled size="1em" /></button>
{
state.game!.isPaused
? <button onClick={() => state.game!.resume()}><IconPlayerPlayFilled size="1em" /></button>
? <>
<button onClick={() => state.game!.resume()}><IconPlayerPlayFilled size="1em" /></button>
<button onClick={() => state.game!.step()}><IconPlayerSkipForwardFilled size="1em" /></button>
</>
: <button onClick={() => state.game!.pause()}><IconPlayerPauseFilled size="1em" /></button>
}
</>
@@ -4,8 +4,80 @@
display: flex;
flex-direction: column;
.script-editor-toolbar {
display: flex;
gap: 6px;
padding: 4px 6px;
background: #2a2a2a;
button {
padding: 2px 10px;
font-size: 12px;
cursor: pointer;
}
}
.blockly-container {
flex: 1;
min-height: 0;
}
.script-editor-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.script-editor-popup {
background: #1e1e1e;
color: #d4d4d4;
border: 1px solid #444;
border-radius: 6px;
width: 70vw;
max-height: 75vh;
display: flex;
flex-direction: column;
overflow: hidden;
.script-editor-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #2d2d2d;
border-bottom: 1px solid #444;
font-size: 13px;
font-weight: bold;
.script-editor-popup-actions {
display: flex;
gap: 8px;
align-items: center;
}
button {
background: none;
border: none;
color: #aaa;
cursor: pointer;
font-size: 13px;
line-height: 1;
&:hover { color: #fff; }
}
}
.script-editor-popup-content {
flex: 1;
overflow: auto;
margin: 0;
padding: 12px;
font-size: 12px;
font-family: monospace;
white-space: pre;
}
}
}
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { runInAction } from "mobx";
import * as Blockly from "blockly";
@@ -39,12 +39,22 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
{ kind: 'block', type: 'player_object_value' },
],
},
{
kind: 'category',
name: 'Loops',
colour: '50',
contents: [
{ kind: 'block', type: 'loop_forever' },
],
},
{
kind: 'category',
name: 'Actions',
colour: '20',
contents: [
{ kind: 'block', type: 'console_log_action' },
{ kind: 'block', type: 'move_to_position_action' },
{ kind: 'block', type: 'move_in_direction_action' },
{ kind: 'block', type: 'physics_apply_impulse_action' },
{ kind: 'block', type: 'wait_seconds_action' },
],
@@ -65,6 +75,7 @@ type ScriptEditorViewProps = {
}
export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorViewProps) {
const [popup, setPopup] = useState<{ title: string; content: string } | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
const objectTypeRef = useRef(objectType);
@@ -86,8 +97,6 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
const serialized = JSON.stringify(Blockly.serialization.workspaces.save(workspace));
current.program = serialized;
console.log(serialized);
try {
current.javascript = generateCode(workspace);
}
@@ -95,7 +104,6 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
console.log(err);
current.javascript = undefined;
}
console.log(current.javascript);
});
};
workspace.addChangeListener(listener);
@@ -139,7 +147,34 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
}
}, [objectType.id, objectType.program]);
const showProgram = () => {
const raw = objectType.program;
setPopup({ title: 'Program JSON', content: raw ? JSON.stringify(JSON.parse(raw), null, 2) : '(empty)' });
};
const showJavascript = () => {
setPopup({ title: 'JavaScript', content: objectType.javascript ?? '(empty)' });
};
return <div className="script-editor">
<div className="script-editor-toolbar">
<button onClick={showProgram}>Program JSON</button>
<button onClick={showJavascript}>JavaScript</button>
</div>
<div ref={containerRef} className="blockly-container" />
{popup && (
<div className="script-editor-overlay" onClick={() => setPopup(null)}>
<div className="script-editor-popup" onClick={e => e.stopPropagation()}>
<div className="script-editor-popup-header">
<span>{popup.title}</span>
<div className="script-editor-popup-actions">
<button onClick={() => navigator.clipboard.writeText(popup.content)}>Copy all</button>
<button onClick={() => setPopup(null)}>✕</button>
</div>
</div>
<pre className="script-editor-popup-content">{popup.content}</pre>
</div>
</div>
)}
</div>;
});