Files
NextEdu/src/modules/lesson-preparation/lib/auto-layout.ts
SpecialX 25dca843be feat(lesson-preparation): major update with AI features, schedules, and new components
- 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
2026-07-04 10:22:10 +08:00

83 lines
2.3 KiB
TypeScript
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.
/**
* 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);
}
// 孤立节点(无边)也要 setNodedagre 会自动排列
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;
}