object move blocks
debug floaters
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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} />
|
||||
</>);
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user