Add visibility and enabled properties to model instances, brushes, and entities

This commit is contained in:
2026-04-12 03:32:23 +02:00
parent 0f4ccab2d6
commit 9300e62e7d
3 changed files with 112 additions and 22 deletions

View File

@@ -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) &&

View File

@@ -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,

View File

@@ -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;
}