auto-git:

[change] src/app/App.tsx
 [change] src/controls/control-surface.ts
 [change] src/document/migrate-scene-document.ts
 [change] src/document/scene-document-validation.ts
 [change] src/document/scene-document.ts
 [change] src/interactions/interaction-links.ts
 [change] src/runtime-three/runtime-audio-system.ts
 [change] src/runtime-three/runtime-host.ts
 [change] src/runtime-three/runtime-interaction-system.ts
 [change] src/runtime-three/runtime-scene-build.ts
 [change] tests/domain/runtime-control-foundation.test.ts
 [change] tests/domain/runtime-interaction-system.test.ts
 [change] tests/serialization/control-interaction-links.test.ts
 [change] tests/unit/runtime-host.test.ts
This commit is contained in:
2026-04-14 01:40:15 +02:00
parent 3ae63c83d1
commit c1f1ff7488
14 changed files with 1354 additions and 810 deletions

View File

@@ -288,7 +288,9 @@ function readSceneEditorPanelPreferences(
viewMode !== "front" &&
viewMode !== "side"
) {
throw new Error(`${label}.viewMode must be a supported viewport view mode.`);
throw new Error(
`${label}.viewMode must be a supported viewport view mode.`
);
}
if (
@@ -1287,7 +1289,8 @@ function readPlayerStartKeyboardBindingCode(
label,
fallback,
(candidate): candidate is typeof fallback =>
typeof candidate === "string" && isPlayerStartKeyboardBindingCode(candidate)
typeof candidate === "string" &&
isPlayerStartKeyboardBindingCode(candidate)
);
}
@@ -2254,7 +2257,7 @@ function readWorldTimeOfDaySettings(
),
ambientColorHex: expectHexColor(
isRecord(nightValue)
? nightValue.ambientColorHex ?? nightDefaults.ambientColorHex
? (nightValue.ambientColorHex ?? nightDefaults.ambientColorHex)
: nightDefaults.ambientColorHex,
`${label}.night.ambientColorHex`
),
@@ -2265,7 +2268,7 @@ function readWorldTimeOfDaySettings(
),
lightColorHex: expectHexColor(
isRecord(nightValue)
? nightValue.lightColorHex ?? nightDefaults.lightColorHex
? (nightValue.lightColorHex ?? nightDefaults.lightColorHex)
: nightDefaults.lightColorHex,
`${label}.night.lightColorHex`
),
@@ -2351,8 +2354,16 @@ function readPointLightEntity(value: unknown, label: string): EntityInstance {
const entity = createPointLightEntity({
id: expectString(value.id, `${label}.id`),
name: readOptionalEntityName(value.name, `${label}.name`),
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
visible: readOptionalBoolean(
value.visible,
`${label}.visible`,
DEFAULT_ENTITY_VISIBLE
),
enabled: readOptionalBoolean(
value.enabled,
`${label}.enabled`,
DEFAULT_ENTITY_ENABLED
),
position: readVec3(value.position, `${label}.position`),
colorHex: expectHexColor(value.colorHex, `${label}.colorHex`),
intensity: expectNonNegativeFiniteNumber(
@@ -2378,8 +2389,16 @@ function readSpotLightEntity(value: unknown, label: string): EntityInstance {
const entity = createSpotLightEntity({
id: expectString(value.id, `${label}.id`),
name: readOptionalEntityName(value.name, `${label}.name`),
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
visible: readOptionalBoolean(
value.visible,
`${label}.visible`,
DEFAULT_ENTITY_VISIBLE
),
enabled: readOptionalBoolean(
value.enabled,
`${label}.enabled`,
DEFAULT_ENTITY_ENABLED
),
position: readVec3(value.position, `${label}.position`),
direction: readVec3(value.direction, `${label}.direction`),
colorHex: expectHexColor(value.colorHex, `${label}.colorHex`),
@@ -2410,8 +2429,16 @@ function readPlayerStartEntity(value: unknown, label: string): EntityInstance {
const entity = createPlayerStartEntity({
id: expectString(value.id, `${label}.id`),
name: readOptionalEntityName(value.name, `${label}.name`),
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
visible: readOptionalBoolean(
value.visible,
`${label}.visible`,
DEFAULT_ENTITY_VISIBLE
),
enabled: readOptionalBoolean(
value.enabled,
`${label}.enabled`,
DEFAULT_ENTITY_ENABLED
),
position: readVec3(value.position, `${label}.position`),
yawDegrees: expectFiniteNumber(value.yawDegrees, `${label}.yawDegrees`),
navigationMode: readPlayerStartNavigationMode(
@@ -2448,8 +2475,16 @@ function readSoundEmitterEntity(value: unknown, label: string): EntityInstance {
const entity = createSoundEmitterEntity({
id: expectString(value.id, `${label}.id`),
name: readOptionalEntityName(value.name, `${label}.name`),
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
visible: readOptionalBoolean(
value.visible,
`${label}.visible`,
DEFAULT_ENTITY_VISIBLE
),
enabled: readOptionalBoolean(
value.enabled,
`${label}.enabled`,
DEFAULT_ENTITY_ENABLED
),
position: readVec3(value.position, `${label}.position`),
audioAssetId:
value.audioAssetId === undefined || value.audioAssetId === null
@@ -2488,8 +2523,16 @@ function readLegacySoundEmitterEntity(
const entity = createSoundEmitterEntity({
id: expectString(value.id, `${label}.id`),
name: readOptionalEntityName(value.name, `${label}.name`),
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
visible: readOptionalBoolean(
value.visible,
`${label}.visible`,
DEFAULT_ENTITY_VISIBLE
),
enabled: readOptionalBoolean(
value.enabled,
`${label}.enabled`,
DEFAULT_ENTITY_ENABLED
),
position: readVec3(value.position, `${label}.position`),
refDistance: radius,
maxDistance: radius,
@@ -2527,8 +2570,16 @@ function readTriggerVolumeEntity(
const entity = createTriggerVolumeEntity({
id: expectString(value.id, `${label}.id`),
name: readOptionalEntityName(value.name, `${label}.name`),
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
visible: readOptionalBoolean(
value.visible,
`${label}.visible`,
DEFAULT_ENTITY_VISIBLE
),
enabled: readOptionalBoolean(
value.enabled,
`${label}.enabled`,
DEFAULT_ENTITY_ENABLED
),
position: readVec3(value.position, `${label}.position`),
size,
triggerOnEnter: expectBoolean(
@@ -2561,8 +2612,16 @@ function readTeleportTargetEntity(
const entity = createTeleportTargetEntity({
id: expectString(value.id, `${label}.id`),
name: readOptionalEntityName(value.name, `${label}.name`),
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
visible: readOptionalBoolean(
value.visible,
`${label}.visible`,
DEFAULT_ENTITY_VISIBLE
),
enabled: readOptionalBoolean(
value.enabled,
`${label}.enabled`,
DEFAULT_ENTITY_ENABLED
),
position: readVec3(value.position, `${label}.position`),
yawDegrees: expectFiniteNumber(value.yawDegrees, `${label}.yawDegrees`)
});
@@ -2587,11 +2646,19 @@ function readInteractableEntity(value: unknown, label: string): EntityInstance {
const entity = createInteractableEntity({
id: expectString(value.id, `${label}.id`),
name: readOptionalEntityName(value.name, `${label}.name`),
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
visible: readOptionalBoolean(
value.visible,
`${label}.visible`,
DEFAULT_ENTITY_VISIBLE
),
enabled:
value.interactionEnabled === undefined
? DEFAULT_ENTITY_ENABLED
: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
: readOptionalBoolean(
value.enabled,
`${label}.enabled`,
DEFAULT_ENTITY_ENABLED
),
position: readVec3(value.position, `${label}.position`),
radius: expectPositiveFiniteNumber(value.radius, `${label}.radius`),
prompt: expectString(value.prompt, `${label}.prompt`),
@@ -2614,8 +2681,16 @@ function readSceneEntryEntity(value: unknown, label: string): EntityInstance {
const entity = createSceneEntryEntity({
id: expectString(value.id, `${label}.id`),
name: readOptionalEntityName(value.name, `${label}.name`),
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
enabled: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
visible: readOptionalBoolean(
value.visible,
`${label}.visible`,
DEFAULT_ENTITY_VISIBLE
),
enabled: readOptionalBoolean(
value.enabled,
`${label}.enabled`,
DEFAULT_ENTITY_ENABLED
),
position: readVec3(value.position, `${label}.position`),
yawDegrees: expectFiniteNumber(value.yawDegrees, `${label}.yawDegrees`)
});
@@ -2703,11 +2778,19 @@ function readSceneExitEntity(value: unknown, label: string): EntityInstance {
const entity = createSceneExitEntity({
id: expectString(value.id, `${label}.id`),
name: readOptionalEntityName(value.name, `${label}.name`),
visible: readOptionalBoolean(value.visible, `${label}.visible`, DEFAULT_ENTITY_VISIBLE),
visible: readOptionalBoolean(
value.visible,
`${label}.visible`,
DEFAULT_ENTITY_VISIBLE
),
enabled:
value.interactionEnabled === undefined
? DEFAULT_ENTITY_ENABLED
: readOptionalBoolean(value.enabled, `${label}.enabled`, DEFAULT_ENTITY_ENABLED),
: readOptionalBoolean(
value.enabled,
`${label}.enabled`,
DEFAULT_ENTITY_ENABLED
),
position: readVec3(value.position, `${label}.position`),
radius: expectPositiveFiniteNumber(value.radius, `${label}.radius`),
prompt: expectString(value.prompt, `${label}.prompt`),
@@ -2811,7 +2894,9 @@ function readControlTargetRef(value: unknown, label: string): ControlTargetRef {
const entityKind = expectString(value.entityKind, `${label}.entityKind`);
if (!isControlEntityTargetKind(entityKind)) {
throw new Error(`${label}.entityKind must be a supported control entity kind.`);
throw new Error(
`${label}.entityKind must be a supported control entity kind.`
);
}
return createEntityControlTargetRef(
@@ -3169,7 +3254,9 @@ function readScenePathValue(value: unknown, label: string): ScenePath {
return createScenePath({
id: expectString(value.id, `${label}.id`),
name:
value.name === undefined ? undefined : expectString(value.name, `${label}.name`),
value.name === undefined
? undefined
: expectString(value.name, `${label}.name`),
visible: expectBoolean(value.visible, `${label}.visible`),
enabled: expectBoolean(value.enabled, `${label}.enabled`),
loop: expectBoolean(value.loop, `${label}.loop`),
@@ -3609,13 +3696,15 @@ export function migrateSceneDocument(source: unknown): SceneDocument {
source.version !== PROJECT_TIME_NIGHT_BACKGROUND_SCENE_DOCUMENT_VERSION &&
source.version !== 33 &&
source.version !== AUTHORED_OBJECT_STATE_SCENE_DOCUMENT_VERSION &&
source.version !== PLAYER_START_AIR_DIRECTION_CONTROL_SCENE_DOCUMENT_VERSION &&
source.version !==
PLAYER_START_AIR_DIRECTION_CONTROL_SCENE_DOCUMENT_VERSION &&
source.version !== PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION &&
source.version !== PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION &&
source.version !== PROJECT_NAME_SCENE_DOCUMENT_VERSION &&
source.version !== STATIC_SIMPLE_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION &&
source.version !== SCENE_EDITOR_PREFERENCES_SCENE_DOCUMENT_VERSION &&
source.version !== PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION &&
source.version !==
PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION &&
source.version !== PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION &&
source.version !== PLAYER_START_NAVIGATION_MODE_SCENE_DOCUMENT_VERSION &&
source.version !== SCENE_TRANSITION_ENTITIES_SCENE_DOCUMENT_VERSION &&

View File

@@ -55,10 +55,7 @@ import {
HOURS_PER_DAY,
type ProjectTimeSettings
} from "./project-time-settings";
import {
MIN_SCENE_PATH_POINT_COUNT,
type ScenePath
} from "./paths";
import { MIN_SCENE_PATH_POINT_COUNT, type ScenePath } from "./paths";
import {
isAdvancedRenderingWaterReflectionMode,
isAdvancedRenderingShadowMapSize,
@@ -368,7 +365,9 @@ function validateWorldSettings(
);
}
if (!isNonNegativeFiniteNumber(world.timeOfDay.night.ambientIntensityFactor)) {
if (
!isNonNegativeFiniteNumber(world.timeOfDay.night.ambientIntensityFactor)
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -1524,7 +1523,11 @@ function validateEntityName(
}
}
function validateScenePath(pathValue: ScenePath, path: string, diagnostics: SceneDiagnostic[]) {
function validateScenePath(
pathValue: ScenePath,
path: string,
diagnostics: SceneDiagnostic[]
) {
if (!isBoolean(pathValue.visible)) {
diagnostics.push(
createDiagnostic(
@@ -1793,9 +1796,7 @@ function validatePlayerStartEntity(
);
}
if (
!isNonNegativeFiniteNumber(entity.movementTemplate?.jump?.coyoteTimeMs)
) {
if (!isNonNegativeFiniteNumber(entity.movementTemplate?.jump?.coyoteTimeMs)) {
diagnostics.push(
createDiagnostic(
"error",
@@ -1916,7 +1917,11 @@ function validatePlayerStartEntity(
);
}
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveForward)) {
if (
!isPlayerStartKeyboardBindingCode(
entity.inputBindings?.keyboard.moveForward
)
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -1927,7 +1932,11 @@ function validatePlayerStartEntity(
);
}
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveBackward)) {
if (
!isPlayerStartKeyboardBindingCode(
entity.inputBindings?.keyboard.moveBackward
)
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -1938,7 +1947,9 @@ function validatePlayerStartEntity(
);
}
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveLeft)) {
if (
!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveLeft)
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -1949,7 +1960,9 @@ function validatePlayerStartEntity(
);
}
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveRight)) {
if (
!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.moveRight)
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -1971,7 +1984,9 @@ function validatePlayerStartEntity(
);
}
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.sprint)) {
if (
!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.sprint)
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -1982,7 +1997,9 @@ function validatePlayerStartEntity(
);
}
if (!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.crouch)) {
if (
!isPlayerStartKeyboardBindingCode(entity.inputBindings?.keyboard.crouch)
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -2004,7 +2021,9 @@ function validatePlayerStartEntity(
);
}
if (!isPlayerStartGamepadBinding(entity.inputBindings?.gamepad.moveBackward)) {
if (
!isPlayerStartGamepadBinding(entity.inputBindings?.gamepad.moveBackward)
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -2048,7 +2067,9 @@ function validatePlayerStartEntity(
);
}
if (!isPlayerStartGamepadActionBinding(entity.inputBindings?.gamepad.sprint)) {
if (
!isPlayerStartGamepadActionBinding(entity.inputBindings?.gamepad.sprint)
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -2059,7 +2080,9 @@ function validatePlayerStartEntity(
);
}
if (!isPlayerStartGamepadActionBinding(entity.inputBindings?.gamepad.crouch)) {
if (
!isPlayerStartGamepadActionBinding(entity.inputBindings?.gamepad.crouch)
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -2761,7 +2784,10 @@ function validateNpcEntity(
);
}
if (typeof entity.actorId !== "string" || entity.actorId.trim().length === 0) {
if (
typeof entity.actorId !== "string" ||
entity.actorId.trim().length === 0
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -2985,7 +3011,9 @@ function validateControlEffect(
return;
}
case "stopModelAnimation":
if (document.modelInstances[effect.target.modelInstanceId] === undefined) {
if (
document.modelInstances[effect.target.modelInstanceId] === undefined
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -3012,7 +3040,10 @@ function validateControlEffect(
return;
}
if (targetEntity.kind !== effect.target.entityKind || targetEntity.kind !== "soundEmitter") {
if (
targetEntity.kind !== effect.target.entityKind ||
targetEntity.kind !== "soundEmitter"
) {
diagnostics.push(
createDiagnostic(
"error",
@@ -3886,9 +3917,7 @@ export function validateSceneDocument(
}
}
for (const [pathKey, pathValue] of Object.entries(
document.paths
)) {
for (const [pathKey, pathValue] of Object.entries(document.paths)) {
const path = `paths.${pathKey}`;
if (pathValue.id !== pathKey) {
@@ -4133,8 +4162,7 @@ export function validateProjectDocument(
}
const targetScenePath = `${scenePath}.entities.${entityId}.targetSceneId`;
const targetEntryPath =
`${scenePath}.entities.${entityId}.targetEntryEntityId`;
const targetEntryPath = `${scenePath}.entities.${entityId}.targetEntryEntityId`;
const targetScene = document.scenes[entity.targetSceneId];
if (targetScene === undefined) {

View File

@@ -28,7 +28,8 @@ export const NPC_COLLIDER_SCENE_DOCUMENT_VERSION = 42 as const;
export const NPC_ENTITY_FOUNDATION_SCENE_DOCUMENT_VERSION = 41 as const;
export const WORLD_TIME_ENVIRONMENT_SCENE_DOCUMENT_VERSION = 40 as const;
export const PROJECT_TIME_NIGHT_BACKGROUND_SCENE_DOCUMENT_VERSION = 39 as const;
export const PROJECT_TIME_DAY_NIGHT_PROFILE_SCENE_DOCUMENT_VERSION = 38 as const;
export const PROJECT_TIME_DAY_NIGHT_PROFILE_SCENE_DOCUMENT_VERSION =
38 as const;
export const PROJECT_TIME_SYSTEM_SCENE_DOCUMENT_VERSION = 37 as const;
export const AUTHORED_OBJECT_STATE_SCENE_DOCUMENT_VERSION = 36 as const;
export const PLAYER_START_AIR_DIRECTION_CONTROL_SCENE_DOCUMENT_VERSION =
@@ -41,7 +42,8 @@ export const PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION =
export const STATIC_SIMPLE_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION = 30 as const;
export const SCENE_EDITOR_PREFERENCES_SCENE_DOCUMENT_VERSION = 29 as const;
export const PROJECT_NAME_SCENE_DOCUMENT_VERSION = 28 as const;
export const PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION = 27 as const;
export const PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION =
27 as const;
export const PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION = 26 as const;
export const PLAYER_START_NAVIGATION_MODE_SCENE_DOCUMENT_VERSION = 25 as const;
export const SCENE_TRANSITION_ENTITIES_SCENE_DOCUMENT_VERSION = 24 as const;
@@ -105,7 +107,10 @@ export interface SceneEditorPreferences {
x: number;
y: number;
};
viewportPanels: Record<SceneEditorViewportPanelId, SceneEditorPanelPreferences>;
viewportPanels: Record<
SceneEditorViewportPanelId,
SceneEditorPanelPreferences
>;
}
export interface SceneLoadingScreenSettings {
@@ -206,12 +211,7 @@ export function createEmptyProjectDocument(
overrides: Partial<
Pick<
ProjectDocument,
| "name"
| "time"
| "activeSceneId"
| "materials"
| "textures"
| "assets"
"name" | "time" | "activeSceneId" | "materials" | "textures" | "assets"
>
> & {
sceneId?: string;