# M2 Placement Transform Resolver ## Metadata | Field | Value | |---|---| | Status | Implemented | | Target/work package | M03 / `M03-RND-M2-PLACEMENT-TRANSFORM-001` | | Owners | Pure ADT M2 placement basis and model-specific origin compensation | | Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-transform`, 2026-07-16 | | Profiles/capabilities | Existing static M2 paths; four calibrated model exceptions | ## Purpose Resolve the unscaled Godot-space orientation and optional local origin offset used by ADT M2 worker grouping, placeholder rendering and instance creation. ## Non-goals - Convert WoW/server/ADT coordinates or own placement positions. - Apply final scale clamps, build transforms, group placements or create Nodes. - Add, generalize or configure model-specific corrections. - Change M2 materials, animation, caches, visibility or quality profiles. - Claim general build-12340 placement parity. ## Context and boundaries ```mermaid flowchart LR ADT[ADT placement rotation/path/scale] --> Resolver[M2PlacementTransformResolver] Resolver --> Basis[Unscaled Basis] Resolver --> Offset[Local origin offset] Basis --> Group[Worker grouping] Basis --> Placeholder[Placeholder MultiMesh path] Basis --> Instance[Instance path] Offset --> Group Offset --> Placeholder Offset --> Instance Group --> Loader[StreamingWorldLoader-owned queues/build] ``` Allowed dependencies are `Vector3`, `Basis`, scalar math and path-string normalization. Node, SceneTree, RenderingServer, ResourceLoader, tasks, caches, files, gameplay, network and editor UI are forbidden. ## Public API | Symbol | Kind | Purpose | Thread/lifetime | Errors | |---|---|---|---|---| | `resolve_basis(rotation_radians, relative_m2_path)` | Pure query | Return unscaled regular or calibrated placement orientation | Worker/main thread; call-local result | Unknown paths use `Basis.from_euler` | | `resolve_origin_offset(rotation_radians, relative_m2_path, scale_value)` | Pure query | Compensate the measured tall-waterfall twist anchor | Worker/main thread; call-local result | Regular/unanchored paths return zero; scale clamps to `0.0001` for compensation | ## Inputs and outputs | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | |---|---|---|---|---|---| | Input | Godot Euler rotation in radians | ADT loader placement record | Resolver | Scalar copy | One call | | Input | Relative M2 path | ADT MMDX/name table | Resolver allowlist | Caller string | One call | | Input | Raw placement scale | ADT placement record | Offset compensation | Scalar copy | One call | | Output | Unscaled `Basis` | Resolver | Group/placeholder/instance adapter | Value copy | One call | | Output | Local `Vector3` offset | Resolver | Transform origin adapter | Value copy | One call | Callers preserve their historical basis scale minima: grouping/instance use `0.0001`, while placeholders use `0.01`. ## Data flow ```mermaid flowchart TD BasisCall[resolve_basis] --> ModelName[Normalize separators/case/basename] ModelName --> Waterfall{Waterfall sheet?} Waterfall -->|yes| WaterfallBasis[+90 degree world yaw * Euler * local-axis twist] Waterfall -->|no| Cliff{Elwynn cliff rock?} Cliff -->|yes| CliffBasis[Corrected Y/BACK/RIGHT composition] Cliff -->|no| Regular[Basis.from_euler] OffsetCall[resolve_origin_offset] --> Anchored{Tall waterfall anchor?} Anchored -->|no| Zero[Vector3.ZERO] Anchored -->|yes| Compensation[(base anchor - twisted anchor) * clamped scale] ``` ## Lifecycle/state The resolver is stateless. It may be shared between the main thread and worker calls because each result is computed solely from value inputs. Construction, world reset and shutdown require no clear, cancellation or resource release. ## Main sequence ```mermaid sequenceDiagram participant Loader as StreamingWorldLoader participant Resolver as M2PlacementTransformResolver participant Consumer as Group/placeholder/instance path Loader->>Resolver: resolve_basis(rotation, path) Resolver-->>Loader: unscaled Basis Loader->>Resolver: resolve_origin_offset(rotation, path, scale) Resolver-->>Loader: local offset Loader->>Consumer: compose historical scaled Transform3D ``` ## Ownership, threading and resources - The resolver retains no mutable state or engine resources. - The loader owns placement records, final transforms, tasks, queues, caches, MultiMesh/Node/Mesh/material/RID creation and cleanup. - Pure methods are safe for current worker grouping and main-thread consumers. - Returned `Basis`/`Vector3` values have no lifetime coupling to the resolver. ## Errors, cancellation and recovery | Failure | Detection | Behavior | Diagnostic | Recovery | |---|---|---|---|---| | Empty/unknown path | Allowlist miss | Regular Euler basis and zero offset | Contract fixture | Expected fallback | | Mixed case/backslashes/MDX | Normalization | Same calibrated model match | Contract fixture | None required | | Zero/negative scale for anchored offset | Numeric clamp | Use `0.0001` compensation scale | Contract fixture | Caller may supply valid ADT scale later | | World reset/shutdown | No retained state | No action | Existing shutdown verifier | New calls remain independent | There is no asynchronous operation or cancellation inside the resolver. ## Configuration and capabilities The resolver introduces no settings. The preserved constants and allowlists are: - world yaw `PI` for `ElwynnCliffRock01/02`; - world yaw `PI/2` for `NewWaterfall` and `ElwynnTallWaterfall01`; - measured local axes/twist signs for both waterfall sheets; - measured tall-waterfall anchor `(-2.667799, 89.62273, 0.00129)`. ## Persistence, cache and migration No persistence, schema or cache format participates. Existing ADT/M2 cache versions and rebuild instructions remain unchanged. ## Diagnostics and observability The resolver emits no logs. The contract verifier compares formulas/constants, checks all three loader consumers and records bounded synthetic timing. Visual and asset-backed evidence remains in the renderer checkpoint workflow. ## Verification - `verify_m2_placement_transform_resolver.gd`: regular fallback, path normalization, both cliff rocks, both waterfalls, twist anchor, negative-scale compensation, three loader adapters, source boundary and 10,000-pair timing. - M2 unique/dedupe, terrain, facade, internal-access, manifest, shutdown, scheduler, streaming and coordinate regressions remain required. - Fidelity evidence is exact extraction of existing calibrated formulas. The private asset/camera comparison that motivated them is not reproduced here. ## Extension points - The next package may compose this resolver into a pure placement grouper. - New model corrections require separate measured fidelity evidence; this module is not a generic override registry. ## Capability status | Capability | Status | Evidence | Gap/next step | |---|---|---|---| | Consistent transform rules across three M2 paths | Implemented | Formula/source/timing contract | Asset-backed visual recheck pending | | General ADT M2 placement parity | Partial | Existing checkpoint notes | Broader fixture coverage required | ## Known gaps and risks - Four model-specific exceptions are empirical compatibility rules. - Most placements use raw Godot Euler behavior; independent client-wide transform validation is incomplete. - Callers intentionally retain different final scale clamps. - Grouping, build, cache and render ownership remain in the streamer. ## Source map | Path | Responsibility | |---|---| | `src/render/m2/m2_placement_transform_resolver.gd` | Pure basis/offset rules and calibrated allowlists | | `src/scenes/streaming/streaming_world_loader.gd` | Three adapters and final Transform3D/resource ownership | | `src/tools/verify_m2_placement_transform_resolver.gd` | Formula, source and performance regression | ## Related decisions and references - [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md) - [`world-renderer.md`](world-renderer.md) - [`../../RENDER.md`](../../RENDER.md) - [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)