Connect canonical projects to persistent workspace runtimes and canvas views

This commit is contained in:
2026-11-08 00:07:27 +08:00
parent 8a76e53a9f
commit e2987de21a
27 changed files with 970 additions and 57 deletions
+21 -11
View File
@@ -6,12 +6,15 @@ import { Grip, Maximize2, RotateCw } from "lucide-react";
import { elementCapabilities, type TextStyle } from "@/lib/canvas-elements";
import type { CanvasNode, NodeDirection } from "@/lib/canvas";
import { resizeFromPointer, rotationFromPointer, type NodeTransform, type ResizeDirection } from "@/lib/canvas-geometry";
import type { EditingLease } from "./editing-lease";
import { NodeStyleToolbar } from "./node-style-toolbar";
import { MembershipBadge } from "./frame-feedback";
import { ContentHandles } from "./content-handles";
import styles from "./canvas.module.css";
export const NodeEditing = createContext({
const defaultEditing = {
beginTransform: undefined as undefined | ((id: string) => boolean | void | EditingLease<NodeTransform>), finishTransform: undefined as undefined | (() => void), cancelTransform: undefined as undefined | (() => void),
endEditing: undefined as undefined | ((id: string) => void),
workspace: "", schema: 1 as 1 | 2,
begin: (_id?: string) => {}, select: (_id: string) => {}, edit: (_id: string, _text: string) => {},
branch: (_id: string, _direction: NodeDirection, _point: XYPosition) => {},
@@ -21,8 +24,10 @@ export const NodeEditing = createContext({
frame: (_id: string, _patch: { title?: string; labelMode?: import("@/lib/canvas-elements").FrameLabelMode; presentation?: import("@/lib/canvas-v2").FramePresentation }) => {},
rotating: null as string | null, rotate: (_id: string | null) => {},
resizing: null as string | null, resize: (_id: string | null) => {},
});
type Gesture = { node: CanvasNode; start: XYPosition; direction: ResizeDirection | "rotate"; target: HTMLElement; pointerId: number };
};
type GestureHooks = 'beginTransform' | 'finishTransform' | 'cancelTransform' | 'endEditing';
export const NodeEditing = createContext<Omit<typeof defaultEditing, GestureHooks> & Partial<Pick<typeof defaultEditing, GestureHooks>>>(defaultEditing);
type Gesture = { lease?: EditingLease<NodeTransform>; node: CanvasNode; start: XYPosition; direction: ResizeDirection | "rotate"; target: HTMLElement; pointerId: number };
export function TransformableNode(props: NodeProps<CanvasNode> & { children: ReactNode; className?: string; hideStyleToolbar?: boolean }) {
const { id, data, selected, width, height, children, className = "" } = props;
@@ -54,9 +59,10 @@ export function TransformableNode(props: NodeProps<CanvasNode> & { children: Rea
event.preventDefault(); event.stopPropagation();
const node = flow.getNode(id);
if (!node) return;
editing.begin(id);
const lease = editing.beginTransform?.(id); if (lease === false) return;
if (!editing.beginTransform) editing.begin(id);
if (!node.selected) editing.select(id);
gesture.current = { node, direction, target: event.currentTarget, pointerId: event.pointerId, start: flow.screenToFlowPosition({ x: event.clientX, y: event.clientY }) };
gesture.current = { lease: lease && typeof lease === 'object' ? lease : undefined, node, direction, target: event.currentTarget, pointerId: event.pointerId, start: flow.screenToFlowPosition({ x: event.clientX, y: event.clientY }) };
event.currentTarget.setPointerCapture(event.pointerId);
}
function move(event: PointerEvent<HTMLElement>) {
@@ -64,18 +70,21 @@ export function TransformableNode(props: NodeProps<CanvasNode> & { children: Rea
event.stopPropagation();
const current = flow.screenToFlowPosition({ x: event.clientX, y: event.clientY });
const { node, start, direction } = gesture.current;
editing.transform(id, direction === "rotate"
const change = direction === "rotate"
? { rotation: rotationFromPointer(node, start, current, event.shiftKey) }
: resizeFromPointer(node, direction, { x: current.x - start.x, y: current.y - start.y }, resizing ? event.shiftKey : undefined));
: resizeFromPointer(node, direction, { x: current.x - start.x, y: current.y - start.y }, resizing ? event.shiftKey : undefined);
if (gesture.current.lease) gesture.current.lease.preview(change); else editing.transform(id, change);
}
function cancelGesture() {
const current = gesture.current; if (!current) return;
gesture.current = null;
editing.transform(id, current.direction === "rotate" ? { rotation: current.node.data.rotation || 0 } : { position: current.node.position, width: current.node.width, height: current.node.height });
if (current.lease) current.lease.cancel(); else if (editing.cancelTransform) editing.cancelTransform(); else editing.transform(id, current.direction === "rotate" ? { rotation: current.node.data.rotation || 0 } : { position: current.node.position, width: current.node.width, height: current.node.height });
if (current.target.hasPointerCapture(current.pointerId)) current.target.releasePointerCapture(current.pointerId);
}
function finish(event: PointerEvent<HTMLElement>) {
event.stopPropagation(); gesture.current = null;
if (!gesture.current) return;
event.stopPropagation(); const current = gesture.current; move(event); gesture.current = null;
if (current.lease) current.lease.commit(); else editing.finishTransform?.();
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
}
return <>
@@ -101,8 +110,9 @@ export function TransformableNode(props: NodeProps<CanvasNode> & { children: Rea
onKeyDown={(event) => {
const delta = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[event.key]; if (!delta) return;
event.preventDefault(); event.stopPropagation(); const node = flow.getNode(id); if (!node) return;
editing.begin(id); const scale = (data.viewScale || 1) * (event.ctrlKey || event.metaKey ? 10 : 1);
editing.transform(id, resizeFromPointer(node, direction, { x: delta[0] * scale, y: delta[1] * scale }, event.shiftKey));
const lease = editing.beginTransform?.(id); if (lease === false) return; if (!editing.beginTransform) editing.begin(id); const scale = (data.viewScale || 1) * (event.ctrlKey || event.metaKey ? 10 : 1);
const change = resizeFromPointer(node, direction, { x: delta[0] * scale, y: delta[1] * scale }, event.shiftKey);
if (lease && typeof lease === 'object') { lease.preview(change); lease.commit(); } else { editing.transform(id, change); editing.finishTransform?.(); }
}} />;
})}
{resizing && <span className={styles.rotationHint}>拖动八向手柄 · Shift 固定比例 · Esc 退出</span>}