mirror of
https://github.com/hansjone/mind-map.git
synced 2026-10-08 23:33:24 +08:00
feat: add topology view mode with router icons and link-style edges
Canvas-global nodeViewMode (bubble/card/topology) can be set at create or via prefs; topology draws NE routers with straight blue center links and exposes router icon to agents. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
200c8bd450
commit
74f0d8e78a
15 changed files with 322 additions and 103 deletions
|
|
@ -139,7 +139,7 @@ export function mindmapToolDefinitions(getBase, getCanvasIdForSession, setActive
|
|||
{
|
||||
name: 'mindmap_create_canvas',
|
||||
description:
|
||||
'Create a brand-new mind-map canvas (new id; does NOT overwrite existing canvases), bind it to this chat session, and open it in the 思维导图 tab. Canvas starts visually empty. Top-level nodes: omit parentId.',
|
||||
'Create a brand-new mind-map canvas (new id; does NOT overwrite existing canvases), bind it to this chat session, and open it in the 思维导图 tab. Canvas starts visually empty. Top-level nodes: omit parentId. Optional nodeViewMode (bubble|card|topology) is a canvas-global preference (can also be changed later via set_prefs).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
|
|
@ -150,6 +150,12 @@ export function mindmapToolDefinitions(getBase, getCanvasIdForSession, setActive
|
|||
description:
|
||||
'Optional internal label / title sync only — there is no visible root node. Prefer title alone.',
|
||||
},
|
||||
nodeViewMode: {
|
||||
type: 'string',
|
||||
enum: ['bubble', 'card', 'topology'],
|
||||
description:
|
||||
'Canvas-global chrome (bubble|card|topology). Prefer setting when creating a topology map; default card. topology = router icons + straight blue links.',
|
||||
},
|
||||
},
|
||||
},
|
||||
output: { schema: OBJECT, render: (_a, v) => jsonText(v) },
|
||||
|
|
@ -636,6 +642,7 @@ There is NO visible root node. Canvas title is metadata only. Internally top-lev
|
|||
|
||||
When the user asks to build a map (e.g. 「输出红楼梦人物关系图」):
|
||||
1. mindmap_create_canvas({ title: "红楼梦人物关系" }) — ALWAYS creates a NEW canvas id (never overwrites an old one). Opens the right tab on that new canvas.
|
||||
Topology diagrams: mindmap_create_canvas({ title: "…拓扑", nodeViewMode: "topology" }) then nodes with icon:"router".
|
||||
2. mindmap_get_spatial_context — orient on the NEW empty canvas
|
||||
3. mindmap_apply_direct({ ops: [ {type:"batch_create", nodes:[...]}, {type:"auto_fit"} ] })
|
||||
4. Keep adding in small batches; never dump a full tree JSON overwrite
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ function promoteChildrenOnDelete(g: MutableGraph, nodeId: string): Op[] {
|
|||
export function createEmptyGraph(
|
||||
title = "未命名画布",
|
||||
rootText?: string,
|
||||
prefsOverride?: Partial<CanvasPrefs>,
|
||||
): MutableGraph {
|
||||
const canvasId = newId("c");
|
||||
const nodeId = newId("n");
|
||||
|
|
@ -137,6 +138,7 @@ export function createEmptyGraph(
|
|||
showHoverCard: true,
|
||||
inspectorMode: "selection",
|
||||
nodeViewMode: "card",
|
||||
...prefsOverride,
|
||||
};
|
||||
const canvas: CanvasMeta = {
|
||||
id: canvasId,
|
||||
|
|
|
|||
|
|
@ -276,8 +276,12 @@ CREATE INDEX IF NOT EXISTS idx_snapshots_canvas ON snapshots(canvas_id);
|
|||
};
|
||||
}
|
||||
|
||||
createCanvas(title?: string, rootText?: string): MutableGraph {
|
||||
const g = createEmptyGraph(title ?? "未命名画布", rootText);
|
||||
createCanvas(
|
||||
title?: string,
|
||||
rootText?: string,
|
||||
prefsOverride?: Partial<CanvasMeta["prefs"]>,
|
||||
): MutableGraph {
|
||||
const g = createEmptyGraph(title ?? "未命名画布", rootText, prefsOverride);
|
||||
this.persistGraph(g, true);
|
||||
this.cache.set(g.canvas.id, g);
|
||||
return g;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export type LayoutInput = {
|
|||
edges: MindEdge[];
|
||||
focusNodeId: string;
|
||||
density?: Density;
|
||||
/** Canvas-wide bubble vs card chrome (affects box sizes). */
|
||||
/** Canvas-wide bubble vs card vs topology chrome (affects box sizes). */
|
||||
nodeViewMode?: NodeViewMode;
|
||||
/** Previous frame positions — reused when hierarchy structure is unchanged. */
|
||||
prevPositions?: PositionMap;
|
||||
|
|
@ -52,8 +52,18 @@ export function nodeLayoutSize(
|
|||
| "pinned"
|
||||
>,
|
||||
dens: Dens,
|
||||
opts?: { asCard?: boolean },
|
||||
opts?: { asCard?: boolean; asTopology?: boolean },
|
||||
): { w: number; h: number } {
|
||||
if (opts?.asTopology) {
|
||||
const icon = dens.nodeW >= 200 ? 36 : 30;
|
||||
const caption = dens.nodeW >= 200 ? 20 : 18;
|
||||
const gap = 6;
|
||||
const pad = dens.nodeW >= 200 ? 8 : 6;
|
||||
return {
|
||||
w: Math.max(dens.nodeW >= 200 ? 88 : 72, icon + pad * 2),
|
||||
h: icon + gap + caption + pad,
|
||||
};
|
||||
}
|
||||
let w = dens.nodeW;
|
||||
let h = dens.nodeH;
|
||||
// meta strip (badges / tags / due)
|
||||
|
|
@ -248,7 +258,7 @@ function buildSubtree(
|
|||
children: Map<string, string[]>,
|
||||
dens: Dens,
|
||||
path: Set<string>,
|
||||
asCard: boolean,
|
||||
sizeOpts: { asCard?: boolean; asTopology?: boolean },
|
||||
childFilter?: (childId: string) => boolean,
|
||||
): FlexDatum | null {
|
||||
if (path.has(id)) return null;
|
||||
|
|
@ -266,12 +276,12 @@ function buildSubtree(
|
|||
children,
|
||||
dens,
|
||||
nextPath,
|
||||
asCard,
|
||||
sizeOpts,
|
||||
);
|
||||
if (child) kids.push(child);
|
||||
}
|
||||
}
|
||||
const box = nodeLayoutSize(node, dens, { asCard });
|
||||
const box = nodeLayoutSize(node, dens, sizeOpts);
|
||||
return {
|
||||
id,
|
||||
// flextree size = full box + gutters (claim real space, not a rigid grid cell)
|
||||
|
|
@ -397,6 +407,7 @@ export function hierarchyFingerprint(edges: MindEdge[]): string {
|
|||
export function layout(input: LayoutInput): PositionMap {
|
||||
const dens = DENSITY[input.density ?? "comfortable"];
|
||||
const asCard = input.nodeViewMode === "card";
|
||||
const asTopology = input.nodeViewMode === "topology";
|
||||
const nodesById = new Map(input.nodes.map((n) => [n.id, n]));
|
||||
if (!nodesById.has(input.focusNodeId)) return {};
|
||||
|
||||
|
|
@ -407,7 +418,7 @@ export function layout(input: LayoutInput): PositionMap {
|
|||
const sizes = new Map<string, { w: number; h: number }>();
|
||||
for (const n of input.nodes) {
|
||||
if (n.deletedAt || hidden.has(n.id)) continue;
|
||||
sizes.set(n.id, nodeLayoutSize(n, dens, { asCard }));
|
||||
sizes.set(n.id, nodeLayoutSize(n, dens, { asCard, asTopology }));
|
||||
}
|
||||
|
||||
const prev = input.prevPositions;
|
||||
|
|
@ -501,7 +512,7 @@ export function layout(input: LayoutInput): PositionMap {
|
|||
children,
|
||||
dens,
|
||||
new Set(),
|
||||
asCard,
|
||||
{ asCard, asTopology },
|
||||
(childId) => sideOf.get(childId) === side,
|
||||
);
|
||||
if (!tree?.children?.length) continue;
|
||||
|
|
|
|||
|
|
@ -342,6 +342,12 @@ export const OP_SCHEMA_DOC = {
|
|||
],
|
||||
_comment: "Bulk relation/hierarchy links (max 100). Prefer after nodes exist.",
|
||||
},
|
||||
{
|
||||
type: "create_node",
|
||||
text: "核心交换机",
|
||||
alias: "core_sw",
|
||||
icon: "router",
|
||||
},
|
||||
{
|
||||
type: "create_node",
|
||||
text: "AIOps诊断",
|
||||
|
|
@ -494,9 +500,11 @@ export const OP_SCHEMA_DOC = {
|
|||
restore_node:
|
||||
"Soft-deleted nodes keep their id. Pass that id (or deleted @alias / unique title). Restores compatible incident edges; children already promoted stay under the new parent.",
|
||||
create_node:
|
||||
"Omit parentId for top-level. Optional pos pins immediately. Optional alias. Optional relations[] for outgoing graph edges (not URL links). Optional description object.",
|
||||
"Omit parentId for top-level. Optional pos pins immediately. Optional alias. Optional icon (server|database|cloud|person|folder|doc|link|warning|check|star|gear|globe|router). Optional relations[] for outgoing graph edges (not URL links). Optional description object.",
|
||||
update_node_meta:
|
||||
"Patch note/description/tags/links/imageUrl/dueAt/startAt/accentColor/icon/alias. description is a JSON object (knowledge fields). Null clears scalar fields. links = URL bookmarks.",
|
||||
"Patch note/description/tags/links/imageUrl/dueAt/startAt/accentColor/icon/alias. icon enum same as create_node. description is a JSON object (knowledge fields). Null clears scalar fields. links = URL bookmarks.",
|
||||
set_prefs:
|
||||
"Partial canvas prefs: density, branchColoring, nodeViewMode (bubble|card|topology; canvas-global), showHoverCard, nodeBadges, showRelationEdges, riskProfile, themeId, inspectorMode.",
|
||||
update_edge_meta:
|
||||
"Patch label/tags/note/weight(1-5)/lineStyle(solid|dashed|dotted)/direction(forward|both|none) on an edge.",
|
||||
set_edge_label: "Need edgeId from spatial_context.edges or get_subgraph.",
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ export type NodeBadges = z.infer<typeof NodeBadgesSchema>;
|
|||
export const InspectorModeSchema = z.enum(["selection", "always"]);
|
||||
export type InspectorMode = z.infer<typeof InspectorModeSchema>;
|
||||
|
||||
/** Whole-canvas node chrome: bubble titles vs property cards. */
|
||||
export const NodeViewModeSchema = z.enum(["bubble", "card"]);
|
||||
/** Whole-canvas node chrome: bubble titles vs property cards vs topology icons. */
|
||||
export const NodeViewModeSchema = z.enum(["bubble", "card", "topology"]);
|
||||
export type NodeViewMode = z.infer<typeof NodeViewModeSchema>;
|
||||
|
||||
/** Controlled node icon set (agent/UI). */
|
||||
|
|
@ -47,6 +47,7 @@ export const NodeIconSchema = z.enum([
|
|||
"star",
|
||||
"gear",
|
||||
"globe",
|
||||
"router",
|
||||
]);
|
||||
export type NodeIcon = z.infer<typeof NodeIconSchema>;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue