@scenography/scene 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +13 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.mts +8 -1
- package/dist/index.mjs +13 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -11,6 +11,18 @@ const isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n) => typeo
|
|
|
11
11
|
/** Type guard: a finite number or a percentage string like `'50%'`. */
|
|
12
12
|
const isMeasure = (m) => typeof m === "number" && Number.isFinite(m) || typeof m === "string" && /^-?\d+(\.\d+)?%$/.test(m);
|
|
13
13
|
//#endregion
|
|
14
|
+
//#region src/texture/texture.ts
|
|
15
|
+
/** Every URL a surface texture loads, for the gallery's loading progress. */
|
|
16
|
+
const surfaceTextureUrls = (texture) => {
|
|
17
|
+
if (!texture) return [];
|
|
18
|
+
return [
|
|
19
|
+
texture.src,
|
|
20
|
+
texture.normal,
|
|
21
|
+
texture.roughness,
|
|
22
|
+
texture.ao
|
|
23
|
+
].filter((url) => url !== void 0);
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
14
26
|
//#region src/validation/issue.ts
|
|
15
27
|
/** Record a `duplicate id` issue for any id that appears more than once. */
|
|
16
28
|
const collectDuplicates = (ids, kind, issues) => {
|
|
@@ -268,6 +280,7 @@ exports.nested = nested;
|
|
|
268
280
|
exports.propIs = propIs;
|
|
269
281
|
exports.scoped = scoped;
|
|
270
282
|
exports.showValidation = showValidation;
|
|
283
|
+
exports.surfaceTextureUrls = surfaceTextureUrls;
|
|
271
284
|
exports.valid = valid;
|
|
272
285
|
exports.validate = validate;
|
|
273
286
|
exports.validateScene = validateScene;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/vec2/vec2.ts","../src/vec3/vec3.ts","../src/measure/measure.ts","../src/validation/issue.ts","../src/validation/validate.ts","../src/obj/get.ts","../src/validation/validators/message.ts","../src/validation/validators/existsProp.ts","../src/validation/validators/propIs.ts","../src/validation/validators/eachPropIs.ts","../src/validation/validators/when.ts","../src/validation/validators/fail.ts","../src/validation/validators/nested.ts","../src/validation/predicates/isString.ts","../src/validation/predicates/isObject.ts","../src/validation/predicates/isFiniteNumber.ts","../src/validation/predicates/isIntegerNumber.ts","../src/validation/predicates/isPositiveNumber.ts","../src/validation/predicates/isNonNegativeNumber.ts","../src/validation/predicates/isOneOf.ts","../src/validation/predicates/isNonEmptyString.ts","../src/validation/predicates/isNumberInRange.ts","../src/texture/validate.ts","../src/camera/validate.ts","../src/environment/validate.ts","../src/scene/validate.ts","../src/scene/scene.ts"],"sourcesContent":["/** A 2D vector, e.g. a size `[width, height]` or a floor coordinate `[x, z]`. */\nexport type Vec2 = [number, number];\n\n/** Type guard: a finite-numeric `[x, y]` pair. */\nexport const isVec2 = (v: unknown): v is Vec2 =>\n Array.isArray(v) &&\n v.length === 2 &&\n v.every((n) => typeof n === \"number\" && Number.isFinite(n));\n","/** A 3D vector `[x, y, z]` in world space. */\nexport type Vec3 = [number, number, number];\n\n/** Type guard: a finite-numeric `[x, y, z]` triple. */\nexport const isVec3 = (v: unknown): v is Vec3 =>\n Array.isArray(v) &&\n v.length === 3 &&\n v.every((n) => typeof n === \"number\" && Number.isFinite(n));\n","/**\n * A length given either as absolute world units (a `number`) or as a percentage\n * string like `'50%'` (relative to whatever it measures along).\n */\nexport type Measure = number | `${number}%`;\n\n/** Type guard: a finite number or a percentage string like `'50%'`. */\nexport const isMeasure = (m: unknown): m is Measure =>\n (typeof m === \"number\" && Number.isFinite(m)) ||\n (typeof m === \"string\" && /^-?\\d+(\\.\\d+)?%$/.test(m));\n","/** A validation problem found in a scene definition. */\nexport interface ValidationIssue {\n /** Dotted path to the offending field, e.g. `artworks[1].location.edge`. */\n path: string;\n message: string;\n}\n\n/** Record a `duplicate id` issue for any id that appears more than once. */\nexport const collectDuplicates = (\n ids: string[],\n kind: string,\n issues: ValidationIssue[],\n): void => {\n const seen = new Set<string>();\n for (const id of ids) {\n if (seen.has(id)) issues.push({ path: kind, message: `duplicate id \"${id}\".` });\n seen.add(id);\n }\n};\n","import type { ValidationIssue } from \"./issue\";\n\n// Core stays environment-agnostic (no DOM lib); `console` exists in every\n// runtime we target, so declare just the slice we use.\ndeclare const console: { error: (message: string) => void };\n\n/** Outcome of running validators against a value: every error found, in order. */\nexport interface ValidationResult {\n isValid: boolean;\n errors: ValidationIssue[];\n}\n\n/** A single validation rule: takes the value, returns what it found. */\nexport type Validator<T = unknown> = (value: T) => ValidationResult;\n\n/** The passing result. */\nexport const valid = (): ValidationResult => ({ isValid: true, errors: [] });\n\n/** A failing result carrying a single issue. */\nexport const invalid = (path: string, message: string): ValidationResult => ({\n isValid: false,\n errors: [{ path, message }],\n});\n\n/**\n * Combine validators into one run: applies them all (never short-circuits) and\n * concatenates their errors in order. Curried so a rule set can be built once\n * and applied to many values.\n */\nexport const validate =\n <T>(validators: ReadonlyArray<Validator<T>>) =>\n (value: T): ValidationResult => {\n const errors = validators.flatMap((validator) => validator(value).errors);\n return { isValid: errors.length === 0, errors };\n };\n\n/**\n * Re-root a result's error paths under a parent path, e.g.\n * `scoped(\"artworks[3]\")` turns `label.side` into `artworks[3].label.side` —\n * how per-element results are folded into a scene-level report.\n */\nexport const scoped =\n (path: string) =>\n (result: ValidationResult): ValidationResult =>\n result.isValid\n ? result\n : {\n isValid: false,\n errors: result.errors.map((error) => ({ ...error, path: `${path}.${error.path}` })),\n };\n\n/**\n * Pass-through reporter: prints the errors to the console when the result is\n * negative, and returns the result unchanged so it composes inside a pipeline.\n */\nexport const showValidation = (result: ValidationResult): ValidationResult => {\n if (!result.isValid) {\n const detail = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n console.error(`Validation failed:\\n${detail}`);\n }\n return result;\n};\n","/**\n * Read a dotted property path (`\"label.side\"`, `\"edges[0].name\"`) off any\n * value; `undefined` when any hop is missing or not an object.\n */\nexport const get = (value: unknown, path: string): unknown =>\n path\n .replace(/\\[(\\d+)\\]/g, \".$1\")\n .split(\".\")\n .reduce<unknown>(\n (current, key) =>\n current !== null && typeof current === \"object\"\n ? (current as Record<string, unknown>)[key]\n : undefined,\n value,\n );\n","/** Error message: fixed text, or derived from the offending value. */\nexport type Message = string | ((value: unknown) => string);\n\nexport const resolveMessage = (message: Message, value: unknown): string =>\n typeof message === \"function\" ? message(value) : message;\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/** The property must be present (truthy). Default message: `Missing <prop>.` */\nexport const existsProp =\n (prop: string, message: Message = `Missing ${prop}.`): Validator =>\n (value) => {\n const found = get(value, prop);\n return found ? valid() : invalid(prop, resolveMessage(message, found));\n };\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/**\n * The property must satisfy `test` **when present**; an absent property passes.\n * Presence is a separate concern: require it with `existsProp` alongside.\n */\nexport const propIs =\n (prop: string, test: (value: unknown) => boolean, message: Message): Validator =>\n (value) => {\n const found = get(value, prop);\n return found === undefined || test(found)\n ? valid()\n : invalid(prop, resolveMessage(message, found));\n };\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/**\n * Every item of the array property must satisfy `test`; errors are addressed\n * per index (`prop[i]`). A missing or non-array property passes — pair with a\n * `propIs(prop, Array.isArray, …)` to report the shape itself.\n */\nexport const eachPropIs =\n (prop: string, test: (item: unknown) => boolean, message: Message): Validator =>\n (value) => {\n const items = get(value, prop);\n if (!Array.isArray(items)) return valid();\n const errors = items.flatMap((item, i) =>\n test(item) ? [] : invalid(`${prop}[${i}]`, resolveMessage(message, item)).errors,\n );\n return { isValid: errors.length === 0, errors };\n };\n","import { valid, validate, type Validator } from \"../validate\";\n\n/** Apply validators only when `test` holds — for shapes hanging off a discriminant. */\nexport const when =\n <T>(test: (value: T) => boolean, ...validators: Validator<T>[]): Validator<T> =>\n (value) =>\n test(value) ? validate(validators)(value) : valid();\n","import { invalid, type Validator } from \"../validate\";\n\n/** Always fails — pair with `when` to reject a branch (e.g. an unknown discriminant). */\nexport const fail =\n (path: string, message: string): Validator =>\n () =>\n invalid(path, message);\n","import { get } from \"../../obj\";\nimport { scoped, valid, type ValidationResult, type Validator } from \"../validate\";\n\n/**\n * Validate a nested object with its own rule set when present, re-rooting its\n * errors under `prop` (e.g. `nested(\"texture\", validateTexture)` reports\n * `texture.src`). An absent property passes.\n */\nexport const nested =\n <S>(prop: string, validateSub: (sub: S) => ValidationResult): Validator =>\n (value) => {\n const found = get(value, prop);\n return found === undefined ? valid() : scoped(prop)(validateSub(found as S));\n };\n","export const isString = (value: unknown): value is string => typeof value === \"string\";\n","export const isObject = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\";\n","export const isFiniteNumber = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isFinite(value);\n","export const isIntegerNumber = (value: unknown): value is number => Number.isInteger(value);\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\nexport const isPositiveNumber = (value: unknown): value is number =>\n isFiniteNumber(value) && value > 0;\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\nexport const isNonNegativeNumber = (value: unknown): value is number =>\n isFiniteNumber(value) && value >= 0;\n","/** Membership in a fixed set of allowed values (curried). */\nexport const isOneOf =\n (values: ReadonlySet<unknown>) =>\n (value: unknown): boolean =>\n values.has(value);\n","import { isString } from \"./isString\";\n\nexport const isNonEmptyString = (value: unknown): value is string =>\n isString(value) && value.length > 0;\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\n/** Finite number within `[min, max]`, bounds included (curried). */\nexport const isNumberInRange =\n (min: number, max: number) =>\n (value: unknown): value is number =>\n isFiniteNumber(value) && value >= min && value <= max;\n","import {\n existsProp,\n isNonEmptyString,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport { isVec2 } from \"../vec2\";\nimport type { SurfaceTexture } from \"./texture\";\n\n/** Validate a texture; paths are relative (callers scope them, e.g. via `nested`). */\nexport const validateTexture = (texture: SurfaceTexture): ValidationResult =>\n validate<SurfaceTexture>([\n existsProp(\"src\", \"texture is missing a src.\"),\n propIs(\"repeat\", isVec2, \"repeat must be [x, y].\"),\n ...([\"normal\", \"roughness\", \"ao\"] as const).map((map) =>\n propIs(map, isNonEmptyString, `${map} must be a non-empty URL.`),\n ),\n ])(texture);\n","import {\n existsProp,\n isFiniteNumber,\n isPositiveNumber,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport { isVec3 } from \"../vec3\";\nimport type { Camera } from \"./camera\";\n\nexport const validateCamera = (camera: Camera | undefined): ValidationResult =>\n validate<Camera | undefined>([\n existsProp(\"start.position\"),\n propIs(\"start.position\", isVec3, \"start.position must be [x, y, z].\"),\n propIs(\"eyeHeight\", isPositiveNumber, \"eyeHeight must be a positive number.\"),\n propIs(\n \"fov\",\n (v) => isFiniteNumber(v) && v > 0 && v < 180,\n \"fov must be between 0 and 180 (degrees).\",\n ),\n ])(camera);\n","import {\n existsProp,\n isFiniteNumber,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport type { Environment } from \"./environment\";\n\nexport const validateEnvironment = (env: Environment): ValidationResult =>\n validate<Environment>([\n existsProp(\"src\", \"Environment is missing a src.\"),\n propIs(\"intensity\", isFiniteNumber, \"intensity must be a finite number.\"),\n ])(env);\n","import { validateCamera } from \"../camera\";\nimport { validateEnvironment } from \"../environment\";\nimport type { DataPlugin, SceneContext } from \"../plugin\";\nimport {\n collectDuplicates,\n eachPropIs,\n isObject,\n isString,\n propIs,\n scoped,\n validate,\n when,\n type ValidationIssue,\n} from \"../validation\";\nimport { isVec2 } from \"../vec2\";\nimport type { Scene } from \"./scene\";\n\n/**\n * Validate a scene definition. Returns every issue found (does not throw), so\n * callers — editors, the runtime's hot-reload path — can surface them. Each\n * element validates itself with relative paths; this aggregator re-roots them\n * (`scoped`) into scene-level paths like `artworks[3].label.side`. Data\n * plugins validate the scene keys they own the same way.\n */\nexport function validateScene(scene: Scene, plugins: DataPlugin[] = []): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n issues.push(...scoped(\"camera\")(validateCamera(scene.camera)).errors);\n if (scene.environment)\n issues.push(...scoped(\"environment\")(validateEnvironment(scene.environment)).errors);\n\n issues.push(\n ...validate<Scene>([\n propIs(\n \"boundary\",\n (b) => Array.isArray(b) && b.length >= 3,\n \"boundary must be a ring of at least 3 [x, z] points.\",\n ),\n when(\n (s: Scene) => Array.isArray(s.boundary) && s.boundary.length >= 3,\n eachPropIs(\"boundary\", isVec2, \"must be [x, z].\"),\n ),\n ])(scene).errors,\n );\n\n const ctx: SceneContext = { scene };\n for (const plugin of plugins) {\n const raw = (scene as unknown as Record<string, unknown>)[plugin.kind];\n if (raw === undefined) continue;\n if (plugin.cardinality === \"single\") {\n if (plugin.validate) issues.push(...scoped(plugin.kind)(plugin.validate(raw, ctx)).errors);\n continue;\n }\n if (!Array.isArray(raw)) {\n issues.push({ path: plugin.kind, message: \"must be an array.\" });\n continue;\n }\n if (plugin.validate)\n raw.forEach((item, i) =>\n issues.push(...scoped(`${plugin.kind}[${i}]`)(plugin.validate!(item, ctx)).errors),\n );\n collectDuplicates(\n raw.map((item) => (isObject(item) && isString(item.id) ? item.id : \"\")).filter(Boolean),\n plugin.kind,\n issues,\n );\n }\n\n return issues;\n}\n","import type { Camera } from \"../camera\";\nimport type { Environment } from \"../environment\";\nimport type { Vec2 } from \"../vec2\";\nimport type { DataPlugin } from \"../plugin\";\nimport { validateScene } from \"./validate\";\n\n/**\n * The complete, serializable description of a gallery space. This is the unit\n * an editor produces, a database stores, and the network transports.\n *\n * The base scene holds only what the kernel owns; element kinds (walls,\n * artworks, lights, sky…) are contributed by plugins, which augment this\n * interface with their own optional key via `declare module \"@scenography/scene\"`.\n */\nexport interface Scene {\n camera: Camera;\n /** HDRI sky / image-based lighting for the space. */\n environment?: Environment;\n /**\n * Walkable-area polygon as an XZ ring (`[x, z]` points). When set, the visitor\n * can't step outside it — use it to contain them within an irregular space\n * such as a cove. At least 3 points.\n */\n boundary?: Vec2[];\n}\n\n/**\n * Define a gallery scene. Validates the definition and returns it unchanged\n * (the identity of pure data), throwing on structural errors so authoring\n * mistakes surface early. The returned value is plain, JSON-serializable data.\n * Pass the data halves of the plugins the scene uses so their keys validate too.\n */\nexport function defineScene(scene: Scene, plugins?: DataPlugin[]): Scene {\n const issues = validateScene(scene, plugins);\n if (issues.length > 0) {\n const detail = issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new Error(`Invalid scene definition:\\n${detail}`);\n }\n return scene;\n}\n\n/**\n * Deep-clone a scene through JSON. Useful for the hot-reload path (snapshotting\n * the previous scene before reconciling) and a guard that the definition really\n * is serializable.\n */\nexport function cloneScene(scene: Scene): Scene {\n return JSON.parse(JSON.stringify(scene)) as Scene;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/vec2/vec2.ts","../src/vec3/vec3.ts","../src/measure/measure.ts","../src/texture/texture.ts","../src/validation/issue.ts","../src/validation/validate.ts","../src/obj/get.ts","../src/validation/validators/message.ts","../src/validation/validators/existsProp.ts","../src/validation/validators/propIs.ts","../src/validation/validators/eachPropIs.ts","../src/validation/validators/when.ts","../src/validation/validators/fail.ts","../src/validation/validators/nested.ts","../src/validation/predicates/isString.ts","../src/validation/predicates/isObject.ts","../src/validation/predicates/isFiniteNumber.ts","../src/validation/predicates/isIntegerNumber.ts","../src/validation/predicates/isPositiveNumber.ts","../src/validation/predicates/isNonNegativeNumber.ts","../src/validation/predicates/isOneOf.ts","../src/validation/predicates/isNonEmptyString.ts","../src/validation/predicates/isNumberInRange.ts","../src/texture/validate.ts","../src/camera/validate.ts","../src/environment/validate.ts","../src/scene/validate.ts","../src/scene/scene.ts"],"sourcesContent":["/** A 2D vector, e.g. a size `[width, height]` or a floor coordinate `[x, z]`. */\nexport type Vec2 = [number, number];\n\n/** Type guard: a finite-numeric `[x, y]` pair. */\nexport const isVec2 = (v: unknown): v is Vec2 =>\n Array.isArray(v) &&\n v.length === 2 &&\n v.every((n) => typeof n === \"number\" && Number.isFinite(n));\n","/** A 3D vector `[x, y, z]` in world space. */\nexport type Vec3 = [number, number, number];\n\n/** Type guard: a finite-numeric `[x, y, z]` triple. */\nexport const isVec3 = (v: unknown): v is Vec3 =>\n Array.isArray(v) &&\n v.length === 3 &&\n v.every((n) => typeof n === \"number\" && Number.isFinite(n));\n","/**\n * A length given either as absolute world units (a `number`) or as a percentage\n * string like `'50%'` (relative to whatever it measures along).\n */\nexport type Measure = number | `${number}%`;\n\n/** Type guard: a finite number or a percentage string like `'50%'`. */\nexport const isMeasure = (m: unknown): m is Measure =>\n (typeof m === \"number\" && Number.isFinite(m)) ||\n (typeof m === \"string\" && /^-?\\d+(\\.\\d+)?%$/.test(m));\n","import type { Vec2 } from \"../vec2\";\n\n/**\n * A material mapped onto a surface (a wall, a floor…). `src` is the color/albedo\n * map; the rest are optional PBR maps. All share the same tiling (`repeat`). The\n * engine picks the loader by file extension, so `.exr`/`.png`/`.jpg` all work.\n * Point `roughness` and `ao` at the same packed ARM image to reuse a single file\n * (its green channel drives roughness, its red channel ambient occlusion).\n */\nexport interface SurfaceTexture {\n /** Color/albedo map (the visible surface; treated as sRGB). */\n src: string;\n /** How many times the maps repeat across the surface `[x, y]`. Defaults to `[1, 1]`. */\n repeat?: Vec2;\n /** Tangent-space normal map for surface relief (OpenGL convention). */\n normal?: string;\n /** Strength of the normal map relief. Defaults to `1`; raise for more depth. */\n normalScale?: number;\n /** Roughness map (grayscale, or the green channel of an ARM map). */\n roughness?: string;\n /** Ambient-occlusion map (grayscale, or the red channel of an ARM map). */\n ao?: string;\n}\n\n/** Every URL a surface texture loads, for the gallery's loading progress. */\nexport const surfaceTextureUrls = (texture: SurfaceTexture | undefined): string[] => {\n if (!texture) return [];\n return [texture.src, texture.normal, texture.roughness, texture.ao].filter(\n (url): url is string => url !== undefined,\n );\n};\n","/** A validation problem found in a scene definition. */\nexport interface ValidationIssue {\n /** Dotted path to the offending field, e.g. `artworks[1].location.edge`. */\n path: string;\n message: string;\n}\n\n/** Record a `duplicate id` issue for any id that appears more than once. */\nexport const collectDuplicates = (\n ids: string[],\n kind: string,\n issues: ValidationIssue[],\n): void => {\n const seen = new Set<string>();\n for (const id of ids) {\n if (seen.has(id)) issues.push({ path: kind, message: `duplicate id \"${id}\".` });\n seen.add(id);\n }\n};\n","import type { ValidationIssue } from \"./issue\";\n\n// Core stays environment-agnostic (no DOM lib); `console` exists in every\n// runtime we target, so declare just the slice we use.\ndeclare const console: { error: (message: string) => void };\n\n/** Outcome of running validators against a value: every error found, in order. */\nexport interface ValidationResult {\n isValid: boolean;\n errors: ValidationIssue[];\n}\n\n/** A single validation rule: takes the value, returns what it found. */\nexport type Validator<T = unknown> = (value: T) => ValidationResult;\n\n/** The passing result. */\nexport const valid = (): ValidationResult => ({ isValid: true, errors: [] });\n\n/** A failing result carrying a single issue. */\nexport const invalid = (path: string, message: string): ValidationResult => ({\n isValid: false,\n errors: [{ path, message }],\n});\n\n/**\n * Combine validators into one run: applies them all (never short-circuits) and\n * concatenates their errors in order. Curried so a rule set can be built once\n * and applied to many values.\n */\nexport const validate =\n <T>(validators: ReadonlyArray<Validator<T>>) =>\n (value: T): ValidationResult => {\n const errors = validators.flatMap((validator) => validator(value).errors);\n return { isValid: errors.length === 0, errors };\n };\n\n/**\n * Re-root a result's error paths under a parent path, e.g.\n * `scoped(\"artworks[3]\")` turns `label.side` into `artworks[3].label.side` —\n * how per-element results are folded into a scene-level report.\n */\nexport const scoped =\n (path: string) =>\n (result: ValidationResult): ValidationResult =>\n result.isValid\n ? result\n : {\n isValid: false,\n errors: result.errors.map((error) => ({ ...error, path: `${path}.${error.path}` })),\n };\n\n/**\n * Pass-through reporter: prints the errors to the console when the result is\n * negative, and returns the result unchanged so it composes inside a pipeline.\n */\nexport const showValidation = (result: ValidationResult): ValidationResult => {\n if (!result.isValid) {\n const detail = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n console.error(`Validation failed:\\n${detail}`);\n }\n return result;\n};\n","/**\n * Read a dotted property path (`\"label.side\"`, `\"edges[0].name\"`) off any\n * value; `undefined` when any hop is missing or not an object.\n */\nexport const get = (value: unknown, path: string): unknown =>\n path\n .replace(/\\[(\\d+)\\]/g, \".$1\")\n .split(\".\")\n .reduce<unknown>(\n (current, key) =>\n current !== null && typeof current === \"object\"\n ? (current as Record<string, unknown>)[key]\n : undefined,\n value,\n );\n","/** Error message: fixed text, or derived from the offending value. */\nexport type Message = string | ((value: unknown) => string);\n\nexport const resolveMessage = (message: Message, value: unknown): string =>\n typeof message === \"function\" ? message(value) : message;\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/** The property must be present (truthy). Default message: `Missing <prop>.` */\nexport const existsProp =\n (prop: string, message: Message = `Missing ${prop}.`): Validator =>\n (value) => {\n const found = get(value, prop);\n return found ? valid() : invalid(prop, resolveMessage(message, found));\n };\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/**\n * The property must satisfy `test` **when present**; an absent property passes.\n * Presence is a separate concern: require it with `existsProp` alongside.\n */\nexport const propIs =\n (prop: string, test: (value: unknown) => boolean, message: Message): Validator =>\n (value) => {\n const found = get(value, prop);\n return found === undefined || test(found)\n ? valid()\n : invalid(prop, resolveMessage(message, found));\n };\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/**\n * Every item of the array property must satisfy `test`; errors are addressed\n * per index (`prop[i]`). A missing or non-array property passes — pair with a\n * `propIs(prop, Array.isArray, …)` to report the shape itself.\n */\nexport const eachPropIs =\n (prop: string, test: (item: unknown) => boolean, message: Message): Validator =>\n (value) => {\n const items = get(value, prop);\n if (!Array.isArray(items)) return valid();\n const errors = items.flatMap((item, i) =>\n test(item) ? [] : invalid(`${prop}[${i}]`, resolveMessage(message, item)).errors,\n );\n return { isValid: errors.length === 0, errors };\n };\n","import { valid, validate, type Validator } from \"../validate\";\n\n/** Apply validators only when `test` holds — for shapes hanging off a discriminant. */\nexport const when =\n <T>(test: (value: T) => boolean, ...validators: Validator<T>[]): Validator<T> =>\n (value) =>\n test(value) ? validate(validators)(value) : valid();\n","import { invalid, type Validator } from \"../validate\";\n\n/** Always fails — pair with `when` to reject a branch (e.g. an unknown discriminant). */\nexport const fail =\n (path: string, message: string): Validator =>\n () =>\n invalid(path, message);\n","import { get } from \"../../obj\";\nimport { scoped, valid, type ValidationResult, type Validator } from \"../validate\";\n\n/**\n * Validate a nested object with its own rule set when present, re-rooting its\n * errors under `prop` (e.g. `nested(\"texture\", validateTexture)` reports\n * `texture.src`). An absent property passes.\n */\nexport const nested =\n <S>(prop: string, validateSub: (sub: S) => ValidationResult): Validator =>\n (value) => {\n const found = get(value, prop);\n return found === undefined ? valid() : scoped(prop)(validateSub(found as S));\n };\n","export const isString = (value: unknown): value is string => typeof value === \"string\";\n","export const isObject = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\";\n","export const isFiniteNumber = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isFinite(value);\n","export const isIntegerNumber = (value: unknown): value is number => Number.isInteger(value);\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\nexport const isPositiveNumber = (value: unknown): value is number =>\n isFiniteNumber(value) && value > 0;\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\nexport const isNonNegativeNumber = (value: unknown): value is number =>\n isFiniteNumber(value) && value >= 0;\n","/** Membership in a fixed set of allowed values (curried). */\nexport const isOneOf =\n (values: ReadonlySet<unknown>) =>\n (value: unknown): boolean =>\n values.has(value);\n","import { isString } from \"./isString\";\n\nexport const isNonEmptyString = (value: unknown): value is string =>\n isString(value) && value.length > 0;\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\n/** Finite number within `[min, max]`, bounds included (curried). */\nexport const isNumberInRange =\n (min: number, max: number) =>\n (value: unknown): value is number =>\n isFiniteNumber(value) && value >= min && value <= max;\n","import {\n existsProp,\n isNonEmptyString,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport { isVec2 } from \"../vec2\";\nimport type { SurfaceTexture } from \"./texture\";\n\n/** Validate a texture; paths are relative (callers scope them, e.g. via `nested`). */\nexport const validateTexture = (texture: SurfaceTexture): ValidationResult =>\n validate<SurfaceTexture>([\n existsProp(\"src\", \"texture is missing a src.\"),\n propIs(\"repeat\", isVec2, \"repeat must be [x, y].\"),\n ...([\"normal\", \"roughness\", \"ao\"] as const).map((map) =>\n propIs(map, isNonEmptyString, `${map} must be a non-empty URL.`),\n ),\n ])(texture);\n","import {\n existsProp,\n isFiniteNumber,\n isPositiveNumber,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport { isVec3 } from \"../vec3\";\nimport type { Camera } from \"./camera\";\n\nexport const validateCamera = (camera: Camera | undefined): ValidationResult =>\n validate<Camera | undefined>([\n existsProp(\"start.position\"),\n propIs(\"start.position\", isVec3, \"start.position must be [x, y, z].\"),\n propIs(\"eyeHeight\", isPositiveNumber, \"eyeHeight must be a positive number.\"),\n propIs(\n \"fov\",\n (v) => isFiniteNumber(v) && v > 0 && v < 180,\n \"fov must be between 0 and 180 (degrees).\",\n ),\n ])(camera);\n","import {\n existsProp,\n isFiniteNumber,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport type { Environment } from \"./environment\";\n\nexport const validateEnvironment = (env: Environment): ValidationResult =>\n validate<Environment>([\n existsProp(\"src\", \"Environment is missing a src.\"),\n propIs(\"intensity\", isFiniteNumber, \"intensity must be a finite number.\"),\n ])(env);\n","import { validateCamera } from \"../camera\";\nimport { validateEnvironment } from \"../environment\";\nimport type { DataPlugin, SceneContext } from \"../plugin\";\nimport {\n collectDuplicates,\n eachPropIs,\n isObject,\n isString,\n propIs,\n scoped,\n validate,\n when,\n type ValidationIssue,\n} from \"../validation\";\nimport { isVec2 } from \"../vec2\";\nimport type { Scene } from \"./scene\";\n\n/**\n * Validate a scene definition. Returns every issue found (does not throw), so\n * callers — editors, the runtime's hot-reload path — can surface them. Each\n * element validates itself with relative paths; this aggregator re-roots them\n * (`scoped`) into scene-level paths like `artworks[3].label.side`. Data\n * plugins validate the scene keys they own the same way.\n */\nexport function validateScene(scene: Scene, plugins: DataPlugin[] = []): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n issues.push(...scoped(\"camera\")(validateCamera(scene.camera)).errors);\n if (scene.environment)\n issues.push(...scoped(\"environment\")(validateEnvironment(scene.environment)).errors);\n\n issues.push(\n ...validate<Scene>([\n propIs(\n \"boundary\",\n (b) => Array.isArray(b) && b.length >= 3,\n \"boundary must be a ring of at least 3 [x, z] points.\",\n ),\n when(\n (s: Scene) => Array.isArray(s.boundary) && s.boundary.length >= 3,\n eachPropIs(\"boundary\", isVec2, \"must be [x, z].\"),\n ),\n ])(scene).errors,\n );\n\n const ctx: SceneContext = { scene };\n for (const plugin of plugins) {\n const raw = (scene as unknown as Record<string, unknown>)[plugin.kind];\n if (raw === undefined) continue;\n if (plugin.cardinality === \"single\") {\n if (plugin.validate) issues.push(...scoped(plugin.kind)(plugin.validate(raw, ctx)).errors);\n continue;\n }\n if (!Array.isArray(raw)) {\n issues.push({ path: plugin.kind, message: \"must be an array.\" });\n continue;\n }\n if (plugin.validate)\n raw.forEach((item, i) =>\n issues.push(...scoped(`${plugin.kind}[${i}]`)(plugin.validate!(item, ctx)).errors),\n );\n collectDuplicates(\n raw.map((item) => (isObject(item) && isString(item.id) ? item.id : \"\")).filter(Boolean),\n plugin.kind,\n issues,\n );\n }\n\n return issues;\n}\n","import type { Camera } from \"../camera\";\nimport type { Environment } from \"../environment\";\nimport type { Vec2 } from \"../vec2\";\nimport type { DataPlugin } from \"../plugin\";\nimport { validateScene } from \"./validate\";\n\n/**\n * The complete, serializable description of a gallery space. This is the unit\n * an editor produces, a database stores, and the network transports.\n *\n * The base scene holds only what the kernel owns; element kinds (walls,\n * artworks, lights, sky…) are contributed by plugins, which augment this\n * interface with their own optional key via `declare module \"@scenography/scene\"`.\n */\nexport interface Scene {\n camera: Camera;\n /** HDRI sky / image-based lighting for the space. */\n environment?: Environment;\n /**\n * Walkable-area polygon as an XZ ring (`[x, z]` points). When set, the visitor\n * can't step outside it — use it to contain them within an irregular space\n * such as a cove. At least 3 points.\n */\n boundary?: Vec2[];\n}\n\n/**\n * Define a gallery scene. Validates the definition and returns it unchanged\n * (the identity of pure data), throwing on structural errors so authoring\n * mistakes surface early. The returned value is plain, JSON-serializable data.\n * Pass the data halves of the plugins the scene uses so their keys validate too.\n */\nexport function defineScene(scene: Scene, plugins?: DataPlugin[]): Scene {\n const issues = validateScene(scene, plugins);\n if (issues.length > 0) {\n const detail = issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new Error(`Invalid scene definition:\\n${detail}`);\n }\n return scene;\n}\n\n/**\n * Deep-clone a scene through JSON. Useful for the hot-reload path (snapshotting\n * the previous scene before reconciling) and a guard that the definition really\n * is serializable.\n */\nexport function cloneScene(scene: Scene): Scene {\n return JSON.parse(JSON.stringify(scene)) as Scene;\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -41,6 +41,8 @@ interface SurfaceTexture {
|
|
|
41
41
|
/** Ambient-occlusion map (grayscale, or the red channel of an ARM map). */
|
|
42
42
|
ao?: string;
|
|
43
43
|
}
|
|
44
|
+
/** Every URL a surface texture loads, for the gallery's loading progress. */
|
|
45
|
+
declare const surfaceTextureUrls: (texture: SurfaceTexture | undefined) => string[];
|
|
44
46
|
//#endregion
|
|
45
47
|
//#region src/validation/issue.d.ts
|
|
46
48
|
/** A validation problem found in a scene definition. */
|
|
@@ -230,6 +232,11 @@ interface DataPlugin<T = unknown> {
|
|
|
230
232
|
* to the `DataPlugin[]` a scene is validated with.
|
|
231
233
|
*/
|
|
232
234
|
validate?(value: T, ctx: SceneContext): ValidationResult;
|
|
235
|
+
/**
|
|
236
|
+
* Asset URLs this element loads, used for the gallery's initial loading
|
|
237
|
+
* progress. Leave it out for elements with no external assets.
|
|
238
|
+
*/
|
|
239
|
+
assets?(value: T): string[];
|
|
233
240
|
}
|
|
234
241
|
//#endregion
|
|
235
242
|
//#region src/scene/scene.d.ts
|
|
@@ -276,5 +283,5 @@ declare function cloneScene(scene: Scene): Scene;
|
|
|
276
283
|
*/
|
|
277
284
|
declare function validateScene(scene: Scene, plugins?: DataPlugin[]): ValidationIssue[];
|
|
278
285
|
//#endregion
|
|
279
|
-
export { type Camera, type CameraStart, type DataPlugin, type Environment, type Measure, type Message, type Scene, type SceneContext, type SurfaceTexture, type ValidationIssue, type ValidationResult, type Validator, type Vec2, type Vec3, cloneScene, defineScene, eachPropIs, existsProp, fail, invalid, isFiniteNumber, isIntegerNumber, isMeasure, isNonEmptyString, isNonNegativeNumber, isNumberInRange, isObject, isOneOf, isPositiveNumber, isString, isVec2, isVec3, nested, propIs, scoped, showValidation, valid, validate, validateScene, validateTexture, when };
|
|
286
|
+
export { type Camera, type CameraStart, type DataPlugin, type Environment, type Measure, type Message, type Scene, type SceneContext, type SurfaceTexture, type ValidationIssue, type ValidationResult, type Validator, type Vec2, type Vec3, cloneScene, defineScene, eachPropIs, existsProp, fail, invalid, isFiniteNumber, isIntegerNumber, isMeasure, isNonEmptyString, isNonNegativeNumber, isNumberInRange, isObject, isOneOf, isPositiveNumber, isString, isVec2, isVec3, nested, propIs, scoped, showValidation, surfaceTextureUrls, valid, validate, validateScene, validateTexture, when };
|
|
280
287
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.mts
CHANGED
|
@@ -41,6 +41,8 @@ interface SurfaceTexture {
|
|
|
41
41
|
/** Ambient-occlusion map (grayscale, or the red channel of an ARM map). */
|
|
42
42
|
ao?: string;
|
|
43
43
|
}
|
|
44
|
+
/** Every URL a surface texture loads, for the gallery's loading progress. */
|
|
45
|
+
declare const surfaceTextureUrls: (texture: SurfaceTexture | undefined) => string[];
|
|
44
46
|
//#endregion
|
|
45
47
|
//#region src/validation/issue.d.ts
|
|
46
48
|
/** A validation problem found in a scene definition. */
|
|
@@ -230,6 +232,11 @@ interface DataPlugin<T = unknown> {
|
|
|
230
232
|
* to the `DataPlugin[]` a scene is validated with.
|
|
231
233
|
*/
|
|
232
234
|
validate?(value: T, ctx: SceneContext): ValidationResult;
|
|
235
|
+
/**
|
|
236
|
+
* Asset URLs this element loads, used for the gallery's initial loading
|
|
237
|
+
* progress. Leave it out for elements with no external assets.
|
|
238
|
+
*/
|
|
239
|
+
assets?(value: T): string[];
|
|
233
240
|
}
|
|
234
241
|
//#endregion
|
|
235
242
|
//#region src/scene/scene.d.ts
|
|
@@ -276,5 +283,5 @@ declare function cloneScene(scene: Scene): Scene;
|
|
|
276
283
|
*/
|
|
277
284
|
declare function validateScene(scene: Scene, plugins?: DataPlugin[]): ValidationIssue[];
|
|
278
285
|
//#endregion
|
|
279
|
-
export { type Camera, type CameraStart, type DataPlugin, type Environment, type Measure, type Message, type Scene, type SceneContext, type SurfaceTexture, type ValidationIssue, type ValidationResult, type Validator, type Vec2, type Vec3, cloneScene, defineScene, eachPropIs, existsProp, fail, invalid, isFiniteNumber, isIntegerNumber, isMeasure, isNonEmptyString, isNonNegativeNumber, isNumberInRange, isObject, isOneOf, isPositiveNumber, isString, isVec2, isVec3, nested, propIs, scoped, showValidation, valid, validate, validateScene, validateTexture, when };
|
|
286
|
+
export { type Camera, type CameraStart, type DataPlugin, type Environment, type Measure, type Message, type Scene, type SceneContext, type SurfaceTexture, type ValidationIssue, type ValidationResult, type Validator, type Vec2, type Vec3, cloneScene, defineScene, eachPropIs, existsProp, fail, invalid, isFiniteNumber, isIntegerNumber, isMeasure, isNonEmptyString, isNonNegativeNumber, isNumberInRange, isObject, isOneOf, isPositiveNumber, isString, isVec2, isVec3, nested, propIs, scoped, showValidation, surfaceTextureUrls, valid, validate, validateScene, validateTexture, when };
|
|
280
287
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -10,6 +10,18 @@ const isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n) => typeo
|
|
|
10
10
|
/** Type guard: a finite number or a percentage string like `'50%'`. */
|
|
11
11
|
const isMeasure = (m) => typeof m === "number" && Number.isFinite(m) || typeof m === "string" && /^-?\d+(\.\d+)?%$/.test(m);
|
|
12
12
|
//#endregion
|
|
13
|
+
//#region src/texture/texture.ts
|
|
14
|
+
/** Every URL a surface texture loads, for the gallery's loading progress. */
|
|
15
|
+
const surfaceTextureUrls = (texture) => {
|
|
16
|
+
if (!texture) return [];
|
|
17
|
+
return [
|
|
18
|
+
texture.src,
|
|
19
|
+
texture.normal,
|
|
20
|
+
texture.roughness,
|
|
21
|
+
texture.ao
|
|
22
|
+
].filter((url) => url !== void 0);
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
13
25
|
//#region src/validation/issue.ts
|
|
14
26
|
/** Record a `duplicate id` issue for any id that appears more than once. */
|
|
15
27
|
const collectDuplicates = (ids, kind, issues) => {
|
|
@@ -245,6 +257,6 @@ function cloneScene(scene) {
|
|
|
245
257
|
return JSON.parse(JSON.stringify(scene));
|
|
246
258
|
}
|
|
247
259
|
//#endregion
|
|
248
|
-
export { cloneScene, defineScene, eachPropIs, existsProp, fail, invalid, isFiniteNumber, isIntegerNumber, isMeasure, isNonEmptyString, isNonNegativeNumber, isNumberInRange, isObject, isOneOf, isPositiveNumber, isString, isVec2, isVec3, nested, propIs, scoped, showValidation, valid, validate, validateScene, validateTexture, when };
|
|
260
|
+
export { cloneScene, defineScene, eachPropIs, existsProp, fail, invalid, isFiniteNumber, isIntegerNumber, isMeasure, isNonEmptyString, isNonNegativeNumber, isNumberInRange, isObject, isOneOf, isPositiveNumber, isString, isVec2, isVec3, nested, propIs, scoped, showValidation, surfaceTextureUrls, valid, validate, validateScene, validateTexture, when };
|
|
249
261
|
|
|
250
262
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/vec2/vec2.ts","../src/vec3/vec3.ts","../src/measure/measure.ts","../src/validation/issue.ts","../src/validation/validate.ts","../src/obj/get.ts","../src/validation/validators/message.ts","../src/validation/validators/existsProp.ts","../src/validation/validators/propIs.ts","../src/validation/validators/eachPropIs.ts","../src/validation/validators/when.ts","../src/validation/validators/fail.ts","../src/validation/validators/nested.ts","../src/validation/predicates/isString.ts","../src/validation/predicates/isObject.ts","../src/validation/predicates/isFiniteNumber.ts","../src/validation/predicates/isIntegerNumber.ts","../src/validation/predicates/isPositiveNumber.ts","../src/validation/predicates/isNonNegativeNumber.ts","../src/validation/predicates/isOneOf.ts","../src/validation/predicates/isNonEmptyString.ts","../src/validation/predicates/isNumberInRange.ts","../src/texture/validate.ts","../src/camera/validate.ts","../src/environment/validate.ts","../src/scene/validate.ts","../src/scene/scene.ts"],"sourcesContent":["/** A 2D vector, e.g. a size `[width, height]` or a floor coordinate `[x, z]`. */\nexport type Vec2 = [number, number];\n\n/** Type guard: a finite-numeric `[x, y]` pair. */\nexport const isVec2 = (v: unknown): v is Vec2 =>\n Array.isArray(v) &&\n v.length === 2 &&\n v.every((n) => typeof n === \"number\" && Number.isFinite(n));\n","/** A 3D vector `[x, y, z]` in world space. */\nexport type Vec3 = [number, number, number];\n\n/** Type guard: a finite-numeric `[x, y, z]` triple. */\nexport const isVec3 = (v: unknown): v is Vec3 =>\n Array.isArray(v) &&\n v.length === 3 &&\n v.every((n) => typeof n === \"number\" && Number.isFinite(n));\n","/**\n * A length given either as absolute world units (a `number`) or as a percentage\n * string like `'50%'` (relative to whatever it measures along).\n */\nexport type Measure = number | `${number}%`;\n\n/** Type guard: a finite number or a percentage string like `'50%'`. */\nexport const isMeasure = (m: unknown): m is Measure =>\n (typeof m === \"number\" && Number.isFinite(m)) ||\n (typeof m === \"string\" && /^-?\\d+(\\.\\d+)?%$/.test(m));\n","/** A validation problem found in a scene definition. */\nexport interface ValidationIssue {\n /** Dotted path to the offending field, e.g. `artworks[1].location.edge`. */\n path: string;\n message: string;\n}\n\n/** Record a `duplicate id` issue for any id that appears more than once. */\nexport const collectDuplicates = (\n ids: string[],\n kind: string,\n issues: ValidationIssue[],\n): void => {\n const seen = new Set<string>();\n for (const id of ids) {\n if (seen.has(id)) issues.push({ path: kind, message: `duplicate id \"${id}\".` });\n seen.add(id);\n }\n};\n","import type { ValidationIssue } from \"./issue\";\n\n// Core stays environment-agnostic (no DOM lib); `console` exists in every\n// runtime we target, so declare just the slice we use.\ndeclare const console: { error: (message: string) => void };\n\n/** Outcome of running validators against a value: every error found, in order. */\nexport interface ValidationResult {\n isValid: boolean;\n errors: ValidationIssue[];\n}\n\n/** A single validation rule: takes the value, returns what it found. */\nexport type Validator<T = unknown> = (value: T) => ValidationResult;\n\n/** The passing result. */\nexport const valid = (): ValidationResult => ({ isValid: true, errors: [] });\n\n/** A failing result carrying a single issue. */\nexport const invalid = (path: string, message: string): ValidationResult => ({\n isValid: false,\n errors: [{ path, message }],\n});\n\n/**\n * Combine validators into one run: applies them all (never short-circuits) and\n * concatenates their errors in order. Curried so a rule set can be built once\n * and applied to many values.\n */\nexport const validate =\n <T>(validators: ReadonlyArray<Validator<T>>) =>\n (value: T): ValidationResult => {\n const errors = validators.flatMap((validator) => validator(value).errors);\n return { isValid: errors.length === 0, errors };\n };\n\n/**\n * Re-root a result's error paths under a parent path, e.g.\n * `scoped(\"artworks[3]\")` turns `label.side` into `artworks[3].label.side` —\n * how per-element results are folded into a scene-level report.\n */\nexport const scoped =\n (path: string) =>\n (result: ValidationResult): ValidationResult =>\n result.isValid\n ? result\n : {\n isValid: false,\n errors: result.errors.map((error) => ({ ...error, path: `${path}.${error.path}` })),\n };\n\n/**\n * Pass-through reporter: prints the errors to the console when the result is\n * negative, and returns the result unchanged so it composes inside a pipeline.\n */\nexport const showValidation = (result: ValidationResult): ValidationResult => {\n if (!result.isValid) {\n const detail = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n console.error(`Validation failed:\\n${detail}`);\n }\n return result;\n};\n","/**\n * Read a dotted property path (`\"label.side\"`, `\"edges[0].name\"`) off any\n * value; `undefined` when any hop is missing or not an object.\n */\nexport const get = (value: unknown, path: string): unknown =>\n path\n .replace(/\\[(\\d+)\\]/g, \".$1\")\n .split(\".\")\n .reduce<unknown>(\n (current, key) =>\n current !== null && typeof current === \"object\"\n ? (current as Record<string, unknown>)[key]\n : undefined,\n value,\n );\n","/** Error message: fixed text, or derived from the offending value. */\nexport type Message = string | ((value: unknown) => string);\n\nexport const resolveMessage = (message: Message, value: unknown): string =>\n typeof message === \"function\" ? message(value) : message;\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/** The property must be present (truthy). Default message: `Missing <prop>.` */\nexport const existsProp =\n (prop: string, message: Message = `Missing ${prop}.`): Validator =>\n (value) => {\n const found = get(value, prop);\n return found ? valid() : invalid(prop, resolveMessage(message, found));\n };\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/**\n * The property must satisfy `test` **when present**; an absent property passes.\n * Presence is a separate concern: require it with `existsProp` alongside.\n */\nexport const propIs =\n (prop: string, test: (value: unknown) => boolean, message: Message): Validator =>\n (value) => {\n const found = get(value, prop);\n return found === undefined || test(found)\n ? valid()\n : invalid(prop, resolveMessage(message, found));\n };\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/**\n * Every item of the array property must satisfy `test`; errors are addressed\n * per index (`prop[i]`). A missing or non-array property passes — pair with a\n * `propIs(prop, Array.isArray, …)` to report the shape itself.\n */\nexport const eachPropIs =\n (prop: string, test: (item: unknown) => boolean, message: Message): Validator =>\n (value) => {\n const items = get(value, prop);\n if (!Array.isArray(items)) return valid();\n const errors = items.flatMap((item, i) =>\n test(item) ? [] : invalid(`${prop}[${i}]`, resolveMessage(message, item)).errors,\n );\n return { isValid: errors.length === 0, errors };\n };\n","import { valid, validate, type Validator } from \"../validate\";\n\n/** Apply validators only when `test` holds — for shapes hanging off a discriminant. */\nexport const when =\n <T>(test: (value: T) => boolean, ...validators: Validator<T>[]): Validator<T> =>\n (value) =>\n test(value) ? validate(validators)(value) : valid();\n","import { invalid, type Validator } from \"../validate\";\n\n/** Always fails — pair with `when` to reject a branch (e.g. an unknown discriminant). */\nexport const fail =\n (path: string, message: string): Validator =>\n () =>\n invalid(path, message);\n","import { get } from \"../../obj\";\nimport { scoped, valid, type ValidationResult, type Validator } from \"../validate\";\n\n/**\n * Validate a nested object with its own rule set when present, re-rooting its\n * errors under `prop` (e.g. `nested(\"texture\", validateTexture)` reports\n * `texture.src`). An absent property passes.\n */\nexport const nested =\n <S>(prop: string, validateSub: (sub: S) => ValidationResult): Validator =>\n (value) => {\n const found = get(value, prop);\n return found === undefined ? valid() : scoped(prop)(validateSub(found as S));\n };\n","export const isString = (value: unknown): value is string => typeof value === \"string\";\n","export const isObject = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\";\n","export const isFiniteNumber = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isFinite(value);\n","export const isIntegerNumber = (value: unknown): value is number => Number.isInteger(value);\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\nexport const isPositiveNumber = (value: unknown): value is number =>\n isFiniteNumber(value) && value > 0;\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\nexport const isNonNegativeNumber = (value: unknown): value is number =>\n isFiniteNumber(value) && value >= 0;\n","/** Membership in a fixed set of allowed values (curried). */\nexport const isOneOf =\n (values: ReadonlySet<unknown>) =>\n (value: unknown): boolean =>\n values.has(value);\n","import { isString } from \"./isString\";\n\nexport const isNonEmptyString = (value: unknown): value is string =>\n isString(value) && value.length > 0;\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\n/** Finite number within `[min, max]`, bounds included (curried). */\nexport const isNumberInRange =\n (min: number, max: number) =>\n (value: unknown): value is number =>\n isFiniteNumber(value) && value >= min && value <= max;\n","import {\n existsProp,\n isNonEmptyString,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport { isVec2 } from \"../vec2\";\nimport type { SurfaceTexture } from \"./texture\";\n\n/** Validate a texture; paths are relative (callers scope them, e.g. via `nested`). */\nexport const validateTexture = (texture: SurfaceTexture): ValidationResult =>\n validate<SurfaceTexture>([\n existsProp(\"src\", \"texture is missing a src.\"),\n propIs(\"repeat\", isVec2, \"repeat must be [x, y].\"),\n ...([\"normal\", \"roughness\", \"ao\"] as const).map((map) =>\n propIs(map, isNonEmptyString, `${map} must be a non-empty URL.`),\n ),\n ])(texture);\n","import {\n existsProp,\n isFiniteNumber,\n isPositiveNumber,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport { isVec3 } from \"../vec3\";\nimport type { Camera } from \"./camera\";\n\nexport const validateCamera = (camera: Camera | undefined): ValidationResult =>\n validate<Camera | undefined>([\n existsProp(\"start.position\"),\n propIs(\"start.position\", isVec3, \"start.position must be [x, y, z].\"),\n propIs(\"eyeHeight\", isPositiveNumber, \"eyeHeight must be a positive number.\"),\n propIs(\n \"fov\",\n (v) => isFiniteNumber(v) && v > 0 && v < 180,\n \"fov must be between 0 and 180 (degrees).\",\n ),\n ])(camera);\n","import {\n existsProp,\n isFiniteNumber,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport type { Environment } from \"./environment\";\n\nexport const validateEnvironment = (env: Environment): ValidationResult =>\n validate<Environment>([\n existsProp(\"src\", \"Environment is missing a src.\"),\n propIs(\"intensity\", isFiniteNumber, \"intensity must be a finite number.\"),\n ])(env);\n","import { validateCamera } from \"../camera\";\nimport { validateEnvironment } from \"../environment\";\nimport type { DataPlugin, SceneContext } from \"../plugin\";\nimport {\n collectDuplicates,\n eachPropIs,\n isObject,\n isString,\n propIs,\n scoped,\n validate,\n when,\n type ValidationIssue,\n} from \"../validation\";\nimport { isVec2 } from \"../vec2\";\nimport type { Scene } from \"./scene\";\n\n/**\n * Validate a scene definition. Returns every issue found (does not throw), so\n * callers — editors, the runtime's hot-reload path — can surface them. Each\n * element validates itself with relative paths; this aggregator re-roots them\n * (`scoped`) into scene-level paths like `artworks[3].label.side`. Data\n * plugins validate the scene keys they own the same way.\n */\nexport function validateScene(scene: Scene, plugins: DataPlugin[] = []): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n issues.push(...scoped(\"camera\")(validateCamera(scene.camera)).errors);\n if (scene.environment)\n issues.push(...scoped(\"environment\")(validateEnvironment(scene.environment)).errors);\n\n issues.push(\n ...validate<Scene>([\n propIs(\n \"boundary\",\n (b) => Array.isArray(b) && b.length >= 3,\n \"boundary must be a ring of at least 3 [x, z] points.\",\n ),\n when(\n (s: Scene) => Array.isArray(s.boundary) && s.boundary.length >= 3,\n eachPropIs(\"boundary\", isVec2, \"must be [x, z].\"),\n ),\n ])(scene).errors,\n );\n\n const ctx: SceneContext = { scene };\n for (const plugin of plugins) {\n const raw = (scene as unknown as Record<string, unknown>)[plugin.kind];\n if (raw === undefined) continue;\n if (plugin.cardinality === \"single\") {\n if (plugin.validate) issues.push(...scoped(plugin.kind)(plugin.validate(raw, ctx)).errors);\n continue;\n }\n if (!Array.isArray(raw)) {\n issues.push({ path: plugin.kind, message: \"must be an array.\" });\n continue;\n }\n if (plugin.validate)\n raw.forEach((item, i) =>\n issues.push(...scoped(`${plugin.kind}[${i}]`)(plugin.validate!(item, ctx)).errors),\n );\n collectDuplicates(\n raw.map((item) => (isObject(item) && isString(item.id) ? item.id : \"\")).filter(Boolean),\n plugin.kind,\n issues,\n );\n }\n\n return issues;\n}\n","import type { Camera } from \"../camera\";\nimport type { Environment } from \"../environment\";\nimport type { Vec2 } from \"../vec2\";\nimport type { DataPlugin } from \"../plugin\";\nimport { validateScene } from \"./validate\";\n\n/**\n * The complete, serializable description of a gallery space. This is the unit\n * an editor produces, a database stores, and the network transports.\n *\n * The base scene holds only what the kernel owns; element kinds (walls,\n * artworks, lights, sky…) are contributed by plugins, which augment this\n * interface with their own optional key via `declare module \"@scenography/scene\"`.\n */\nexport interface Scene {\n camera: Camera;\n /** HDRI sky / image-based lighting for the space. */\n environment?: Environment;\n /**\n * Walkable-area polygon as an XZ ring (`[x, z]` points). When set, the visitor\n * can't step outside it — use it to contain them within an irregular space\n * such as a cove. At least 3 points.\n */\n boundary?: Vec2[];\n}\n\n/**\n * Define a gallery scene. Validates the definition and returns it unchanged\n * (the identity of pure data), throwing on structural errors so authoring\n * mistakes surface early. The returned value is plain, JSON-serializable data.\n * Pass the data halves of the plugins the scene uses so their keys validate too.\n */\nexport function defineScene(scene: Scene, plugins?: DataPlugin[]): Scene {\n const issues = validateScene(scene, plugins);\n if (issues.length > 0) {\n const detail = issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new Error(`Invalid scene definition:\\n${detail}`);\n }\n return scene;\n}\n\n/**\n * Deep-clone a scene through JSON. Useful for the hot-reload path (snapshotting\n * the previous scene before reconciling) and a guard that the definition really\n * is serializable.\n */\nexport function cloneScene(scene: Scene): Scene {\n return JSON.parse(JSON.stringify(scene)) as Scene;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/vec2/vec2.ts","../src/vec3/vec3.ts","../src/measure/measure.ts","../src/texture/texture.ts","../src/validation/issue.ts","../src/validation/validate.ts","../src/obj/get.ts","../src/validation/validators/message.ts","../src/validation/validators/existsProp.ts","../src/validation/validators/propIs.ts","../src/validation/validators/eachPropIs.ts","../src/validation/validators/when.ts","../src/validation/validators/fail.ts","../src/validation/validators/nested.ts","../src/validation/predicates/isString.ts","../src/validation/predicates/isObject.ts","../src/validation/predicates/isFiniteNumber.ts","../src/validation/predicates/isIntegerNumber.ts","../src/validation/predicates/isPositiveNumber.ts","../src/validation/predicates/isNonNegativeNumber.ts","../src/validation/predicates/isOneOf.ts","../src/validation/predicates/isNonEmptyString.ts","../src/validation/predicates/isNumberInRange.ts","../src/texture/validate.ts","../src/camera/validate.ts","../src/environment/validate.ts","../src/scene/validate.ts","../src/scene/scene.ts"],"sourcesContent":["/** A 2D vector, e.g. a size `[width, height]` or a floor coordinate `[x, z]`. */\nexport type Vec2 = [number, number];\n\n/** Type guard: a finite-numeric `[x, y]` pair. */\nexport const isVec2 = (v: unknown): v is Vec2 =>\n Array.isArray(v) &&\n v.length === 2 &&\n v.every((n) => typeof n === \"number\" && Number.isFinite(n));\n","/** A 3D vector `[x, y, z]` in world space. */\nexport type Vec3 = [number, number, number];\n\n/** Type guard: a finite-numeric `[x, y, z]` triple. */\nexport const isVec3 = (v: unknown): v is Vec3 =>\n Array.isArray(v) &&\n v.length === 3 &&\n v.every((n) => typeof n === \"number\" && Number.isFinite(n));\n","/**\n * A length given either as absolute world units (a `number`) or as a percentage\n * string like `'50%'` (relative to whatever it measures along).\n */\nexport type Measure = number | `${number}%`;\n\n/** Type guard: a finite number or a percentage string like `'50%'`. */\nexport const isMeasure = (m: unknown): m is Measure =>\n (typeof m === \"number\" && Number.isFinite(m)) ||\n (typeof m === \"string\" && /^-?\\d+(\\.\\d+)?%$/.test(m));\n","import type { Vec2 } from \"../vec2\";\n\n/**\n * A material mapped onto a surface (a wall, a floor…). `src` is the color/albedo\n * map; the rest are optional PBR maps. All share the same tiling (`repeat`). The\n * engine picks the loader by file extension, so `.exr`/`.png`/`.jpg` all work.\n * Point `roughness` and `ao` at the same packed ARM image to reuse a single file\n * (its green channel drives roughness, its red channel ambient occlusion).\n */\nexport interface SurfaceTexture {\n /** Color/albedo map (the visible surface; treated as sRGB). */\n src: string;\n /** How many times the maps repeat across the surface `[x, y]`. Defaults to `[1, 1]`. */\n repeat?: Vec2;\n /** Tangent-space normal map for surface relief (OpenGL convention). */\n normal?: string;\n /** Strength of the normal map relief. Defaults to `1`; raise for more depth. */\n normalScale?: number;\n /** Roughness map (grayscale, or the green channel of an ARM map). */\n roughness?: string;\n /** Ambient-occlusion map (grayscale, or the red channel of an ARM map). */\n ao?: string;\n}\n\n/** Every URL a surface texture loads, for the gallery's loading progress. */\nexport const surfaceTextureUrls = (texture: SurfaceTexture | undefined): string[] => {\n if (!texture) return [];\n return [texture.src, texture.normal, texture.roughness, texture.ao].filter(\n (url): url is string => url !== undefined,\n );\n};\n","/** A validation problem found in a scene definition. */\nexport interface ValidationIssue {\n /** Dotted path to the offending field, e.g. `artworks[1].location.edge`. */\n path: string;\n message: string;\n}\n\n/** Record a `duplicate id` issue for any id that appears more than once. */\nexport const collectDuplicates = (\n ids: string[],\n kind: string,\n issues: ValidationIssue[],\n): void => {\n const seen = new Set<string>();\n for (const id of ids) {\n if (seen.has(id)) issues.push({ path: kind, message: `duplicate id \"${id}\".` });\n seen.add(id);\n }\n};\n","import type { ValidationIssue } from \"./issue\";\n\n// Core stays environment-agnostic (no DOM lib); `console` exists in every\n// runtime we target, so declare just the slice we use.\ndeclare const console: { error: (message: string) => void };\n\n/** Outcome of running validators against a value: every error found, in order. */\nexport interface ValidationResult {\n isValid: boolean;\n errors: ValidationIssue[];\n}\n\n/** A single validation rule: takes the value, returns what it found. */\nexport type Validator<T = unknown> = (value: T) => ValidationResult;\n\n/** The passing result. */\nexport const valid = (): ValidationResult => ({ isValid: true, errors: [] });\n\n/** A failing result carrying a single issue. */\nexport const invalid = (path: string, message: string): ValidationResult => ({\n isValid: false,\n errors: [{ path, message }],\n});\n\n/**\n * Combine validators into one run: applies them all (never short-circuits) and\n * concatenates their errors in order. Curried so a rule set can be built once\n * and applied to many values.\n */\nexport const validate =\n <T>(validators: ReadonlyArray<Validator<T>>) =>\n (value: T): ValidationResult => {\n const errors = validators.flatMap((validator) => validator(value).errors);\n return { isValid: errors.length === 0, errors };\n };\n\n/**\n * Re-root a result's error paths under a parent path, e.g.\n * `scoped(\"artworks[3]\")` turns `label.side` into `artworks[3].label.side` —\n * how per-element results are folded into a scene-level report.\n */\nexport const scoped =\n (path: string) =>\n (result: ValidationResult): ValidationResult =>\n result.isValid\n ? result\n : {\n isValid: false,\n errors: result.errors.map((error) => ({ ...error, path: `${path}.${error.path}` })),\n };\n\n/**\n * Pass-through reporter: prints the errors to the console when the result is\n * negative, and returns the result unchanged so it composes inside a pipeline.\n */\nexport const showValidation = (result: ValidationResult): ValidationResult => {\n if (!result.isValid) {\n const detail = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join(\"\\n\");\n console.error(`Validation failed:\\n${detail}`);\n }\n return result;\n};\n","/**\n * Read a dotted property path (`\"label.side\"`, `\"edges[0].name\"`) off any\n * value; `undefined` when any hop is missing or not an object.\n */\nexport const get = (value: unknown, path: string): unknown =>\n path\n .replace(/\\[(\\d+)\\]/g, \".$1\")\n .split(\".\")\n .reduce<unknown>(\n (current, key) =>\n current !== null && typeof current === \"object\"\n ? (current as Record<string, unknown>)[key]\n : undefined,\n value,\n );\n","/** Error message: fixed text, or derived from the offending value. */\nexport type Message = string | ((value: unknown) => string);\n\nexport const resolveMessage = (message: Message, value: unknown): string =>\n typeof message === \"function\" ? message(value) : message;\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/** The property must be present (truthy). Default message: `Missing <prop>.` */\nexport const existsProp =\n (prop: string, message: Message = `Missing ${prop}.`): Validator =>\n (value) => {\n const found = get(value, prop);\n return found ? valid() : invalid(prop, resolveMessage(message, found));\n };\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/**\n * The property must satisfy `test` **when present**; an absent property passes.\n * Presence is a separate concern: require it with `existsProp` alongside.\n */\nexport const propIs =\n (prop: string, test: (value: unknown) => boolean, message: Message): Validator =>\n (value) => {\n const found = get(value, prop);\n return found === undefined || test(found)\n ? valid()\n : invalid(prop, resolveMessage(message, found));\n };\n","import { get } from \"../../obj\";\nimport { invalid, valid, type Validator } from \"../validate\";\nimport { resolveMessage, type Message } from \"./message\";\n\n/**\n * Every item of the array property must satisfy `test`; errors are addressed\n * per index (`prop[i]`). A missing or non-array property passes — pair with a\n * `propIs(prop, Array.isArray, …)` to report the shape itself.\n */\nexport const eachPropIs =\n (prop: string, test: (item: unknown) => boolean, message: Message): Validator =>\n (value) => {\n const items = get(value, prop);\n if (!Array.isArray(items)) return valid();\n const errors = items.flatMap((item, i) =>\n test(item) ? [] : invalid(`${prop}[${i}]`, resolveMessage(message, item)).errors,\n );\n return { isValid: errors.length === 0, errors };\n };\n","import { valid, validate, type Validator } from \"../validate\";\n\n/** Apply validators only when `test` holds — for shapes hanging off a discriminant. */\nexport const when =\n <T>(test: (value: T) => boolean, ...validators: Validator<T>[]): Validator<T> =>\n (value) =>\n test(value) ? validate(validators)(value) : valid();\n","import { invalid, type Validator } from \"../validate\";\n\n/** Always fails — pair with `when` to reject a branch (e.g. an unknown discriminant). */\nexport const fail =\n (path: string, message: string): Validator =>\n () =>\n invalid(path, message);\n","import { get } from \"../../obj\";\nimport { scoped, valid, type ValidationResult, type Validator } from \"../validate\";\n\n/**\n * Validate a nested object with its own rule set when present, re-rooting its\n * errors under `prop` (e.g. `nested(\"texture\", validateTexture)` reports\n * `texture.src`). An absent property passes.\n */\nexport const nested =\n <S>(prop: string, validateSub: (sub: S) => ValidationResult): Validator =>\n (value) => {\n const found = get(value, prop);\n return found === undefined ? valid() : scoped(prop)(validateSub(found as S));\n };\n","export const isString = (value: unknown): value is string => typeof value === \"string\";\n","export const isObject = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\";\n","export const isFiniteNumber = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isFinite(value);\n","export const isIntegerNumber = (value: unknown): value is number => Number.isInteger(value);\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\nexport const isPositiveNumber = (value: unknown): value is number =>\n isFiniteNumber(value) && value > 0;\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\nexport const isNonNegativeNumber = (value: unknown): value is number =>\n isFiniteNumber(value) && value >= 0;\n","/** Membership in a fixed set of allowed values (curried). */\nexport const isOneOf =\n (values: ReadonlySet<unknown>) =>\n (value: unknown): boolean =>\n values.has(value);\n","import { isString } from \"./isString\";\n\nexport const isNonEmptyString = (value: unknown): value is string =>\n isString(value) && value.length > 0;\n","import { isFiniteNumber } from \"./isFiniteNumber\";\n\n/** Finite number within `[min, max]`, bounds included (curried). */\nexport const isNumberInRange =\n (min: number, max: number) =>\n (value: unknown): value is number =>\n isFiniteNumber(value) && value >= min && value <= max;\n","import {\n existsProp,\n isNonEmptyString,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport { isVec2 } from \"../vec2\";\nimport type { SurfaceTexture } from \"./texture\";\n\n/** Validate a texture; paths are relative (callers scope them, e.g. via `nested`). */\nexport const validateTexture = (texture: SurfaceTexture): ValidationResult =>\n validate<SurfaceTexture>([\n existsProp(\"src\", \"texture is missing a src.\"),\n propIs(\"repeat\", isVec2, \"repeat must be [x, y].\"),\n ...([\"normal\", \"roughness\", \"ao\"] as const).map((map) =>\n propIs(map, isNonEmptyString, `${map} must be a non-empty URL.`),\n ),\n ])(texture);\n","import {\n existsProp,\n isFiniteNumber,\n isPositiveNumber,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport { isVec3 } from \"../vec3\";\nimport type { Camera } from \"./camera\";\n\nexport const validateCamera = (camera: Camera | undefined): ValidationResult =>\n validate<Camera | undefined>([\n existsProp(\"start.position\"),\n propIs(\"start.position\", isVec3, \"start.position must be [x, y, z].\"),\n propIs(\"eyeHeight\", isPositiveNumber, \"eyeHeight must be a positive number.\"),\n propIs(\n \"fov\",\n (v) => isFiniteNumber(v) && v > 0 && v < 180,\n \"fov must be between 0 and 180 (degrees).\",\n ),\n ])(camera);\n","import {\n existsProp,\n isFiniteNumber,\n propIs,\n validate,\n type ValidationResult,\n} from \"../validation\";\nimport type { Environment } from \"./environment\";\n\nexport const validateEnvironment = (env: Environment): ValidationResult =>\n validate<Environment>([\n existsProp(\"src\", \"Environment is missing a src.\"),\n propIs(\"intensity\", isFiniteNumber, \"intensity must be a finite number.\"),\n ])(env);\n","import { validateCamera } from \"../camera\";\nimport { validateEnvironment } from \"../environment\";\nimport type { DataPlugin, SceneContext } from \"../plugin\";\nimport {\n collectDuplicates,\n eachPropIs,\n isObject,\n isString,\n propIs,\n scoped,\n validate,\n when,\n type ValidationIssue,\n} from \"../validation\";\nimport { isVec2 } from \"../vec2\";\nimport type { Scene } from \"./scene\";\n\n/**\n * Validate a scene definition. Returns every issue found (does not throw), so\n * callers — editors, the runtime's hot-reload path — can surface them. Each\n * element validates itself with relative paths; this aggregator re-roots them\n * (`scoped`) into scene-level paths like `artworks[3].label.side`. Data\n * plugins validate the scene keys they own the same way.\n */\nexport function validateScene(scene: Scene, plugins: DataPlugin[] = []): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n\n issues.push(...scoped(\"camera\")(validateCamera(scene.camera)).errors);\n if (scene.environment)\n issues.push(...scoped(\"environment\")(validateEnvironment(scene.environment)).errors);\n\n issues.push(\n ...validate<Scene>([\n propIs(\n \"boundary\",\n (b) => Array.isArray(b) && b.length >= 3,\n \"boundary must be a ring of at least 3 [x, z] points.\",\n ),\n when(\n (s: Scene) => Array.isArray(s.boundary) && s.boundary.length >= 3,\n eachPropIs(\"boundary\", isVec2, \"must be [x, z].\"),\n ),\n ])(scene).errors,\n );\n\n const ctx: SceneContext = { scene };\n for (const plugin of plugins) {\n const raw = (scene as unknown as Record<string, unknown>)[plugin.kind];\n if (raw === undefined) continue;\n if (plugin.cardinality === \"single\") {\n if (plugin.validate) issues.push(...scoped(plugin.kind)(plugin.validate(raw, ctx)).errors);\n continue;\n }\n if (!Array.isArray(raw)) {\n issues.push({ path: plugin.kind, message: \"must be an array.\" });\n continue;\n }\n if (plugin.validate)\n raw.forEach((item, i) =>\n issues.push(...scoped(`${plugin.kind}[${i}]`)(plugin.validate!(item, ctx)).errors),\n );\n collectDuplicates(\n raw.map((item) => (isObject(item) && isString(item.id) ? item.id : \"\")).filter(Boolean),\n plugin.kind,\n issues,\n );\n }\n\n return issues;\n}\n","import type { Camera } from \"../camera\";\nimport type { Environment } from \"../environment\";\nimport type { Vec2 } from \"../vec2\";\nimport type { DataPlugin } from \"../plugin\";\nimport { validateScene } from \"./validate\";\n\n/**\n * The complete, serializable description of a gallery space. This is the unit\n * an editor produces, a database stores, and the network transports.\n *\n * The base scene holds only what the kernel owns; element kinds (walls,\n * artworks, lights, sky…) are contributed by plugins, which augment this\n * interface with their own optional key via `declare module \"@scenography/scene\"`.\n */\nexport interface Scene {\n camera: Camera;\n /** HDRI sky / image-based lighting for the space. */\n environment?: Environment;\n /**\n * Walkable-area polygon as an XZ ring (`[x, z]` points). When set, the visitor\n * can't step outside it — use it to contain them within an irregular space\n * such as a cove. At least 3 points.\n */\n boundary?: Vec2[];\n}\n\n/**\n * Define a gallery scene. Validates the definition and returns it unchanged\n * (the identity of pure data), throwing on structural errors so authoring\n * mistakes surface early. The returned value is plain, JSON-serializable data.\n * Pass the data halves of the plugins the scene uses so their keys validate too.\n */\nexport function defineScene(scene: Scene, plugins?: DataPlugin[]): Scene {\n const issues = validateScene(scene, plugins);\n if (issues.length > 0) {\n const detail = issues.map((i) => ` - ${i.path}: ${i.message}`).join(\"\\n\");\n throw new Error(`Invalid scene definition:\\n${detail}`);\n }\n return scene;\n}\n\n/**\n * Deep-clone a scene through JSON. Useful for the hot-reload path (snapshotting\n * the previous scene before reconciling) and a guard that the definition really\n * is serializable.\n */\nexport function cloneScene(scene: Scene): Scene {\n return JSON.parse(JSON.stringify(scene)) as Scene;\n}\n"],"mappings":"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"}
|