Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/BOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,4 @@
| 导出历史进导出标签(画布重设计稿「页签去重」表:导出历史 → 导出标签,第二批) | 🅿️ | PR-6 页签去重撤掉画布底栏时,底栏的「导出历史」页签本来就只画了标签、没有内容,随底栏一起删了,没有搬。要做:在导出标签里列历次导出(文件、时间、镜数、用的是哪些 take),数据来源要先定——仓库眼下不记导出历史(导出服务只产出文件),得先有一张导出记录表与对应的前向迁移;交付目录里写出的 metadata.json(delivery_metadata.dart)能回读一部分,但只覆盖「交付」,不覆盖普通的「导出 mp4」 |
| 检查器「历史」页的「固定」:稿上 take 行带图钉,固定的那张「往下游流」。现在没有固定这个数据模型——下游用哪张 take 由连线的源节点决定,所以历史页改画「在用」(有出边的 take:首帧 / 尾帧 / 参考),不画图钉 | 🅿️ | 2026-10-14 画布页去重 PR。要做得先定语义:固定 = 替换下游所有引用它兄弟 take 的边的源?还是节点级的「当前 take」字段(要加列 + 迁移)? |
| 任务弹层的「耗时」:稿上行 meta 写「万相 2.2 · 已用 12 秒 / 完成 · 18 秒」。JobState 没有起止时间(原渲染队列同样因此去掉了耗时列),所以弹层只写 provider · 状态 | 🅿️ | 2026-10-14 画布页去重 PR。要做得给 JobState 加 startedAt / finishedAt(队列编排处打点),注意持久化的 jobs 表与启动孤儿回收 |
| 画布右键菜单的剪切 / 复制 / 粘贴、导入媒体(画布重设计稿 › 画布交互「右键菜单」:空白处「粘贴 Ctrl V」「导入媒体… Ctrl U」,节点「剪切 Ctrl X」「复制 Ctrl C」) | 🅿️ | 2026-10-15 新建菜单 PR(PR-8a)按「无后端不画」不放:画布没有剪贴板(节点 / 连线的序列化载荷、粘贴时的 id 重映射与落点规则都没有),也没有媒体导入(把本地图片 / 视频拷进项目目录、落成 result 节点的那条写入路径)。空白处右键菜单这一版只有新建… / 导入脚本… / 全选 / 适应视图。要做先定剪贴板载荷(可复用 LB-12 导入重映射器的 id 重映射)与粘贴落点,再补媒体导入的写入服务;菜单届时直接加项 |
4 changes: 2 additions & 2 deletions docs/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,8 @@ lib/
│ ├── canvas/ # Node canvas
│ │ ├── models/
│ │ ├── providers/ # Riverpod ViewModels; canvas_compact_cards.dart(缩放 < 0.6 时节点卡只画类型图标——只在跨阈值时通知,缩放时不逐帧重建卡片);left_rail_state.dart(PR-4 画布左栏:停在哪一页〔大纲 / 素材 / 角色〕+ 面板展开 / 收起,非 autoDispose——「管理」链接与 Ctrl+\ / ⌘\ 都是跨子树来开合 / 切页;取代 P4 的 project_panel_tab.dart)+ character_usage.dart(角色 id → 引用它的 config 节点,只数 config、跨全项目画布、按节点 id 去重;数据源复用 galleryGraphProvider,挂/摘角色后由 CharactersSection 定点 invalidate)+ canvas_bootstrap_controller.dart(短剧示例:单事务建项目/画布/泳道/config 节点,并把 assets/samples/ 的两张打包成片落盘 + 种成 result 节点——P7 把内置示例页并进来了;成片 best-effort,读不出来就少种两个节点)+ inspector_submit_controller.dart(提交四态 + type_config 单写入链 + 自动保存失败态;并实现 `PendingWriteFlushable`、在 build 里注册进 `pendingWriteRegistryProvider`——退出序列在关连接池之前冲一次挂起的防抖 patch 与 `_unsaved`,BOARD 第 201 行)+ canvas_tool_mode.dart(浮动工具条的工具态:选择 / 平移 + 按住空格临时平移,两个来源分开记、按画布 autoDispose;实际是否平移 = 二者取或,舞台指针闸 / 手形光标 / 工具条高亮共读 canvasPanActiveProvider)+ inspector_tab.dart(PR-6 检查器停在「属性」还是「历史」;autoDispose,换选中节点不重置)
│ │ ├── util/ # incl. narrative_order.dart (SB-5 chain ordering) + node_artifacts.dart (node → latest result) + camera_labels.dart (运镜 / 景别 / 机位角度 / 焦段 → ARB 文案 + parseCameraMovement + shotLanguageSummary) + batch_slot_view.dart (P4: slot 行 → 四种呈现态 promoted/success/error/generating 的唯一判据 + canPromote/aspectRatio) + ref_image_budget.dart (CH-2②: 参考图配额纯判据——边连 reference 入边 + 挂载角色图合计 vs maxRefImages,算出"谁被截掉几张";检查器提交前点名、生成侧落 warn 用同一口径。首/尾帧不占配额) + edge_style.dart(连线外观的唯一判据:叙事 / 首帧 / 尾帧 / 参考 / 产出五种线型、颜色槽、标签,选中只换色)+ lineage_edges.dart(产出线由 source_node_id 推导、不入库不可点不可删;端口占用判据)+ result_placement.dart(成片落点:config 右侧一列、第 k 张往下错开;与 config 同道) + prompt_bar_width.dart(提示词条宽 = 画布宽 × 0.6,夹在 480–880;窄于 512 贴边留 16)+ canvas_tool_actions.dart(浮动工具条与单键快捷键 V/I/G/T/F/L 共用的唯一实现:视口中心建节点 / 建泳道 / 切连线 / 选择工具)+ outline_model.dart(PR-4 左栏大纲的分组纯函数:链上配置节点按泳道分组〔组序 = 泳道 sortOrder、组内 = 叙事链序〕→ 未分泳道 → 未入链〔按位置、不编号〕,名称 / 提示词筛选;narrativeChainMembers / narrativeSequenceIndex 是「在链上」与链序号的唯一判据,⌘K 镜头组共用)+ viewport_focus.dart(保持缩放、把节点渲染矩形〔世界 + 分道位移〕移到视口中心的纯函数)
│ │ └── widgets/ # canvas_screen.dart(左栏 | 舞台 + 浮层 | 检查器;PR-6 页签去重撤掉了舞台上方的画布头与底部的渲染队列——画布名在面包屑、缩放比例在缩放条、任务进状态栏胶囊,舞台上沿贴外壳标签栏、下沿贴状态栏)+ canvas_toolbar.dart(舞台顶部居中的浮动工具条:选择 · 平移 | 图像 · 视频 · 文本 · 分镜 · 泳道 · 连线 | 更多〔导入脚本〕;取代原左侧 37px 工具条与右下角加号)+ canvas_zoom_bar.dart(舞台左下的浮动缩放条)+ canvas_inspector_panel.dart(右侧检查器,页签「属性 / 历史」,原「状态」页签已删)+ inspector_history_panel.dart(检查器「历史」页:选中节点的 take 列表——配置节点列名下全部产物、产物节点列同源兄弟并高亮自己;take 按时间正序编号;有 data 出边的标「在用」;点行 = 单选 + 居中)+ canvas_shortcuts.dart(删除 / Esc / 全选 / 缩放 + 工具单键 V/H/I/G/T/F/L,键位常量 CanvasToolKeys 与工具条 tooltip 同源;按住空格临时平移走 Focus.onKeyEvent,只在画布本身持焦时接管;Ctrl+\ / ⌘\ 开合左栏)。PR-4 左栏 left_rail/:canvas_left_rail.dart(43px 图标导航条:大纲 / 素材 / 角色三页键 + 快捷键〔开设置的快捷键页〕+ 收起;剧本 / 帮助无后端不画)/ left_rail_panel.dart(241 面板宿主,收起时不占宽)/ outline_panel.dart(泳道分组的镜头行 + 产物子行 + 生成中子行,状态走 node_card 的 nodeStatusOf;点行 = 单选 + 移到视口中心)/ canvas_switcher.dart(大纲面板头的画布切换胶囊,接替旧画布树;同项目 openCanvas 不带 withProject)/ assets_panel.dart + assets_model.dart(本画布 / 项目全部 + 类型 chip + 两列网格,数据复用画廊 galleryGraphProvider / galleryController;点格子 = 当前画布定位或走画廊「在画布中定位」)/ left_panel_parts.dart(面板头 + 筛选框 + 说明行)/ left_rail_actions.dart(focusNodeOnCanvas:单选 + 居中)。P4 批量:batch_results_grid.dart(检查器内联 2×N 网格,宿主 image_result_inspector.dart 只剩一层 s12 内边距——宽度跟面板走)/ batch_compare_overlay.dart(全屏并排对比;「叠加对比」是禁用标签,不做清单)/ batch_slot_parts.dart(BatchSlotImage + BatchTappable + runBatchSlotAction——slot 动作的统一执行口,只捕具体失败类型并落 toast)。P4 角色:character_library_panel.dart(左栏角色页,PR-4 起顶上是面板头 + 「搜索角色…」筛选;行 = 缩略图 + 名 + 「N 张参考图 · M 处引用」+ ⋯ 菜单)/ character_edit_dialog.dart(改名 + 描述 + 参考图增删拖排 + 「被引用」)
│ │ ├── util/ # incl. narrative_order.dart (SB-5 chain ordering) + node_artifacts.dart (node → latest result) + camera_labels.dart (运镜 / 景别 / 机位角度 / 焦段 → ARB 文案 + parseCameraMovement + shotLanguageSummary) + batch_slot_view.dart (P4: slot 行 → 四种呈现态 promoted/success/error/generating 的唯一判据 + canPromote/aspectRatio) + ref_image_budget.dart (CH-2②: 参考图配额纯判据——边连 reference 入边 + 挂载角色图合计 vs maxRefImages,算出"谁被截掉几张";检查器提交前点名、生成侧落 warn 用同一口径。首/尾帧不占配额) + edge_style.dart(连线外观的唯一判据:叙事 / 首帧 / 尾帧 / 参考 / 产出五种线型、颜色槽、标签,选中只换色)+ lineage_edges.dart(产出线由 source_node_id 推导、不入库不可点不可删;端口占用判据)+ result_placement.dart(成片落点:config 右侧一列、第 k 张往下错开;与 config 同道) + prompt_bar_width.dart(提示词条宽 = 画布宽 × 0.6,夹在 480–880;窄于 512 贴边留 16)+ canvas_tool_actions.dart(浮动工具条与单键快捷键 V/I/G/T/F/L 共用的唯一实现:视口中心建节点 / 建泳道 / 切连线 / 选择工具;PR-8a 起还有新建菜单的动作:在菜单锚点建节点并连好线〔先建节点再连线,边写失败回滚删节点 + 错误卡,成功挂定位请求单选〕、端口拖到卡上连线〔走 linkTo〕、全选、适应视图)+ create_menu_model.dart(PR-8a 新建菜单项判据:带连线上下文时源带图像产物出首帧 / 尾帧 / 参考三项、配置节点〔shot / image / video〕出「下一镜」;不带上下文出图像配置 / 视频配置 / 文本 / 镜头 / 泳道;搜索过滤)+ drop_placement.dart(落点换算:拖卡片落点与新建菜单落点共用的泳道归属判定 resolveLaneDrop、视口点 → 世界坐标、舞台上一点落在哪张卡上)+ outline_model.dart(PR-4 左栏大纲的分组纯函数:链上配置节点按泳道分组〔组序 = 泳道 sortOrder、组内 = 叙事链序〕→ 未分泳道 → 未入链〔按位置、不编号〕,名称 / 提示词筛选;narrativeChainMembers / narrativeSequenceIndex 是「在链上」与链序号的唯一判据,⌘K 镜头组共用)+ viewport_focus.dart(保持缩放、把节点渲染矩形〔世界 + 分道位移〕移到视口中心的纯函数)
│ │ └── widgets/ # canvas_screen.dart(左栏 | 舞台 + 浮层 | 检查器;PR-6 页签去重撤掉了舞台上方的画布头与底部的渲染队列——画布名在面包屑、缩放比例在缩放条、任务进状态栏胶囊,舞台上沿贴外壳标签栏、下沿贴状态栏)+ canvas_toolbar.dart(舞台顶部居中的浮动工具条:选择 · 平移 | 图像 · 视频 · 文本 · 分镜 · 泳道 · 连线 | 更多〔导入脚本〕;取代原左侧 37px 工具条与右下角加号)+ canvas_zoom_bar.dart(舞台左下的浮动缩放条)+ canvas_inspector_panel.dart(右侧检查器,页签「属性 / 历史」,原「状态」页签已删)+ inspector_history_panel.dart(检查器「历史」页:选中节点的 take 列表——配置节点列名下全部产物、产物节点列同源兄弟并高亮自己;take 按时间正序编号;有 data 出边的标「在用」;点行 = 单选 + 居中)+ canvas_shortcuts.dart(删除 / Esc / 全选 / 缩放 + 工具单键 V/H/I/G/T/F/L,键位常量 CanvasToolKeys 与工具条 tooltip 同源;按住空格临时平移走 Focus.onKeyEvent,只在画布本身持焦时接管;Ctrl+\ / ⌘\ 开合左栏;Tab = 新建…,同样只在画布本身持主焦点时接管、别处的 Tab 放行给焦点遍历)+ create_node_menu.dart(PR-8a 新建菜单:从出端口拖线到空白处 / 双击空白 / Tab / 空白处右键「新建…」开同一个带搜索的菜单;canvasMenuRequestProvider〔开着哪个菜单、锚点、源节点〕+ canvasWireDragProvider〔拖线实时状态,Esc 取消〕+ CanvasMenuHost〔OverlayPortal 浮层,点外面 / 失焦关〕+ CanvasMenuPanel〔↑↓ / ↵ / Esc〕;空白处右键菜单只有新建… / 导入脚本… / 全选 / 适应视图)。PR-4 左栏 left_rail/:canvas_left_rail.dart(43px 图标导航条:大纲 / 素材 / 角色三页键 + 快捷键〔开设置的快捷键页〕+ 收起;剧本 / 帮助无后端不画)/ left_rail_panel.dart(241 面板宿主,收起时不占宽)/ outline_panel.dart(泳道分组的镜头行 + 产物子行 + 生成中子行,状态走 node_card 的 nodeStatusOf;点行 = 单选 + 移到视口中心)/ canvas_switcher.dart(大纲面板头的画布切换胶囊,接替旧画布树;同项目 openCanvas 不带 withProject)/ assets_panel.dart + assets_model.dart(本画布 / 项目全部 + 类型 chip + 两列网格,数据复用画廊 galleryGraphProvider / galleryController;点格子 = 当前画布定位或走画廊「在画布中定位」)/ left_panel_parts.dart(面板头 + 筛选框 + 说明行)/ left_rail_actions.dart(focusNodeOnCanvas:单选 + 居中)。P4 批量:batch_results_grid.dart(检查器内联 2×N 网格,宿主 image_result_inspector.dart 只剩一层 s12 内边距——宽度跟面板走)/ batch_compare_overlay.dart(全屏并排对比;「叠加对比」是禁用标签,不做清单)/ batch_slot_parts.dart(BatchSlotImage + BatchTappable + runBatchSlotAction——slot 动作的统一执行口,只捕具体失败类型并落 toast)。P4 角色:character_library_panel.dart(左栏角色页,PR-4 起顶上是面板头 + 「搜索角色…」筛选;行 = 缩略图 + 名 + 「N 张参考图 · M 处引用」+ ⋯ 菜单)/ character_edit_dialog.dart(改名 + 描述 + 参考图增删拖排 + 「被引用」)
│ ├── command_palette/ # ⌘K/Ctrl+K command palette (PL-1; app-level, wraps InkShell). Screens 稿第 4 屏右:跨实体搜索,三组「镜头 · 当前画布 / 产物 / 动作」
│ │ ├── palette_entry.dart # PaletteEntry / PaletteChoice(hand-written value objects; run = ↵ 打开, locate = ⌘↵ 在画布中定位)
│ │ ├── palette_search.dart # buildPaletteEntries — 镜头组只搜当前画布已加载节点(不为搜索读库)、产物组复用 galleryController、动作组 = buildCommandActions
Expand Down
21 changes: 14 additions & 7 deletions lib/features/canvas/providers/canvas_focus_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,31 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// 一条定位请求:在 [canvasId] 上单选 [nodeId] 并把它移到视口中心。
/// 一条定位请求:在 [canvasId] 上单选 [nodeId];[center] 为真时再把它移到视口中心。
@immutable
class CanvasFocusRequest {
const CanvasFocusRequest({required this.canvasId, required this.nodeId});
const CanvasFocusRequest({required this.canvasId, required this.nodeId, this.center = true});

final String canvasId;
final String nodeId;

/// 落地时要不要把节点移到视口中心。「定位」类入口要(节点可能在视口外);新建菜单建出来的
/// 节点就落在用户刚点的地方,挪相机只会让人找不着北——那里传 false,只单选。
final bool center;

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CanvasFocusRequest && other.canvasId == canvasId && other.nodeId == nodeId;
other is CanvasFocusRequest &&
other.canvasId == canvasId &&
other.nodeId == nodeId &&
other.center == center;

@override
int get hashCode => Object.hash(canvasId, nodeId);
int get hashCode => Object.hash(canvasId, nodeId, center);

@override
String toString() => 'CanvasFocusRequest($canvasId, $nodeId)';
String toString() => 'CanvasFocusRequest($canvasId, $nodeId, center: $center)';
}

final canvasFocusRequestProvider =
Expand All @@ -40,8 +47,8 @@ class CanvasFocusRequestController extends Notifier<CanvasFocusRequest?> {
CanvasFocusRequest? build() => null;

/// 挂起一条请求;已有未消费的请求时以新的为准(用户最后一次点的那个)。
void request({required String canvasId, required String nodeId}) =>
state = CanvasFocusRequest(canvasId: canvasId, nodeId: nodeId);
void request({required String canvasId, required String nodeId, bool center = true}) =>
state = CanvasFocusRequest(canvasId: canvasId, nodeId: nodeId, center: center);

/// 消费 [request]:只清它自己——落地在帧后,期间可能已经挂起了新的一条。
void consume(CanvasFocusRequest request) {
Expand Down
3 changes: 3 additions & 0 deletions lib/features/canvas/providers/canvas_nodes_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class CanvasNodesController
required CanvasNodeType type,
NodeRole role = NodeRole.config,
String? sourceNodeId,
String? laneId,
Offset position = Offset.zero,
Size? size,
Map<String, Object?> typeConfig = const <String, Object?>{},
Expand All @@ -95,6 +96,7 @@ class CanvasNodesController
nodeRole: role.name,
label: label,
sourceNodeId: sourceNodeId,
laneId: laneId,
positionX: position.dx,
positionY: position.dy,
width: nodeSize.width,
Expand All @@ -108,6 +110,7 @@ class CanvasNodesController
role: role,
canvasId: canvasId,
sourceNodeId: sourceNodeId,
laneId: laneId,
typeConfig: typeConfig,
position: position,
size: nodeSize,
Expand Down
Loading
Loading