Feature: Implement climbable status for box faces across UI and runtime geometry
This commit is contained in:
@@ -34,6 +34,7 @@ import { createMoveBoxBrushCommand } from "../commands/move-box-brush-command";
|
||||
import { createRotateBoxBrushCommand } from "../commands/rotate-box-brush-command";
|
||||
import { createResizeBoxBrushCommand } from "../commands/resize-box-brush-command";
|
||||
import { createSetBoxBrushAllFaceMaterialsCommand } from "../commands/set-box-brush-all-face-materials-command";
|
||||
import { createSetBoxBrushFaceClimbableCommand } from "../commands/set-box-brush-face-climbable-command";
|
||||
import { createSetBoxBrushFaceMaterialCommand } from "../commands/set-box-brush-face-material-command";
|
||||
import { createSetBoxBrushAuthoredStateCommand } from "../commands/set-box-brush-authored-state-command";
|
||||
import { createSetBoxBrushNameCommand } from "../commands/set-box-brush-name-command";
|
||||
@@ -13380,6 +13381,43 @@ export function App({ store, initialStatusMessage }: AppProps) {
|
||||
);
|
||||
};
|
||||
|
||||
const handleFaceClimbableChange = (climbable: boolean) => {
|
||||
if (
|
||||
selectedBrush === null ||
|
||||
selectedFaceId === null ||
|
||||
selectedFace === null
|
||||
) {
|
||||
setStatusMessage("Select a single box face before editing climbability.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedFace.climbable === climbable) {
|
||||
setStatusMessage(
|
||||
`${getBrushFaceLabel(selectedBrush, selectedFaceId)} is already ${
|
||||
climbable ? "climbable" : "not climbable"
|
||||
}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
store.executeCommand(
|
||||
createSetBoxBrushFaceClimbableCommand({
|
||||
brushId: selectedBrush.id,
|
||||
faceId: selectedFaceId,
|
||||
climbable
|
||||
})
|
||||
);
|
||||
setStatusMessage(
|
||||
climbable
|
||||
? `Marked ${getBrushFaceLabel(selectedBrush, selectedFaceId)} climbable.`
|
||||
: `Cleared climbable from ${getBrushFaceLabel(selectedBrush, selectedFaceId)}.`
|
||||
);
|
||||
} catch (error) {
|
||||
setStatusMessage(getErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
const applyFaceUvState = (
|
||||
uvState: FaceUvState,
|
||||
label: string,
|
||||
@@ -25230,6 +25268,25 @@ export function App({ store, initialStatusMessage }: AppProps) {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{materialInspectorScope === "face" &&
|
||||
selectedFace !== null ? (
|
||||
<div className="form-section">
|
||||
<label className="form-field form-field--toggle">
|
||||
<span className="label">Climbable</span>
|
||||
<input
|
||||
data-testid="face-climbable"
|
||||
type="checkbox"
|
||||
checked={selectedFace.climbable}
|
||||
onChange={(event) =>
|
||||
handleFaceClimbableChange(
|
||||
event.currentTarget.checked
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="form-section">
|
||||
<div className="label">Material</div>
|
||||
<div className="material-browser">
|
||||
|
||||
@@ -217,7 +217,8 @@ export type PlayerStartMovementAction =
|
||||
export const PLAYER_START_LOCOMOTION_ACTIONS = [
|
||||
"jump",
|
||||
"sprint",
|
||||
"crouch"
|
||||
"crouch",
|
||||
"climb"
|
||||
] as const;
|
||||
export type PlayerStartLocomotionAction =
|
||||
(typeof PLAYER_START_LOCOMOTION_ACTIONS)[number];
|
||||
@@ -283,6 +284,7 @@ export interface PlayerStartKeyboardBindings {
|
||||
jump: PlayerStartKeyboardBindingCode;
|
||||
sprint: PlayerStartKeyboardBindingCode;
|
||||
crouch: PlayerStartKeyboardBindingCode;
|
||||
climb: PlayerStartKeyboardBindingCode;
|
||||
interact: PlayerStartKeyboardBindingCode;
|
||||
clearTarget: PlayerStartKeyboardBindingCode;
|
||||
pauseTime: PlayerStartKeyboardBindingCode;
|
||||
@@ -296,6 +298,7 @@ export interface PlayerStartGamepadBindings {
|
||||
jump: PlayerStartGamepadActionBinding;
|
||||
sprint: PlayerStartGamepadActionBinding;
|
||||
crouch: PlayerStartGamepadActionBinding;
|
||||
climb: PlayerStartGamepadActionBinding;
|
||||
interact: PlayerStartGamepadActionBinding;
|
||||
clearTarget: PlayerStartGamepadActionBinding;
|
||||
pauseTime: PlayerStartGamepadActionBinding;
|
||||
@@ -551,6 +554,7 @@ export const DEFAULT_PLAYER_START_KEYBOARD_BINDINGS: PlayerStartKeyboardBindings
|
||||
jump: "Space",
|
||||
sprint: "ShiftLeft",
|
||||
crouch: "ControlLeft",
|
||||
climb: "KeyE",
|
||||
interact: "MouseLeft",
|
||||
clearTarget: "KeyQ",
|
||||
pauseTime: "KeyP"
|
||||
@@ -564,6 +568,7 @@ export const DEFAULT_PLAYER_START_GAMEPAD_BINDINGS: PlayerStartGamepadBindings =
|
||||
jump: "buttonSouth",
|
||||
sprint: "leftStickPress",
|
||||
crouch: "buttonEast",
|
||||
climb: "rightShoulder",
|
||||
interact: "buttonWest",
|
||||
clearTarget: "buttonNorth",
|
||||
pauseTime: "buttonMenu",
|
||||
|
||||
@@ -263,7 +263,8 @@ function createRuntimeGeometryBrush(brush: RuntimeBoxBrushInstance): Brush {
|
||||
faceId,
|
||||
{
|
||||
materialId: face.materialId,
|
||||
uv: cloneFaceUvState(face.uv)
|
||||
uv: cloneFaceUvState(face.uv),
|
||||
climbable: face.climbable
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
type BoxBrushLightFalloffMode,
|
||||
type BoxBrushVolumeSettings,
|
||||
type WhiteboxFaceId,
|
||||
type WhiteboxVertexId,
|
||||
type FaceUvState
|
||||
} from "../document/brushes";
|
||||
import type { ProjectDocument, SceneDocument } from "../document/scene-document";
|
||||
@@ -142,6 +143,16 @@ export interface RuntimeBrushFace {
|
||||
materialId: string | null;
|
||||
material: MaterialDef | null;
|
||||
uv: FaceUvState;
|
||||
climbable: boolean;
|
||||
}
|
||||
|
||||
export interface RuntimeBrushColliderFace {
|
||||
faceId: WhiteboxFaceId;
|
||||
climbable: boolean;
|
||||
normal: Vec3;
|
||||
vertexIds: readonly WhiteboxVertexId[];
|
||||
vertices: Vec3[];
|
||||
triangles: Array<readonly [number, number, number]>;
|
||||
}
|
||||
|
||||
export interface RuntimeBoxBrushInstance {
|
||||
@@ -241,6 +252,7 @@ export interface RuntimeBrushTriMeshCollider {
|
||||
rotationDegrees: Vec3;
|
||||
vertices: Float32Array;
|
||||
indices: Uint32Array;
|
||||
faces: RuntimeBrushColliderFace[];
|
||||
worldBounds: {
|
||||
min: Vec3;
|
||||
max: Vec3;
|
||||
@@ -768,7 +780,8 @@ function buildRuntimeBrush(
|
||||
{
|
||||
materialId: face.materialId,
|
||||
material: resolveRuntimeMaterial(document, face.materialId),
|
||||
uv: cloneFaceUvState(face.uv)
|
||||
uv: cloneFaceUvState(face.uv),
|
||||
climbable: face.climbable
|
||||
}
|
||||
];
|
||||
})
|
||||
@@ -848,6 +861,16 @@ function buildRuntimeCollider(brush: Brush): RuntimeBrushTriMeshCollider {
|
||||
rotationDegrees: cloneVec3(brush.rotationDegrees),
|
||||
vertices: derivedMesh.colliderVertices,
|
||||
indices: derivedMesh.colliderIndices,
|
||||
faces: derivedMesh.faceSurfaces.map((surface) => ({
|
||||
faceId: surface.faceId,
|
||||
climbable: brush.faces[surface.faceId]?.climbable ?? false,
|
||||
normal: cloneVec3(surface.normal),
|
||||
vertexIds: surface.vertexIds,
|
||||
vertices: surface.vertexIds.map((vertexId) =>
|
||||
cloneVec3(brush.geometry.vertices[vertexId])
|
||||
),
|
||||
triangles: surface.triangles
|
||||
})),
|
||||
worldBounds: {
|
||||
min: cloneVec3(bounds.min),
|
||||
max: cloneVec3(bounds.max)
|
||||
|
||||
Reference in New Issue
Block a user