- Add actions-schedules.ts and data-access-schedules.ts for schedule management - Add AI differentiation, AI feedback, consistency check dialogs - Add attachment-picker, curriculum-heatmap, print-view, version-diff-view - Add lesson-plan-mobile-view and schedule-dialog components - Add lib: ai-differentiation, ai-feedback, auto-layout, consistency-check, curriculum-coverage, export, version-diff - Add history-slice hook for version history - Update existing components, hooks, providers, services, types - Add teacher lesson-plans heatmap and library pages
83 lines
2.3 KiB
TypeScript
83 lines
2.3 KiB
TypeScript
/**
|
||
* V5-8:画布自动布局
|
||
*
|
||
* 使用 dagre 计算 DAG 布局,将教学节点按流程关系自动排列。
|
||
* 仅对可拖动的教学节点(非正文节点)布局,正文节点位置保持不变。
|
||
*/
|
||
import dagre from "@dagrejs/dagre";
|
||
import type {
|
||
AnyLessonPlanNode,
|
||
AnyLessonPlanEdge,
|
||
} from "../types";
|
||
|
||
export interface AutoLayoutOptions {
|
||
/** 布局方向:TB(上→下)/ LR(左→右),默认 TB */
|
||
direction?: "TB" | "LR";
|
||
/** 节点宽度,默认 240 */
|
||
nodeWidth?: number;
|
||
/** 节点高度,默认 120 */
|
||
nodeHeight?: number;
|
||
/** 节点水平间距,默认 40 */
|
||
rankSep?: number;
|
||
/** 节点垂直间距,默认 60 */
|
||
nodeSep?: number;
|
||
}
|
||
|
||
/**
|
||
* 计算自动布局,返回每个节点的新位置(含原始位置作为兜底)
|
||
*/
|
||
export function computeAutoLayout(
|
||
nodes: AnyLessonPlanNode[],
|
||
edges: AnyLessonPlanEdge[],
|
||
options: AutoLayoutOptions = {},
|
||
): Map<string, { x: number; y: number }> {
|
||
const {
|
||
direction = "TB",
|
||
nodeWidth = 240,
|
||
nodeHeight = 120,
|
||
rankSep = 60,
|
||
nodeSep = 40,
|
||
} = options;
|
||
|
||
const result = new Map<string, { x: number; y: number }>();
|
||
|
||
if (nodes.length === 0) return result;
|
||
|
||
const g = new dagre.graphlib.Graph();
|
||
g.setGraph({ rankdir: direction, ranksep: rankSep, nodesep: nodeSep });
|
||
g.setDefaultEdgeLabel(() => ({}));
|
||
|
||
// 仅对教学节点布局(正文节点固定位置)
|
||
const layoutableNodes = nodes.filter((n) => n.type !== "textbook_content");
|
||
const layoutableIds = new Set(layoutableNodes.map((n) => n.id));
|
||
|
||
for (const node of layoutableNodes) {
|
||
g.setNode(node.id, { width: nodeWidth, height: nodeHeight });
|
||
}
|
||
|
||
// 仅添加两端都可布局的 flow 边
|
||
for (const edge of edges) {
|
||
if (edge.type !== "flow") continue;
|
||
if (!layoutableIds.has(edge.source) || !layoutableIds.has(edge.target)) continue;
|
||
g.setEdge(edge.source, edge.target);
|
||
}
|
||
|
||
// 孤立节点(无边)也要 setNode,dagre 会自动排列
|
||
dagre.layout(g);
|
||
|
||
for (const node of layoutableNodes) {
|
||
const laid = g.node(node.id);
|
||
if (laid) {
|
||
// dagre 返回中心点,React Flow 使用左上角,需减去半宽/半高
|
||
result.set(node.id, {
|
||
x: laid.x - nodeWidth / 2,
|
||
y: laid.y - nodeHeight / 2,
|
||
});
|
||
} else {
|
||
result.set(node.id, node.position);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|