文件
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

139 行
4.8 KiB
TypeScript

import { CSSProperties } from 'react';
import { Edge as FlowEdge, MarkerType, Node as FlowNode } from '@xyflow/react';
import { Canvas, CanvasColor, CanvasEdge, CanvasNode, CanvasNodeSide } from '@joplin/lib/services/whiteboard/jsoncanvas';
export type WhiteboardNodeData = {
canvasNode: CanvasNode;
};
export type WhiteboardFlowNode = FlowNode<WhiteboardNodeData>;
// The `color` field mirrors CanvasEdge.color but is stored at the flow-edge
// data level (like `label` on the flow edge itself) so live edits reflect the
// current value instead of the value captured when the edge was first loaded.
export type WhiteboardFlowEdge = FlowEdge<{ canvasEdge: CanvasEdge; color?: CanvasColor }>;
const flowTypeForCanvasType = (type: CanvasNode['type']): string => {
switch (type) {
case 'text': return 'wbText';
case 'file': return 'wbFile';
case 'link': return 'wbLink';
case 'group': return 'wbGroup';
}
};
export const canvasNodeToFlowNode = (n: CanvasNode): WhiteboardFlowNode => {
const isGroup = n.type === 'group';
// React Flow forces `pointerEvents: 'all'` as an inline style on the node
// wrapper, which beats any CSS class rule. For groups we override it back
// to 'none' via the node's own `style` (which React Flow spreads after its
// own pointerEvents). Children re-enable pointer-events selectively, so
// only the handle / label actually catches mouse events.
const style: CSSProperties = { width: n.width, height: n.height };
if (isGroup) style.pointerEvents = 'none';
return {
id: n.id,
type: flowTypeForCanvasType(n.type),
position: { x: n.x, y: n.y },
data: { canvasNode: n },
style,
width: n.width,
height: n.height,
selectable: true,
draggable: true,
// Restrict drag/select initiation to the explicit handle and label so
// clicks elsewhere bubble through to the pane (panning) or to a card
// on top (interacting with it).
...(isGroup ? { dragHandle: '.whiteboard-group-handle' } : {}),
};
};
const sideToHandle = (side: CanvasNodeSide | undefined): string | undefined => {
if (!side) return undefined;
return side;
};
// markerUnits userSpaceOnUse keeps arrowhead size fixed regardless of edge
// stroke width — selected edges have a thicker stroke and would otherwise
// render a proportionally bigger arrow.
const arrowMarker = () => ({ type: MarkerType.ArrowClosed, width: 27, height: 27, markerUnits: 'userSpaceOnUse' });
export interface CanvasToFlowResult {
nodes: WhiteboardFlowNode[];
edges: WhiteboardFlowEdge[];
}
export const canvasToFlow = (canvas: Canvas): CanvasToFlowResult => {
const nodes: WhiteboardFlowNode[] = canvas.nodes.map(canvasNodeToFlowNode);
const edges: WhiteboardFlowEdge[] = canvas.edges.map(e => ({
id: e.id,
source: e.fromNode,
target: e.toNode,
sourceHandle: sideToHandle(e.fromSide),
targetHandle: sideToHandle(e.toSide),
label: e.label,
data: { canvasEdge: e, color: e.color },
type: 'default',
markerStart: e.fromEnd === 'arrow' ? arrowMarker() : undefined,
markerEnd: e.toEnd === 'none' ? undefined : arrowMarker(),
}));
return { nodes, edges };
};
const handleToSide = (handle?: string | null): CanvasNodeSide | undefined => {
if (handle === 'top' || handle === 'right' || handle === 'bottom' || handle === 'left') return handle;
return undefined;
};
export const flowToCanvas = (
flowNodes: WhiteboardFlowNode[],
flowEdges: WhiteboardFlowEdge[],
): Canvas => {
const nodes: CanvasNode[] = flowNodes.map(fn => {
const orig = fn.data?.canvasNode;
const width = (typeof fn.width === 'number' ? fn.width : (typeof fn.style?.width === 'number' ? fn.style.width : orig?.width)) ?? 200;
const height = (typeof fn.height === 'number' ? fn.height : (typeof fn.style?.height === 'number' ? fn.style.height : orig?.height)) ?? 100;
const base = {
id: fn.id,
x: fn.position.x,
y: fn.position.y,
width,
height,
};
if (!orig) {
return { ...base, type: 'text', text: '' };
}
return { ...orig, ...base };
});
// JSONCanvas z-order is array order: first = bottom, last = top. Groups
// are visual containers and must render behind regular nodes, so we sort
// them to the front of the array on serialize.
nodes.sort((a, b) => {
if (a.type === 'group' && b.type !== 'group') return -1;
if (a.type !== 'group' && b.type === 'group') return 1;
return 0;
});
const edges: CanvasEdge[] = flowEdges.map(fe => {
const orig = fe.data?.canvasEdge;
const fromEnd: CanvasEdge['fromEnd'] = fe.markerStart ? 'arrow' : 'none';
const toEnd: CanvasEdge['toEnd'] = fe.markerEnd ? 'arrow' : 'none';
const color = fe.data?.color;
return {
id: fe.id,
fromNode: fe.source,
toNode: fe.target,
fromSide: handleToSide(fe.sourceHandle),
toSide: handleToSide(fe.targetHandle),
label: typeof fe.label === 'string' ? fe.label : (orig?.label),
...(color ? { color } : {}),
fromEnd,
toEnd,
};
});
return { nodes, edges };
};