feat: ship end-to-end interior design MVP

This commit is contained in:
Codex
2026-08-02 08:57:10 +08:00
parent 4d97a30616
commit a96b1bf9f9
12 changed files with 2497 additions and 485 deletions
+73
View File
@@ -61,6 +61,27 @@ class PlanIssue(BaseModel):
resolved: bool = False
class RoomProfile(BaseModel):
id: str
name: str
kind: str
area_sqm: float | None = None
confidence: float = Field(default=0.5, ge=0, le=1)
notes: str = ""
class StructureAnalysis(BaseModel):
status: str = "pending"
summary: str = ""
rooms: list[RoomProfile] = Field(default_factory=list)
circulation: str = ""
daylight: str = ""
layout_opportunities: list[str] = Field(default_factory=list)
risks: list[str] = Field(default_factory=list)
model_name: str = ""
degraded: bool = False
class PlanState(BaseModel):
source_name: str
source_kind: str
@@ -81,6 +102,8 @@ class PlanState(BaseModel):
issues: list[PlanIssue] = Field(default_factory=list)
scale_mm_per_unit: float | None = None
ceiling_height_mm: int = 2800
gross_area_sqm: float | None = None
structure: StructureAnalysis = Field(default_factory=StructureAnalysis)
class CameraView(BaseModel):
@@ -113,6 +136,52 @@ class StyleState(BaseModel):
density: str = "适度留白"
avoid: list[str] = Field(default_factory=list)
locked_decisions: list[str] = Field(default_factory=list)
selected_direction_id: str | None = None
class DesignBrief(BaseModel):
residents: str = ""
lifestyle: list[str] = Field(default_factory=list)
focus_rooms: list[str] = Field(default_factory=list)
preferred_styles: list[str] = Field(default_factory=list)
preferred_colors: list[str] = Field(default_factory=list)
disliked_elements: list[str] = Field(default_factory=list)
must_keep: list[str] = Field(default_factory=list)
budget_level: str = "适中"
additional_notes: str = ""
completed: bool = False
class StyleDirection(BaseModel):
id: str
name: str
thesis: str
keywords: list[str] = Field(default_factory=list)
palette: list[ColorToken] = Field(default_factory=list)
materials: list[str] = Field(default_factory=list)
lighting: str = ""
prompt: str = ""
model_name: str = ""
class RenderAsset(BaseModel):
id: str
direction_id: str
room: str
view: str
object_key: str
prompt: str
model_name: str
parent_id: str | None = None
edit_instruction: str = ""
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
class ProjectEvent(BaseModel):
id: str
kind: str
message: str
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
class ProjectSnapshot(BaseModel):
@@ -123,6 +192,10 @@ class ProjectSnapshot(BaseModel):
plan: PlanState
scene: SceneState = Field(default_factory=SceneState)
style: StyleState = Field(default_factory=StyleState)
brief: DesignBrief = Field(default_factory=DesignBrief)
directions: list[StyleDirection] = Field(default_factory=list)
renders: list[RenderAsset] = Field(default_factory=list)
events: list[ProjectEvent] = Field(default_factory=list)
available_commands: list[WorkflowCommand] = Field(default_factory=list)
last_stable_stage: WorkflowStage | None = None
failure_reason: str | None = None
+41
View File
@@ -2,8 +2,11 @@ from copy import deepcopy
from datetime import UTC, datetime
from app.domain.models import (
CameraView,
CommandRequest,
DesignBrief,
ProjectSnapshot,
RoomProfile,
WorkflowCommand,
WorkflowDefinition,
WorkflowStage,
@@ -91,13 +94,51 @@ def apply_command(project: ProjectSnapshot, request: CommandRequest) -> ProjectS
updated.plan.ceiling_height_mm = int(
request.payload.get("ceiling_height_mm", updated.plan.ceiling_height_mm)
)
if request.payload.get("gross_area_sqm") is not None:
updated.plan.gross_area_sqm = float(request.payload["gross_area_sqm"])
if request.payload.get("room_names"):
existing = {room.name: room for room in updated.plan.structure.rooms}
updated.plan.structure.rooms = [
existing.get(name)
or RoomProfile(
id=f"room-{index + 1}",
name=name,
kind="other",
confidence=1,
notes="用户确认",
)
for index, raw_name in enumerate(request.payload["room_names"])
if (name := str(raw_name).strip())
]
for issue in updated.plan.issues:
if issue.kind in {"scale", "ocr"}:
issue.resolved = True
elif request.command == WorkflowCommand.BUILD_BLOCKOUT:
room_names = [room.name for room in updated.plan.structure.rooms]
focus = room_names[:3] or ["客餐厅", "主卧", "入口"]
updated.scene.cameras = [
CameraView(id=f"camera-{index + 1}", label=f"{room}主视角", room=room)
for index, room in enumerate(focus)
]
elif request.command == WorkflowCommand.SUBMIT_STYLE_BRIEF:
updated.brief = DesignBrief.model_validate({**updated.brief.model_dump(), **request.payload, "completed": True})
for field in ("concept", "lighting", "forms", "density"):
if field in request.payload:
setattr(updated.style, field, request.payload[field])
for field in ("keywords", "materials", "avoid", "locked_decisions"):
if field in request.payload:
setattr(updated.style, field, list(request.payload[field]))
elif request.command == WorkflowCommand.SELECT_DIRECTION:
direction_id = str(request.payload.get("direction_id", ""))
direction = next((item for item in updated.directions if item.id == direction_id), None)
if direction is None:
raise InvalidTransitionError(f"Direction '{direction_id}' does not exist in this project.")
updated.style.selected_direction_id = direction_id
updated.style.concept = direction.thesis
updated.style.keywords = direction.keywords
updated.style.palette = direction.palette
updated.style.materials = direction.materials
updated.style.lighting = direction.lighting
updated.available_commands = available_commands(updated.stage)
return updated