From e55a2339741b3dc36bb2959682003379de9b9efd Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 13 May 2026 03:39:00 +0200 Subject: [PATCH] Add validation and normalization utilities for ScenePathRepeater properties --- src/document/paths.ts | 183 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/src/document/paths.ts b/src/document/paths.ts index 137c6bbd..ce9c6687 100644 --- a/src/document/paths.ts +++ b/src/document/paths.ts @@ -562,6 +562,189 @@ export function createScenePathRoadSettings( }; } +export function isScenePathRepeaterPlacement( + value: unknown +): value is ScenePathRepeaterPlacement { + return ( + typeof value === "string" && + SCENE_PATH_REPEATER_PLACEMENTS.includes( + value as ScenePathRepeaterPlacement + ) + ); +} + +export function normalizeScenePathRepeaterPlacement( + value: unknown +): ScenePathRepeaterPlacement { + if (isScenePathRepeaterPlacement(value)) { + return value; + } + + throw new Error("Path repeater placement must be center, left, or right."); +} + +export function normalizeScenePathRepeaterAssetId(value: string): string { + const trimmedValue = value.trim(); + + if (trimmedValue.length === 0) { + throw new Error("Path repeater asset id must be non-empty."); + } + + return trimmedValue; +} + +export function normalizeScenePathRepeaterOffset(value: number): number { + return normalizeFiniteRange( + value, + MIN_SCENE_PATH_REPEATER_OFFSET, + MAX_SCENE_PATH_REPEATER_OFFSET, + "Path repeater offset" + ); +} + +export function normalizeScenePathRepeaterSpacing(value: number): number { + return normalizeFiniteRange( + value, + MIN_SCENE_PATH_REPEATER_SPACING, + MAX_SCENE_PATH_REPEATER_SPACING, + "Path repeater spacing" + ); +} + +export function normalizeScenePathRepeaterInset(value: number): number { + return normalizeFiniteRange( + value, + MIN_SCENE_PATH_REPEATER_INSET, + MAX_SCENE_PATH_REPEATER_INSET, + "Path repeater inset" + ); +} + +export function normalizeScenePathRepeaterScale(value: number): number { + return normalizeFiniteRange( + value, + MIN_SCENE_PATH_REPEATER_SCALE, + MAX_SCENE_PATH_REPEATER_SCALE, + "Path repeater scale" + ); +} + +export function normalizeScenePathRepeaterRandomScale(value: number): number { + return normalizeFiniteRange( + value, + MIN_SCENE_PATH_REPEATER_RANDOM_SCALE, + MAX_SCENE_PATH_REPEATER_RANDOM_SCALE, + "Path repeater random scale" + ); +} + +export function normalizeScenePathRepeaterRandomYawDegrees( + value: number +): number { + return normalizeFiniteRange( + value, + MIN_SCENE_PATH_REPEATER_RANDOM_YAW_DEGREES, + MAX_SCENE_PATH_REPEATER_RANDOM_YAW_DEGREES, + "Path repeater random yaw" + ); +} + +export function normalizeScenePathRepeaterYawOffsetDegrees( + value: number +): number { + if (!Number.isFinite(value)) { + throw new Error("Path repeater yaw offset must be a finite number."); + } + + return value; +} + +export function normalizeScenePathRepeaterHeightOffset(value: number): number { + if (!Number.isFinite(value)) { + throw new Error("Path repeater height offset must be a finite number."); + } + + return value; +} + +export function createScenePathRepeater( + overrides: ScenePathRepeaterOverrides = {} +): ScenePathRepeater { + const enabled = overrides.enabled ?? DEFAULT_SCENE_PATH_REPEATER_ENABLED; + const terrainConform = + overrides.terrainConform ?? DEFAULT_SCENE_PATH_REPEATER_TERRAIN_CONFORM; + const alignToSpline = + overrides.alignToSpline ?? DEFAULT_SCENE_PATH_REPEATER_ALIGN_TO_SPLINE; + const seed = overrides.seed ?? 1; + + if (typeof enabled !== "boolean") { + throw new Error("Path repeater enabled must be a boolean."); + } + + if (typeof terrainConform !== "boolean") { + throw new Error("Path repeater terrain conform must be a boolean."); + } + + if (typeof alignToSpline !== "boolean") { + throw new Error("Path repeater align to spline must be a boolean."); + } + + if (!Number.isFinite(seed) || !Number.isInteger(seed)) { + throw new Error("Path repeater seed must be a finite integer."); + } + + return { + id: overrides.id ?? createOpaqueId("path_repeater"), + name: normalizeScenePathName(overrides.name), + enabled, + assetId: normalizeScenePathRepeaterAssetId( + overrides.assetId ?? DEFAULT_SCENE_PATH_REPEATER_ASSET_ID + ), + placement: + overrides.placement === undefined + ? DEFAULT_SCENE_PATH_REPEATER_PLACEMENT + : normalizeScenePathRepeaterPlacement(overrides.placement), + offset: normalizeScenePathRepeaterOffset( + overrides.offset ?? DEFAULT_SCENE_PATH_REPEATER_OFFSET + ), + spacing: normalizeScenePathRepeaterSpacing( + overrides.spacing ?? DEFAULT_SCENE_PATH_REPEATER_SPACING + ), + startInset: normalizeScenePathRepeaterInset( + overrides.startInset ?? DEFAULT_SCENE_PATH_REPEATER_START_INSET + ), + endInset: normalizeScenePathRepeaterInset( + overrides.endInset ?? DEFAULT_SCENE_PATH_REPEATER_END_INSET + ), + scale: normalizeScenePathRepeaterScale( + overrides.scale ?? DEFAULT_SCENE_PATH_REPEATER_SCALE + ), + randomScale: normalizeScenePathRepeaterRandomScale( + overrides.randomScale ?? DEFAULT_SCENE_PATH_REPEATER_RANDOM_SCALE + ), + randomYawDegrees: normalizeScenePathRepeaterRandomYawDegrees( + overrides.randomYawDegrees ?? + DEFAULT_SCENE_PATH_REPEATER_RANDOM_YAW_DEGREES + ), + yawOffsetDegrees: normalizeScenePathRepeaterYawOffsetDegrees( + overrides.yawOffsetDegrees ?? + DEFAULT_SCENE_PATH_REPEATER_YAW_OFFSET_DEGREES + ), + terrainConform, + heightOffset: normalizeScenePathRepeaterHeightOffset( + overrides.heightOffset ?? DEFAULT_SCENE_PATH_REPEATER_HEIGHT_OFFSET + ), + alignToSpline, + seed + }; +} + +export function cloneScenePathRepeater( + repeater: ScenePathRepeater +): ScenePathRepeater { + return createScenePathRepeater(repeater); +} + function resolvePathSegmentSample( path: ResolvedPathLike, progress: number