Files
2026-07-13 13:08:41 +08:00

375 lines
11 KiB
TypeScript

"use client";
import { useEffect, useRef, type RefObject } from "react";
import type Konva from "konva";
import { Transformer } from "react-konva";
const CORNER_HANDLE_SIZE = 14;
const EDGE_HANDLE_LENGTH = 28;
const EDGE_HANDLE_THICKNESS = 9;
const ROTATION_HANDLE_SIZE = 28;
const BOTTOM_CENTER_ROTATION_ANCHOR_OFFSET = 26;
const BOTTOM_CENTER_ROTATION_ANCHOR_ANGLE = 180;
const ROTATION_ICON_SIZE = 18;
const ROTATION_ICON_VIEWBOX_SIZE = 24;
const REFRESH_CW_ICON_PATHS = [
"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",
"M21 3v5h-5",
"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",
"M8 16H3v5",
];
const SHADOW_EVENT_NAMESPACE = ".presentonSelectionShadows";
const BOTTOM_CENTER_ROTATION_ANCHOR_EVENT_NAMESPACE =
".presentonBottomCenterRotationAnchor";
const MULTI_SELECTION_GROUP_DASH = [5, 5];
const MULTI_SELECTION_MEMBER_DASH = [7, 4];
const HORIZONTAL_ONLY_ANCHORS = ["middle-left", "middle-right"];
let refreshCwIconPaths: Path2D[] | null = null;
type SelectionKind = "component" | "multi-component" | "element" | null;
type TemplateV2SelectionTransformersProps = {
nodeRefs: RefObject<Map<string, Konva.Node>>;
parentComponentKey: string | null;
selectedKey: string | null;
selectedKeys?: string[];
selectionKind: SelectionKind;
horizontalResizeOnly?: boolean;
suppressSelectedOutline?: boolean;
};
function drawRotationHandle(context: Konva.Context, shape: Konva.Shape) {
const center = ROTATION_HANDLE_SIZE / 2;
context.beginPath();
context.arc(center, center, center - 1, 0, Math.PI * 2, false);
context.closePath();
context.fillStrokeShape(shape);
context.save();
context.setAttr("strokeStyle", "#111111");
context.setAttr("lineWidth", 2);
context.setAttr("lineCap", "round");
context.setAttr("lineJoin", "round");
context.translate(
center - ROTATION_ICON_SIZE / 2,
center - ROTATION_ICON_SIZE / 2,
);
context.scale(
ROTATION_ICON_SIZE / ROTATION_ICON_VIEWBOX_SIZE,
ROTATION_ICON_SIZE / ROTATION_ICON_VIEWBOX_SIZE,
);
if (!refreshCwIconPaths && typeof Path2D !== "undefined") {
refreshCwIconPaths = REFRESH_CW_ICON_PATHS.map((path) => new Path2D(path));
}
refreshCwIconPaths?.forEach((path) => context.stroke(path));
context.restore();
}
function styleAnchor(anchor: Konva.Rect) {
const name = anchor.name();
const isRotationHandle = name.includes("rotater");
const isHorizontalEdge =
name.includes("top-center") || name.includes("bottom-center");
const isVerticalEdge =
name.includes("middle-left") || name.includes("middle-right");
let width = CORNER_HANDLE_SIZE;
let height = CORNER_HANDLE_SIZE;
if (isHorizontalEdge) {
width = EDGE_HANDLE_LENGTH;
height = EDGE_HANDLE_THICKNESS;
} else if (isVerticalEdge) {
width = EDGE_HANDLE_THICKNESS;
height = EDGE_HANDLE_LENGTH;
} else if (isRotationHandle) {
width = ROTATION_HANDLE_SIZE;
height = ROTATION_HANDLE_SIZE;
}
anchor.setAttrs({
width,
height,
offsetX: width / 2,
offsetY: height / 2,
cornerRadius: Math.min(width, height) / 2,
fill: "#FFFFFF",
stroke: "#E5E7EB",
strokeWidth: 1,
shadowColor: "#101828",
shadowBlur: isRotationHandle ? 5 : 4,
shadowOffsetX: 0,
shadowOffsetY: isRotationHandle ? 2 : 2,
shadowOpacity: isRotationHandle ? 0.16 : 0.13,
shadowForStrokeEnabled: false,
perfectDrawEnabled: false,
});
if (isRotationHandle) anchor.sceneFunc(drawRotationHandle);
}
function drawContextBoundary(context: Konva.Context, shape: Konva.Shape) {
const width = shape.width();
const height = shape.height();
context.save();
context.setAttr("lineWidth", 2);
context.setAttr("lineJoin", "miter");
context.setLineDash([]);
context.setAttr("strokeStyle", "#D9D9DE");
context.beginPath();
context.rect(0, 0, width, height);
context.stroke();
context.setLineDash([4, 8]);
context.setAttr("strokeStyle", "#FFFFFF");
context.beginPath();
context.rect(0, 0, width, height);
context.stroke();
context.restore();
}
function applyContextBoundaryStyle(transformer: Konva.Transformer) {
const back = transformer.findOne<Konva.Rect>(".back");
back?.sceneFunc(drawContextBoundary);
back?.setAttrs({
shadowColor: "#101828",
shadowBlur: 6,
shadowOffsetX: 0,
shadowOffsetY: 1,
shadowOpacity: 0.16,
shadowForStrokeEnabled: true,
perfectDrawEnabled: false,
});
}
function setTransformerShadowsEnabled(
transformers: Array<Konva.Transformer | null>,
enabled: boolean,
) {
transformers.forEach((transformer) => {
if (!transformer) return;
transformer.find<Konva.Rect>("._anchor").forEach((anchor) => {
anchor.shadowEnabled(enabled);
});
transformer.findOne<Konva.Rect>(".back")?.shadowEnabled(enabled);
});
transformers[0]?.getLayer()?.batchDraw();
}
function TemplateV2MultiSelectionMemberOutline({
nodeRefs,
selectedKey,
}: {
nodeRefs: RefObject<Map<string, Konva.Node>>;
selectedKey: string;
}) {
const transformerRef = useRef<Konva.Transformer | null>(null);
useEffect(() => {
const transformer = transformerRef.current;
if (!transformer) return;
const node = nodeRefs.current?.get(selectedKey);
transformer.nodes(node ? [node] : []);
transformer.getLayer()?.batchDraw();
return () => {
transformer.nodes([]);
transformer.getLayer()?.batchDraw();
};
}, [nodeRefs, selectedKey]);
return (
<Transformer
ref={transformerRef}
anchorSize={0}
borderDash={MULTI_SELECTION_MEMBER_DASH}
borderEnabled
borderStroke="#7A5AF8"
borderStrokeWidth={1}
enabledAnchors={[]}
listening={false}
resizeEnabled={false}
rotateEnabled={false}
rotateLineVisible={false}
/>
);
}
function getBottomCenterRotationAnchorAngle(
node: Konva.Node | null | undefined,
) {
return (
BOTTOM_CENTER_ROTATION_ANCHOR_ANGLE - (node?.getAbsoluteRotation() ?? 0)
);
}
function applyBottomCenterRotationAnchor(
transformer: Konva.Transformer | null,
node: Konva.Node | null | undefined,
) {
if (!transformer) return;
transformer.rotateAnchorAngle(getBottomCenterRotationAnchorAngle(node));
transformer.rotateAnchorOffset(BOTTOM_CENTER_ROTATION_ANCHOR_OFFSET);
transformer.forceUpdate();
transformer.getLayer()?.batchDraw();
}
export function TemplateV2SelectionTransformers({
nodeRefs,
parentComponentKey,
selectedKey,
selectedKeys,
selectionKind,
horizontalResizeOnly = false,
suppressSelectedOutline = false,
}: TemplateV2SelectionTransformersProps) {
const selectedTransformerRef = useRef<Konva.Transformer | null>(null);
const contextTransformerRef = useRef<Konva.Transformer | null>(null);
const isMultiComponentSelection = selectionKind === "multi-component";
const selectedNode =
selectionKind === "component" && selectedKey
? nodeRefs.current?.get(selectedKey)
: null;
const bottomCenterRotationAnchorAngle =
getBottomCenterRotationAnchorAngle(selectedNode);
const multiSelectionMemberKeys =
isMultiComponentSelection && !suppressSelectedOutline
? selectedKeys ?? []
: [];
useEffect(() => {
const keys = selectedKeys?.length
? selectedKeys
: selectedKey
? [selectedKey]
: [];
const selectedNodes = suppressSelectedOutline
? []
: keys.flatMap((key) => {
const node = nodeRefs.current?.get(key);
return node ? [node] : [];
});
const parentComponentNode = parentComponentKey
? nodeRefs.current?.get(parentComponentKey)
: null;
const selectedTransformer = selectedTransformerRef.current;
if (selectedTransformer) {
selectedTransformer.nodes(selectedNodes);
}
const contextTransformer = contextTransformerRef.current;
if (contextTransformer) {
contextTransformer.nodes(parentComponentNode ? [parentComponentNode] : []);
applyContextBoundaryStyle(contextTransformer);
}
const selectedRotationNode =
selectionKind === "component" && selectedNodes.length === 1
? selectedNodes[0]
: null;
const refreshBottomCenterRotationAnchor = () => {
if (selectedTransformer?.isTransforming()) return;
applyBottomCenterRotationAnchor(selectedTransformer, selectedRotationNode);
};
refreshBottomCenterRotationAnchor();
selectedTransformer?.getLayer()?.batchDraw();
const transformers = [selectedTransformer, contextTransformer];
const dragNodes = Array.from(
new Set([...selectedNodes, parentComponentNode].filter(Boolean)),
) as Konva.Node[];
const disableShadows = () =>
setTransformerShadowsEnabled(transformers, false);
const enableShadows = () =>
setTransformerShadowsEnabled(transformers, true);
dragNodes.forEach((node) => {
node.on(`dragstart${SHADOW_EVENT_NAMESPACE}`, disableShadows);
node.on(`dragend${SHADOW_EVENT_NAMESPACE}`, enableShadows);
node.on(`transformstart${SHADOW_EVENT_NAMESPACE}`, disableShadows);
node.on(`transformend${SHADOW_EVENT_NAMESPACE}`, enableShadows);
});
selectedRotationNode?.on(
`rotationChange${BOTTOM_CENTER_ROTATION_ANCHOR_EVENT_NAMESPACE}`,
refreshBottomCenterRotationAnchor,
);
selectedRotationNode?.on(
`absoluteTransformChange${BOTTOM_CENTER_ROTATION_ANCHOR_EVENT_NAMESPACE}`,
refreshBottomCenterRotationAnchor,
);
selectedRotationNode?.on(
`transformend${BOTTOM_CENTER_ROTATION_ANCHOR_EVENT_NAMESPACE}`,
refreshBottomCenterRotationAnchor,
);
return () => {
dragNodes.forEach((node) => node.off(SHADOW_EVENT_NAMESPACE));
selectedRotationNode?.off(BOTTOM_CENTER_ROTATION_ANCHOR_EVENT_NAMESPACE);
enableShadows();
};
}, [
nodeRefs,
parentComponentKey,
selectedKey,
selectedKeys,
selectionKind,
suppressSelectedOutline,
]);
return (
<>
<Transformer
ref={contextTransformerRef}
anchorCornerRadius={7}
anchorFill="#FFFFFF"
anchorSize={CORNER_HANDLE_SIZE}
anchorStroke="#D0D5DD"
anchorStrokeWidth={1}
anchorStyleFunc={styleAnchor}
borderStroke="#D9D9DE"
borderStrokeWidth={1}
rotateEnabled={false}
/>
<Transformer
ref={selectedTransformerRef}
anchorCornerRadius={7}
anchorFill="#FFFFFF"
anchorSize={CORNER_HANDLE_SIZE}
anchorStroke="#E5E7EB"
anchorStrokeWidth={1}
anchorStyleFunc={styleAnchor}
borderDash={
isMultiComponentSelection ? MULTI_SELECTION_GROUP_DASH : undefined
}
borderEnabled
borderStroke={isMultiComponentSelection ? "#D9D9DE" : "#7A5AF8"}
borderStrokeWidth={1}
enabledAnchors={
selectionKind === "component"
? horizontalResizeOnly
? HORIZONTAL_ONLY_ANCHORS
: undefined
: []
}
resizeEnabled={selectionKind === "component"}
rotateAnchorAngle={bottomCenterRotationAnchorAngle}
rotateAnchorOffset={BOTTOM_CENTER_ROTATION_ANCHOR_OFFSET}
rotateEnabled={selectionKind === "component"}
rotateLineVisible={false}
/>
{multiSelectionMemberKeys.map((key) => (
<TemplateV2MultiSelectionMemberOutline
key={key}
nodeRefs={nodeRefs}
selectedKey={key}
/>
))}
</>
);
}