Section 01 hasn't been written yet. That's fine -- this section is independent per the dependency graph.

Now I have everything needed. Let me compose the section content.

# Section 02: GDScript Handler -- `_handle_build_scene`

## Overview

This section adds the GDScript-side handler for the `build_scene` WebSocket method to the Godot editor addon. When the Python MCP tool sends a `build_scene` request over WebSocket, the addon receives it in `_dispatch()`, expands high-level patterns (grid, line, scatter, nodes) into flat node specs, creates all nodes in a single undo/redo action, and returns a summary response.

All code changes are in a single file: `/Users/salvatorefarruggio/Desktop/godotiq/godot-addon/addons/godotiq/godotiq_server.gd`.

## Dependencies

- **Section 01 (Python-side tool)**: Sends the WebSocket `build_scene` request that this handler receives. The two sections are parallelizable -- they communicate via a well-defined JSON protocol over WebSocket.
- No other sections are required.

## Background: Existing Patterns

The handler must follow the same conventions already used by `_handle_node_ops` in `godotiq_server.gd`:

- **Scene root check**: Call `EditorInterface.get_edited_scene_root()`. If null, send error `"NO_SCENE"`.
- **Undo/redo manager check**: If `undo_redo` is null, send error `"NO_UNDO_REDO"`.
- **Action lifecycle**: `undo_redo.create_action(name)`, register do/undo methods, then `undo_redo.commit_action()` only if at least one operation succeeded.
- **Action history**: Append to `_godotiq_action_history` after commit, trim to `MAX_HISTORY_SIZE`.
- **Node lookup**: Use `_find_node_by_name_or_path(name_or_path, scene_root)` to resolve parent nodes.
- **Node creation (deferred undo/redo)**: The pattern from `_op_add_child`:
  - `ur.add_do_method(parent, "add_child", node)`
  - `ur.add_do_method(self, "_set_owner_recursive", node, scene_root)`
  - `ur.add_do_reference(node)`
  - `ur.add_undo_method(parent, "remove_child", node)`
  - Then set position/rotation/scale via `ur.add_do_property(node, prop_name, value)`.
- **Response format**: Call `send_response(peer_id, id, result_dict)` for success, `_send_error(peer_id, id, code, message)` for fatal errors.

## Tests (Manual/E2E)

GDScript tests require a running Godot editor with the addon loaded. These are documented for manual testing or future E2E integration. They are not automated pytest tests.

### Grid expansion tests

- Grid 3x3 with spacing 2.0 creates 9 nodes at correct positions.
- Grid with override at "1,1" uses override scene and name.
- Grid with override that has custom position uses that position, not calculated.
- Grid axis="xy" places nodes in XY plane instead of XZ.
- Grid with origin=[5,0,5] offsets all positions.

### Line expansion tests

- Line with 2 points, spacing=1.0, creates correct number of nodes.
- Line endpoint is included (node at last point).
- Line endpoint epsilon check: if last node is very close to endpoint, no duplicate.
- Line align_to_path rotates nodes to face direction.
- Line multi-segment carries remainder correctly.

### Node creation tests

- Node from scene path loads and instantiates.
- Node from type name creates correct class.
- Node with invalid scene path returns error (partial success).
- 3D node gets Vector3 position, rotation_degrees, scale.
- 2D node gets Vector2 position, scale, no rotation.
- Custom properties are set via add_do_property.
- All created nodes have owner set (for scene saving).

### Safety tests

- GDScript caps at 256 nodes even if more are expanded.
- Parent not found returns PARENT_NOT_FOUND error.
- No scene open returns NO_SCENE error.
- Single undo (Ctrl+Z) removes all created nodes.

## Implementation

### Step 1: Add dispatch entry

In the `_dispatch()` function (around line 146), add a new match arm before the wildcard `_:` case:

```gdscript
"build_scene":
    _handle_build_scene(peer_id, id, params)
```

This goes in the existing `match method:` block alongside entries like `"node_ops"`, `"scene_tree"`, etc.

### Step 2: Main handler -- `_handle_build_scene`

Add a new function to the file. The signature:

```gdscript
func _handle_build_scene(peer_id: int, id: String, params: Dictionary) -> void:
```

The handler logic, in order:

1. Get `edited_scene_root` via `EditorInterface.get_edited_scene_root()`. If null, call `_send_error(peer_id, id, "NO_SCENE", "No scene is currently open in the editor")` and return.

2. Check `undo_redo` is not null. If null, send `"NO_UNDO_REDO"` error and return.

3. Resolve the parent node. Extract `params.get("parent", "")` as a String. If it is empty, use the scene root as the parent. Otherwise, call `_find_node_by_name_or_path(parent_str, scene_root)`. If the result is null, send error `"PARENT_NOT_FOUND"` with the parent path and return.

4. Call `_expand_build_pattern(params)` to get a flat Array of node spec dictionaries.

5. **Safety cap**: If the expanded array size exceeds 256, truncate it to the first 256 entries and set a local `truncated` flag to true. Otherwise `truncated` is false.

6. If the array is empty after expansion, send error `"NO_NODES"` with message `"Pattern expansion produced no nodes"` and return.

7. Create one undo/redo action: `undo_redo.create_action("GodotIQ: build %d nodes" % specs.size())`.

8. Iterate over each spec in the expanded array. For each, call `_create_build_node(spec, parent, scene_root, undo_redo)`. Collect results. Track `created` count (where `result.ok == true`) and collect error entries.

9. If `created > 0`, commit the undo action and append to `_godotiq_action_history` (following the same trim logic as `_handle_node_ops`).

10. Send response with: `{ "created": created_count, "errors": errors_array, "total_requested": original_expanded_size, "parent": parent.name, "truncated": truncated }`.

### Step 3: Pattern dispatcher -- `_expand_build_pattern`

```gdscript
func _expand_build_pattern(params: Dictionary) -> Array:
```

This function checks which mode key is present in `params` and delegates:

- If `params.has("grid")`: return `_expand_build_grid(params["grid"])`
- If `params.has("line")`: return `_expand_build_line(params["line"])`
- If `params.has("scatter")`: return the items list -- `params["scatter"].get("items", [])`
- If `params.has("nodes")`: return `params["nodes"]` directly
- Otherwise: return an empty array `[]`

The Python side already validates that exactly one mode is specified, so the check order does not matter for correctness. If somehow multiple keys are present, the first match wins.

### Step 4: Grid expansion -- `_expand_build_grid`

```gdscript
func _expand_build_grid(grid: Dictionary) -> Array:
```

**Parameters from the grid dictionary:**

| Key | Default | Description |
|-----|---------|-------------|
| `scene` | (none) | PackedScene resource path |
| `prefix` | `"Node"` | Name prefix for generated nodes |
| `rows` | (required) | Number of rows |
| `cols` | (required) | Number of columns |
| `spacing` | `1.0` | Distance between nodes |
| `origin` | `[0, 0, 0]` | Starting corner position |
| `axis` | `"xz"` | Placement plane: `"xz"` or `"xy"` |
| `overrides` | `{}` | Per-cell overrides, keyed by `"row,col"` |

**Algorithm:**

For each row in `range(rows)`, for each col in `range(cols)`:

1. Build the cell key string: `"%d,%d" % [row, col]`
2. Check if `overrides` has this key. If yes, start with the override dict as the spec. If the override does not have a `"scene"` key, inherit `scene` from the grid-level `scene`.
3. If no override, create a fresh spec dict with `{"scene": grid_scene}`.
4. If the spec does not have a `"name"` key, set it to `"%s_%d_%d" % [prefix, row, col]`.
5. If the spec does not already have a `"position"` key, calculate position based on axis:
   - `"xz"`: `[origin[0] + col * spacing, origin[1], origin[2] + row * spacing]`
   - `"xy"`: `[origin[0] + col * spacing, origin[1] + row * spacing, origin[2]]`
6. Append the spec to the result array.

Return the result array.

### Step 5: Line expansion -- `_expand_build_line`

```gdscript
func _expand_build_line(line: Dictionary) -> Array:
```

**Parameters from the line dictionary:**

| Key | Default | Description |
|-----|---------|-------------|
| `scene` | (none) | PackedScene resource path |
| `prefix` | `"Node"` | Name prefix |
| `points` | (required) | Array of `[x, y, z]` arrays defining the path |
| `spacing` | `1.0` | Distance between placed nodes |
| `align_to_path` | `false` | Whether to rotate nodes to face the path direction |

**Algorithm:**

1. Initialize an empty result array, a node index counter starting at 0, and a `remainder` of `0.0` (distance carried from the previous segment).

2. Place the first node at `points[0]` with name `"%s_%d" % [prefix, index]`. Increment index.

3. For each segment from `points[i]` to `points[i+1]`:
   a. Convert both to `Vector3`.
   b. Compute direction vector: `(end - start).normalized()`
   c. Compute segment length: `(end - start).length()`
   d. If `align_to_path`, compute Y rotation in degrees: `rad_to_deg(atan2(dir.x, dir.z))`
   e. Start walking along the segment. The first placement distance along this segment is `spacing - remainder`.
   f. While the current distance along the segment is less than or equal to the segment length:
      - Compute position: `start + dir * current_distance`
      - Create spec with `scene`, `name` (`"%s_%d" % [prefix, index]`), `position` as `[pos.x, pos.y, pos.z]`
      - If `align_to_path`, add `"rotation": [0, y_rotation, 0]`
      - Append spec, increment index
      - Advance current distance by `spacing`
   g. Set `remainder` to `segment_length - (current_distance - spacing)` (the leftover distance past the end of this segment). Equivalently: `remainder = current_distance - segment_length` if the last placement overshot, but more precisely track how far past the segment end the next placement would be, so `remainder = spacing - (segment_length - last_placed_distance)` where `last_placed_distance` is the distance of the last placed node along this segment.

4. **Endpoint inclusion**: After processing all segments, check if the last placed node's position is within `spacing * 0.1` of the final point (`points[-1]`). If not, place a final node at the endpoint position. This epsilon check prevents near-duplicate nodes when the path length is an exact multiple of spacing.

5. Return the result array.

**Important detail on remainder tracking**: The remainder represents how much of the next `spacing` interval has already been "used up" by the previous segment. At the start of each new segment, the first node placement is at distance `spacing - remainder` from the segment start.

### Step 6: Node creation -- `_create_build_node`

```gdscript
func _create_build_node(spec: Dictionary, parent: Node, scene_root: Node, ur) -> Dictionary:
```

This function creates a single node from a spec dictionary and registers it with the undo/redo system. It follows the same deferred pattern as `_op_add_child`.

**Steps:**

1. **Instantiate the node:**
   - If `spec` has a `"scene"` key (non-empty string): call `ResourceLoader.load(scene_path)`. If the result is null or not a `PackedScene`, return `{"ok": false, "error": "Failed to load scene: %s" % scene_path}`. Otherwise, call `packed.instantiate()`.
   - Else if `spec` has a `"type"` key (non-empty string): check `ClassDB.class_exists(type_name)` and `ClassDB.can_instantiate(type_name)`. If either fails, return error. Otherwise, call `ClassDB.instantiate(type_name)`.
   - Else: return `{"ok": false, "error": "Spec must have 'scene' or 'type'"}`.

2. **Set node name:** If `spec` has a `"name"` key, set `node.name = spec["name"]`. Godot auto-renames duplicates by appending `@N`; this is expected behavior.

3. **Register with undo/redo (deferred):**
   - `ur.add_do_method(parent, "add_child", node)`
   - `ur.add_do_method(self, "_set_owner_recursive", node, scene_root)`
   - `ur.add_do_reference(node)`
   - `ur.add_undo_method(parent, "remove_child", node)`

4. **Set transform properties (deferred via `ur.add_do_property`):**

   Detection order: check `node is Node3D` first, then `node is Node2D` or `node is Control`.

   **For Node3D nodes:**
   - If `spec` has `"position"` (Array of 3): `ur.add_do_property(node, "position", Vector3(pos[0], pos[1], pos[2]))`
   - If `spec` has `"rotation"` (Array of 3, in degrees): `ur.add_do_property(node, "rotation_degrees", Vector3(rot[0], rot[1], rot[2]))`
   - If `spec` has `"scale"` (Array of 3): `ur.add_do_property(node, "scale", Vector3(s[0], s[1], s[2]))`

   **For Node2D or Control nodes:**
   - If `spec` has `"position"` (Array of at least 2): `ur.add_do_property(node, "position", Vector2(pos[0], pos[1]))`
   - If `spec` has `"scale"` (Array of at least 2): `ur.add_do_property(node, "scale", Vector2(s[0], s[1]))`
   - No rotation support for 2D nodes (Node2D uses a single float in radians, which is too different for auto-detection).

5. **Set custom properties:** If `spec` has a `"properties"` key (Dictionary), iterate its entries and call `ur.add_do_property(node, prop_name, value)` for each.

6. **Return result:** `{"ok": true, "name": node.name}` on success.

## Protocol Contract

The Python side sends a WebSocket message with:

```json
{
  "id": "<request_id>",
  "method": "build_scene",
  "params": {
    "parent": "Terrain",
    "target_scene": "",
    "grid": {
      "scene": "res://tiles/grass.tscn",
      "rows": 8,
      "cols": 8,
      "spacing": 2.0
    }
  }
}
```

Exactly one of `grid`, `line`, `scatter`, or `nodes` will be present (Python validates this). The `target_scene` parameter is not used by the GDScript handler (it is used by the Python bridge layer to select which scene to operate on before sending).

The response on success:

```json
{
  "id": "<request_id>",
  "status": "ok",
  "result": {
    "created": 64,
    "errors": [],
    "total_requested": 64,
    "parent": "Terrain",
    "truncated": false
  }
}
```

On partial success (some nodes failed), `created` will be less than `total_requested` and `errors` will contain error detail dicts. On fatal error (no scene, parent not found), the standard `_send_error` format is used.

## Where to Place the New Functions

All new functions go in `godotiq_server.gd`. The recommended placement:

- The `_handle_build_scene` function should be placed near the other editor-side handlers, after the existing handler functions (e.g., after `_handle_camera` or after `_handle_save_scene`).
- The helper functions (`_expand_build_pattern`, `_expand_build_grid`, `_expand_build_line`, `_create_build_node`) should be placed immediately after `_handle_build_scene` as a logical group.
- The dispatch entry goes in the existing `match method:` block in `_dispatch()`, before the wildcard `_:` case.

## Key Design Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Undo/redo pattern | Deferred (match `_op_add_child`) | Consistency with existing codebase |
| Error handling | Partial success | More useful for large batches -- one bad scene path should not block 63 good nodes |
| Node limit | 256 per call (GDScript cap) | Defense-in-depth against Python-side estimation errors; supports 16x16 grid |
| 2D support | Auto-detect via `is Node3D` then `is Node2D`/`is Control` | Uses Vector3 for 3D, Vector2 for 2D |
| 2D rotation | Not supported | Node2D uses single float radians; too different from 3D's Vector3 degrees |
| Line endpoint | Included with epsilon check (spacing * 0.1) | Prevents near-duplicate nodes at path end |
| Scatter expansion | Just returns `items` array directly | No math needed; items are already fully specified |
| Nodes expansion | Returns the list directly | Pass-through; no expansion logic needed |

## Implementation Notes (Post-Build)

**Actual files modified:**
- `godot-addon/addons/godotiq/godotiq_server.gd` — Added dispatch entry + 5 new functions

**Deviations from plan:**
- Added `type` key propagation in `_expand_build_grid` and `_expand_build_line` (not in plan, but `_create_build_node` supports it)
- `total_requested` now correctly reports pre-truncation count (code review fix)
- First and endpoint line nodes now include `align_to_path` rotation (code review fix for consistency)
- Guard added for `points.size() < 2 or spacing <= 0` in `_expand_build_line`

**Tests:** Manual/E2E only (require running Godot editor)