import { Object3D } from "three"; import { type NeedleAddress } from "@needle-tools/addressing"; import { Context } from "../engine_setup.js"; import type { Model } from "../engine_types.js"; /** * Needle project document — evaluator for the composition format * (needle-editor plans/project-format-v0.md). * * A project document is COMPOSITION ONLY: it references glTF assets, places them, * adds components (defs) and patches properties (overs) on the loaded content. * Content itself always lives in glTF. Evaluation is ONE-SHOT and happens before * the component lifecycle starts: loaded components exist dormant until the engine's * pre-setup phase, so `active`/`enabled` opinions apply before anything ever executes. */ /** the version discriminator key every project document must carry */ export declare const PROJECT_DOCUMENT_KEY = "needle:project"; export type ProjectDocumentAsset = { /** where the asset lives — resolved against `EvaluateProjectDocumentOptions.baseUrl` */ src: string; /** * Optional PROVIDER IDENTITY for assets whose resources (textures, buffers) live at * provider-specific urls that `src` alone can't reach (e.g. PolyHaven). The host recovers * the provider's resolver from this identity at load time (see * `EvaluateProjectDocumentOptions.resolveAssetUrl`). Self-contained assets omit it and * load by `src`. The engine ignores it; only the host (editor) reads it. */ asset?: { id: string; provider?: string; }; /** * Optional SEMANTIC KIND, decided at USE time by the writer ("model", "material", * "hdr", "texture", "image", "video", "audio", "code", "text") — the same file can * be a texture on a material slot or a plain image in UI, and downstream * processing (compression at publish) follows the use, not the extension. The * engine ignores it; only hosts/tooling read it. */ kind?: string; }; export type ProjectDocumentReference = { /** project-local id of this reference — the resolution scope (or entity handle) for targets */ id: string; /** key into the document's `assets` map */ asset: string; /** * ELEMENT REFERENCE discriminator: when present, this reference instantiates the * addressed element INSIDE the asset (e.g. a material) into the composition's ENTITY * POOL instead of placing a scene subtree. Entity references are first-class composed * entities: overs can target them (`{ ref: , path: "map/repeat" }`) and values can * point at them (`{ ref: }` — e.g. a mesh's material assignment). N consumers of * the same entity share ONE runtime instance by construction (USD: a Material-prim * reference; assignment ≈ material:binding). */ address?: NeedleAddress; /** mount the placed root under a node of another reference; scene references only. * NOTE: placement TRS is NOT a reference field — it is ordinary overs on the root * (`{ ref, path: "position" }`), like every other property (USD: xformOps on the * referencing prim are plain authored attributes). One mechanism, no precedence * ambiguity between a transform field and a position over. */ parent?: { ref: string; address: NeedleAddress; }; }; export type ProjectDocumentTarget = { /** which placed reference the address resolves WITHIN; omitted → the composition root */ ref?: string; /** fat address of the element inside the reference (needle-id / anchor / namePath) */ address?: NeedleAddress; /** * Property path within the resolved element, "/"-separated. * * A segment landing on a COLLECTION may be a `:` qualifier instead of an * index — `components/type:DragControls/enabled`, `components/guid:6f1f…/enabled` — * so a document never depends on array order (`userData.components` reorders whenever * a component is added or removed). A bare numeric segment on an array is still an * index, but it is now an explicit opt-in rather than the only option. * `components` is a synthesized segment on a node: the live layout * (`userData.components`) stays an implementation detail. * * Reserved LAST segment: `active` — with `value: false` it removes whatever the rest * of the path resolved to (the node itself, or a component) from the composed result. */ path?: string; }; export type ProjectDocumentDef = { target: ProjectDocumentTarget; /** * When present, the def CREATES a new node (USD-proper: def defines prims) under the * resolved target (composition root when the target is empty) and the components * apply to IT — e.g. the runtime start camera. Without `create`, components apply to * the resolved existing node. */ create?: { name?: string; position?: number[]; quaternion?: number[]; scale?: number[]; }; /** VERBATIM NEEDLE_components wire entries ({ name, guid, ...fields }) */ components: Array<{ name: string; } & Record>; }; export type ProjectDocumentOver = { target: ProjectDocumentTarget; value: unknown; }; export type ProjectDocument = { [PROJECT_DOCUMENT_KEY]: string; /** layer metadata (USD's "layer metadata" concept — e.g. `name`); free-form */ metadata?: Record; assets?: Record; references?: ProjectDocumentReference[]; defs?: ProjectDocumentDef[]; overs?: ProjectDocumentOver[]; /** unknown arcs are ignored by evaluation and must be preserved by writers */ [arc: string]: unknown; }; /** structural check for the version discriminator — the opener/sniffing entry point */ export declare function isProjectDocument(value: unknown): value is ProjectDocument; export type ProjectEvaluationIssue = { arc: "reference" | "def" | "over"; message: string; }; export type ProjectEvaluationResult = { /** the object all non-mounted references were added to */ root: Object3D; /** placed reference roots by reference id. * POPULATED AT `completed`: references are INSTANTIATED from a parse-once template * in the engine's pre-setup phase (after the template's component deserialization), * so the map is empty until the context ticked once — await `completed` first. */ references: Map; /** per-reference Model views by reference id (`scene` is that reference's placed * INSTANCE; parser etc. come from the shared template). Populated at `completed`, * like `references`. */ models: Map; /** * misses and errors — an issue never aborts evaluation, the arc is skipped. * Populated up to `completed` (defs/overs apply in the engine's pre-setup phase). */ issues: ProjectEvaluationIssue[]; /** * Resolves once defs/overs have been applied. They run in the engine's pre-setup * phase (AFTER the assets' own component deserialization, BEFORE awake) — the * context must tick once (held or running) for this to happen. */ completed: Promise; }; export type EvaluateProjectDocumentOptions = { /** where references are placed; defaults to `context.scene` */ parent?: Object3D; /** resolves `assets.src` for the document — tests/tools provide in-memory data here */ resolveAsset?: (assetId: string, src: string) => Promise; /** base for resolving relative `assets.src` urls with the default (fetch) resolver */ baseUrl?: string; /** * Rewrites a resource url (texture / external buffer) encountered while parsing an * asset — the asset service's `resolveUrl` re-entering at load time. Needed for provider * assets (e.g. PolyHaven) whose textures live at urls that plain relative resolution * against `assets.src` cannot reach. Applied on an isolated per-parse manager (see * {@link parseSync}); the host (editor opener) supplies it from `IAssetsService` so this * evaluator carries NO provider knowledge. */ resolveAssetUrl?: (assetId: string, url: string) => string; /** * AGGREGATE download progress across the document's assets: `loaded` bytes summed * over all asset downloads, `total` summed over the known Content-Lengths (0 while * nothing reported a length). Fired per received chunk — the loader chain adapts it * to the ProgressEvent shape the loading UI expects. */ onProgress?: (loaded: number, total: number) => void; }; /** * Evaluate a project document into the given context. One-shot; never throws for * content issues (they are collected in the result), only for a non-document input. * * Order (plans/project-format-v0.md): load + place references (document order) → * mounts → [pre-setup phase:] asset component deserialization (the engine's own * queue, FIFO) → defs → overs. Resolution is scoped to the target reference's * subtree; misses are reported and skipped (exact resolution only in v0). */ export declare function evaluateProjectDocument(context: Context, document: ProjectDocument, options?: EvaluateProjectDocumentOptions): Promise; export declare function splitPath(path: string): string[];