@scenography/scene 0.2.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 ADDED
@@ -0,0 +1,277 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/vec2/vec2.ts
3
+ /** Type guard: a finite-numeric `[x, y]` pair. */
4
+ const isVec2 = (v) => Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === "number" && Number.isFinite(n));
5
+ //#endregion
6
+ //#region src/vec3/vec3.ts
7
+ /** Type guard: a finite-numeric `[x, y, z]` triple. */
8
+ const isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number" && Number.isFinite(n));
9
+ //#endregion
10
+ //#region src/measure/measure.ts
11
+ /** Type guard: a finite number or a percentage string like `'50%'`. */
12
+ const isMeasure = (m) => typeof m === "number" && Number.isFinite(m) || typeof m === "string" && /^-?\d+(\.\d+)?%$/.test(m);
13
+ //#endregion
14
+ //#region src/validation/issue.ts
15
+ /** Record a `duplicate id` issue for any id that appears more than once. */
16
+ const collectDuplicates = (ids, kind, issues) => {
17
+ const seen = /* @__PURE__ */ new Set();
18
+ for (const id of ids) {
19
+ if (seen.has(id)) issues.push({
20
+ path: kind,
21
+ message: `duplicate id "${id}".`
22
+ });
23
+ seen.add(id);
24
+ }
25
+ };
26
+ //#endregion
27
+ //#region src/validation/validate.ts
28
+ /** The passing result. */
29
+ const valid = () => ({
30
+ isValid: true,
31
+ errors: []
32
+ });
33
+ /** A failing result carrying a single issue. */
34
+ const invalid = (path, message) => ({
35
+ isValid: false,
36
+ errors: [{
37
+ path,
38
+ message
39
+ }]
40
+ });
41
+ /**
42
+ * Combine validators into one run: applies them all (never short-circuits) and
43
+ * concatenates their errors in order. Curried so a rule set can be built once
44
+ * and applied to many values.
45
+ */
46
+ const validate = (validators) => (value) => {
47
+ const errors = validators.flatMap((validator) => validator(value).errors);
48
+ return {
49
+ isValid: errors.length === 0,
50
+ errors
51
+ };
52
+ };
53
+ /**
54
+ * Re-root a result's error paths under a parent path, e.g.
55
+ * `scoped("artworks[3]")` turns `label.side` into `artworks[3].label.side` —
56
+ * how per-element results are folded into a scene-level report.
57
+ */
58
+ const scoped = (path) => (result) => result.isValid ? result : {
59
+ isValid: false,
60
+ errors: result.errors.map((error) => ({
61
+ ...error,
62
+ path: `${path}.${error.path}`
63
+ }))
64
+ };
65
+ /**
66
+ * Pass-through reporter: prints the errors to the console when the result is
67
+ * negative, and returns the result unchanged so it composes inside a pipeline.
68
+ */
69
+ const showValidation = (result) => {
70
+ if (!result.isValid) {
71
+ const detail = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
72
+ console.error(`Validation failed:\n${detail}`);
73
+ }
74
+ return result;
75
+ };
76
+ //#endregion
77
+ //#region src/obj/get.ts
78
+ /**
79
+ * Read a dotted property path (`"label.side"`, `"edges[0].name"`) off any
80
+ * value; `undefined` when any hop is missing or not an object.
81
+ */
82
+ const get = (value, path) => path.replace(/\[(\d+)\]/g, ".$1").split(".").reduce((current, key) => current !== null && typeof current === "object" ? current[key] : void 0, value);
83
+ //#endregion
84
+ //#region src/validation/validators/message.ts
85
+ const resolveMessage = (message, value) => typeof message === "function" ? message(value) : message;
86
+ //#endregion
87
+ //#region src/validation/validators/existsProp.ts
88
+ /** The property must be present (truthy). Default message: `Missing <prop>.` */
89
+ const existsProp = (prop, message = `Missing ${prop}.`) => (value) => {
90
+ const found = get(value, prop);
91
+ return found ? valid() : invalid(prop, resolveMessage(message, found));
92
+ };
93
+ //#endregion
94
+ //#region src/validation/validators/propIs.ts
95
+ /**
96
+ * The property must satisfy `test` **when present**; an absent property passes.
97
+ * Presence is a separate concern: require it with `existsProp` alongside.
98
+ */
99
+ const propIs = (prop, test, message) => (value) => {
100
+ const found = get(value, prop);
101
+ return found === void 0 || test(found) ? valid() : invalid(prop, resolveMessage(message, found));
102
+ };
103
+ //#endregion
104
+ //#region src/validation/validators/eachPropIs.ts
105
+ /**
106
+ * Every item of the array property must satisfy `test`; errors are addressed
107
+ * per index (`prop[i]`). A missing or non-array property passes — pair with a
108
+ * `propIs(prop, Array.isArray, …)` to report the shape itself.
109
+ */
110
+ const eachPropIs = (prop, test, message) => (value) => {
111
+ const items = get(value, prop);
112
+ if (!Array.isArray(items)) return valid();
113
+ const errors = items.flatMap((item, i) => test(item) ? [] : invalid(`${prop}[${i}]`, resolveMessage(message, item)).errors);
114
+ return {
115
+ isValid: errors.length === 0,
116
+ errors
117
+ };
118
+ };
119
+ //#endregion
120
+ //#region src/validation/validators/when.ts
121
+ /** Apply validators only when `test` holds — for shapes hanging off a discriminant. */
122
+ const when = (test, ...validators) => (value) => test(value) ? validate(validators)(value) : valid();
123
+ //#endregion
124
+ //#region src/validation/validators/fail.ts
125
+ /** Always fails — pair with `when` to reject a branch (e.g. an unknown discriminant). */
126
+ const fail = (path, message) => () => invalid(path, message);
127
+ //#endregion
128
+ //#region src/validation/validators/nested.ts
129
+ /**
130
+ * Validate a nested object with its own rule set when present, re-rooting its
131
+ * errors under `prop` (e.g. `nested("texture", validateTexture)` reports
132
+ * `texture.src`). An absent property passes.
133
+ */
134
+ const nested = (prop, validateSub) => (value) => {
135
+ const found = get(value, prop);
136
+ return found === void 0 ? valid() : scoped(prop)(validateSub(found));
137
+ };
138
+ //#endregion
139
+ //#region src/validation/predicates/isString.ts
140
+ const isString = (value) => typeof value === "string";
141
+ //#endregion
142
+ //#region src/validation/predicates/isObject.ts
143
+ const isObject = (value) => value !== null && typeof value === "object";
144
+ //#endregion
145
+ //#region src/validation/predicates/isFiniteNumber.ts
146
+ const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
147
+ //#endregion
148
+ //#region src/validation/predicates/isIntegerNumber.ts
149
+ const isIntegerNumber = (value) => Number.isInteger(value);
150
+ //#endregion
151
+ //#region src/validation/predicates/isPositiveNumber.ts
152
+ const isPositiveNumber = (value) => isFiniteNumber(value) && value > 0;
153
+ //#endregion
154
+ //#region src/validation/predicates/isNonNegativeNumber.ts
155
+ const isNonNegativeNumber = (value) => isFiniteNumber(value) && value >= 0;
156
+ //#endregion
157
+ //#region src/validation/predicates/isOneOf.ts
158
+ /** Membership in a fixed set of allowed values (curried). */
159
+ const isOneOf = (values) => (value) => values.has(value);
160
+ //#endregion
161
+ //#region src/validation/predicates/isNonEmptyString.ts
162
+ const isNonEmptyString = (value) => isString(value) && value.length > 0;
163
+ //#endregion
164
+ //#region src/validation/predicates/isNumberInRange.ts
165
+ /** Finite number within `[min, max]`, bounds included (curried). */
166
+ const isNumberInRange = (min, max) => (value) => isFiniteNumber(value) && value >= min && value <= max;
167
+ //#endregion
168
+ //#region src/texture/validate.ts
169
+ /** Validate a texture; paths are relative (callers scope them, e.g. via `nested`). */
170
+ const validateTexture = (texture) => validate([
171
+ existsProp("src", "texture is missing a src."),
172
+ propIs("repeat", isVec2, "repeat must be [x, y]."),
173
+ ...[
174
+ "normal",
175
+ "roughness",
176
+ "ao"
177
+ ].map((map) => propIs(map, isNonEmptyString, `${map} must be a non-empty URL.`))
178
+ ])(texture);
179
+ //#endregion
180
+ //#region src/camera/validate.ts
181
+ const validateCamera = (camera) => validate([
182
+ existsProp("start.position"),
183
+ propIs("start.position", isVec3, "start.position must be [x, y, z]."),
184
+ propIs("eyeHeight", isPositiveNumber, "eyeHeight must be a positive number."),
185
+ propIs("fov", (v) => isFiniteNumber(v) && v > 0 && v < 180, "fov must be between 0 and 180 (degrees).")
186
+ ])(camera);
187
+ //#endregion
188
+ //#region src/environment/validate.ts
189
+ const validateEnvironment = (env) => validate([existsProp("src", "Environment is missing a src."), propIs("intensity", isFiniteNumber, "intensity must be a finite number.")])(env);
190
+ //#endregion
191
+ //#region src/scene/validate.ts
192
+ /**
193
+ * Validate a scene definition. Returns every issue found (does not throw), so
194
+ * callers — editors, the runtime's hot-reload path — can surface them. Each
195
+ * element validates itself with relative paths; this aggregator re-roots them
196
+ * (`scoped`) into scene-level paths like `artworks[3].label.side`. Data
197
+ * plugins validate the scene keys they own the same way.
198
+ */
199
+ function validateScene(scene, plugins = []) {
200
+ const issues = [];
201
+ issues.push(...scoped("camera")(validateCamera(scene.camera)).errors);
202
+ if (scene.environment) issues.push(...scoped("environment")(validateEnvironment(scene.environment)).errors);
203
+ issues.push(...validate([propIs("boundary", (b) => Array.isArray(b) && b.length >= 3, "boundary must be a ring of at least 3 [x, z] points."), when((s) => Array.isArray(s.boundary) && s.boundary.length >= 3, eachPropIs("boundary", isVec2, "must be [x, z]."))])(scene).errors);
204
+ const ctx = { scene };
205
+ for (const plugin of plugins) {
206
+ const raw = scene[plugin.kind];
207
+ if (raw === void 0) continue;
208
+ if (plugin.cardinality === "single") {
209
+ if (plugin.validate) issues.push(...scoped(plugin.kind)(plugin.validate(raw, ctx)).errors);
210
+ continue;
211
+ }
212
+ if (!Array.isArray(raw)) {
213
+ issues.push({
214
+ path: plugin.kind,
215
+ message: "must be an array."
216
+ });
217
+ continue;
218
+ }
219
+ if (plugin.validate) raw.forEach((item, i) => issues.push(...scoped(`${plugin.kind}[${i}]`)(plugin.validate(item, ctx)).errors));
220
+ collectDuplicates(raw.map((item) => isObject(item) && isString(item.id) ? item.id : "").filter(Boolean), plugin.kind, issues);
221
+ }
222
+ return issues;
223
+ }
224
+ //#endregion
225
+ //#region src/scene/scene.ts
226
+ /**
227
+ * Define a gallery scene. Validates the definition and returns it unchanged
228
+ * (the identity of pure data), throwing on structural errors so authoring
229
+ * mistakes surface early. The returned value is plain, JSON-serializable data.
230
+ * Pass the data halves of the plugins the scene uses so their keys validate too.
231
+ */
232
+ function defineScene(scene, plugins) {
233
+ const issues = validateScene(scene, plugins);
234
+ if (issues.length > 0) {
235
+ const detail = issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n");
236
+ throw new Error(`Invalid scene definition:\n${detail}`);
237
+ }
238
+ return scene;
239
+ }
240
+ /**
241
+ * Deep-clone a scene through JSON. Useful for the hot-reload path (snapshotting
242
+ * the previous scene before reconciling) and a guard that the definition really
243
+ * is serializable.
244
+ */
245
+ function cloneScene(scene) {
246
+ return JSON.parse(JSON.stringify(scene));
247
+ }
248
+ //#endregion
249
+ exports.cloneScene = cloneScene;
250
+ exports.defineScene = defineScene;
251
+ exports.eachPropIs = eachPropIs;
252
+ exports.existsProp = existsProp;
253
+ exports.fail = fail;
254
+ exports.invalid = invalid;
255
+ exports.isFiniteNumber = isFiniteNumber;
256
+ exports.isIntegerNumber = isIntegerNumber;
257
+ exports.isMeasure = isMeasure;
258
+ exports.isNonEmptyString = isNonEmptyString;
259
+ exports.isNonNegativeNumber = isNonNegativeNumber;
260
+ exports.isNumberInRange = isNumberInRange;
261
+ exports.isObject = isObject;
262
+ exports.isOneOf = isOneOf;
263
+ exports.isPositiveNumber = isPositiveNumber;
264
+ exports.isString = isString;
265
+ exports.isVec2 = isVec2;
266
+ exports.isVec3 = isVec3;
267
+ exports.nested = nested;
268
+ exports.propIs = propIs;
269
+ exports.scoped = scoped;
270
+ exports.showValidation = showValidation;
271
+ exports.valid = valid;
272
+ exports.validate = validate;
273
+ exports.validateScene = validateScene;
274
+ exports.validateTexture = validateTexture;
275
+ exports.when = when;
276
+
277
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,280 @@
1
+ //#region src/vec2/vec2.d.ts
2
+ /** A 2D vector, e.g. a size `[width, height]` or a floor coordinate `[x, z]`. */
3
+ type Vec2 = [number, number];
4
+ /** Type guard: a finite-numeric `[x, y]` pair. */
5
+ declare const isVec2: (v: unknown) => v is Vec2;
6
+ //#endregion
7
+ //#region src/vec3/vec3.d.ts
8
+ /** A 3D vector `[x, y, z]` in world space. */
9
+ type Vec3 = [number, number, number];
10
+ /** Type guard: a finite-numeric `[x, y, z]` triple. */
11
+ declare const isVec3: (v: unknown) => v is Vec3;
12
+ //#endregion
13
+ //#region src/measure/measure.d.ts
14
+ /**
15
+ * A length given either as absolute world units (a `number`) or as a percentage
16
+ * string like `'50%'` (relative to whatever it measures along).
17
+ */
18
+ type Measure = number | `${number}%`;
19
+ /** Type guard: a finite number or a percentage string like `'50%'`. */
20
+ declare const isMeasure: (m: unknown) => m is Measure;
21
+ //#endregion
22
+ //#region src/texture/texture.d.ts
23
+ /**
24
+ * A material mapped onto a surface (a wall, a floor…). `src` is the color/albedo
25
+ * map; the rest are optional PBR maps. All share the same tiling (`repeat`). The
26
+ * engine picks the loader by file extension, so `.exr`/`.png`/`.jpg` all work.
27
+ * Point `roughness` and `ao` at the same packed ARM image to reuse a single file
28
+ * (its green channel drives roughness, its red channel ambient occlusion).
29
+ */
30
+ interface SurfaceTexture {
31
+ /** Color/albedo map (the visible surface; treated as sRGB). */
32
+ src: string;
33
+ /** How many times the maps repeat across the surface `[x, y]`. Defaults to `[1, 1]`. */
34
+ repeat?: Vec2;
35
+ /** Tangent-space normal map for surface relief (OpenGL convention). */
36
+ normal?: string;
37
+ /** Strength of the normal map relief. Defaults to `1`; raise for more depth. */
38
+ normalScale?: number;
39
+ /** Roughness map (grayscale, or the green channel of an ARM map). */
40
+ roughness?: string;
41
+ /** Ambient-occlusion map (grayscale, or the red channel of an ARM map). */
42
+ ao?: string;
43
+ }
44
+ //#endregion
45
+ //#region src/validation/issue.d.ts
46
+ /** A validation problem found in a scene definition. */
47
+ interface ValidationIssue {
48
+ /** Dotted path to the offending field, e.g. `artworks[1].location.edge`. */
49
+ path: string;
50
+ message: string;
51
+ }
52
+ //#endregion
53
+ //#region src/validation/validate.d.ts
54
+ /** Outcome of running validators against a value: every error found, in order. */
55
+ interface ValidationResult {
56
+ isValid: boolean;
57
+ errors: ValidationIssue[];
58
+ }
59
+ /** A single validation rule: takes the value, returns what it found. */
60
+ type Validator<T = unknown> = (value: T) => ValidationResult;
61
+ /** The passing result. */
62
+ declare const valid: () => ValidationResult;
63
+ /** A failing result carrying a single issue. */
64
+ declare const invalid: (path: string, message: string) => ValidationResult;
65
+ /**
66
+ * Combine validators into one run: applies them all (never short-circuits) and
67
+ * concatenates their errors in order. Curried so a rule set can be built once
68
+ * and applied to many values.
69
+ */
70
+ declare const validate: <T>(validators: ReadonlyArray<Validator<T>>) => (value: T) => ValidationResult;
71
+ /**
72
+ * Re-root a result's error paths under a parent path, e.g.
73
+ * `scoped("artworks[3]")` turns `label.side` into `artworks[3].label.side` —
74
+ * how per-element results are folded into a scene-level report.
75
+ */
76
+ declare const scoped: (path: string) => (result: ValidationResult) => ValidationResult;
77
+ /**
78
+ * Pass-through reporter: prints the errors to the console when the result is
79
+ * negative, and returns the result unchanged so it composes inside a pipeline.
80
+ */
81
+ declare const showValidation: (result: ValidationResult) => ValidationResult;
82
+ //#endregion
83
+ //#region src/validation/validators/message.d.ts
84
+ /** Error message: fixed text, or derived from the offending value. */
85
+ type Message = string | ((value: unknown) => string);
86
+ //#endregion
87
+ //#region src/validation/validators/existsProp.d.ts
88
+ /** The property must be present (truthy). Default message: `Missing <prop>.` */
89
+ declare const existsProp: (prop: string, message?: Message) => Validator;
90
+ //#endregion
91
+ //#region src/validation/validators/propIs.d.ts
92
+ /**
93
+ * The property must satisfy `test` **when present**; an absent property passes.
94
+ * Presence is a separate concern: require it with `existsProp` alongside.
95
+ */
96
+ declare const propIs: (prop: string, test: (value: unknown) => boolean, message: Message) => Validator;
97
+ //#endregion
98
+ //#region src/validation/validators/eachPropIs.d.ts
99
+ /**
100
+ * Every item of the array property must satisfy `test`; errors are addressed
101
+ * per index (`prop[i]`). A missing or non-array property passes — pair with a
102
+ * `propIs(prop, Array.isArray, …)` to report the shape itself.
103
+ */
104
+ declare const eachPropIs: (prop: string, test: (item: unknown) => boolean, message: Message) => Validator;
105
+ //#endregion
106
+ //#region src/validation/validators/when.d.ts
107
+ /** Apply validators only when `test` holds — for shapes hanging off a discriminant. */
108
+ declare const when: <T>(test: (value: T) => boolean, ...validators: Validator<T>[]) => Validator<T>;
109
+ //#endregion
110
+ //#region src/validation/validators/fail.d.ts
111
+ /** Always fails — pair with `when` to reject a branch (e.g. an unknown discriminant). */
112
+ declare const fail: (path: string, message: string) => Validator;
113
+ //#endregion
114
+ //#region src/validation/validators/nested.d.ts
115
+ /**
116
+ * Validate a nested object with its own rule set when present, re-rooting its
117
+ * errors under `prop` (e.g. `nested("texture", validateTexture)` reports
118
+ * `texture.src`). An absent property passes.
119
+ */
120
+ declare const nested: <S>(prop: string, validateSub: (sub: S) => ValidationResult) => Validator;
121
+ //#endregion
122
+ //#region src/validation/predicates/isString.d.ts
123
+ declare const isString: (value: unknown) => value is string;
124
+ //#endregion
125
+ //#region src/validation/predicates/isObject.d.ts
126
+ declare const isObject: (value: unknown) => value is Record<string, unknown>;
127
+ //#endregion
128
+ //#region src/validation/predicates/isFiniteNumber.d.ts
129
+ declare const isFiniteNumber: (value: unknown) => value is number;
130
+ //#endregion
131
+ //#region src/validation/predicates/isIntegerNumber.d.ts
132
+ declare const isIntegerNumber: (value: unknown) => value is number;
133
+ //#endregion
134
+ //#region src/validation/predicates/isPositiveNumber.d.ts
135
+ declare const isPositiveNumber: (value: unknown) => value is number;
136
+ //#endregion
137
+ //#region src/validation/predicates/isNonNegativeNumber.d.ts
138
+ declare const isNonNegativeNumber: (value: unknown) => value is number;
139
+ //#endregion
140
+ //#region src/validation/predicates/isOneOf.d.ts
141
+ /** Membership in a fixed set of allowed values (curried). */
142
+ declare const isOneOf: (values: ReadonlySet<unknown>) => (value: unknown) => boolean;
143
+ //#endregion
144
+ //#region src/validation/predicates/isNonEmptyString.d.ts
145
+ declare const isNonEmptyString: (value: unknown) => value is string;
146
+ //#endregion
147
+ //#region src/validation/predicates/isNumberInRange.d.ts
148
+ /** Finite number within `[min, max]`, bounds included (curried). */
149
+ declare const isNumberInRange: (min: number, max: number) => (value: unknown) => value is number;
150
+ //#endregion
151
+ //#region src/texture/validate.d.ts
152
+ /** Validate a texture; paths are relative (callers scope them, e.g. via `nested`). */
153
+ declare const validateTexture: (texture: SurfaceTexture) => ValidationResult;
154
+ //#endregion
155
+ //#region src/camera/camera.d.ts
156
+ /**
157
+ * Where the visitor's camera starts. The runtime camera is free-roaming from
158
+ * this pose; richer camera paths/tours are a future addition (data only).
159
+ */
160
+ interface CameraStart {
161
+ /** Initial camera position `[x, y, z]`. */
162
+ position: Vec3;
163
+ /** Initial yaw (Y-axis rotation) in radians. Defaults to `0`. */
164
+ yaw?: number;
165
+ }
166
+ interface Camera {
167
+ start: CameraStart;
168
+ /**
169
+ * Eye height above the ground (world units) while free-roaming over terrain.
170
+ * When omitted the runtime derives it from the start position's Y over the
171
+ * ground below it (re-derived once an async floor heightmap loads). Prefer
172
+ * setting it explicitly on heightmap floors for a predictable stature.
173
+ */
174
+ eyeHeight?: number;
175
+ /**
176
+ * Vertical field of view in degrees. Lower values reduce the perspective
177
+ * stretching at the edges of the frame. Defaults to `55`.
178
+ */
179
+ fov?: number;
180
+ }
181
+ //#endregion
182
+ //#region src/environment/environment.d.ts
183
+ /**
184
+ * An equirectangular HDRI used as the scene's sky and/or image-based lighting.
185
+ * Pure data: a URL plus what to use it for. The runtime loads the `.hdr`/`.exr`
186
+ * and wires it into the three.js scene background and environment.
187
+ */
188
+ interface Environment {
189
+ /** URL of an equirectangular HDRI (`.hdr` → RGBELoader, `.exr` → EXRLoader). */
190
+ src: string;
191
+ /** Show the HDRI as the visible sky background. Defaults to `true`. */
192
+ background?: boolean;
193
+ /** Use the HDRI for image-based lighting (ambient + reflections). Defaults to `true`. */
194
+ lighting?: boolean;
195
+ /** Brightness multiplier for the background sky. Defaults to `1`. */
196
+ intensity?: number;
197
+ }
198
+ //#endregion
199
+ //#region src/plugin/dataPlugin.d.ts
200
+ /**
201
+ * Cross-element queries available while a plugin validates its elements (e.g.
202
+ * an artwork checking that a wall edge exists). Carries the whole definition;
203
+ * richer, more focused queries can grow here without breaking plugins.
204
+ */
205
+ interface SceneContext {
206
+ scene: Scene;
207
+ }
208
+ /**
209
+ * The data half of a plugin: it owns one key of the scene definition and knows
210
+ * how to validate its value. Pure data layer — no three.js, importable by
211
+ * editors and backends. A plugin's runtime half (`EnginePlugin` in
212
+ * `@scenography/engine`) embeds one of these and adds the materialization.
213
+ *
214
+ * Plugins extend the `Scene` type itself via module augmentation:
215
+ *
216
+ * ```ts
217
+ * declare module "@scenography/scene" {
218
+ * interface Scene { water?: Water }
219
+ * }
220
+ * ```
221
+ */
222
+ interface DataPlugin<T = unknown> {
223
+ /** The scene key this plugin owns (e.g. `"water"`). */
224
+ kind: string;
225
+ /** Whether the key holds a single element or a collection of them (with ids). */
226
+ cardinality: "single" | "many";
227
+ /**
228
+ * Validate one element; paths relative to it (the scene validator scopes
229
+ * them). Method syntax on purpose: it keeps `DataPlugin<Water>` assignable
230
+ * to the `DataPlugin[]` a scene is validated with.
231
+ */
232
+ validate?(value: T, ctx: SceneContext): ValidationResult;
233
+ }
234
+ //#endregion
235
+ //#region src/scene/scene.d.ts
236
+ /**
237
+ * The complete, serializable description of a gallery space. This is the unit
238
+ * an editor produces, a database stores, and the network transports.
239
+ *
240
+ * The base scene holds only what the kernel owns; element kinds (walls,
241
+ * artworks, lights, sky…) are contributed by plugins, which augment this
242
+ * interface with their own optional key via `declare module "@scenography/scene"`.
243
+ */
244
+ interface Scene {
245
+ camera: Camera;
246
+ /** HDRI sky / image-based lighting for the space. */
247
+ environment?: Environment;
248
+ /**
249
+ * Walkable-area polygon as an XZ ring (`[x, z]` points). When set, the visitor
250
+ * can't step outside it — use it to contain them within an irregular space
251
+ * such as a cove. At least 3 points.
252
+ */
253
+ boundary?: Vec2[];
254
+ }
255
+ /**
256
+ * Define a gallery scene. Validates the definition and returns it unchanged
257
+ * (the identity of pure data), throwing on structural errors so authoring
258
+ * mistakes surface early. The returned value is plain, JSON-serializable data.
259
+ * Pass the data halves of the plugins the scene uses so their keys validate too.
260
+ */
261
+ declare function defineScene(scene: Scene, plugins?: DataPlugin[]): Scene;
262
+ /**
263
+ * Deep-clone a scene through JSON. Useful for the hot-reload path (snapshotting
264
+ * the previous scene before reconciling) and a guard that the definition really
265
+ * is serializable.
266
+ */
267
+ declare function cloneScene(scene: Scene): Scene;
268
+ //#endregion
269
+ //#region src/scene/validate.d.ts
270
+ /**
271
+ * Validate a scene definition. Returns every issue found (does not throw), so
272
+ * callers — editors, the runtime's hot-reload path — can surface them. Each
273
+ * element validates itself with relative paths; this aggregator re-roots them
274
+ * (`scoped`) into scene-level paths like `artworks[3].label.side`. Data
275
+ * plugins validate the scene keys they own the same way.
276
+ */
277
+ declare function validateScene(scene: Scene, plugins?: DataPlugin[]): ValidationIssue[];
278
+ //#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 };
280
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,280 @@
1
+ //#region src/vec2/vec2.d.ts
2
+ /** A 2D vector, e.g. a size `[width, height]` or a floor coordinate `[x, z]`. */
3
+ type Vec2 = [number, number];
4
+ /** Type guard: a finite-numeric `[x, y]` pair. */
5
+ declare const isVec2: (v: unknown) => v is Vec2;
6
+ //#endregion
7
+ //#region src/vec3/vec3.d.ts
8
+ /** A 3D vector `[x, y, z]` in world space. */
9
+ type Vec3 = [number, number, number];
10
+ /** Type guard: a finite-numeric `[x, y, z]` triple. */
11
+ declare const isVec3: (v: unknown) => v is Vec3;
12
+ //#endregion
13
+ //#region src/measure/measure.d.ts
14
+ /**
15
+ * A length given either as absolute world units (a `number`) or as a percentage
16
+ * string like `'50%'` (relative to whatever it measures along).
17
+ */
18
+ type Measure = number | `${number}%`;
19
+ /** Type guard: a finite number or a percentage string like `'50%'`. */
20
+ declare const isMeasure: (m: unknown) => m is Measure;
21
+ //#endregion
22
+ //#region src/texture/texture.d.ts
23
+ /**
24
+ * A material mapped onto a surface (a wall, a floor…). `src` is the color/albedo
25
+ * map; the rest are optional PBR maps. All share the same tiling (`repeat`). The
26
+ * engine picks the loader by file extension, so `.exr`/`.png`/`.jpg` all work.
27
+ * Point `roughness` and `ao` at the same packed ARM image to reuse a single file
28
+ * (its green channel drives roughness, its red channel ambient occlusion).
29
+ */
30
+ interface SurfaceTexture {
31
+ /** Color/albedo map (the visible surface; treated as sRGB). */
32
+ src: string;
33
+ /** How many times the maps repeat across the surface `[x, y]`. Defaults to `[1, 1]`. */
34
+ repeat?: Vec2;
35
+ /** Tangent-space normal map for surface relief (OpenGL convention). */
36
+ normal?: string;
37
+ /** Strength of the normal map relief. Defaults to `1`; raise for more depth. */
38
+ normalScale?: number;
39
+ /** Roughness map (grayscale, or the green channel of an ARM map). */
40
+ roughness?: string;
41
+ /** Ambient-occlusion map (grayscale, or the red channel of an ARM map). */
42
+ ao?: string;
43
+ }
44
+ //#endregion
45
+ //#region src/validation/issue.d.ts
46
+ /** A validation problem found in a scene definition. */
47
+ interface ValidationIssue {
48
+ /** Dotted path to the offending field, e.g. `artworks[1].location.edge`. */
49
+ path: string;
50
+ message: string;
51
+ }
52
+ //#endregion
53
+ //#region src/validation/validate.d.ts
54
+ /** Outcome of running validators against a value: every error found, in order. */
55
+ interface ValidationResult {
56
+ isValid: boolean;
57
+ errors: ValidationIssue[];
58
+ }
59
+ /** A single validation rule: takes the value, returns what it found. */
60
+ type Validator<T = unknown> = (value: T) => ValidationResult;
61
+ /** The passing result. */
62
+ declare const valid: () => ValidationResult;
63
+ /** A failing result carrying a single issue. */
64
+ declare const invalid: (path: string, message: string) => ValidationResult;
65
+ /**
66
+ * Combine validators into one run: applies them all (never short-circuits) and
67
+ * concatenates their errors in order. Curried so a rule set can be built once
68
+ * and applied to many values.
69
+ */
70
+ declare const validate: <T>(validators: ReadonlyArray<Validator<T>>) => (value: T) => ValidationResult;
71
+ /**
72
+ * Re-root a result's error paths under a parent path, e.g.
73
+ * `scoped("artworks[3]")` turns `label.side` into `artworks[3].label.side` —
74
+ * how per-element results are folded into a scene-level report.
75
+ */
76
+ declare const scoped: (path: string) => (result: ValidationResult) => ValidationResult;
77
+ /**
78
+ * Pass-through reporter: prints the errors to the console when the result is
79
+ * negative, and returns the result unchanged so it composes inside a pipeline.
80
+ */
81
+ declare const showValidation: (result: ValidationResult) => ValidationResult;
82
+ //#endregion
83
+ //#region src/validation/validators/message.d.ts
84
+ /** Error message: fixed text, or derived from the offending value. */
85
+ type Message = string | ((value: unknown) => string);
86
+ //#endregion
87
+ //#region src/validation/validators/existsProp.d.ts
88
+ /** The property must be present (truthy). Default message: `Missing <prop>.` */
89
+ declare const existsProp: (prop: string, message?: Message) => Validator;
90
+ //#endregion
91
+ //#region src/validation/validators/propIs.d.ts
92
+ /**
93
+ * The property must satisfy `test` **when present**; an absent property passes.
94
+ * Presence is a separate concern: require it with `existsProp` alongside.
95
+ */
96
+ declare const propIs: (prop: string, test: (value: unknown) => boolean, message: Message) => Validator;
97
+ //#endregion
98
+ //#region src/validation/validators/eachPropIs.d.ts
99
+ /**
100
+ * Every item of the array property must satisfy `test`; errors are addressed
101
+ * per index (`prop[i]`). A missing or non-array property passes — pair with a
102
+ * `propIs(prop, Array.isArray, …)` to report the shape itself.
103
+ */
104
+ declare const eachPropIs: (prop: string, test: (item: unknown) => boolean, message: Message) => Validator;
105
+ //#endregion
106
+ //#region src/validation/validators/when.d.ts
107
+ /** Apply validators only when `test` holds — for shapes hanging off a discriminant. */
108
+ declare const when: <T>(test: (value: T) => boolean, ...validators: Validator<T>[]) => Validator<T>;
109
+ //#endregion
110
+ //#region src/validation/validators/fail.d.ts
111
+ /** Always fails — pair with `when` to reject a branch (e.g. an unknown discriminant). */
112
+ declare const fail: (path: string, message: string) => Validator;
113
+ //#endregion
114
+ //#region src/validation/validators/nested.d.ts
115
+ /**
116
+ * Validate a nested object with its own rule set when present, re-rooting its
117
+ * errors under `prop` (e.g. `nested("texture", validateTexture)` reports
118
+ * `texture.src`). An absent property passes.
119
+ */
120
+ declare const nested: <S>(prop: string, validateSub: (sub: S) => ValidationResult) => Validator;
121
+ //#endregion
122
+ //#region src/validation/predicates/isString.d.ts
123
+ declare const isString: (value: unknown) => value is string;
124
+ //#endregion
125
+ //#region src/validation/predicates/isObject.d.ts
126
+ declare const isObject: (value: unknown) => value is Record<string, unknown>;
127
+ //#endregion
128
+ //#region src/validation/predicates/isFiniteNumber.d.ts
129
+ declare const isFiniteNumber: (value: unknown) => value is number;
130
+ //#endregion
131
+ //#region src/validation/predicates/isIntegerNumber.d.ts
132
+ declare const isIntegerNumber: (value: unknown) => value is number;
133
+ //#endregion
134
+ //#region src/validation/predicates/isPositiveNumber.d.ts
135
+ declare const isPositiveNumber: (value: unknown) => value is number;
136
+ //#endregion
137
+ //#region src/validation/predicates/isNonNegativeNumber.d.ts
138
+ declare const isNonNegativeNumber: (value: unknown) => value is number;
139
+ //#endregion
140
+ //#region src/validation/predicates/isOneOf.d.ts
141
+ /** Membership in a fixed set of allowed values (curried). */
142
+ declare const isOneOf: (values: ReadonlySet<unknown>) => (value: unknown) => boolean;
143
+ //#endregion
144
+ //#region src/validation/predicates/isNonEmptyString.d.ts
145
+ declare const isNonEmptyString: (value: unknown) => value is string;
146
+ //#endregion
147
+ //#region src/validation/predicates/isNumberInRange.d.ts
148
+ /** Finite number within `[min, max]`, bounds included (curried). */
149
+ declare const isNumberInRange: (min: number, max: number) => (value: unknown) => value is number;
150
+ //#endregion
151
+ //#region src/texture/validate.d.ts
152
+ /** Validate a texture; paths are relative (callers scope them, e.g. via `nested`). */
153
+ declare const validateTexture: (texture: SurfaceTexture) => ValidationResult;
154
+ //#endregion
155
+ //#region src/camera/camera.d.ts
156
+ /**
157
+ * Where the visitor's camera starts. The runtime camera is free-roaming from
158
+ * this pose; richer camera paths/tours are a future addition (data only).
159
+ */
160
+ interface CameraStart {
161
+ /** Initial camera position `[x, y, z]`. */
162
+ position: Vec3;
163
+ /** Initial yaw (Y-axis rotation) in radians. Defaults to `0`. */
164
+ yaw?: number;
165
+ }
166
+ interface Camera {
167
+ start: CameraStart;
168
+ /**
169
+ * Eye height above the ground (world units) while free-roaming over terrain.
170
+ * When omitted the runtime derives it from the start position's Y over the
171
+ * ground below it (re-derived once an async floor heightmap loads). Prefer
172
+ * setting it explicitly on heightmap floors for a predictable stature.
173
+ */
174
+ eyeHeight?: number;
175
+ /**
176
+ * Vertical field of view in degrees. Lower values reduce the perspective
177
+ * stretching at the edges of the frame. Defaults to `55`.
178
+ */
179
+ fov?: number;
180
+ }
181
+ //#endregion
182
+ //#region src/environment/environment.d.ts
183
+ /**
184
+ * An equirectangular HDRI used as the scene's sky and/or image-based lighting.
185
+ * Pure data: a URL plus what to use it for. The runtime loads the `.hdr`/`.exr`
186
+ * and wires it into the three.js scene background and environment.
187
+ */
188
+ interface Environment {
189
+ /** URL of an equirectangular HDRI (`.hdr` → RGBELoader, `.exr` → EXRLoader). */
190
+ src: string;
191
+ /** Show the HDRI as the visible sky background. Defaults to `true`. */
192
+ background?: boolean;
193
+ /** Use the HDRI for image-based lighting (ambient + reflections). Defaults to `true`. */
194
+ lighting?: boolean;
195
+ /** Brightness multiplier for the background sky. Defaults to `1`. */
196
+ intensity?: number;
197
+ }
198
+ //#endregion
199
+ //#region src/plugin/dataPlugin.d.ts
200
+ /**
201
+ * Cross-element queries available while a plugin validates its elements (e.g.
202
+ * an artwork checking that a wall edge exists). Carries the whole definition;
203
+ * richer, more focused queries can grow here without breaking plugins.
204
+ */
205
+ interface SceneContext {
206
+ scene: Scene;
207
+ }
208
+ /**
209
+ * The data half of a plugin: it owns one key of the scene definition and knows
210
+ * how to validate its value. Pure data layer — no three.js, importable by
211
+ * editors and backends. A plugin's runtime half (`EnginePlugin` in
212
+ * `@scenography/engine`) embeds one of these and adds the materialization.
213
+ *
214
+ * Plugins extend the `Scene` type itself via module augmentation:
215
+ *
216
+ * ```ts
217
+ * declare module "@scenography/scene" {
218
+ * interface Scene { water?: Water }
219
+ * }
220
+ * ```
221
+ */
222
+ interface DataPlugin<T = unknown> {
223
+ /** The scene key this plugin owns (e.g. `"water"`). */
224
+ kind: string;
225
+ /** Whether the key holds a single element or a collection of them (with ids). */
226
+ cardinality: "single" | "many";
227
+ /**
228
+ * Validate one element; paths relative to it (the scene validator scopes
229
+ * them). Method syntax on purpose: it keeps `DataPlugin<Water>` assignable
230
+ * to the `DataPlugin[]` a scene is validated with.
231
+ */
232
+ validate?(value: T, ctx: SceneContext): ValidationResult;
233
+ }
234
+ //#endregion
235
+ //#region src/scene/scene.d.ts
236
+ /**
237
+ * The complete, serializable description of a gallery space. This is the unit
238
+ * an editor produces, a database stores, and the network transports.
239
+ *
240
+ * The base scene holds only what the kernel owns; element kinds (walls,
241
+ * artworks, lights, sky…) are contributed by plugins, which augment this
242
+ * interface with their own optional key via `declare module "@scenography/scene"`.
243
+ */
244
+ interface Scene {
245
+ camera: Camera;
246
+ /** HDRI sky / image-based lighting for the space. */
247
+ environment?: Environment;
248
+ /**
249
+ * Walkable-area polygon as an XZ ring (`[x, z]` points). When set, the visitor
250
+ * can't step outside it — use it to contain them within an irregular space
251
+ * such as a cove. At least 3 points.
252
+ */
253
+ boundary?: Vec2[];
254
+ }
255
+ /**
256
+ * Define a gallery scene. Validates the definition and returns it unchanged
257
+ * (the identity of pure data), throwing on structural errors so authoring
258
+ * mistakes surface early. The returned value is plain, JSON-serializable data.
259
+ * Pass the data halves of the plugins the scene uses so their keys validate too.
260
+ */
261
+ declare function defineScene(scene: Scene, plugins?: DataPlugin[]): Scene;
262
+ /**
263
+ * Deep-clone a scene through JSON. Useful for the hot-reload path (snapshotting
264
+ * the previous scene before reconciling) and a guard that the definition really
265
+ * is serializable.
266
+ */
267
+ declare function cloneScene(scene: Scene): Scene;
268
+ //#endregion
269
+ //#region src/scene/validate.d.ts
270
+ /**
271
+ * Validate a scene definition. Returns every issue found (does not throw), so
272
+ * callers — editors, the runtime's hot-reload path — can surface them. Each
273
+ * element validates itself with relative paths; this aggregator re-roots them
274
+ * (`scoped`) into scene-level paths like `artworks[3].label.side`. Data
275
+ * plugins validate the scene keys they own the same way.
276
+ */
277
+ declare function validateScene(scene: Scene, plugins?: DataPlugin[]): ValidationIssue[];
278
+ //#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 };
280
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,250 @@
1
+ //#region src/vec2/vec2.ts
2
+ /** Type guard: a finite-numeric `[x, y]` pair. */
3
+ const isVec2 = (v) => Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === "number" && Number.isFinite(n));
4
+ //#endregion
5
+ //#region src/vec3/vec3.ts
6
+ /** Type guard: a finite-numeric `[x, y, z]` triple. */
7
+ const isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === "number" && Number.isFinite(n));
8
+ //#endregion
9
+ //#region src/measure/measure.ts
10
+ /** Type guard: a finite number or a percentage string like `'50%'`. */
11
+ const isMeasure = (m) => typeof m === "number" && Number.isFinite(m) || typeof m === "string" && /^-?\d+(\.\d+)?%$/.test(m);
12
+ //#endregion
13
+ //#region src/validation/issue.ts
14
+ /** Record a `duplicate id` issue for any id that appears more than once. */
15
+ const collectDuplicates = (ids, kind, issues) => {
16
+ const seen = /* @__PURE__ */ new Set();
17
+ for (const id of ids) {
18
+ if (seen.has(id)) issues.push({
19
+ path: kind,
20
+ message: `duplicate id "${id}".`
21
+ });
22
+ seen.add(id);
23
+ }
24
+ };
25
+ //#endregion
26
+ //#region src/validation/validate.ts
27
+ /** The passing result. */
28
+ const valid = () => ({
29
+ isValid: true,
30
+ errors: []
31
+ });
32
+ /** A failing result carrying a single issue. */
33
+ const invalid = (path, message) => ({
34
+ isValid: false,
35
+ errors: [{
36
+ path,
37
+ message
38
+ }]
39
+ });
40
+ /**
41
+ * Combine validators into one run: applies them all (never short-circuits) and
42
+ * concatenates their errors in order. Curried so a rule set can be built once
43
+ * and applied to many values.
44
+ */
45
+ const validate = (validators) => (value) => {
46
+ const errors = validators.flatMap((validator) => validator(value).errors);
47
+ return {
48
+ isValid: errors.length === 0,
49
+ errors
50
+ };
51
+ };
52
+ /**
53
+ * Re-root a result's error paths under a parent path, e.g.
54
+ * `scoped("artworks[3]")` turns `label.side` into `artworks[3].label.side` —
55
+ * how per-element results are folded into a scene-level report.
56
+ */
57
+ const scoped = (path) => (result) => result.isValid ? result : {
58
+ isValid: false,
59
+ errors: result.errors.map((error) => ({
60
+ ...error,
61
+ path: `${path}.${error.path}`
62
+ }))
63
+ };
64
+ /**
65
+ * Pass-through reporter: prints the errors to the console when the result is
66
+ * negative, and returns the result unchanged so it composes inside a pipeline.
67
+ */
68
+ const showValidation = (result) => {
69
+ if (!result.isValid) {
70
+ const detail = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
71
+ console.error(`Validation failed:\n${detail}`);
72
+ }
73
+ return result;
74
+ };
75
+ //#endregion
76
+ //#region src/obj/get.ts
77
+ /**
78
+ * Read a dotted property path (`"label.side"`, `"edges[0].name"`) off any
79
+ * value; `undefined` when any hop is missing or not an object.
80
+ */
81
+ const get = (value, path) => path.replace(/\[(\d+)\]/g, ".$1").split(".").reduce((current, key) => current !== null && typeof current === "object" ? current[key] : void 0, value);
82
+ //#endregion
83
+ //#region src/validation/validators/message.ts
84
+ const resolveMessage = (message, value) => typeof message === "function" ? message(value) : message;
85
+ //#endregion
86
+ //#region src/validation/validators/existsProp.ts
87
+ /** The property must be present (truthy). Default message: `Missing <prop>.` */
88
+ const existsProp = (prop, message = `Missing ${prop}.`) => (value) => {
89
+ const found = get(value, prop);
90
+ return found ? valid() : invalid(prop, resolveMessage(message, found));
91
+ };
92
+ //#endregion
93
+ //#region src/validation/validators/propIs.ts
94
+ /**
95
+ * The property must satisfy `test` **when present**; an absent property passes.
96
+ * Presence is a separate concern: require it with `existsProp` alongside.
97
+ */
98
+ const propIs = (prop, test, message) => (value) => {
99
+ const found = get(value, prop);
100
+ return found === void 0 || test(found) ? valid() : invalid(prop, resolveMessage(message, found));
101
+ };
102
+ //#endregion
103
+ //#region src/validation/validators/eachPropIs.ts
104
+ /**
105
+ * Every item of the array property must satisfy `test`; errors are addressed
106
+ * per index (`prop[i]`). A missing or non-array property passes — pair with a
107
+ * `propIs(prop, Array.isArray, …)` to report the shape itself.
108
+ */
109
+ const eachPropIs = (prop, test, message) => (value) => {
110
+ const items = get(value, prop);
111
+ if (!Array.isArray(items)) return valid();
112
+ const errors = items.flatMap((item, i) => test(item) ? [] : invalid(`${prop}[${i}]`, resolveMessage(message, item)).errors);
113
+ return {
114
+ isValid: errors.length === 0,
115
+ errors
116
+ };
117
+ };
118
+ //#endregion
119
+ //#region src/validation/validators/when.ts
120
+ /** Apply validators only when `test` holds — for shapes hanging off a discriminant. */
121
+ const when = (test, ...validators) => (value) => test(value) ? validate(validators)(value) : valid();
122
+ //#endregion
123
+ //#region src/validation/validators/fail.ts
124
+ /** Always fails — pair with `when` to reject a branch (e.g. an unknown discriminant). */
125
+ const fail = (path, message) => () => invalid(path, message);
126
+ //#endregion
127
+ //#region src/validation/validators/nested.ts
128
+ /**
129
+ * Validate a nested object with its own rule set when present, re-rooting its
130
+ * errors under `prop` (e.g. `nested("texture", validateTexture)` reports
131
+ * `texture.src`). An absent property passes.
132
+ */
133
+ const nested = (prop, validateSub) => (value) => {
134
+ const found = get(value, prop);
135
+ return found === void 0 ? valid() : scoped(prop)(validateSub(found));
136
+ };
137
+ //#endregion
138
+ //#region src/validation/predicates/isString.ts
139
+ const isString = (value) => typeof value === "string";
140
+ //#endregion
141
+ //#region src/validation/predicates/isObject.ts
142
+ const isObject = (value) => value !== null && typeof value === "object";
143
+ //#endregion
144
+ //#region src/validation/predicates/isFiniteNumber.ts
145
+ const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
146
+ //#endregion
147
+ //#region src/validation/predicates/isIntegerNumber.ts
148
+ const isIntegerNumber = (value) => Number.isInteger(value);
149
+ //#endregion
150
+ //#region src/validation/predicates/isPositiveNumber.ts
151
+ const isPositiveNumber = (value) => isFiniteNumber(value) && value > 0;
152
+ //#endregion
153
+ //#region src/validation/predicates/isNonNegativeNumber.ts
154
+ const isNonNegativeNumber = (value) => isFiniteNumber(value) && value >= 0;
155
+ //#endregion
156
+ //#region src/validation/predicates/isOneOf.ts
157
+ /** Membership in a fixed set of allowed values (curried). */
158
+ const isOneOf = (values) => (value) => values.has(value);
159
+ //#endregion
160
+ //#region src/validation/predicates/isNonEmptyString.ts
161
+ const isNonEmptyString = (value) => isString(value) && value.length > 0;
162
+ //#endregion
163
+ //#region src/validation/predicates/isNumberInRange.ts
164
+ /** Finite number within `[min, max]`, bounds included (curried). */
165
+ const isNumberInRange = (min, max) => (value) => isFiniteNumber(value) && value >= min && value <= max;
166
+ //#endregion
167
+ //#region src/texture/validate.ts
168
+ /** Validate a texture; paths are relative (callers scope them, e.g. via `nested`). */
169
+ const validateTexture = (texture) => validate([
170
+ existsProp("src", "texture is missing a src."),
171
+ propIs("repeat", isVec2, "repeat must be [x, y]."),
172
+ ...[
173
+ "normal",
174
+ "roughness",
175
+ "ao"
176
+ ].map((map) => propIs(map, isNonEmptyString, `${map} must be a non-empty URL.`))
177
+ ])(texture);
178
+ //#endregion
179
+ //#region src/camera/validate.ts
180
+ const validateCamera = (camera) => validate([
181
+ existsProp("start.position"),
182
+ propIs("start.position", isVec3, "start.position must be [x, y, z]."),
183
+ propIs("eyeHeight", isPositiveNumber, "eyeHeight must be a positive number."),
184
+ propIs("fov", (v) => isFiniteNumber(v) && v > 0 && v < 180, "fov must be between 0 and 180 (degrees).")
185
+ ])(camera);
186
+ //#endregion
187
+ //#region src/environment/validate.ts
188
+ const validateEnvironment = (env) => validate([existsProp("src", "Environment is missing a src."), propIs("intensity", isFiniteNumber, "intensity must be a finite number.")])(env);
189
+ //#endregion
190
+ //#region src/scene/validate.ts
191
+ /**
192
+ * Validate a scene definition. Returns every issue found (does not throw), so
193
+ * callers — editors, the runtime's hot-reload path — can surface them. Each
194
+ * element validates itself with relative paths; this aggregator re-roots them
195
+ * (`scoped`) into scene-level paths like `artworks[3].label.side`. Data
196
+ * plugins validate the scene keys they own the same way.
197
+ */
198
+ function validateScene(scene, plugins = []) {
199
+ const issues = [];
200
+ issues.push(...scoped("camera")(validateCamera(scene.camera)).errors);
201
+ if (scene.environment) issues.push(...scoped("environment")(validateEnvironment(scene.environment)).errors);
202
+ issues.push(...validate([propIs("boundary", (b) => Array.isArray(b) && b.length >= 3, "boundary must be a ring of at least 3 [x, z] points."), when((s) => Array.isArray(s.boundary) && s.boundary.length >= 3, eachPropIs("boundary", isVec2, "must be [x, z]."))])(scene).errors);
203
+ const ctx = { scene };
204
+ for (const plugin of plugins) {
205
+ const raw = scene[plugin.kind];
206
+ if (raw === void 0) continue;
207
+ if (plugin.cardinality === "single") {
208
+ if (plugin.validate) issues.push(...scoped(plugin.kind)(plugin.validate(raw, ctx)).errors);
209
+ continue;
210
+ }
211
+ if (!Array.isArray(raw)) {
212
+ issues.push({
213
+ path: plugin.kind,
214
+ message: "must be an array."
215
+ });
216
+ continue;
217
+ }
218
+ if (plugin.validate) raw.forEach((item, i) => issues.push(...scoped(`${plugin.kind}[${i}]`)(plugin.validate(item, ctx)).errors));
219
+ collectDuplicates(raw.map((item) => isObject(item) && isString(item.id) ? item.id : "").filter(Boolean), plugin.kind, issues);
220
+ }
221
+ return issues;
222
+ }
223
+ //#endregion
224
+ //#region src/scene/scene.ts
225
+ /**
226
+ * Define a gallery scene. Validates the definition and returns it unchanged
227
+ * (the identity of pure data), throwing on structural errors so authoring
228
+ * mistakes surface early. The returned value is plain, JSON-serializable data.
229
+ * Pass the data halves of the plugins the scene uses so their keys validate too.
230
+ */
231
+ function defineScene(scene, plugins) {
232
+ const issues = validateScene(scene, plugins);
233
+ if (issues.length > 0) {
234
+ const detail = issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n");
235
+ throw new Error(`Invalid scene definition:\n${detail}`);
236
+ }
237
+ return scene;
238
+ }
239
+ /**
240
+ * Deep-clone a scene through JSON. Useful for the hot-reload path (snapshotting
241
+ * the previous scene before reconciling) and a guard that the definition really
242
+ * is serializable.
243
+ */
244
+ function cloneScene(scene) {
245
+ return JSON.parse(JSON.stringify(scene));
246
+ }
247
+ //#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 };
249
+
250
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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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"}
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@scenography/scene",
3
+ "version": "0.2.0",
4
+ "description": "The scenography scene language: serializable data definitions for 3D galleries. Zero three.js.",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.mjs",
12
+ "types": "./dist/index.d.cts",
13
+ "exports": {
14
+ ".": {
15
+ "import": {
16
+ "types": "./dist/index.d.mts",
17
+ "default": "./dist/index.mjs"
18
+ },
19
+ "require": {
20
+ "types": "./dist/index.d.cts",
21
+ "default": "./dist/index.cjs"
22
+ }
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsdown",
27
+ "dev": "tsdown --watch",
28
+ "typecheck": "tsc --noEmit"
29
+ }
30
+ }