Add runtime entity collection and focus target handling
This commit is contained in:
@@ -2,7 +2,7 @@ import type { Vec3 } from "../core/vector";
|
||||
import type { BoxBrush, BoxFaceId, FaceUvState } from "../document/brushes";
|
||||
import type { SceneDocument, WorldSettings } from "../document/scene-document";
|
||||
import { cloneWorldSettings } from "../document/world-settings";
|
||||
import { getPrimaryPlayerStartEntity } from "../entities/entity-instances";
|
||||
import { getEntityInstances, getPrimaryPlayerStartEntity, type EntityInstance } from "../entities/entity-instances";
|
||||
import { getBoxBrushBounds } from "../geometry/box-brush";
|
||||
import { cloneMaterialDef, type MaterialDef } from "../materials/starter-material-library";
|
||||
import { cloneFaceUvState } from "../document/brushes";
|
||||
@@ -44,6 +44,45 @@ export interface RuntimePlayerStart {
|
||||
yawDegrees: number;
|
||||
}
|
||||
|
||||
export interface RuntimeSoundEmitter {
|
||||
entityId: string;
|
||||
position: Vec3;
|
||||
radius: number;
|
||||
gain: number;
|
||||
autoplay: boolean;
|
||||
loop: boolean;
|
||||
}
|
||||
|
||||
export interface RuntimeTriggerVolume {
|
||||
entityId: string;
|
||||
position: Vec3;
|
||||
size: Vec3;
|
||||
triggerOnEnter: boolean;
|
||||
triggerOnExit: boolean;
|
||||
}
|
||||
|
||||
export interface RuntimeTeleportTarget {
|
||||
entityId: string;
|
||||
position: Vec3;
|
||||
yawDegrees: number;
|
||||
}
|
||||
|
||||
export interface RuntimeInteractable {
|
||||
entityId: string;
|
||||
position: Vec3;
|
||||
radius: number;
|
||||
prompt: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface RuntimeEntityCollection {
|
||||
playerStarts: RuntimePlayerStart[];
|
||||
soundEmitters: RuntimeSoundEmitter[];
|
||||
triggerVolumes: RuntimeTriggerVolume[];
|
||||
teleportTargets: RuntimeTeleportTarget[];
|
||||
interactables: RuntimeInteractable[];
|
||||
}
|
||||
|
||||
export interface RuntimeSpawnPoint {
|
||||
source: "playerStart" | "fallback";
|
||||
entityId: string | null;
|
||||
@@ -56,6 +95,7 @@ export interface RuntimeSceneDefinition {
|
||||
brushes: RuntimeBoxBrushInstance[];
|
||||
colliders: RuntimeBoxCollider[];
|
||||
sceneBounds: RuntimeSceneBounds | null;
|
||||
entities: RuntimeEntityCollection;
|
||||
playerStart: RuntimePlayerStart | null;
|
||||
spawn: RuntimeSpawnPoint;
|
||||
}
|
||||
@@ -197,12 +237,78 @@ function buildFallbackSpawn(sceneBounds: RuntimeSceneBounds | null): RuntimeSpaw
|
||||
};
|
||||
}
|
||||
|
||||
function buildRuntimeEntityCollection(document: SceneDocument): RuntimeEntityCollection {
|
||||
const runtimeEntities: RuntimeEntityCollection = {
|
||||
playerStarts: [],
|
||||
soundEmitters: [],
|
||||
triggerVolumes: [],
|
||||
teleportTargets: [],
|
||||
interactables: []
|
||||
};
|
||||
|
||||
for (const entity of getEntityInstances(document.entities)) {
|
||||
switch (entity.kind) {
|
||||
case "playerStart":
|
||||
runtimeEntities.playerStarts.push({
|
||||
entityId: entity.id,
|
||||
position: cloneVec3(entity.position),
|
||||
yawDegrees: entity.yawDegrees
|
||||
});
|
||||
break;
|
||||
case "soundEmitter":
|
||||
runtimeEntities.soundEmitters.push({
|
||||
entityId: entity.id,
|
||||
position: cloneVec3(entity.position),
|
||||
radius: entity.radius,
|
||||
gain: entity.gain,
|
||||
autoplay: entity.autoplay,
|
||||
loop: entity.loop
|
||||
});
|
||||
break;
|
||||
case "triggerVolume":
|
||||
runtimeEntities.triggerVolumes.push({
|
||||
entityId: entity.id,
|
||||
position: cloneVec3(entity.position),
|
||||
size: cloneVec3(entity.size),
|
||||
triggerOnEnter: entity.triggerOnEnter,
|
||||
triggerOnExit: entity.triggerOnExit
|
||||
});
|
||||
break;
|
||||
case "teleportTarget":
|
||||
runtimeEntities.teleportTargets.push({
|
||||
entityId: entity.id,
|
||||
position: cloneVec3(entity.position),
|
||||
yawDegrees: entity.yawDegrees
|
||||
});
|
||||
break;
|
||||
case "interactable":
|
||||
runtimeEntities.interactables.push({
|
||||
entityId: entity.id,
|
||||
position: cloneVec3(entity.position),
|
||||
radius: entity.radius,
|
||||
prompt: entity.prompt,
|
||||
enabled: entity.enabled
|
||||
});
|
||||
break;
|
||||
default:
|
||||
assertNever(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return runtimeEntities;
|
||||
}
|
||||
|
||||
function assertNever(value: never): never {
|
||||
throw new Error(`Unsupported runtime entity: ${String((value as EntityInstance).kind)}`);
|
||||
}
|
||||
|
||||
export function buildRuntimeSceneFromDocument(document: SceneDocument, options: BuildRuntimeSceneOptions = {}): RuntimeSceneDefinition {
|
||||
assertRuntimeSceneBuildable(document, options.navigationMode ?? "orbitVisitor");
|
||||
|
||||
const brushes = Object.values(document.brushes).map((brush) => buildRuntimeBrush(brush, document));
|
||||
const colliders = Object.values(document.brushes).map((brush) => buildRuntimeCollider(brush));
|
||||
const sceneBounds = combineColliderBounds(colliders);
|
||||
const entities = buildRuntimeEntityCollection(document);
|
||||
const playerStartEntity = getPrimaryPlayerStartEntity(document.entities);
|
||||
const playerStart =
|
||||
playerStartEntity === null
|
||||
@@ -218,6 +324,7 @@ export function buildRuntimeSceneFromDocument(document: SceneDocument, options:
|
||||
brushes,
|
||||
colliders,
|
||||
sceneBounds,
|
||||
entities,
|
||||
playerStart,
|
||||
spawn:
|
||||
playerStart === null
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getSingleSelectedBrushId, getSingleSelectedEntityId, type EditorSelecti
|
||||
import type { Vec3 } from "../core/vector";
|
||||
import type { BoxBrush } from "../document/brushes";
|
||||
import type { SceneDocument } from "../document/scene-document";
|
||||
import type { EntityInstance } from "../entities/entity-instances";
|
||||
|
||||
const PLAYER_START_FOCUS_HALF_EXTENTS: Vec3 = {
|
||||
x: 0.35,
|
||||
@@ -9,6 +10,12 @@ const PLAYER_START_FOCUS_HALF_EXTENTS: Vec3 = {
|
||||
z: 0.55
|
||||
};
|
||||
|
||||
const TELEPORT_TARGET_FOCUS_HALF_EXTENTS: Vec3 = {
|
||||
x: 0.42,
|
||||
y: 0.28,
|
||||
z: 0.42
|
||||
};
|
||||
|
||||
interface FocusBoundsAccumulator {
|
||||
min: Vec3;
|
||||
max: Vec3;
|
||||
@@ -73,24 +80,6 @@ function createBrushFocusTarget(brush: BoxBrush): ViewportFocusTarget {
|
||||
};
|
||||
}
|
||||
|
||||
function createPlayerStartFocusTarget(position: Vec3): ViewportFocusTarget {
|
||||
return {
|
||||
center: {
|
||||
x: position.x,
|
||||
y: position.y + PLAYER_START_FOCUS_HALF_EXTENTS.y,
|
||||
z: position.z
|
||||
},
|
||||
radius: Math.max(
|
||||
0.45,
|
||||
Math.hypot(
|
||||
PLAYER_START_FOCUS_HALF_EXTENTS.x,
|
||||
PLAYER_START_FOCUS_HALF_EXTENTS.y,
|
||||
PLAYER_START_FOCUS_HALF_EXTENTS.z
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function includeBrush(bounds: FocusBoundsAccumulator, brush: BoxBrush) {
|
||||
const halfSize = {
|
||||
x: brush.size.x * 0.5,
|
||||
@@ -129,6 +118,155 @@ function includePlayerStart(bounds: FocusBoundsAccumulator, position: Vec3) {
|
||||
);
|
||||
}
|
||||
|
||||
function createBoundsFocusTarget(center: Vec3, halfExtents: Vec3, minimumRadius: number): ViewportFocusTarget {
|
||||
return {
|
||||
center,
|
||||
radius: Math.max(minimumRadius, Math.hypot(halfExtents.x, halfExtents.y, halfExtents.z))
|
||||
};
|
||||
}
|
||||
|
||||
function createPlayerStartFocusTarget(position: Vec3): ViewportFocusTarget {
|
||||
return createBoundsFocusTarget(
|
||||
{
|
||||
x: position.x,
|
||||
y: position.y + PLAYER_START_FOCUS_HALF_EXTENTS.y,
|
||||
z: position.z
|
||||
},
|
||||
PLAYER_START_FOCUS_HALF_EXTENTS,
|
||||
0.45
|
||||
);
|
||||
}
|
||||
|
||||
function includeTeleportTarget(bounds: FocusBoundsAccumulator, position: Vec3) {
|
||||
includeBounds(
|
||||
bounds,
|
||||
{
|
||||
x: position.x - TELEPORT_TARGET_FOCUS_HALF_EXTENTS.x,
|
||||
y: position.y,
|
||||
z: position.z - TELEPORT_TARGET_FOCUS_HALF_EXTENTS.z
|
||||
},
|
||||
{
|
||||
x: position.x + TELEPORT_TARGET_FOCUS_HALF_EXTENTS.x,
|
||||
y: position.y + TELEPORT_TARGET_FOCUS_HALF_EXTENTS.y * 2,
|
||||
z: position.z + TELEPORT_TARGET_FOCUS_HALF_EXTENTS.z
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createTeleportTargetFocusTarget(position: Vec3): ViewportFocusTarget {
|
||||
return createBoundsFocusTarget(
|
||||
{
|
||||
x: position.x,
|
||||
y: position.y + TELEPORT_TARGET_FOCUS_HALF_EXTENTS.y,
|
||||
z: position.z
|
||||
},
|
||||
TELEPORT_TARGET_FOCUS_HALF_EXTENTS,
|
||||
0.45
|
||||
);
|
||||
}
|
||||
|
||||
function includeSphereEntity(bounds: FocusBoundsAccumulator, position: Vec3, radius: number) {
|
||||
includeBounds(
|
||||
bounds,
|
||||
{
|
||||
x: position.x - radius,
|
||||
y: position.y - radius,
|
||||
z: position.z - radius
|
||||
},
|
||||
{
|
||||
x: position.x + radius,
|
||||
y: position.y + radius,
|
||||
z: position.z + radius
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createSphereEntityFocusTarget(position: Vec3, radius: number, minimumRadius: number): ViewportFocusTarget {
|
||||
return {
|
||||
center: {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z
|
||||
},
|
||||
radius: Math.max(minimumRadius, radius)
|
||||
};
|
||||
}
|
||||
|
||||
function includeTriggerVolume(bounds: FocusBoundsAccumulator, position: Vec3, size: Vec3) {
|
||||
const halfSize = {
|
||||
x: size.x * 0.5,
|
||||
y: size.y * 0.5,
|
||||
z: size.z * 0.5
|
||||
};
|
||||
|
||||
includeBounds(
|
||||
bounds,
|
||||
{
|
||||
x: position.x - halfSize.x,
|
||||
y: position.y - halfSize.y,
|
||||
z: position.z - halfSize.z
|
||||
},
|
||||
{
|
||||
x: position.x + halfSize.x,
|
||||
y: position.y + halfSize.y,
|
||||
z: position.z + halfSize.z
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createTriggerVolumeFocusTarget(position: Vec3, size: Vec3): ViewportFocusTarget {
|
||||
const halfSize = {
|
||||
x: size.x * 0.5,
|
||||
y: size.y * 0.5,
|
||||
z: size.z * 0.5
|
||||
};
|
||||
|
||||
return createBoundsFocusTarget(
|
||||
{
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z
|
||||
},
|
||||
halfSize,
|
||||
0.75
|
||||
);
|
||||
}
|
||||
|
||||
function includeEntity(bounds: FocusBoundsAccumulator, entity: EntityInstance) {
|
||||
switch (entity.kind) {
|
||||
case "playerStart":
|
||||
includePlayerStart(bounds, entity.position);
|
||||
break;
|
||||
case "soundEmitter":
|
||||
includeSphereEntity(bounds, entity.position, Math.max(0.4, entity.radius));
|
||||
break;
|
||||
case "triggerVolume":
|
||||
includeTriggerVolume(bounds, entity.position, entity.size);
|
||||
break;
|
||||
case "teleportTarget":
|
||||
includeTeleportTarget(bounds, entity.position);
|
||||
break;
|
||||
case "interactable":
|
||||
includeSphereEntity(bounds, entity.position, Math.max(0.4, entity.radius));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function createEntityFocusTarget(entity: EntityInstance): ViewportFocusTarget {
|
||||
switch (entity.kind) {
|
||||
case "playerStart":
|
||||
return createPlayerStartFocusTarget(entity.position);
|
||||
case "soundEmitter":
|
||||
return createSphereEntityFocusTarget(entity.position, entity.radius, 0.75);
|
||||
case "triggerVolume":
|
||||
return createTriggerVolumeFocusTarget(entity.position, entity.size);
|
||||
case "teleportTarget":
|
||||
return createTeleportTargetFocusTarget(entity.position);
|
||||
case "interactable":
|
||||
return createSphereEntityFocusTarget(entity.position, entity.radius, 0.65);
|
||||
}
|
||||
}
|
||||
|
||||
function getSceneFocusTarget(document: SceneDocument): ViewportFocusTarget | null {
|
||||
const bounds = createEmptyBoundsAccumulator();
|
||||
|
||||
@@ -137,9 +275,7 @@ function getSceneFocusTarget(document: SceneDocument): ViewportFocusTarget | nul
|
||||
}
|
||||
|
||||
for (const entity of Object.values(document.entities)) {
|
||||
if (entity.kind === "playerStart") {
|
||||
includePlayerStart(bounds, entity.position);
|
||||
}
|
||||
includeEntity(bounds, entity);
|
||||
}
|
||||
|
||||
return finishBounds(bounds);
|
||||
@@ -161,8 +297,8 @@ export function resolveViewportFocusTarget(document: SceneDocument, selection: E
|
||||
if (selectedEntityId !== null) {
|
||||
const entity = document.entities[selectedEntityId];
|
||||
|
||||
if (entity?.kind === "playerStart") {
|
||||
return createPlayerStartFocusTarget(entity.position);
|
||||
if (entity !== undefined) {
|
||||
return createEntityFocusTarget(entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user