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 { createRotateBoxBrushCommand } from "../commands/rotate-box-brush-command";
|
||||||
import { createResizeBoxBrushCommand } from "../commands/resize-box-brush-command";
|
import { createResizeBoxBrushCommand } from "../commands/resize-box-brush-command";
|
||||||
import { createSetBoxBrushAllFaceMaterialsCommand } from "../commands/set-box-brush-all-face-materials-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 { createSetBoxBrushFaceMaterialCommand } from "../commands/set-box-brush-face-material-command";
|
||||||
import { createSetBoxBrushAuthoredStateCommand } from "../commands/set-box-brush-authored-state-command";
|
import { createSetBoxBrushAuthoredStateCommand } from "../commands/set-box-brush-authored-state-command";
|
||||||
import { createSetBoxBrushNameCommand } from "../commands/set-box-brush-name-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 = (
|
const applyFaceUvState = (
|
||||||
uvState: FaceUvState,
|
uvState: FaceUvState,
|
||||||
label: string,
|
label: string,
|
||||||
@@ -25230,6 +25268,25 @@ export function App({ store, initialStatusMessage }: AppProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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="form-section">
|
||||||
<div className="label">Material</div>
|
<div className="label">Material</div>
|
||||||
<div className="material-browser">
|
<div className="material-browser">
|
||||||
|
|||||||
@@ -217,7 +217,8 @@ export type PlayerStartMovementAction =
|
|||||||
export const PLAYER_START_LOCOMOTION_ACTIONS = [
|
export const PLAYER_START_LOCOMOTION_ACTIONS = [
|
||||||
"jump",
|
"jump",
|
||||||
"sprint",
|
"sprint",
|
||||||
"crouch"
|
"crouch",
|
||||||
|
"climb"
|
||||||
] as const;
|
] as const;
|
||||||
export type PlayerStartLocomotionAction =
|
export type PlayerStartLocomotionAction =
|
||||||
(typeof PLAYER_START_LOCOMOTION_ACTIONS)[number];
|
(typeof PLAYER_START_LOCOMOTION_ACTIONS)[number];
|
||||||
@@ -283,6 +284,7 @@ export interface PlayerStartKeyboardBindings {
|
|||||||
jump: PlayerStartKeyboardBindingCode;
|
jump: PlayerStartKeyboardBindingCode;
|
||||||
sprint: PlayerStartKeyboardBindingCode;
|
sprint: PlayerStartKeyboardBindingCode;
|
||||||
crouch: PlayerStartKeyboardBindingCode;
|
crouch: PlayerStartKeyboardBindingCode;
|
||||||
|
climb: PlayerStartKeyboardBindingCode;
|
||||||
interact: PlayerStartKeyboardBindingCode;
|
interact: PlayerStartKeyboardBindingCode;
|
||||||
clearTarget: PlayerStartKeyboardBindingCode;
|
clearTarget: PlayerStartKeyboardBindingCode;
|
||||||
pauseTime: PlayerStartKeyboardBindingCode;
|
pauseTime: PlayerStartKeyboardBindingCode;
|
||||||
@@ -296,6 +298,7 @@ export interface PlayerStartGamepadBindings {
|
|||||||
jump: PlayerStartGamepadActionBinding;
|
jump: PlayerStartGamepadActionBinding;
|
||||||
sprint: PlayerStartGamepadActionBinding;
|
sprint: PlayerStartGamepadActionBinding;
|
||||||
crouch: PlayerStartGamepadActionBinding;
|
crouch: PlayerStartGamepadActionBinding;
|
||||||
|
climb: PlayerStartGamepadActionBinding;
|
||||||
interact: PlayerStartGamepadActionBinding;
|
interact: PlayerStartGamepadActionBinding;
|
||||||
clearTarget: PlayerStartGamepadActionBinding;
|
clearTarget: PlayerStartGamepadActionBinding;
|
||||||
pauseTime: PlayerStartGamepadActionBinding;
|
pauseTime: PlayerStartGamepadActionBinding;
|
||||||
@@ -551,6 +554,7 @@ export const DEFAULT_PLAYER_START_KEYBOARD_BINDINGS: PlayerStartKeyboardBindings
|
|||||||
jump: "Space",
|
jump: "Space",
|
||||||
sprint: "ShiftLeft",
|
sprint: "ShiftLeft",
|
||||||
crouch: "ControlLeft",
|
crouch: "ControlLeft",
|
||||||
|
climb: "KeyE",
|
||||||
interact: "MouseLeft",
|
interact: "MouseLeft",
|
||||||
clearTarget: "KeyQ",
|
clearTarget: "KeyQ",
|
||||||
pauseTime: "KeyP"
|
pauseTime: "KeyP"
|
||||||
@@ -564,6 +568,7 @@ export const DEFAULT_PLAYER_START_GAMEPAD_BINDINGS: PlayerStartGamepadBindings =
|
|||||||
jump: "buttonSouth",
|
jump: "buttonSouth",
|
||||||
sprint: "leftStickPress",
|
sprint: "leftStickPress",
|
||||||
crouch: "buttonEast",
|
crouch: "buttonEast",
|
||||||
|
climb: "rightShoulder",
|
||||||
interact: "buttonWest",
|
interact: "buttonWest",
|
||||||
clearTarget: "buttonNorth",
|
clearTarget: "buttonNorth",
|
||||||
pauseTime: "buttonMenu",
|
pauseTime: "buttonMenu",
|
||||||
|
|||||||
@@ -263,7 +263,8 @@ function createRuntimeGeometryBrush(brush: RuntimeBoxBrushInstance): Brush {
|
|||||||
faceId,
|
faceId,
|
||||||
{
|
{
|
||||||
materialId: face.materialId,
|
materialId: face.materialId,
|
||||||
uv: cloneFaceUvState(face.uv)
|
uv: cloneFaceUvState(face.uv),
|
||||||
|
climbable: face.climbable
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
type BoxBrushLightFalloffMode,
|
type BoxBrushLightFalloffMode,
|
||||||
type BoxBrushVolumeSettings,
|
type BoxBrushVolumeSettings,
|
||||||
type WhiteboxFaceId,
|
type WhiteboxFaceId,
|
||||||
|
type WhiteboxVertexId,
|
||||||
type FaceUvState
|
type FaceUvState
|
||||||
} from "../document/brushes";
|
} from "../document/brushes";
|
||||||
import type { ProjectDocument, SceneDocument } from "../document/scene-document";
|
import type { ProjectDocument, SceneDocument } from "../document/scene-document";
|
||||||
@@ -142,6 +143,16 @@ export interface RuntimeBrushFace {
|
|||||||
materialId: string | null;
|
materialId: string | null;
|
||||||
material: MaterialDef | null;
|
material: MaterialDef | null;
|
||||||
uv: FaceUvState;
|
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 {
|
export interface RuntimeBoxBrushInstance {
|
||||||
@@ -241,6 +252,7 @@ export interface RuntimeBrushTriMeshCollider {
|
|||||||
rotationDegrees: Vec3;
|
rotationDegrees: Vec3;
|
||||||
vertices: Float32Array;
|
vertices: Float32Array;
|
||||||
indices: Uint32Array;
|
indices: Uint32Array;
|
||||||
|
faces: RuntimeBrushColliderFace[];
|
||||||
worldBounds: {
|
worldBounds: {
|
||||||
min: Vec3;
|
min: Vec3;
|
||||||
max: Vec3;
|
max: Vec3;
|
||||||
@@ -768,7 +780,8 @@ function buildRuntimeBrush(
|
|||||||
{
|
{
|
||||||
materialId: face.materialId,
|
materialId: face.materialId,
|
||||||
material: resolveRuntimeMaterial(document, 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),
|
rotationDegrees: cloneVec3(brush.rotationDegrees),
|
||||||
vertices: derivedMesh.colliderVertices,
|
vertices: derivedMesh.colliderVertices,
|
||||||
indices: derivedMesh.colliderIndices,
|
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: {
|
worldBounds: {
|
||||||
min: cloneVec3(bounds.min),
|
min: cloneVec3(bounds.min),
|
||||||
max: cloneVec3(bounds.max)
|
max: cloneVec3(bounds.max)
|
||||||
|
|||||||
Reference in New Issue
Block a user