Files
ttstd-infinite-canvas/PREVIS_REDESIGN_EXECUTION_PLAN.md

678 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Previs 预演台 UI 重构执行计划
## 执行原则
1. **严格按顺序执行**:每完成一个 Phase,运行验证命令通过后才继续下一个
2. **保持功能完整**:所有现有能力必须保留,E2E 测试必须通过
3. **增量提交**:每个 Phase 完成后独立提交,commit message 用 conventional-commits skill
4. **视觉优先**:UI 必须高端、有质感、专业,参考 Blender / Unreal / Unity 的深色专业界面
## 当前问题诊断
### P0 缺陷
1. **画布卡片无缩略图**:`canvas-previs-node-panel.tsx` 只在已回写构图时显示 `<img>`,未回写时显示文字占位
2. **高频工作流功能不全**:缺少新建物品(盒子/球体/圆柱/平面)、灯光、相机的直接入口
3. **角色人偶材质单薄**:当前 `applyActorReferenceMaterial` 只是纯色 `MeshStandardMaterial`,无层次感
4. **整体布局陈旧**:间距、圆角、字号、层次、对比度不足,缺少高级感
### 设计目标
- **画布卡片**:实时 3D 缩略图(俯视/透视小视口),显示对象数/机位数/时长,玻璃态卡片
- **高频工作流**:覆盖 100% 高频操作,分组清晰(演员/道具/灯光/机位/姿态/颜色/运镜),大图标+文字
- **角色人偶**:程序化生成质感材质(fresnel rim、ambient occlusion 近似、subtle gradient),不同演员默认不同鲜明颜色,面板可改色
- **工作台布局**:深色玻璃态、更大间距、清晰层次、减少嵌套、增加留白
---
## Phase 1: 角色人偶材质升级(独立,无 UI 依赖)
### 目标
高质量程序化人偶材质,不依赖贴图,纯 shader 实现 rim light + 环境色 + 颜色差异化。
### 实现文件
- `web/src/components/canvas/previs/previs-viewport-rig.ts`
### 具体改动
#### 1.1 新增高级材质构造函数
在 `previs-viewport-rig.ts` 中,找到 `applyActorReferenceMaterial` 函数,**完全替换**为:
```typescript
/**
* 为演员人偶应用程序化高质感材质:
* - 基础颜色由 object.color 控制
* - Fresnel rim light 提供轮廓光
* - 微妙的顶部-底部渐变模拟天光与地面反射
* - roughness 0.7 + metalness 0.05 产生柔和但有质感的反射
*/
export function applyActorReferenceMaterial(model: Object3D, baseColor: string) {
const color = new Color(baseColor);
// 构造自定义 ShaderMaterial 提供 rim + gradient
const material = new ShaderMaterial({
uniforms: {
uBaseColor: { value: color },
uRimColor: { value: new Color(0xffffff) },
uRimPower: { value: 3.0 },
uRimIntensity: { value: 0.35 },
uTopColor: { value: new Color(0x87ceeb).multiplyScalar(0.15) },
uBottomColor: { value: new Color(0x2c2c2c).multiplyScalar(0.3) },
},
vertexShader: `
varying vec3 vNormal;
varying vec3 vViewPosition;
varying float vHeight;
void main() {
vNormal = normalize(normalMatrix * normal);
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vViewPosition = -mvPosition.xyz;
vHeight = position.y;
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform vec3 uBaseColor;
uniform vec3 uRimColor;
uniform float uRimPower;
uniform float uRimIntensity;
uniform vec3 uTopColor;
uniform vec3 uBottomColor;
varying vec3 vNormal;
varying vec3 vViewPosition;
varying float vHeight;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vViewPosition);
// Fresnel rim
float rim = 1.0 - max(dot(normal, viewDir), 0.0);
rim = pow(rim, uRimPower) * uRimIntensity;
// 顶部-底部渐变(模拟天光)
float heightFactor = clamp((vHeight + 0.5) / 2.0, 0.0, 1.0);
vec3 ambientGradient = mix(uBottomColor, uTopColor, heightFactor);
// 合成
vec3 finalColor = uBaseColor + ambientGradient + rim * uRimColor;
gl_FragColor = vec4(finalColor, 1.0);
}
`,
});
model.traverse((child) => {
const mesh = child as Mesh;
if (mesh.isMesh && !mesh.userData.previsNoMaterialOverride) {
disposePrevisMaterials(mesh.material);
mesh.material = material;
}
});
}
export function updateActorReferenceColor(model: Object3D, baseColor: string) {
const color = new Color(baseColor);
model.traverse((child) => {
const mesh = child as Mesh;
if (mesh.isMesh && mesh.material && "uniforms" in mesh.material) {
const uniforms = (mesh.material as ShaderMaterial).uniforms;
if (uniforms.uBaseColor) uniforms.uBaseColor.value = color;
}
});
}
```
#### 1.2 确保新演员自动分配不同颜色
在 `web/src/components/canvas/previs/canvas-previs-workbench.tsx` 中,找到 `addActor` 函数,确保:
```typescript
const addActor = () => {
const existingActors = draft.objects.filter((obj) => obj.kind === "actor" || obj.primitive === "character");
const colorIndex = existingActors.length % PREVIS_ACTOR_COLORS.length;
const nextColor = PREVIS_ACTOR_COLORS[colorIndex];
addObject(createPrevisActor(`演员 ${existingActors.length + 1}`, [0, 0, 0], nextColor));
};
```
#### 1.3 验证命令
```bash
cd web
./node_modules/.bin/tsc --noEmit
./node_modules/.bin/vite build
node scripts/previs-p0-chrome-e2e.mjs
```
**通过标准**:TypeScript 无错误,Vite 构建成功,E2E 62/62 通过。
**提交**:
```bash
git add web/src/components/canvas/previs/previs-viewport-rig.ts web/src/components/canvas/previs/canvas-previs-workbench.tsx
# 用 conventional-commits skill 生成 message
```
---
## Phase 2: 画布节点卡片实时 3D 缩略图
### 目标
未回写构图时,画布卡片也能显示场景内容:渲染一个小的 3D 俯视图。
### 实现文件
- `web/src/components/canvas/previs/canvas-previs-node-panel.tsx`
- `web/src/components/canvas/previs/previs-mini-viewport.tsx` (新建)
### 具体改动
#### 2.1 新建 `previs-mini-viewport.tsx`
创建 `web/src/components/canvas/previs/previs-mini-viewport.tsx`:
```typescript
import { Canvas } from "@react-three/fiber";
import { OrthographicCamera } from "@react-three/drei";
import { Suspense } from "react";
import type { PrevisScene } from "@/types/previs";
/**
* 画布卡片缩略图:固定俯视正交视角,只显示对象轮廓。
* 不加载真实模型,用简化几何体代替,确保快速渲染。
*/
export function PrevisMiniViewport({ scene }: { scene: PrevisScene }) {
return (
<div className="relative h-full w-full overflow-hidden rounded-lg" style={{ background: "linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%)" }}>
<Canvas gl={{ alpha: true, antialias: true }} dpr={[1, 1.5]}>
<OrthographicCamera makeDefault position={[0, 8, 0]} zoom={50} up={[0, 0, -1]} />
<ambientLight intensity={0.4} />
<directionalLight position={[5, 10, 5]} intensity={0.6} />
<Suspense fallback={null}>
<group rotation={[-Math.PI / 2, 0, 0]}>
<gridHelper args={[20, 20, "#444", "#222"]} position={[0, 0, 0]} />
{scene.objects.filter((obj) => obj.visible !== false).map((obj) => {
const pos = obj.transform.position;
const color = obj.color || "#d6d9dd";
const primitive = obj.primitive || "box";
if (primitive === "sphere") {
return <mesh key={obj.id} position={[pos[0], pos[2], -pos[1]]}><sphereGeometry args={[0.5, 16, 16]} /><meshStandardMaterial color={color} roughness={0.7} metalness={0.1} /></mesh>;
}
if (primitive === "cylinder") {
return <mesh key={obj.id} position={[pos[0], pos[2], -pos[1]]}><cylinderGeometry args={[0.5, 0.5, 1, 16]} /><meshStandardMaterial color={color} roughness={0.7} metalness={0.1} /></mesh>;
}
if (primitive === "character" || obj.kind === "actor") {
return <mesh key={obj.id} position={[pos[0], pos[2], -pos[1]]}><capsuleGeometry args={[0.3, 1.2, 8, 16]} /><meshStandardMaterial color={color} roughness={0.7} metalness={0.1} /></mesh>;
}
// 默认 box
return <mesh key={obj.id} position={[pos[0], pos[2], -pos[1]]}><boxGeometry args={[0.8, 0.8, 0.8]} /><meshStandardMaterial color={color} roughness={0.7} metalness={0.1} /></mesh>;
})}
</group>
</Suspense>
</Canvas>
</div>
);
}
```
#### 2.2 改造 `canvas-previs-node-panel.tsx`
在 `EmptyPreview` 组件中,**替换** `loading ? "正在准备场景" : "尚未生成预览"` 的分支为:
```typescript
if (scene && scene.objects.length > 0) {
return (
<div className="relative h-full w-full overflow-hidden rounded-lg">
<PrevisMiniViewport scene={scene} />
<div className="absolute bottom-2 left-2 right-2 flex items-center justify-between rounded-md px-2 py-1 text-xs backdrop-blur-sm" style={{ background: "rgba(0,0,0,0.7)", color: "#fff" }}>
<Stat icon={<Box className="size-3" />} value={scene.objects.length} label="对象" />
<Stat icon={<Camera className="size-3" />} value={scene.cameras.length} label="机位" />
<Stat icon={<Lightbulb className="size-3" />} value={scene.lights.length} label="灯光" />
</div>
</div>
);
}
return (
<div className="grid h-full place-items-center gap-2 p-4 text-center" style={{ background: "linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%)" }}>
<span className="grid size-10 shrink-0 place-items-center rounded-[var(--r-lg)]" style={{ background: theme.toolbar.activeBg }}>
<Clapperboard className="size-4" style={{ color: theme.node.muted }} aria-hidden />
</span>
<span className="max-w-full truncate text-[var(--fs-tiny)] font-semibold" style={{ color: theme.node.text }}>{loading ? "正在准备场景" : "进入预演台开始创作"}</span>
</div>
);
```
导入新组件:
```typescript
import { PrevisMiniViewport } from "./previs-mini-viewport";
```
#### 2.3 验证命令
```bash
cd web
./node_modules/.bin/tsc --noEmit
./node_modules/.bin/vite build
```
手动检查:打开画布,创建一个预演场景节点,确认卡片显示 3D 俯视缩略图。
**提交**:
```bash
git add web/src/components/canvas/previs/canvas-previs-node-panel.tsx web/src/components/canvas/previs/previs-mini-viewport.tsx
```
---
## Phase 3: 高频工作流功能完整化
### 目标
高频工作流模式包含所有高频操作:新建演员/盒子/球体/圆柱/平面、灯光、相机、姿态、颜色、景别、运镜。
### 实现文件
- `web/src/components/canvas/previs/canvas-previs-workbench.tsx`
### 具体改动
#### 3.1 高频工作流左侧面板重构
找到快速模式的 `<aside className="thin-scrollbar ...">` 部分,**完全替换**为:
```tsx
<aside className="thin-scrollbar min-h-0 overflow-y-auto border-r" style={{ background: "var(--pd-surface)", borderColor: "var(--pd-line)", padding: "16px 12px" }}>
{/* 演员与道具 */}
<div className="mb-4">
<div className="mb-2 text-xs font-semibold uppercase tracking-wide opacity-60">演员与道具</div>
<div className="grid grid-cols-2 gap-2">
<button type="button" className="flex flex-col items-center gap-1 rounded-lg border p-3 transition hover:bg-black/5" style={{ borderColor: "var(--pd-line)" }} onClick={() => addActor()}>
<User className="size-5" />
<span className="text-xs">演员</span>
</button>
<button type="button" className="flex flex-col items-center gap-1 rounded-lg border p-3 transition hover:bg-black/5" style={{ borderColor: "var(--pd-line)" }} onClick={() => addPrimitive("box", "盒子")}>
<Box className="size-5" />
<span className="text-xs">盒子</span>
</button>
<button type="button" className="flex flex-col items-center gap-1 rounded-lg border p-3 transition hover:bg-black/5" style={{ borderColor: "var(--pd-line)" }} onClick={() => addPrimitive("sphere", "球体")}>
<Circle className="size-5" />
<span className="text-xs">球体</span>
</button>
<button type="button" className="flex flex-col items-center gap-1 rounded-lg border p-3 transition hover:bg-black/5" style={{ borderColor: "var(--pd-line)" }} onClick={() => addPrimitive("cylinder", "圆柱")}>
<Cylinder className="size-5" />
<span className="text-xs">圆柱</span>
</button>
<button type="button" className="flex flex-col items-center gap-1 rounded-lg border p-3 transition hover:bg-black/5" style={{ borderColor: "var(--pd-line)" }} onClick={() => addPrimitive("plane", "平面")}>
<Square className="size-5" />
<span className="text-xs">平面</span>
</button>
</div>
</div>
{/* 灯光与相机 */}
<div className="mb-4">
<div className="mb-2 text-xs font-semibold uppercase tracking-wide opacity-60">灯光与相机</div>
<div className="grid grid-cols-2 gap-2">
<button type="button" className="flex flex-col items-center gap-1 rounded-lg border p-3 transition hover:bg-black/5" style={{ borderColor: "var(--pd-line)" }} onClick={() => addLight("directional")}>
<Lightbulb className="size-5" />
<span className="text-xs">平行光</span>
</button>
<button type="button" className="flex flex-col items-center gap-1 rounded-lg border p-3 transition hover:bg-black/5" style={{ borderColor: "var(--pd-line)" }} onClick={() => addLight("point")}>
<Zap className="size-5" />
<span className="text-xs">点光源</span>
</button>
<button type="button" className="flex flex-col items-center gap-1 rounded-lg border p-3 transition hover:bg-black/5" style={{ borderColor: "var(--pd-line)" }} onClick={() => commit((current) => ({ ...current, cameras: [...current.cameras, createPrevisCamera(`相机 ${current.cameras.length + 1}`, [0, 1.6, 5])] }))}>
<Camera className="size-5" />
<span className="text-xs">相机</span>
</button>
</div>
</div>
{/* 选中演员快捷操作 */}
{quickSelectedActor ? (
<>
<div className="mb-4">
<div className="mb-2 text-xs font-semibold uppercase tracking-wide opacity-60">姿态</div>
<div className="grid grid-cols-3 gap-1">
{quickPoseOptions.map((pose) => (
<button key={pose.value} type="button" className="rounded border px-2 py-1.5 text-xs transition hover:bg-black/5" style={{ borderColor: "var(--pd-line)" }} onClick={() => applyQuickPose(pose.value)}>{pose.label}</button>
))}
</div>
</div>
<div className="mb-4">
<div className="mb-2 text-xs font-semibold uppercase tracking-wide opacity-60">颜色</div>
<div className="grid grid-cols-6 gap-1.5">
{PREVIS_ACTOR_COLORS.map((color) => (
<button key={color} type="button" className="size-8 rounded-md border-2 transition hover:scale-110" style={{ background: color, borderColor: quickSelectedActor.color === color ? "#2f8cff" : "transparent" }} onClick={() => updateObject(quickSelectedActor.id, { color })} />
))}
</div>
</div>
<div className="mb-4">
<button type="button" className="w-full rounded-lg border bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-700" onClick={placeQuickActor}>
<Move3d className="mr-1.5 inline size-4" />
落位到场景
</button>
</div>
</>
) : (
<div className="rounded-lg border p-3 text-center text-xs opacity-60" style={{ borderColor: "var(--pd-line)" }}>
选择一个演员后显示姿态与颜色工具
</div>
)}
</aside>
```
导入缺失图标:
```typescript
import { User, Box, Circle, Cylinder, Square, Zap, Move3d } from "lucide-react";
```
添加缺失函数(如果不存在):
```typescript
const addLight = (type: "directional" | "point" | "spot" | "ambient") => {
commit((current) => ({
...current,
lights: [...current.lights, createPrevisLight(type, `${type === "directional" ? "平行光" : type === "point" ? "点光源" : type === "spot" ? "聚光灯" : "环境光"} ${current.lights.length + 1}`, [3, 5, 3])],
}));
};
```
#### 3.2 验证命令
```bash
cd web
./node_modules/.bin/tsc --noEmit
./node_modules/.bin/vite build
node scripts/previs-p0-chrome-e2e.mjs
```
**通过标准**:E2E 62/62 通过,高频工作流面板能添加所有类型对象。
**提交**:
```bash
git add web/src/components/canvas/previs/canvas-previs-workbench.tsx
```
---
## Phase 4: 工作台整体视觉升级
### 目标
深色玻璃态、更大间距、更清晰的层次、专业配色。
### 实现文件
- `web/src/components/canvas/previs/canvas-previs-workbench.css`
- `web/src/components/canvas/previs/canvas-previs-workbench.tsx`
### 具体改动
#### 4.1 CSS 变量与全局样式升级
**完全替换** `canvas-previs-workbench.css` 的前 100 行为:
```css
/* 预演台专业深色主题 */
:root {
--previs-bg-primary: #0a0a0a;
--previs-bg-secondary: #141414;
--previs-bg-surface: #1a1a1a;
--previs-bg-elevated: #222222;
--previs-border: rgba(255, 255, 255, 0.08);
--previs-border-strong: rgba(255, 255, 255, 0.12);
--previs-text-primary: #ffffff;
--previs-text-secondary: rgba(255, 255, 255, 0.7);
--previs-text-muted: rgba(255, 255, 255, 0.45);
--previs-accent: #3b82f6;
--previs-accent-hover: #2563eb;
--previs-glass-bg: rgba(20, 20, 20, 0.85);
--previs-glass-border: rgba(255, 255, 255, 0.1);
}
.previs-workbench {
position: fixed;
inset: 0;
z-index: var(--z-editor);
display: flex;
flex-direction: column;
background: var(--previs-bg-primary);
color: var(--previs-text-primary);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
.previs-desk-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--previs-border);
background: var(--previs-bg-secondary);
backdrop-filter: blur(20px);
}
.previs-desk-header-left {
display: flex;
align-items: center;
gap: 16px;
}
.previs-desk-header-title {
font-size: 15px;
font-weight: 600;
letter-spacing: -0.01em;
}
.previs-desk-header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.previs-desk-header-action {
display: inline-flex;
align-items: center;
gap: 8px;
height: 36px;
padding: 0 14px;
border: 1px solid var(--previs-border-strong);
border-radius: 8px;
background: var(--previs-bg-elevated);
color: var(--previs-text-primary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.previs-desk-header-action:hover {
background: var(--previs-bg-surface);
border-color: var(--previs-border-strong);
transform: translateY(-1px);
}
.previs-desk-header-action.is-primary {
background: var(--previs-accent);
border-color: var(--previs-accent);
color: #ffffff;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.previs-desk-header-action.is-primary:hover {
background: var(--previs-accent-hover);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
.previs-desk-header-action:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
/* 视口容器 */
.previs-viewport-shell {
position: relative;
flex: 1;
min-height: 0;
background: var(--previs-bg-primary);
border-radius: 12px;
overflow: hidden;
}
/* 侧边栏 */
aside.thin-scrollbar {
background: var(--previs-bg-secondary) !important;
border-color: var(--previs-border) !important;
}
/* 按钮与控件 */
button[type="button"] {
border-radius: 8px;
transition: all 0.15s ease;
}
button[type="button"]:hover:not(:disabled) {
transform: translateY(-1px);
}
/* 输入框 */
input, select, textarea {
border-radius: 6px;
border: 1px solid var(--previs-border);
background: var(--previs-bg-elevated);
color: var(--previs-text-primary);
padding: 8px 12px;
font-size: 13px;
transition: all 0.15s ease;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--previs-accent);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
```
#### 4.2 工作台头部优化
在 `canvas-previs-workbench.tsx` 的 `<header className="previs-desk-header">` 部分,确保使用新的 class 和间距。
#### 4.3 验证命令
```bash
cd web
./node_modules/.bin/tsc --noEmit
./node_modules/.bin/vite build
node scripts/previs-p0-chrome-e2e.mjs
```
手动检查:打开预演台,确认:
- 深色玻璃态背景
- 按钮有微妙的 hover 抬升效果
- 间距更宽松
- 整体更专业
**提交**:
```bash
git add web/src/components/canvas/previs/canvas-previs-workbench.css web/src/components/canvas/previs/canvas-previs-workbench.tsx
```
---
## Phase 5: E2E 测试适配与最终验收
### 目标
确保所有改动不破坏现有 E2E 测试契约。
### 检查点
1. **按钮选择器**:E2E 使用 `aria-label` 和 `data-testid`,确保新按钮都有对应属性
2. **统一工作区入口**:E2E 调用 `openUnifiedWorkbench`,确保单一入口仍然存在且可点击
3. **画布渲染**:E2E 等待 `.previs-viewport-shell canvas`,确保视口正常渲染
### 验证命令
```bash
cd web
./node_modules/.bin/tsc --noEmit
./node_modules/.bin/vite build
node scripts/previs-p0-chrome-e2e.mjs
```
**通过标准**:
- TypeScript 无错误
- Vite 构建成功
- E2E 62/62 通过
如果失败,根据错误信息调整 `aria-label` 或 `data-testid`。
**最终提交**:
```bash
git add -A
# 用 conventional-commits skill 生成总结 commit
```
---
## 交付检查清单
- [ ] Phase 1: 角色人偶材质有 rim light + gradient,不同演员默认不同颜色
- [ ] Phase 2: 画布卡片显示 3D 俯视缩略图,即使未回写构图
- [ ] Phase 3: 高频工作流包含演员/盒子/球体/圆柱/平面/灯光/相机的直接入口
- [ ] Phase 4: 工作台整体视觉为深色玻璃态,间距宽松,按钮有 hover 效果
- [ ] Phase 5: `./node_modules/.bin/tsc --noEmit` 通过
- [ ] Phase 5: `./node_modules/.bin/vite build` 通过
- [ ] Phase 5: `node scripts/previs-p0-chrome-e2e.mjs` 62/62 通过
- [ ] 手动截图确认:画布卡片、高频工作流面板、角色人偶、整体布局
---
## 执行给 GPT 的完整指令模板
```
你现在要执行 Previs 预演台 UI 重构。
1. 阅读 PREVIS_REDESIGN_EXECUTION_PLAN.md 的 Phase 1
2. 严格按照 Phase 1 的代码改动执行
3. 运行 Phase 1 的验证命令
4. 如果通过,提交 Phase 1,然后继续 Phase 2
5. 如果失败,修复错误直到通过,再继续
每个 Phase 完成后,回复:
- Phase X 完成
- 验证结果:[通过/失败 + 错误信息]
- commit hash: [实际 hash]
禁止跳过任何 Phase,禁止合并 Phase,禁止省略验证命令。
```
---
## 注意事项
1. **不要删除现有功能**:所有旧分裂工作台的能力必须保留
2. **E2E 是最高优先级**:任何改动导致 E2E 失败都必须回滚
3. **视觉一致性**:所有新增控件必须使用新的 CSS 变量
4. **渐进增强**:每个 Phase 独立可用,不依赖后续 Phase
---
## 参考设计
- **Blender** 深色主题:`#1a1a1a` 背景,`rgba(255,255,255,0.08)` 边框
- **Unreal Engine** 按钮:hover 时微妙抬升,accent 色为蓝色系
- **Unity** 侧边栏:宽松间距,清晰分组,大图标+小文字
---
## 最终交付物
- 所有改动的文件
- 通过的 E2E 报告截图
- 画布卡片、高频工作流、角色人偶、整体布局的截图各一张
- 一份简短的中文交付说明(100 字内)
---
**版本**: 1.0
**更新日期**: 2026-10-04
**责任人**: Kiro (规划) + GPT (执行)