Skip to main content

Furniture Planning

Module: src-tauri/src/pipeline/furniture.rs Function: plan_furniture(scene: &Value, data_dir: &Path) -> Result<Value, String>

The furniture planning stage uses a text-only LLM call to generate context-aware furniture placement based on room type, area, and door/window positions.

Pipeline Position

Normalize → Repair → Validate → [Furniture] → Save → 3D Render

In the hybrid pipeline, furniture planning is gated on quality scores (see Validation).

How It Works

Input Preparation

  1. Filter rooms: skip balcony, corridor, and rooms with area outside 1-100 m^2
  2. Find openings: for each room, find doors/windows within its AABB (with 0.5m margin)
  3. Build compact input: room id, type, name, area, polygon, and nearby openings

LLM Call

The planner sends a structured text prompt to a chat LLM (not a vision model). This is approximately 10x cheaper than the VLM call.

System prompt (FURNITURE_PLANNER_SYSTEM):

You are an interior designer. Given room descriptions (type, area, polygon,
door/window positions), plan furniture placement for each room.

AVAILABLE CATEGORIES (use ONLY these exact keys):
sofa, coffee_table, tv_stand, bed_double, bed_single, nightstand, wardrobe,
dining_table, dining_chair, desk, bookshelf, bathroom_sink, toilet, shower,
kitchen_counter, fridge

RULES:
1. Choose furniture categories appropriate for each room type
2. Adjust quantity based on room area:
- < 8 m^2: minimal furniture only (1-2 items)
- 8-15 m^2: standard set
- > 15 m^2: can add extra items
3. Place furniture logically:
- Sofas face the center of the room or windows
- Beds: headboard against a wall, nightstands on both sides
- Wardrobes, bookshelves, kitchen_counter: align flush against walls
- Never block doorways (leave 0.8m clearance from door position)
- Never place furniture in front of windows
4. Positions MUST be [x, y] coordinates in meters, within the room polygon
5. Rotation is in radians around Y axis
6. Each dining_chair needs a separate entry

User message (template with {style} and {rooms_json} replaced):

Style: modern_luxury

Rooms:
[
{
"id": "room_1",
"type": "living_room",
"name": "客厅",
"area": 18.13,
"polygon": [[1.0, 1.666], [4.833, 1.666], [4.833, 4.333], [1.0, 4.333]],
"openings": [
{"type": "door", "position": [2.083, 1.666], "width": 0.9},
{"type": "window", "position": [1.0, 0.833], "width": 1.2}
]
}
]

Output ONLY a JSON object with an "objects" array. Each object must have:
room_id, category, position [x,y], rotation (radians).
No explanation, no markdown fences, just the JSON.

Retry Logic

  • Up to 2 attempts
  • Only retries on timeout errors
  • Waits 5 * attempt seconds between retries

Response Processing

  1. Extract JSON from the LLM response
  2. Validate each object:
    • Category must be in the known CATEGORY_SIZES table
    • Position must have at least 2 coordinates
    • Room ID must match an existing room
    • Position must be within the room's AABB (with 0.3m margin)
  3. Build output objects with proper formatting

Available Furniture Categories

Each category has a defined size [width, height, depth] in meters:

CategoryWidthHeightDepthTypical Room
sofa2.20.850.9living_room
coffee_table1.20.450.6living_room
tv_stand1.80.50.4living_room
bed_double2.00.551.6bedroom
bed_single2.00.551.0bedroom
nightstand0.50.550.4bedroom
wardrobe1.82.20.6bedroom
dining_table1.60.750.9dining_room
dining_chair0.450.90.45dining_room
desk1.40.750.7study
bookshelf1.02.00.35study
bathroom_sink0.60.850.5bathroom
toilet0.40.750.65bathroom
shower1.02.11.0bathroom
kitchen_counter2.40.90.6kitchen
fridge0.71.80.65kitchen

Output Format

Each furniture object in the scene JSON:

{
"id": "furniture_1",
"type": "furniture",
"category": "sofa",
"room_ref": "room_1",
"position": [2.917, 0, 4.0],
"rotation": [0, 0, 0],
"scale": [1, 1, 1],
"size": [2.2, 0.85, 0.9]
}

Note: the Y component of position is always 0 (floor level). The LLM receives [x, y] (2D) coordinates; the pipeline adds the Y=0 floor component.

Full LLM Response Example

{
"objects": [
{
"room_id": "room_1",
"category": "sofa",
"position": [2.917, 4.0],
"rotation": 0.0,
"reasoning": "Place sofa against the south wall, facing north"
},
{
"room_id": "room_1",
"category": "coffee_table",
"position": [2.917, 3.2],
"rotation": 0.0
},
{
"room_id": "room_1",
"category": "tv_stand",
"position": [2.917, 1.85],
"rotation": 0.0
},
{
"room_id": "room_2",
"category": "bed_double",
"position": [6.167, 3.0],
"rotation": 0.0
},
{
"room_id": "room_2",
"category": "wardrobe",
"position": [7.2, 2.5],
"rotation": 1.5708
},
{
"room_id": "room_2",
"category": "nightstand",
"position": [5.5, 3.0],
"rotation": 0.0
},
{
"room_id": "room_2",
"category": "nightstand",
"position": [6.833, 3.0],
"rotation": 0.0
}
]
}

Fallback Behavior

If the LLM call fails (timeout, API error, invalid JSON), the planner returns the scene unchanged with objects: []. The frontend buildObjects.ts then falls back to a hardcoded roomFurnitureMap:

const roomFurnitureMap: Record<string, FurnitureItem[]> = {
living_room: [
{ category: 'sofa', count: 1, placement: 'wall_adjacent' },
{ category: 'coffee_table', count: 1, placement: 'center' },
{ category: 'tv_stand', count: 1, placement: 'wall_adjacent' },
],
bedroom: [
{ category: 'bed', count: 1, placement: 'center' },
{ category: 'wardrobe', count: 1, placement: 'wall_adjacent' },
{ category: 'nightstand', count: 2, placement: 'wall_adjacent' },
],
// ...
}

This ensures the pipeline always produces a renderable result.

Cost

StepTypeApproximate TokensRelative Cost
VLM ParseImage + text~4000 input + ~2000 output1x
Furniture LLMText only~800 input + ~600 output~0.1x

The furniture planner is ~10x cheaper than VLM parsing because it processes structured text, not images.

Style Awareness

The furniture planner receives the scene's style (modern_luxury, cream, nordic) and is instructed to choose furniture matching the aesthetic. The LLM may vary furniture selection and quantity based on style, though the available categories remain the same.

Validation Checks

After receiving the LLM response, the planner validates:

  1. Unknown categories: objects with categories not in CATEGORY_SIZES are skipped
  2. Missing positions: objects with fewer than 2 position coordinates are skipped
  3. Unknown rooms: objects referencing non-existent room IDs are skipped
  4. Out-of-bounds: objects whose position falls outside the room's AABB (with 0.3m margin) are skipped

The planner logs how many objects passed validation vs. how many the LLM proposed:

Furniture planner: 12/15 objects validated