# Lookdev

> 人在回路中的 Web 工作室，通过肉眼调优 AI 生成的输出。搭建本地交互式工作室（滑块、选择器、拖拽手柄）或用于文本与媒体的内联编辑/高亮/评论标注工作室，而非猜测数值或交付静态对比网格。触发词：lookdev、调优、微调外观、对比变化、交互调整、编辑标注、审阅标注。

- Skill: `kscz0000/lookdev` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kscz0000/lookdev`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kscz0000/lookdev/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: kscz0000 (https://skillmd.com/u/kscz0000)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/kscz0000/lookdev

---

## 何时使用

当用户说"lookdev"，或要求调优/微调/迭代外观、凭感觉对比变化，或审阅/编辑/标注博客文章、文档、文案或媒体集时使用。当"给我看，我来选"胜过让用户指定一个数字时使用，当你本应交回一个静态网格或一堵文字墙供审阅时使用。

_来源：[connerkward/lookdev-studio-skill](https://github.com/connerkward/lookdev-studio-skill) (MIT)。_

# Lookdev

当用户说 **"lookdev"** — 或者以下任何一个：*调优*、*微调*、*迭代外观*、*对比变化*、*让我调整*、*让我编辑/标注/标记*、*审阅此文章/文档/文案* — 他们的意思是**构建一个用户直接操控的浏览器内交互工具**。不是 N 个变体的静态网格。不是让他们指定数字的问答。不是让他们在聊天中阅读和回复的一堵文字墙。而是一个实时工作室，用户对制品执行操作，变更被捕获。

**两种工作室形态 — 根据调优对象选择：**

- **视觉参数 lookdev** — 制品的*外观*由数字/选择决定（颜色、字体、布局、图像处理、动画、3D）。控件 = 滑块、选择器、拖拽手柄。这是本技能的主体内容（见下文）。
- **文本与媒体 lookdev** — 制品是*文档、博客文章、文案或媒体集*，用户正在编辑/策划：改写句子、删减无聊段落、高亮、留边注、标记"此处需要图表"/"图片错误，替换"。控件 = **直接内联编辑 + 选区高亮 + 锚定评论 + 媒体标注**。见下方专门章节。**博客文章/文档/脚本审阅就是此模式 — 永远不要交回一个长 markdown 文件让用户在聊天中回应。搭建标注工作室。**

## 覆盖范围

任何用户凭感觉而非规格做出的视觉决策。按需扩展此列表：

- **图像处理** — 抖动、半调、色调分离、ASCII、模糊、边缘、量化、马赛克、调色
- **颜色** — 色板提取（显示覆盖率百分比）、各频段选择器、饱和度/对比度/伽马曲线、和谐预设、主题令牌
- **排版** — 字体选择器、字号/字重/行距/字距/行长、实时样本文本、回退字体栈
- **布局、定位、取景、间距** — 可拖拽和可选择的元素；缩放手柄；外边距/内边距标尺；对齐参考线；对齐网格；锁定宽高比切换
- **裁剪与取景** — 带宽高比锁定的可拖拽裁剪矩形；生产尺寸的实时裁剪预览
- **动画/过渡** — 缓动曲线编辑器、时长滑块、擦洗器、重放
- **组件变体** — 在一页上并排渲染悬停/聚焦/禁用/加载/深色状态
- **图标** — 描边粗细、圆角半径、画布上的字形
- **AI 生成内容** — 提示词输入 + 参数滑块 + 并排重新生成网格
- **其他任何"给我看，我来选"** 胜过"让我指定一个数字"的场景

## 控件在检视时必须保持可达

如果工作室显示列表、网格或长滚动变体集，**控件必须在每个滚动位置都可见**。用户必须能够在查看第 14 行时拖动滑块，而不是每次都滚回顶部。

两种方案，根据布局选择：

- **粘性栏** (`position: sticky; top: 0`) 在滚动容器顶部。保持栏的视觉区分 — 纸张背景 + 模糊背景 + 底部边框 — 这样它不会与后面滚动的样本混淆。粘性定位相对于*最近的有定义边界的滚动祖先*；如果你将它嵌套在一个有尺寸的父元素内（一个有 `margin-bottom` 的 `<header>`，一个有固定高度的 `<div>`），它会在该父元素的底边停止粘性。将其提升为 `<body>`（或页面包裹层）的直接子元素，使粘性覆盖整个页面。
- **浮动覆盖层** (`position: fixed`) 用于快捷键切换的控件 — 例如按 `d` 显示。组合的 `.debug-ctl` 模式就是这种：固定在左上角，召唤前透明。当控件不应占据永久屏幕空间时使用（最终查看者不应看到它们；作者可按需召唤）。

反模式：用户滚过就再也看不到的页面顶部控制面板。他们会盲目调参、放弃或猜测。要么让控件保持可见，*或者*在每个变体行旁复制一个紧凑的控制栏。

## 文本与媒体 lookdev — 直接编辑、高亮、评论、标注

当制品是**博客文章、文档、文案稿、脚本或媒体集**时，用户不是在拧旋钮 — 他们是*像编辑标记手稿一样标记作品*。工作室渲染**真实制品的所见即所得**（实际渲染的博客及其真实组件/媒体，而非原始 markdown 文本区）并让用户直接对其进行操作。为文档审阅构建此功能是强制性的：**不要将长文件粘贴到聊天中问"你觉得怎么样？" — 那正是用户拒绝的无聊文字墙。** 搭建标注工作室让他们就地编辑。

### 四项可供性（构建所有适用的）

1. **直接内联编辑。** 每个文本块就地可编辑 — 点击段落/标题然后输入。每个块使用 `contentEditable`（或点击切换为 `<textarea>`），每个块携带一个稳定的 `data-block-id`，映射回源位置（markdown/MDX 行范围、JSX 节点或内容键）。捕获每个块的*已编辑*文本；代理将差异应用到源文件。不要让他们在单独的字段中重新输入 — 他们编辑渲染后的句子。
2. **选区高亮。** 选择文本 → 工具栏（或快捷键）应用彩色高亮（`<mark>`）。多种颜色 = 用户定义的图例（例如黄色"删掉这个"，绿色"喜欢"，红色"错误/需核实"）。每个高亮存储 `{blockId, startOffset, endOffset, color, optional note}`。
3. **锚定评论/边注。** 选择文本或点击媒体区域 → 附加一条评论，显示在**侧边栏**（边距中的图钉，悬停/点击展开）或带编号的上标。评论 = `{anchor, text}`，其中 anchor 是块+范围或媒体区域。这是用户说"此处需要图表"、"太长了，删到两句话"、"需要真实截图"的方式。
4. **媒体标注。** 对于图片/图形：在图片上画框/放图钉/画箭头并附加备注（`{mediaId, x, y, w, h, note}`）；加上每个媒体的**标记菜单** — "替换"、"模型错误"、"重新生成"、"缺失 — 在此生成一个"。占位符（"DIAGRAM HERE"、"MEDIA?"）渲染为可见的拖放区域，用户点击指定他们想要什么，直接解决"图表在哪里/媒体在哪里"的问题。

### 往返是强制性的（与设置 JSON 相同规则）

如果代理无法读回标记，工作室就毫无价值。每次编辑、高亮、评论和媒体标记必须导出为**一个机器可读的补丁**，带一个**复制**按钮（并持久化到 `localStorage`/URL，这样刷新不会丢失工作 — 这是人工标注数据；参见 `human-labeled-data-rule`）。格式：

```json
{
  "edits":      [{ "blockId": "p-12", "text": "new rewritten text" }],
  "highlights": [{ "blockId": "p-3", "range": [40, 88], "color": "cut", "note": "boring, drop" }],
  "comments":   [{ "anchor": "p-7", "text": "diagram goes here — flow of the save loop" }],
  "media":      [{ "mediaId": "fig-2", "flag": "replace", "note": "use a real screenshot, not ASCII" }]
}
```

代理接收此数据并烘焙：将内联编辑应用到源文件，处理每条评论/标记，替换/生成被标记的媒体，解决高亮（删除"cut"跨度等）。然后重新提供更新后的制品进行下一轮。**不允许存在不在导出数据中的标记** — 否则你又回到了用户手动口述变更的局面。

### 实现机制

- **渲染真实内容。** MDX/React 博客 → 挂载实际组件；静态页面 → 渲染真实 HTML/CSS。按架构 #5 所见即所得。在虚假预览上的标注层会对结果撒谎。
- **选区 → 偏移量。** 使用 `Selection`/`Range` API；存储相对于块文本内容的字符偏移量（不是 DOM 节点路径，后者在重新渲染时会失效）。加载时通过遍历每个块的文本到存储的偏移量来重新应用高亮/评论。
- **编辑工具栏跟随选区浮动**（选区矩形处的小弹出框）或粘性顶栏 — 控件保持可达（见上方章节）。快捷键：高亮用一个键（例如 `h`），评论用 `c`。
- **保持编辑/标注模式独立**，这样误触不会在用户想高亮时搞乱文本 — 模式切换（编辑 · 高亮 · 评论）或修饰键。
- 其他所有内容 — 在空闲端口上本地服务、无头验证、烘焙后拆除 — 与下方视觉参数工作流相同。

## 控件模式

根据决策的实际类型选择控件。

| 决策类型 | 控件 |
|---|---|
| 连续值（强度、大小、不透明度、k） | `<input type=range>` **配对一个可编辑的 `<input type=number>`**（不是静态标签）— 拖动或点击输入；二者双向同步 |
| 离散选择（模式、混合、缓动类型） | 分段按钮或单选芯片 |
| 颜色 | `<input type=color>` 色板；相关时预提取带覆盖率百分比的主色板 |
| 画布上的位置/大小 | **直接拖拽元素本身** — 手柄，而非数字输入 |
| 裁剪区域 | 可拖拽矩形 + 锁定宽高比切换 |
| 多个离散状态 | 在一页上的标记卡片中各渲染一个 |
| 字体选择 | 可搜索选择器 + 可编辑样本文本输入 |

**空间规则：** 如果用户可以指向某个东西并拖动它，那*就是*控件。当拖拽手柄是显然的操作方式时，不要添加 `x:` 滑块。

**手势捕获 — 永远不要让执行手势的手离开手势。** 当控件切换用户正在执行的*实时鼠标动作* — 录制光标路径、擦洗、自由绘制、演示动作 — 开始/停止**绝不能**是他们必须点击的按钮。点击它会把鼠标拖离路径，污染正在捕获的动作的起止点，并迫使他们来回移动。将开始/停止绑定到**键盘（默认空格键）** — `keydown` 在 `Space` 上，`e.preventDefault()` 阻止页面滚动，切换与按钮相同的处理程序。也保留按钮（可发现性），但快捷键才是真正的控件。泛化：任何一只手致力于主输入的模态捕获，用*另一*模态进行模式切换 — 手势→按键，反之亦然，键盘密集的捕获用脚踏/鼠标切换。测试标准：如果触发控件会移动你正在捕获的东西，那就是错误的模态。

## 连贯的控件范围 — 边界必须传播

当一个控件为另一个控件设置了**边界**（最小值、最大值、阈值、允许集），被约束控件的 UI 必须在你更改它的瞬间反映新边界。一个 `min`/`max` 属性与其声明边界脱节的"擦洗"滑块是最常见的静默 bug — 用户移动边界滑块，下游没有可见变化，他们认为两个都坏了。

规则：

- **单一真相源。** 在状态中保存一次边界。每个*显示*它的输入（它自己的滑块、依赖控件的 `min`/`max`、其他任何东西）在每次更新时从该状态读取。
- **在每次状态变更时重新渲染 `min`/`max`。** 不要依赖浏览器缓存的属性值；每次渲染时通过 JS 重写它们。`dependent.min = state.lo; dependent.max = state.hi`。
- **立即将依赖值钳入新范围。** 如果用户将上界缩小到当前依赖值以下，依赖值必须跳入范围，而不是在滑块显示它钉在轨道上时静默地留在范围外。
- **无效区域是滑块 bug。** 如果拖动滑块超过某个值后下游零效果（因为某个其他控件的边界限制了它），那就是连贯性 bug — 要么缩小此滑块的范围到它实际有效的区间，要么改变行为使其有效。有死区的滑块会让用户认为工作室坏了。
- **通过可视化测试，而非数值快照。** 截图，更改边界滑块，再截图。两者必须在视觉上有明显差异 — 否则滑块只是装饰。显示状态变化的数值 `snapshot()` 不能证明像素变了。

模式：每次 `applyState()` 运行时（或其拆分等价物），调用 `syncBounds()` 辅助函数，遍历依赖输入注册表并将活动边界推入每个 `min`/`max`/`disabled` 属性。在同一遍中将值钳入新边界。

### 配对控件不得交叉

一种常见模式是**两个滑块共同定义一个区间** — `min ⟷ max`、`near ⟷ far`、`tightEnd ⟷ wideEnd`、`start ⟷ end`。如果用户可以将一个拖过另一个，区间就会反转或坍缩。下游数学通常做 `(x - lo) / (hi - lo)`，这会**除以零或返回负 `t`** — 产生 `NaN` 坐标、坍缩的视图或反转的线性插值。用户看到工作室"坏了"但没有错误触发。

防御的两端：

- **UI 不变量。** 阻止两个滑块交叉。在每次 `syncBounds()` 遍历时：`lower.max = upper.value - MIN_SPAN` 和 `upper.min = lower.value + MIN_SPAN`（小的 epsilon，例如 2 个单位，这样它们甚至不能接触）。用户无法物理拖过另一个锚点。
- **数学不变量。** 消费代码（线性插值、归一化、比率）必须守卫 `denominator > 0` 并为退化情况选择合理的回退值（例如钳位 `t = 1` 或 `t = 0`）。UI 可能与数学竞争 — 始终假设数学可能遇到交叉的边界（URL 哈希、JSON 粘贴回、编程状态变更）。
- **显式测试边界。** 当 lookdev 公开区间的两端时，写一个快速检查：将 `tightEnd` 拖到与 `wideEnd` 相同的值，验证场景不会崩溃。将 `tightEnd` 拖过 `wideEnd`，验证同样。如果你能通过两次滑块拖动使工作室崩溃，那就是发布阻塞问题。

## 架构

1. **单页 HTML** — `<canvas>` 和/或 DOM，原生 JS，侧边栏控件。无构建步骤，无框架，无依赖（除非确实需要）。放在项目本地临时目录中（例如 `scripts/.lookdev-<name>/` 或 `scripts/.preview-<name>/`），**加入 gitignore**。
2. **在每个 `input` 事件上实时重新渲染。** 通过 `requestAnimationFrame` 对重型工作进行防抖。保持循环足够紧凑，感觉像真正的滑块，而不是问卷调查。
   - **每个数值控件都是双输入（强制）：一个范围滑块和一个可编辑的 `<input type=number>`，双向同步。** 拖动用于探索；输入数字用于命中精确值（并读取当前值）。静态的 `<span>` 读数不够 — 用户必须能够点击它并输入。同步规则：滑块 `input` 时，写入数字字段；数字 `input`/`change` 时，更新状态并重新渲染 — 但**不要在有焦点时覆盖字段**（用 `document.activeElement` 守卫），否则输入会在击键中途被覆盖。在提交时（`change`）钳位到 [min,max]，而不是每次击键时，这样"1"在变成"12"之前的中间值不会被捕捉。
   - **始终包含重置控件**，一键将每个控件恢复到默认值（保留一个 `DEFAULTS` 对象；`Object.assign(state, DEFAULTS)` 然后重新渲染）。添加成本低，但一旦用户偏离基线很远就至关重要。
   - **始终构建撤销/重做历史（强制）。** 微调是迭代且有损的 — 用户*一定会*超过一个好的外观并需要回退。绑定 **Ctrl/Cmd-Z**（撤销）和 **Ctrl/Cmd-Shift-Z** / **Ctrl-Y**（重做），并显示可见的 **↶ Undo / ↷ Redo** 按钮。快照*完整*的序列化状态 — 每个控件**加上任何绘制/空间状态**（多边形、裁剪矩形、拖拽手柄、色板），即与设置往返（#3）相同的 blob，不只是滑块标量。防抖使连续拖动折叠为**一个**历史步骤（在最后一次 `input` 后约 350 毫秒快照，而非每个事件），保持有界栈（约 100–120 条目），在撤销后新编辑时截断重做分支。通过加载器使用的相同 `applyState` 路径重新应用快照来恢复（这样就不会漂移）。当焦点在 `<input>`/`<textarea>` 中时守卫按键处理程序，使原生文本撤销仍然有效。没有撤销的 lookdev 惩罚探索 — 这正是工具的全部意义。
3. **结构化设置往返（强制）。** 每个 lookdev 必须将其完整当前状态公开为机器可读、可复制粘贴的文本 — 一个覆盖*每个*控件的设置 JSON（或等价物），带一键**复制**按钮和可见的实时读数。这是不可协商的：代理不能通过目视截图来烘焙，用户也不应该描述他们调了什么。往返是：用户拖动 → 工作室序列化确切状态 → 用户粘贴回 blob（或它持久化到 URL/localStorage）→ 代理用相同数学从这些字面值烘焙。不允许任何控件可调但不出现在导出 blob 中。将状态镜像到 URL 查询中，使外观可通过链接分享。
4. **可复现导出。** 除设置 blob 外，根据提交的内容选择：
   - **复制设置 JSON** — 用户粘贴回，代理用相同数学烘焙（将渲染器移植到 Python/构建脚本/等，并验证烘焙匹配）。
   - **下载资产** — 页面以全分辨率渲染最终制品并触发下载（PNG / SVG / WebP / JSON）。
   - **使导出资产可重新加载 — 附属文件 + 嵌入元数据。** 当下载是*非 JSON* 资产（STL、PNG、GLB、SVG、WebP、视频…）时，产生它的外观不应不可追溯。在格式允许的情况下，两者都做：
     - **附属文件：** 下载一个**zip**，包含资产*及其* `settings.json`，这样确切状态随结果一起发布。
     - **将设置嵌入文件本身**，这样仅凭裸资产就能恢复外观 — 然后添加一个**拖放/文件输入加载器**，通过与 JSON 粘贴相同的 `applyState` 路径读回。按格式钩子：**binary STL** → 在三角形数据后追加 `MAGIC + uint32 len + JSON`（CAM 忽略尾随字节；在字节 80 解析 `count`，尾部在 `84 + count*50`）并在 80 字节头部放一条人类注释；**PNG** → 一个 `tEXt`/`iTXt` 块；**SVG/XML** → 一个 `<metadata>` 元素或注释；**JPEG/MP4** → EXIF/XMP `UserComment`；**GLB** → 一个 `extras` 字段。回报：用户将上周的 STL 拖回视口，工作室自动重新调参 — 无需"哪个设置产生了这个？"的考古。验证往返（导出 → 重置 → 加载 → 断言状态匹配）并确认资产仍可在其原生工具中打开（尾部/边缘元数据不得损坏它）。仅当格式没有安全存放字节的地方时跳过；附属 zip 始终作为后备。
5. **所见即所得。** 预览框架必须匹配生产环境 — 相同背景色、相同加载的字体、相同容器最大宽度、相同 `object-fit`。通用居中画布不是所见即所得。
6. **框架路由变体。** 当 lookdev 用于现有应用内的 UI 布局时，将其构建为应用中的**临时路由**（`app/dev/...` 或等价路径），这样真实组件、样式和令牌在对比中。**烘焙后删除路由。**

## 3D lookdev — 方位小控件（当相机环绕时强制）

任何具有**非固定相机**的 lookdev（OrbitControls、轨迹球、自由飞行 — 任何用户可以旋转/翻转视图的情况）必须在角落包含一个**CAD 风格的 ViewCube**。仅靠自由环绕会让人迷失：用户分不清哪边是上，无法获得可重复的标准视图，无法判断自己看的是正面还是背面。方块同时解决这两个问题 — 它既是方位*指示器*也是*控制器*。**复制 Autodesk/Fusion 360 的 ViewCube** — 那是用户期望的交互方式；不要发明不同的控件。

必需行为（这很便宜 — 约 70 行 Three.js，没有理由跳过）：

- **实时方位读数。** 角落中的一个小第二场景/渲染器绘制一个带标签的方块（FRONT/BACK/LEFT/RIGHT/TOP/BOTTOM）。每帧，从*主*相机的视图方向驱动控件相机（`gizmoCam.position = (mainCam.position − target).normalize() * d; gizmoCam.up = mainCam.up; gizmoCam.lookAt(0,0,0)`），使方块始终镜像场景的当前方位。
- **点击面/边/角吸附**（Fusion 的定义行为；26 个预设视图 — 6 个面、12 条边、8 个角）。对控件进行射线检测，将本地命中点的每个分量吸附（`|c|>0.55 ? sign(c) : 0`）以推导视图方向。一个可拾取方块即可从单个网格产生**面 → 正交视图、边 → 45° 边视图、角 → 等轴视图** — 不需要单独的命中区域。将主相机动画到 `target + dir*currentDist`，使用短线性插值（约 0.28/帧），而非瞬间切换 — 运动正是让用户保持方向感的关键。
- **拖拽方块自由环绕**（也是 Fusion 的功能，也是强制性的 — 用户一定会试图抓取它）。使用指针事件与**点击-vs-拖拽区分**：`pointerdown` 时记录起点和 `setPointerCapture`；`pointermove` 时，一旦移动超过约 4px 就翻转为拖拽模式，并按指针增量环绕*主*相机（将相机偏移转换为围绕目标的球坐标，`theta -= dx*k; phi = clamp(phi - dy*k, ε, π−ε)`）；`pointerup` 时，如果从未成为拖拽，则视为吸附点击。捕获意味着当指针离开小画布时拖拽仍然有效。拖拽开始时取消任何进行中的吸附动画。
- **旋转箭头 = Fusion 的"旋转"。** 方块旁的两个弧形箭头按钮（⟲ ⟳）**将当前视图绕视图轴滚动 90°**（绕归一化的 `position−target` 轴旋转 `camera.up` ±90°）。这就是用户说控件"不能旋转"时指的旋转 — 拖拽环绕*不是*替代品。对滚动后的 up 方向进行吸附清理，使接近基数的分量精确落在 0/±1（保留真正的对角线）。**让它可在任何视图中滚动，包括等轴视图** — 不要限制在正视图中或先自动吸附到面。（我试过"Fusion 只在标准视图中滚动"的守卫，结果适得其反：它阻止用户将*等轴*视图滚动到他们想要的确切方向，而这正是他们伸手找箭头的主要原因。滚动等轴视图是有效的常见操作。）
- **透视 ⇄ 正交切换。** 任何 3D lookdev 都应公开投影切换。透视用于自然阅读；**正交用于 CAD/测量/剖面工作**（平行边缘、真实标高、无透视缩短 — 在判断厚度或对齐面时至关重要）。通过构建另一种相机、复制 `position`/`up`/`target`、重建控件来切换；从当前目标距离确定正交视锥体大小（`h = 2·dist·tan(fov/2)`），使切换不会跳变缩放。为两者处理调整大小（`isPerspectiveCamera` → 设置 `aspect`；正交 → 从 aspect 重新计算 `left/right`，保持高度）。
- **`camera.up` + OrbitControls 是陷阱 — 读这段。** Three 的 OrbitControls（r160）在构造时从 `camera.up` **一次性**捕获其环绕轴四元数。如果你之后修改 `camera.up`（例如"修复"顶视图，或滚动）并保持它，主视口的拖拽会静默失效 — OrbitControls 继续绕*旧的* up 环绕，而相机用*新的* up 渲染。对控件的两个后果：**(a)** 不要为顶/底吸附翻转 `camera.up`。保持 `(0,1,0)` 并改为将吸附*方向*微偏极点（`dir = (0,±1,0.0009)`），这样 `lookAt` 配合 `up=+Y` 不会万向节锁。**(b)** 当你确实需要新的 up（旋转箭头），在设置 `camera.up` 后**销毁并重建 OrbitControls**，跨拷 `target`，使其重新捕获轴。吸附和 Home 应重置为 `up=(0,1,0)` 并在当前已滚动时重建。
- **Home/重置视图按钮** 在方块旁（Fusion 的房屋图标），重新框定对象、重置 `camera.up=(0,1,0)` 并在已滚动时重建控件。
- **悬停高亮精确区域，而非整个面。** Fusion 将每个面细分为 3×3 网格 — 中心格 = 面，边格 = 边，角格 = 角 — 并高亮悬停格*跨越相邻面环绕*。用最多 3 个半透明四边形的小池实现：从悬停方向 `d`（1/2/3 个非零轴），对每个非零轴在该面上放置一个四边形，偏移为 `(其他轴符号)*⅔`。一个角点亮 3 个四边形（每个相邻面一个），一条边 2 个，一个面 1 个。纯整面着色是错的 — 用户无法区分角拾取和面拾取。同时设置 `grab`/`grabbing` 光标使方块看起来可拖拽。

**将模型定向为 FRONT 是用户关心的面。** 方块的标签固定在世界轴上，所以你如何放置模型决定了"FRONT"显示什么。对于浮雕/面板/任何有主面的东西，让它立起来使主面朝向世界 **+Z**（= FRONT）且图像上方朝向 **+Y** — 不要平放朝向 +Y，否则 FRONT 显示无意义的边缘而 TOP 显示主面（对用户来说出乎意料且"错误"）。注意位移轴符号：Three 的 `PlaneGeometry` 推向 `-y`，所以顶点行 0 是 **+Y（顶部）** — 将图像行 0（顶部）映射到它时**无需翻转**，否则你的浮雕会倒置。通过吸附 FRONT 并与源图像肉眼比对来验证；不要信任索引数学。

**构建实体，而非浮动薄片。** 一个位移的 `PlaneGeometry` 是单一中空表面 — 快速查看可以，用户一旦检视就错了。在 X 光（或任何侧视图）中，凸起读作浮在基底上方有间隙的中空圆顶，且对 STL/CAM 不水密。如果东西是真实物体（浮雕、地形块、雕刻面板），构建**实体高度场**：位移顶面 + 周边裙墙 + 平底，使其扎根在基底上。用户*一定会*注意到"背面没有接触背板"。设置材质 `DoubleSide` 使手工构建的墙壁永远不会渲染为黑色。

**剖面/X 光用于隐藏的内部尺寸。** 当控件设置从外部看不到的东西 — 壁厚、背板、内部间隙、拔模角 — 添加**X 光/剖面切换**，让用户实际看到他们在调什么。最便宜版本：使外壳半透明（`transparent, opacity~0.15, depthWrite:false`），将被测实体（背板厚块、剩余壁厚）渲染为**不透明且颜色鲜明的网格**，在关键边界处有明亮的边线；配合侧面正交吸附使尺寸读数为干净的带状。（带盖的真实裁剪平面剖面是更花哨的版本；通常不值得模板工作。）当一个切换就能显示时，不要让用户仅凭数字推断隐藏的厚度。

保持在世界/视图空间中，与模型*显示*方式对齐（考虑你应用的任何根旋转）。**通过可视化验证，而非数学**（这些都坑过我）：点击 TOP 然后拖拽*主视口*并截图 — 确认它仍然可以环绕（捕获 `camera.up` 陷阱）；悬停一个角并截图方块 — 确认角区域跨面点亮，而非整面；从等轴视图点击旋转箭头并截图 — 确认它吸附到面（而非对角线滚动）；吸附 FRONT 确认主面正立。真正可选的 Fusion 额外功能：相邻面三角形箭头（拖拽环绕已覆盖）、N/E/S/W 指南针环、右键"将当前视图设为 Home"菜单 — 除非被要求否则跳过。

## 工作流

1. **为具体问题构建工作室。** 不要做通用的。如果用户在选择主图裁剪，工作室显示实际的主图。如果他们在选择字体，工作室显示样本文本。
2. **本地服务。** 永远不要硬编码端口 — 将静态服务器绑定到端口 0（操作系统返回空闲端口）用于静态 HTML，或使用项目的开发服务器用于框架路由。给用户 URL。
3. **在交给用户前无头验证**（无头 Playwright）。不要让用户调试你的脚手架。
4. **用户迭代。** 他们粘贴回设置 JSON、点击下载按钮，或说"用 N"/"用这个"。
5. **烘焙。** 用可复现的数学将选定的状态渲染为提交的资产/生产代码。验证烘焙结果与他们调的一致（快速截图差异即可）。
6. **拆除脚手架。** 删除 lookdev 目录/开发路由 — 它是决策时的脚手架，不是生产代码。提交 + 部署。

## 反模式

- **静态 N×M 对比网格** — 将用户限制在你的猜测中；比切换器耗时更长；不给他们实际想要的中间点。
- **滑块之前的数字提示** — "你想要什么饱和度？"是错误的问题；让他们拖。
- **空间决策的数字输入** — 拖拽元素。不透明度用滑块，位置用拖拽手柄。
- **预览数学与烘焙数学之间的漂移** — 当 JS 预览和 Python 烘焙都存在时，移植一个匹配另一个并在已知输入上验证。
- **在生产路由内构建** — 保持脚手架隔离且可简单删除。用 `app/dev/...` 然后清除它。
- **跳过所见即所得细节** — 没有真实字体/容器/背景的预览对用户撒谎。
- **没有结构化方式读回状态** — 没有可复制设置 blob 的工作室迫使代理从截图烘焙，用户手动口述值。每个控件必须通过机器可读导出往返（见架构 #3）。
- **交回一堵文字墙用于"审阅"** — 将长文档/博客粘贴到聊天中（或发送 markdown 文件）让用户回应不是 lookdev。对于任何文档/文案/媒体审阅，构建**文本与媒体标注工作室**（直接编辑 + 高亮 + 评论 + 媒体标记），让用户标注渲染后的制品，标注作为补丁往返。用户必须阅读并在聊天中回复的无聊文字堆正是此技能要替代的东西。

## 工作示例

一个工作示例 — 图像处理工作室：
提取 Lab-k-means 主色板及覆盖率百分比，公开滑块
（分辨率、着色、饱和度、间隙、字形、对比度）、各频段颜色选择器、亮度-vs-最近映射切换、复制设置 JSON
按钮，以及一个 `--bake-json` Python 路径，用与 JS 预览
完全相同的数学将选定状态渲染为提交的 PNG/WebP。预览
画布与生产的缩略图和主图形状完全匹配。

## 相关（工作室/叙事家族）

lookdev 是两个旗舰叙事之一 — **人在回路中**（你，人类，判断和
调优）。其确定性叙事的兄弟是 deterministic-design（渲染 → *测量*
UI，数字而非感觉）。它链接的家族：

- **deterministic-design** — 另一个旗舰；确定性地测量/判断设计输出。
- **screenstudio-alternative** — 人在回路中的视频/演示润色工作室（NLE 时间线）。
- **macos-screen-recorder** — 捕获工作室会话或演示（显示器 + 系统音频）。
- **lookdev-auto** — *自动化*对应物：视觉模型代替你判断。
  lookdev 论点的对立面 — 当没有人类坐镇循环时使用。

## 局限性

- Lookdev 仅在用户可以检视或标注渲染变体时有用；对于小型确定性编辑来说是过度设计。
- 工作室必须忠实地镜像生产字体、媒体、容器和约束，否则选定的设置可能具有误导性。
- 人类偏好仍然是真相来源，因此工作流无法保证普遍"最佳"的设计或媒体处理。

