import type { Aabb, Footprint, Vec2 } from "../world/geometry.js"; /** * Wrap `degrees` into `[0, 360)` — the canonical heading range every policy and conversion here works * in. Non-finite input collapses to `0` so bad metadata never leaks a `NaN` heading downstream. * @capability asset-heading normalize a placement heading into the engine's degree convention */ export declare function normalizeDegrees(degrees: number): number; /** * Convert a compass heading (degrees, `0` = engine north, increasing clockwise) into the Three.js * Y-rotation (radians, wrapped to `(-PI, PI]`) a renderer applies. Sign matches * `world/placementController.quarterTurnsToRotationY`, so heading `90` equals one clockwise quarter * turn. This is the one place degrees cross into the private radian space Three.js requires. * @capability asset-heading turn a north-relative degree heading into a Three.js Y-rotation */ export declare function headingToRotationY(headingDegrees: number): number; /** * Inverse of {@link headingToRotationY}: recover the `[0, 360)` compass heading from a Three.js * Y-rotation. Lets a gizmo or inspector present radians as the degrees authors edit. * @capability asset-heading read a Three.js Y-rotation back as a north-relative degree heading */ export declare function rotationYToHeading(rotationY: number): number; /** Bridge the legacy 0–3 quarter-turn placement model onto degrees (each turn is 90 degrees clockwise). @internal */ export declare function quarterTurnsToHeading(quarterTurns: number): number; /** * A per-asset/per-tool rotation constraint, authored as data instead of hardcoded in build code: * `locked` pins every placement to one heading, `snap` quantizes to an increment (15, 45, …), and * `free` allows any heading. Serializable — this is the shape stored on a catalog entry or a tool. */ export type PlacementRotationPolicy = { readonly mode: "locked"; readonly degrees?: number; } | { readonly mode: "snap"; readonly snapDegrees: number; } | { readonly mode: "free"; }; /** * Quantize `degrees` to the nearest multiple of `increment` (both in the `[0, 360)` convention). A * non-positive increment is treated as free rotation and only normalizes. The math behind a * `snap`-mode {@link PlacementRotationPolicy}. * @capability placement-rotation snap a heading to a fixed degree increment */ export declare function snapHeading(degrees: number, increment: number): number; /** * Resolve a raw drag/scroll heading through a {@link PlacementRotationPolicy} into the heading a * placement should actually commit: `locked` ignores the input for its fixed heading, `snap` quantizes, * `free` (or an absent policy) normalizes. Deterministic — the single interpreter preview, editor * placement, and runtime spawning share instead of each re-implementing the modes. * @capability placement-rotation apply a rotation policy to a candidate placement heading */ export declare function applyRotationPolicy(degrees: number, policy: PlacementRotationPolicy | undefined): number; /** * Where a model's pivot sits within its footprint: `center` (origin at the footprint center), * `corner` (origin at the min-X/min-Z corner), or a normalized `{ x, z }` fraction in `[-0.5, 0.5]`. */ export type AssetAnchor = "center" | "corner" | { readonly x: number; readonly z: number; }; /** * Catalog-level asset-space metadata: how a model is authored relative to the engine, owned by the * catalog entry rather than re-derived per game. Every field is optional so an unmeasured asset stays * valid; {@link resolveFacingRotationY}/{@link assetUnitScale}/{@link resolveAnchorOffset} supply the * documented defaults. */ export interface AssetSpace { /** Compass heading (degrees, `0` = north) the model's front visually points at `rotationY = 0`. Default `0`. */ forwardDegrees?: number; /** Multiply source-space lengths by this to reach engine meters (a kit's native tile size correction). Default `1`. */ unitScale?: number; /** Authored footprint in engine meters — the grid/placement extent, independent of the raw mesh bounds. */ footprint?: Footprint; /** Authored grid cell size in engine meters this asset snaps to. */ gridSize?: number; /** Where the model origin sits within {@link footprint}. Default `center`. */ anchor?: AssetAnchor; /** Optional asset-local placement bounds (XZ) a tool clamps a drop position into. */ bounds?: Aabb; /** Default rotation constraint when this asset is placed. Absent means free rotation. */ rotation?: PlacementRotationPolicy; } /** * The Three.js Y-rotation that makes a model whose front is authored at `forwardDegrees` visually point * `headingDegrees` (engine north = `0`). This is the catalog-owned replacement for per-game corrective * yaw: a model authored facing south (`forwardDegrees: 180`) placed toward north resolves to `Math.PI`. * @capability asset-facing resolve a placement heading into a Three.js Y-rotation using canonical forward */ export declare function resolveFacingRotationY(headingDegrees: number, space?: Pick): number; /** The effective, guarded unit scale for an asset — a finite positive `unitScale`, else `1`. @internal */ export declare function assetUnitScale(space?: Pick): number; /** * Convert a source-space length to engine meters through the asset's {@link AssetSpace.unitScale} — the * data-owned replacement for per-game scale constants (a native ~4-unit kit tile down to a 1-unit grid). * @capability asset-units convert a source-space length to engine meters via catalog unit scale */ export declare function toEngineUnits(sourceValue: number, space?: Pick): number; /** * The pivot's offset from the footprint center, in engine meters, implied by {@link AssetSpace.anchor} * and {@link AssetSpace.footprint}. `center` yields `[0, 0]`; `corner` yields the min corner; a * normalized `{ x, z }` scales by the footprint. Subtract it to seat a footprint centered on a point. * @capability asset-anchor resolve a model's pivot offset from its footprint center */ export declare function resolveAnchorOffset(space?: Pick): Vec2; /** * The axis-aligned footprint extent after rotating a rectangular {@link Footprint} by `headingDegrees` * — footprint-aware placement: a 90-degree turn swaps width/depth, and arbitrary headings grow the * bounding box so overlap and snap checks see the space the rotated asset truly occupies. * @capability asset-footprint rotate a rectangular footprint into its axis-aligned extent */ export declare function rotatedFootprint(footprint: Footprint, headingDegrees: number): Footprint; /** One problem found by {@link validateAssetSpace}: which metadata field and why it is invalid. */ export interface AssetSpaceIssue { field: string; message: string; } /** * Report contradictory or malformed asset-space metadata so catalog generation can reject it before it * reaches a tool: non-finite headings, non-positive scales/sizes, inverted bounds, unknown anchors, and * ill-formed rotation policies (a `snap` with a non-positive increment, an unknown `mode`). An empty * array means valid. Validation lives here so tools consume resolved metadata instead of reinterpreting. * @capability asset-space validate catalog asset-space metadata during index generation */ export declare function validateAssetSpace(raw: unknown): AssetSpaceIssue[]; /** * Parse an untrusted rotation policy (from a scene document or catalog JSON) into a valid * {@link PlacementRotationPolicy}, defaulting to `free` when absent or malformed. The lenient companion * to {@link validateAssetSpace} for the read path. * @capability placement-rotation parse a serialized rotation policy, defaulting to free */ export declare function parseRotationPolicy(raw: unknown): PlacementRotationPolicy; /** * Parse untrusted JSON into an {@link AssetSpace}, dropping malformed fields, or `undefined` when * nothing valid remains — the round-trippable read path for serialized catalog/scene metadata. * @capability asset-space parse serialized asset-space metadata into a validated shape */ export declare function parseAssetSpace(raw: unknown): AssetSpace | undefined;