# M2 Placement Grouper

## Metadata

| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-PLACEMENT-GROUPER-001` |
| Owners | Pure ADT M2 placement validation, normalization and transform grouping |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-grouper`, 2026-07-16 |
| Profiles/capabilities | Existing static M2 worker-grouping path |

## Purpose

Convert valid raw ADT M2 placement records into ordered tile-local transforms
grouped by the historically normalized relative model path.

## Non-goals

- Reserve cross-tile unique IDs or choose placement ownership.
- Own WorkerThreadPool tasks, mutexes, result queues or tile lifecycle.
- Load models, choose animated/static rendering or build MultiMesh batches.
- Create Node, Mesh, material, Resource or RID objects.
- Change path case, transform rules, batching or visual fidelity.

## Context and boundaries

```mermaid
flowchart LR
    ADT[ADT names + placements] --> Registry[M2UniquePlacementRegistry]
    Registry --> Grouper[M2PlacementGrouper]
    Grouper --> Resolver[M2PlacementTransformResolver]
    Resolver --> Grouper
    Grouper --> Groups[Path to ordered Transform3D arrays]
    Groups --> Worker[Loader worker result adapter]
    Worker --> Build[Loader-owned build queue]
```

Allowed dependencies are value containers, `Vector3`, `Basis`, `Transform3D`
and `M2PlacementTransformResolver`. SceneTree, Node, RenderingServer,
WorkerThreadPool, mutexes, ResourceLoader, files, gameplay, network and editor UI
are forbidden.

## Public API

| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `group_placements(tile_origin, m2_names, m2_placements)` | Pure query | Return valid placements grouped as ordered tile-local transforms | Worker/main thread; call-local output | Invalid variants/name IDs/empty paths are skipped |

## Inputs and outputs

| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Godot-space tile origin | Loader tile state | Local-position calculation | Value copy | One call |
| Input | ADT MMDX name table | Parsed ADT tile | Path lookup | Read-only PackedStringArray | One call |
| Input | Raw placement dictionaries | Unique registry result | Validation/transforms | Read-only shallow values | One call |
| Output | Relative path to ordered `Transform3D` arrays | Grouper | Loader worker/build adapter | Fresh Dictionary and arrays | One call/build job |

Output retains first-seen group-key order through Godot Dictionary insertion
order and placement order inside each array. Inputs are not retained or returned.

## Data flow

```mermaid
flowchart TD
    Call[group_placements] --> Next[Read placement]
    Next --> Dict{Dictionary?}
    Dict -->|no| Next
    Dict -->|yes| Name{Valid name_id?}
    Name -->|no| Next
    Name -->|yes| Normalize[Backslash to slash; lowercase MDX/MDL to M2]
    Normalize --> Empty{Path empty?}
    Empty -->|yes| Next
    Empty -->|no| Local[world position - tile origin]
    Local --> Resolve[Resolve basis and origin offset]
    Resolve --> Scale[Apply max scale 0.0001]
    Scale --> Append[Append Transform3D to path group]
    Append --> Next
    Next -->|complete| Return[Return fresh groups]
```

## Lifecycle/state

The grouper retains no state. Every call constructs one stateless transform
resolver and fresh output containers. Reset, cancellation and shutdown require
no grouper action.

## Main sequence

```mermaid
sequenceDiagram
    participant Loader as Loader worker callback
    participant Grouper as M2PlacementGrouper
    participant Resolver as M2PlacementTransformResolver
    Loader->>Grouper: group_placements(origin, names, placements)
    loop valid placement
        Grouper->>Resolver: resolve basis and origin offset
        Resolver-->>Grouper: value-only basis and offset
        Grouper->>Grouper: append local Transform3D
    end
    Grouper-->>Loader: fresh path groups
    Loader->>Loader: mutex-protected result enqueue
```

## Ownership, threading and resources

- The grouper owns only call-local containers and transforms.
- The loader owns tasks, mutex/result queue, tile/build state, caches, model
  loading, Nodes, MultiMesh, Mesh, materials and RIDs.
- Worker use is safe because the grouper and resolver mutate no shared state.
- The caller owns returned containers; mutation cannot affect later calls.

## Errors, cancellation and recovery

| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Non-Dictionary placement | Type guard | Skip | Contract fixture | Repair parser input |
| Invalid name ID | Bounds guard | Skip | Contract fixture | Repair name table/placement |
| Empty path | Empty guard | Skip | Contract fixture | Supply valid MMDX entry |
| Missing position/rotation/scale | Defaults | Zero position/rotation and scale 1 | Contract fixture | Expected fallback |
| Zero/negative scale | Historical clamp | Basis scale uses `0.0001` | Contract fixture | Supply valid ADT scale |
| Cancellation after grouping | Loader state check | Discard stale result | Loader regressions | Re-request eligible detail |

Cancellation stays loader-owned because grouping has no task or partial state.

## Configuration and capabilities

No setting or profile branch is introduced. Preserved behavior includes
backslash replacement without lowercasing, case-sensitive lowercase `.mdx/.mdl`
conversion, first-seen ordering, tile-local subtraction and scale clamp `0.0001`.

## Persistence, cache and migration

No persistence or cache format participates. ADT and M2 cache versions are
unchanged, so no rebuild or migration is required.

## Diagnostics and observability

The service emits no logs. Its verifier records contract results, source
boundaries and bounded synthetic 100-by-256 placement timing. Loader metrics
continue to own task/build/cache observability.

## Verification

- `verify_m2_placement_grouper.gd`: invalid input, path behavior, ordering,
  defaults, local origin, regular/waterfall transforms, scale clamp, fresh output,
  loader/source boundary and timing.
- `verify_m2_placement_transform_resolver.gd`: three transform consumers remain
  covered as two direct loader adapters plus the grouper adapter.
- Registry/dedupe, facade, internal-access, manifest, shutdown, documentation and
  coordination regressions remain required.
- Fidelity evidence is exact extraction; no new asset-backed parity claim is made.

## Extension points

- `M2BuildBatchPlanner` now owns pure batch sizing/cursor progression; the
  remaining build-job state machine stays behind explicit loader budgets.
- Spatial cells require measured culling evidence and are outside this contract.

## Capability status

| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Pure path/transform grouping | Implemented extraction | Contract/source/timing verifier | Asset-backed traversal timing pending |
| Worker/build orchestration | Remains in loader | Existing regressions | Separate stateful extraction required |
| Spatial MultiMesh cells | Partial | Current path batches | Culling-driven design/evidence pending |

## Known gaps and risks

- Raw placement dictionaries remain the ADT parser boundary.
- Uppercase extensions remain unchanged even though transform exception matching
  is case-insensitive.
- Grouping is by model path, not spatial culling cell.
- Asset-backed p95/p99 and visual comparison remain unavailable.

## Source map

| Path | Responsibility |
|---|---|
| `src/render/m2/m2_placement_grouper.gd` | Validation, normalization and grouping |
| `src/render/m2/m2_placement_transform_resolver.gd` | Basis and origin rules |
| `src/render/m2/m2_build_batch_planner.gd` | Downstream batch sizing and cursor planning |
| `src/scenes/streaming/streaming_world_loader.gd` | Worker/result/build lifecycle and resources |
| `src/tools/verify_m2_placement_grouper.gd` | Contract, dependency and timing regression |

## Related decisions and references

- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md)
- [`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)
