Add visibility and enabled properties to model instances, brushes, and entities
This commit is contained in:
@@ -16,6 +16,8 @@ export interface ModelInstance {
|
||||
kind: "modelInstance";
|
||||
assetId: string;
|
||||
name?: string;
|
||||
visible: boolean;
|
||||
enabled: boolean;
|
||||
position: Vec3;
|
||||
rotationDegrees: Vec3;
|
||||
scale: Vec3;
|
||||
@@ -42,6 +44,9 @@ export const DEFAULT_MODEL_INSTANCE_SCALE: Vec3 = {
|
||||
z: 1
|
||||
};
|
||||
|
||||
export const DEFAULT_MODEL_INSTANCE_VISIBLE = true;
|
||||
export const DEFAULT_MODEL_INSTANCE_ENABLED = true;
|
||||
|
||||
export const DEFAULT_MODEL_INSTANCE_COLLISION_SETTINGS: ModelInstanceCollisionSettings = {
|
||||
mode: "none",
|
||||
visible: false
|
||||
@@ -117,7 +122,7 @@ function assertPositiveFiniteVec3(vector: Vec3, label: string) {
|
||||
|
||||
export function createModelInstance(
|
||||
overrides: Partial<
|
||||
Pick<ModelInstance, "id" | "name" | "position" | "rotationDegrees" | "scale" | "collision" | "animationClipName" | "animationAutoplay">
|
||||
Pick<ModelInstance, "id" | "name" | "visible" | "enabled" | "position" | "rotationDegrees" | "scale" | "collision" | "animationClipName" | "animationAutoplay">
|
||||
> &
|
||||
Pick<ModelInstance, "assetId">
|
||||
): ModelInstance {
|
||||
@@ -125,6 +130,8 @@ export function createModelInstance(
|
||||
const rotationDegrees = cloneVec3(overrides.rotationDegrees ?? DEFAULT_MODEL_INSTANCE_ROTATION_DEGREES);
|
||||
const scale = cloneVec3(overrides.scale ?? DEFAULT_MODEL_INSTANCE_SCALE);
|
||||
const collision = cloneModelInstanceCollisionSettings(overrides.collision ?? DEFAULT_MODEL_INSTANCE_COLLISION_SETTINGS);
|
||||
const visible = overrides.visible ?? DEFAULT_MODEL_INSTANCE_VISIBLE;
|
||||
const enabled = overrides.enabled ?? DEFAULT_MODEL_INSTANCE_ENABLED;
|
||||
|
||||
if (overrides.assetId.trim().length === 0) {
|
||||
throw new Error("Model instance assetId must be a non-empty string.");
|
||||
@@ -134,11 +141,21 @@ export function createModelInstance(
|
||||
assertFiniteVec3(rotationDegrees, "Model instance rotation");
|
||||
assertPositiveFiniteVec3(scale, "Model instance scale");
|
||||
|
||||
if (typeof visible !== "boolean") {
|
||||
throw new Error("Model instance visible must be a boolean.");
|
||||
}
|
||||
|
||||
if (typeof enabled !== "boolean") {
|
||||
throw new Error("Model instance enabled must be a boolean.");
|
||||
}
|
||||
|
||||
return {
|
||||
id: overrides.id ?? createOpaqueId("model-instance"),
|
||||
kind: "modelInstance",
|
||||
assetId: overrides.assetId,
|
||||
name: normalizeModelInstanceName(overrides.name),
|
||||
visible,
|
||||
enabled,
|
||||
position,
|
||||
rotationDegrees,
|
||||
scale,
|
||||
@@ -178,6 +195,8 @@ export function areModelInstancesEqual(left: ModelInstance, right: ModelInstance
|
||||
left.kind === right.kind &&
|
||||
left.assetId === right.assetId &&
|
||||
left.name === right.name &&
|
||||
left.visible === right.visible &&
|
||||
left.enabled === right.enabled &&
|
||||
areVec3Equal(left.position, right.position) &&
|
||||
areVec3Equal(left.rotationDegrees, right.rotationDegrees) &&
|
||||
areVec3Equal(left.scale, right.scale) &&
|
||||
|
||||
@@ -122,6 +122,8 @@ export interface BoxBrush {
|
||||
id: string;
|
||||
kind: "box";
|
||||
name?: string;
|
||||
visible: boolean;
|
||||
enabled: boolean;
|
||||
center: Vec3;
|
||||
rotationDegrees: Vec3;
|
||||
size: Vec3;
|
||||
@@ -152,6 +154,9 @@ export const DEFAULT_BOX_BRUSH_ROTATION_DEGREES: Vec3 = {
|
||||
z: 0
|
||||
};
|
||||
|
||||
export const DEFAULT_BOX_BRUSH_VISIBLE = true;
|
||||
export const DEFAULT_BOX_BRUSH_ENABLED = true;
|
||||
|
||||
export const DEFAULT_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT = 6;
|
||||
export const MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT = 24;
|
||||
|
||||
@@ -482,7 +487,7 @@ export function cloneBoxBrushVolumeSettings(volume: BoxBrushVolumeSettings): Box
|
||||
|
||||
export function createBoxBrush(
|
||||
overrides: Partial<
|
||||
Pick<BoxBrush, "id" | "name" | "center" | "rotationDegrees" | "size" | "geometry" | "faces" | "volume" | "layerId" | "groupId">
|
||||
Pick<BoxBrush, "id" | "name" | "visible" | "enabled" | "center" | "rotationDegrees" | "size" | "geometry" | "faces" | "volume" | "layerId" | "groupId">
|
||||
> = {}
|
||||
): BoxBrush {
|
||||
const center = cloneVec3(overrides.center ?? DEFAULT_BOX_BRUSH_CENTER);
|
||||
@@ -490,15 +495,27 @@ export function createBoxBrush(
|
||||
const fallbackSize = cloneVec3(overrides.size ?? DEFAULT_BOX_BRUSH_SIZE);
|
||||
const geometry = overrides.geometry === undefined ? createDefaultBoxBrushGeometry(fallbackSize) : cloneBoxBrushGeometry(overrides.geometry);
|
||||
const size = deriveBoxBrushSizeFromGeometry(geometry);
|
||||
const visible = overrides.visible ?? DEFAULT_BOX_BRUSH_VISIBLE;
|
||||
const enabled = overrides.enabled ?? DEFAULT_BOX_BRUSH_ENABLED;
|
||||
|
||||
if (!hasPositiveBoxSize(size)) {
|
||||
throw new Error("Box brush size must remain positive on every axis.");
|
||||
}
|
||||
|
||||
if (typeof visible !== "boolean") {
|
||||
throw new Error("Box brush visible must be a boolean.");
|
||||
}
|
||||
|
||||
if (typeof enabled !== "boolean") {
|
||||
throw new Error("Box brush enabled must be a boolean.");
|
||||
}
|
||||
|
||||
return {
|
||||
id: overrides.id ?? createOpaqueId("brush"),
|
||||
kind: "box",
|
||||
name: normalizeBrushName(overrides.name),
|
||||
visible,
|
||||
enabled,
|
||||
center,
|
||||
rotationDegrees,
|
||||
size,
|
||||
|
||||
@@ -5,6 +5,8 @@ import { isHexColorString } from "../document/world-settings";
|
||||
interface PositionedEntity {
|
||||
id: string;
|
||||
name?: string;
|
||||
visible: boolean;
|
||||
enabled: boolean;
|
||||
position: Vec3;
|
||||
}
|
||||
|
||||
@@ -218,14 +220,14 @@ export interface InteractableEntity extends PositionedEntity {
|
||||
kind: "interactable";
|
||||
radius: number;
|
||||
prompt: string;
|
||||
enabled: boolean;
|
||||
interactionEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface SceneExitEntity extends PositionedEntity {
|
||||
kind: "sceneExit";
|
||||
radius: number;
|
||||
prompt: string;
|
||||
enabled: boolean;
|
||||
interactionEnabled: boolean;
|
||||
targetSceneId: string;
|
||||
targetEntryEntityId: string;
|
||||
}
|
||||
@@ -290,6 +292,9 @@ export const DEFAULT_ENTITY_POSITION: Vec3 = {
|
||||
z: 0
|
||||
};
|
||||
|
||||
export const DEFAULT_ENTITY_VISIBLE = true;
|
||||
export const DEFAULT_ENTITY_ENABLED = true;
|
||||
|
||||
export const DEFAULT_PLAYER_START_POSITION = DEFAULT_ENTITY_POSITION;
|
||||
export const DEFAULT_PLAYER_START_YAW_DEGREES = 0;
|
||||
export const DEFAULT_PLAYER_START_NAVIGATION_MODE: PlayerStartNavigationMode =
|
||||
@@ -1062,6 +1067,20 @@ export function normalizeEntityName(name: string | null | undefined): string | u
|
||||
return trimmedName.length === 0 ? undefined : trimmedName;
|
||||
}
|
||||
|
||||
function resolveAuthoredEntityVisibility(visible: boolean | undefined): boolean {
|
||||
const resolvedVisible = visible ?? DEFAULT_ENTITY_VISIBLE;
|
||||
|
||||
assertBoolean(resolvedVisible, "Entity visible");
|
||||
return resolvedVisible;
|
||||
}
|
||||
|
||||
function resolveAuthoredEntityEnabled(enabled: boolean | undefined): boolean {
|
||||
const resolvedEnabled = enabled ?? DEFAULT_ENTITY_ENABLED;
|
||||
|
||||
assertBoolean(resolvedEnabled, "Entity enabled");
|
||||
return resolvedEnabled;
|
||||
}
|
||||
|
||||
export function normalizeYawDegrees(yawDegrees: number): number {
|
||||
const normalizedYaw = yawDegrees % 360;
|
||||
return normalizedYaw < 0 ? normalizedYaw + 360 : normalizedYaw;
|
||||
@@ -1078,7 +1097,7 @@ export function normalizeInteractablePrompt(prompt: string): string {
|
||||
}
|
||||
|
||||
export function createPointLightEntity(
|
||||
overrides: Partial<Pick<PointLightEntity, "id" | "name" | "position" | "colorHex" | "intensity" | "distance">> = {}
|
||||
overrides: Partial<Pick<PointLightEntity, "id" | "name" | "visible" | "enabled" | "position" | "colorHex" | "intensity" | "distance">> = {}
|
||||
): PointLightEntity {
|
||||
const position = cloneVec3(overrides.position ?? DEFAULT_POINT_LIGHT_POSITION);
|
||||
const colorHex = overrides.colorHex ?? DEFAULT_POINT_LIGHT_COLOR_HEX;
|
||||
@@ -1094,6 +1113,8 @@ export function createPointLightEntity(
|
||||
id: overrides.id ?? createOpaqueId("entity-point-light"),
|
||||
kind: "pointLight",
|
||||
name: normalizeEntityName(overrides.name),
|
||||
visible: resolveAuthoredEntityVisibility(overrides.visible),
|
||||
enabled: resolveAuthoredEntityEnabled(overrides.enabled),
|
||||
position,
|
||||
colorHex,
|
||||
intensity,
|
||||
@@ -1102,7 +1123,7 @@ export function createPointLightEntity(
|
||||
}
|
||||
|
||||
export function createSpotLightEntity(
|
||||
overrides: Partial<Pick<SpotLightEntity, "id" | "name" | "position" | "direction" | "colorHex" | "intensity" | "distance" | "angleDegrees">> = {}
|
||||
overrides: Partial<Pick<SpotLightEntity, "id" | "name" | "visible" | "enabled" | "position" | "direction" | "colorHex" | "intensity" | "distance" | "angleDegrees">> = {}
|
||||
): SpotLightEntity {
|
||||
const position = cloneVec3(overrides.position ?? DEFAULT_SPOT_LIGHT_POSITION);
|
||||
const direction = cloneVec3(overrides.direction ?? DEFAULT_SPOT_LIGHT_DIRECTION);
|
||||
@@ -1126,6 +1147,8 @@ export function createSpotLightEntity(
|
||||
id: overrides.id ?? createOpaqueId("entity-spot-light"),
|
||||
kind: "spotLight",
|
||||
name: normalizeEntityName(overrides.name),
|
||||
visible: resolveAuthoredEntityVisibility(overrides.visible),
|
||||
enabled: resolveAuthoredEntityEnabled(overrides.enabled),
|
||||
position,
|
||||
direction,
|
||||
colorHex,
|
||||
@@ -1139,7 +1162,7 @@ export function createPlayerStartEntity(
|
||||
overrides: Partial<
|
||||
Pick<
|
||||
PlayerStartEntity,
|
||||
"id" | "name" | "position" | "yawDegrees" | "navigationMode"
|
||||
"id" | "name" | "visible" | "enabled" | "position" | "yawDegrees" | "navigationMode"
|
||||
>
|
||||
> & {
|
||||
movementTemplate?: PlayerStartMovementTemplateOverrides;
|
||||
@@ -1173,6 +1196,8 @@ export function createPlayerStartEntity(
|
||||
id: overrides.id ?? createOpaqueId("entity-player-start"),
|
||||
kind: "playerStart",
|
||||
name: normalizeEntityName(overrides.name),
|
||||
visible: resolveAuthoredEntityVisibility(overrides.visible),
|
||||
enabled: resolveAuthoredEntityEnabled(overrides.enabled),
|
||||
position,
|
||||
yawDegrees: normalizeYawDegrees(yawDegrees),
|
||||
navigationMode,
|
||||
@@ -1183,7 +1208,7 @@ export function createPlayerStartEntity(
|
||||
}
|
||||
|
||||
export function createSceneEntryEntity(
|
||||
overrides: Partial<Pick<SceneEntryEntity, "id" | "name" | "position" | "yawDegrees">> = {}
|
||||
overrides: Partial<Pick<SceneEntryEntity, "id" | "name" | "visible" | "enabled" | "position" | "yawDegrees">> = {}
|
||||
): SceneEntryEntity {
|
||||
const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION);
|
||||
const yawDegrees = overrides.yawDegrees ?? DEFAULT_SCENE_ENTRY_YAW_DEGREES;
|
||||
@@ -1198,6 +1223,8 @@ export function createSceneEntryEntity(
|
||||
id: overrides.id ?? createOpaqueId("entity-scene-entry"),
|
||||
kind: "sceneEntry",
|
||||
name: normalizeEntityName(overrides.name),
|
||||
visible: resolveAuthoredEntityVisibility(overrides.visible),
|
||||
enabled: resolveAuthoredEntityEnabled(overrides.enabled),
|
||||
position,
|
||||
yawDegrees: normalizeYawDegrees(yawDegrees)
|
||||
};
|
||||
@@ -1207,7 +1234,7 @@ export function createSoundEmitterEntity(
|
||||
overrides: Partial<
|
||||
Pick<
|
||||
SoundEmitterEntity,
|
||||
"id" | "name" | "position" | "audioAssetId" | "volume" | "refDistance" | "maxDistance" | "autoplay" | "loop"
|
||||
"id" | "name" | "visible" | "enabled" | "position" | "audioAssetId" | "volume" | "refDistance" | "maxDistance" | "autoplay" | "loop"
|
||||
>
|
||||
> = {}
|
||||
): SoundEmitterEntity {
|
||||
@@ -1235,6 +1262,8 @@ export function createSoundEmitterEntity(
|
||||
id: overrides.id ?? createOpaqueId("entity-sound-emitter"),
|
||||
kind: "soundEmitter",
|
||||
name: normalizeEntityName(overrides.name),
|
||||
visible: resolveAuthoredEntityVisibility(overrides.visible),
|
||||
enabled: resolveAuthoredEntityEnabled(overrides.enabled),
|
||||
position,
|
||||
audioAssetId,
|
||||
volume,
|
||||
@@ -1246,7 +1275,7 @@ export function createSoundEmitterEntity(
|
||||
}
|
||||
|
||||
export function createTriggerVolumeEntity(
|
||||
overrides: Partial<Pick<TriggerVolumeEntity, "id" | "name" | "position" | "size" | "triggerOnEnter" | "triggerOnExit">> = {}
|
||||
overrides: Partial<Pick<TriggerVolumeEntity, "id" | "name" | "visible" | "enabled" | "position" | "size" | "triggerOnEnter" | "triggerOnExit">> = {}
|
||||
): TriggerVolumeEntity {
|
||||
const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION);
|
||||
const size = cloneVec3(overrides.size ?? DEFAULT_TRIGGER_VOLUME_SIZE);
|
||||
@@ -1262,6 +1291,8 @@ export function createTriggerVolumeEntity(
|
||||
id: overrides.id ?? createOpaqueId("entity-trigger-volume"),
|
||||
kind: "triggerVolume",
|
||||
name: normalizeEntityName(overrides.name),
|
||||
visible: resolveAuthoredEntityVisibility(overrides.visible),
|
||||
enabled: resolveAuthoredEntityEnabled(overrides.enabled),
|
||||
position,
|
||||
size,
|
||||
triggerOnEnter,
|
||||
@@ -1270,7 +1301,7 @@ export function createTriggerVolumeEntity(
|
||||
}
|
||||
|
||||
export function createTeleportTargetEntity(
|
||||
overrides: Partial<Pick<TeleportTargetEntity, "id" | "name" | "position" | "yawDegrees">> = {}
|
||||
overrides: Partial<Pick<TeleportTargetEntity, "id" | "name" | "visible" | "enabled" | "position" | "yawDegrees">> = {}
|
||||
): TeleportTargetEntity {
|
||||
const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION);
|
||||
const yawDegrees = overrides.yawDegrees ?? DEFAULT_TELEPORT_TARGET_YAW_DEGREES;
|
||||
@@ -1285,31 +1316,35 @@ export function createTeleportTargetEntity(
|
||||
id: overrides.id ?? createOpaqueId("entity-teleport-target"),
|
||||
kind: "teleportTarget",
|
||||
name: normalizeEntityName(overrides.name),
|
||||
visible: resolveAuthoredEntityVisibility(overrides.visible),
|
||||
enabled: resolveAuthoredEntityEnabled(overrides.enabled),
|
||||
position,
|
||||
yawDegrees: normalizeYawDegrees(yawDegrees)
|
||||
};
|
||||
}
|
||||
|
||||
export function createInteractableEntity(
|
||||
overrides: Partial<Pick<InteractableEntity, "id" | "name" | "position" | "radius" | "prompt" | "enabled">> = {}
|
||||
overrides: Partial<Pick<InteractableEntity, "id" | "name" | "visible" | "enabled" | "position" | "radius" | "prompt" | "interactionEnabled">> = {}
|
||||
): InteractableEntity {
|
||||
const position = cloneVec3(overrides.position ?? DEFAULT_ENTITY_POSITION);
|
||||
const radius = overrides.radius ?? DEFAULT_INTERACTABLE_RADIUS;
|
||||
const prompt = normalizeInteractablePrompt(overrides.prompt ?? DEFAULT_INTERACTABLE_PROMPT);
|
||||
const enabled = overrides.enabled ?? true;
|
||||
const interactionEnabled = overrides.interactionEnabled ?? true;
|
||||
|
||||
assertFiniteVec3(position, "Interactable position");
|
||||
assertPositiveFiniteNumber(radius, "Interactable radius");
|
||||
assertBoolean(enabled, "Interactable enabled");
|
||||
assertBoolean(interactionEnabled, "Interactable interactionEnabled");
|
||||
|
||||
return {
|
||||
id: overrides.id ?? createOpaqueId("entity-interactable"),
|
||||
kind: "interactable",
|
||||
name: normalizeEntityName(overrides.name),
|
||||
visible: resolveAuthoredEntityVisibility(overrides.visible),
|
||||
enabled: resolveAuthoredEntityEnabled(overrides.enabled),
|
||||
position,
|
||||
radius,
|
||||
prompt,
|
||||
enabled
|
||||
interactionEnabled
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1319,10 +1354,12 @@ export function createSceneExitEntity(
|
||||
SceneExitEntity,
|
||||
| "id"
|
||||
| "name"
|
||||
| "visible"
|
||||
| "enabled"
|
||||
| "position"
|
||||
| "radius"
|
||||
| "prompt"
|
||||
| "enabled"
|
||||
| "interactionEnabled"
|
||||
| "targetSceneId"
|
||||
| "targetEntryEntityId"
|
||||
>
|
||||
@@ -1333,7 +1370,7 @@ export function createSceneExitEntity(
|
||||
const prompt = normalizeInteractablePrompt(
|
||||
overrides.prompt ?? DEFAULT_SCENE_EXIT_PROMPT
|
||||
);
|
||||
const enabled = overrides.enabled ?? true;
|
||||
const interactionEnabled = overrides.interactionEnabled ?? true;
|
||||
const targetSceneId = normalizeSceneReferenceId(
|
||||
overrides.targetSceneId,
|
||||
"Scene Exit target scene id"
|
||||
@@ -1345,16 +1382,18 @@ export function createSceneExitEntity(
|
||||
|
||||
assertFiniteVec3(position, "Scene Exit position");
|
||||
assertPositiveFiniteNumber(radius, "Scene Exit radius");
|
||||
assertBoolean(enabled, "Scene Exit enabled");
|
||||
assertBoolean(interactionEnabled, "Scene Exit interactionEnabled");
|
||||
|
||||
return {
|
||||
id: overrides.id ?? createOpaqueId("entity-scene-exit"),
|
||||
kind: "sceneExit",
|
||||
name: normalizeEntityName(overrides.name),
|
||||
visible: resolveAuthoredEntityVisibility(overrides.visible),
|
||||
enabled: resolveAuthoredEntityEnabled(overrides.enabled),
|
||||
position,
|
||||
radius,
|
||||
prompt,
|
||||
enabled,
|
||||
interactionEnabled,
|
||||
targetSceneId,
|
||||
targetEntryEntityId
|
||||
};
|
||||
@@ -1488,7 +1527,14 @@ export function cloneEntityRegistry(entities: Record<string, EntityInstance>): R
|
||||
}
|
||||
|
||||
export function areEntityInstancesEqual(left: EntityInstance, right: EntityInstance): boolean {
|
||||
if (left.kind !== right.kind || left.id !== right.id || left.name !== right.name || !areVec3Equal(left.position, right.position)) {
|
||||
if (
|
||||
left.kind !== right.kind ||
|
||||
left.id !== right.id ||
|
||||
left.name !== right.name ||
|
||||
left.visible !== right.visible ||
|
||||
left.enabled !== right.enabled ||
|
||||
!areVec3Equal(left.position, right.position)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1560,14 +1606,18 @@ export function areEntityInstancesEqual(left: EntityInstance, right: EntityInsta
|
||||
}
|
||||
case "interactable": {
|
||||
const typedRight = right as InteractableEntity;
|
||||
return left.radius === typedRight.radius && left.prompt === typedRight.prompt && left.enabled === typedRight.enabled;
|
||||
return (
|
||||
left.radius === typedRight.radius &&
|
||||
left.prompt === typedRight.prompt &&
|
||||
left.interactionEnabled === typedRight.interactionEnabled
|
||||
);
|
||||
}
|
||||
case "sceneExit": {
|
||||
const typedRight = right as SceneExitEntity;
|
||||
return (
|
||||
left.radius === typedRight.radius &&
|
||||
left.prompt === typedRight.prompt &&
|
||||
left.enabled === typedRight.enabled &&
|
||||
left.interactionEnabled === typedRight.interactionEnabled &&
|
||||
left.targetSceneId === typedRight.targetSceneId &&
|
||||
left.targetEntryEntityId === typedRight.targetEntryEntityId
|
||||
);
|
||||
@@ -1605,6 +1655,10 @@ export function getPrimaryPlayerStartEntity(entities: Record<string, EntityInsta
|
||||
return getPlayerStartEntities(entities)[0] ?? null;
|
||||
}
|
||||
|
||||
export function getPrimaryEnabledPlayerStartEntity(entities: Record<string, EntityInstance>): PlayerStartEntity | null {
|
||||
return getPlayerStartEntities(entities).find((entity) => entity.enabled) ?? null;
|
||||
}
|
||||
|
||||
export function getEntityKindLabel(kind: EntityKind): string {
|
||||
return getEntityRegistryEntry(kind).label;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user