Updated transformer to also transform attached tokens

This commit is contained in:
Mitchell McCaffrey 2021-08-13 17:45:15 +10:00
parent 54debeb61c
commit 3ed6e63080
5 changed files with 201 additions and 103 deletions

View File

@ -287,7 +287,7 @@ function Note({
</animated.Group>
<Transformer
active={(!note.locked && selected) || isTransforming}
nodeRef={noteRef}
nodes={() => (noteRef.current ? [noteRef.current] : [])}
onTransformEnd={handleTransformEnd}
onTransformStart={handleTransformStart}
gridScale={map?.grid.measurement.scale || ""}

View File

@ -1,4 +1,4 @@
import { useState, useRef } from "react";
import { useState, useRef, useCallback, useMemo } from "react";
import { Image as KonvaImage, Group } from "react-konva";
import { useSpring, animated } from "@react-spring/konva";
import Konva from "konva";
@ -31,9 +31,11 @@ import {
TokenMenuCloseChangeEventHandler,
TokenMenuOpenChangeEventHandler,
TokenStateChangeEventHandler,
TokenTransformEventHandler,
} from "../../types/Events";
import Transformer from "./Transformer";
import TokenAttachment from "./TokenAttachment";
import { MapState } from "../../types/MapState";
type MapTokenProps = {
tokenState: TokenState;
@ -42,10 +44,14 @@ type MapTokenProps = {
onTokenMenuClose: TokenMenuCloseChangeEventHandler;
onTokenDragStart: TokenDragEventHandler;
onTokenDragEnd: TokenDragEventHandler;
onTokenTransformStart: TokenTransformEventHandler;
onTokenTransformEnd: TokenTransformEventHandler;
transforming: boolean;
draggable: boolean;
selectable: boolean;
fadeOnHover: boolean;
map: Map;
mapState: MapState;
selected: boolean;
};
@ -56,10 +62,14 @@ function Token({
onTokenMenuClose,
onTokenDragStart,
onTokenDragEnd,
onTokenTransformStart,
onTokenTransformEnd,
transforming,
draggable,
selectable,
fadeOnHover,
map,
mapState,
selected,
}: MapTokenProps) {
const userId = useUserId();
@ -86,64 +96,24 @@ function Token({
const [attachmentOverCharacter, setAttachmentOverCharacter] = useState(false);
// The characters that we're present when an attachment is dragged, used to highlight the attachment
const attachmentCharactersRef = useRef<Konva.Node[]>([]);
const attachmentThreshold = Vector2.componentMin(gridCellPixelSize) / 4;
const attachmentThreshold = useMemo(
() => Vector2.componentMin(gridCellPixelSize) / 4,
[gridCellPixelSize]
);
function handleDragStart(event: Konva.KonvaEventObject<DragEvent>) {
const tokenGroup = event.target as Konva.Shape;
const layer = tokenGroup.getLayer();
if (!layer) {
return;
}
previousDragPositionRef.current = tokenGroup.position();
if (tokenState.category === "vehicle") {
const tokenIntersection = new Intersection(
getScaledOutline(tokenState, tokenWidth, tokenHeight),
{ x: tokenX - tokenWidth / 2, y: tokenY - tokenHeight / 2 },
{ x: tokenX, y: tokenY },
tokenState.rotation
);
// Find all other characters on the map and check whether they're
// intersecting the vehicle
const characters = layer.find(".character");
const attachments = layer.find(".attachment");
const tokens = [...characters, ...attachments];
for (let other of tokens) {
if (other === tokenGroup) {
continue;
}
if (tokenIntersection.intersects(other.position())) {
attachedTokensRef.current.push(other);
}
}
}
attachedTokensRef.current = getAttachedTokens();
if (tokenState.category === "attachment") {
// If we're dragging an attachment add all characters to the attachment characters
// So we can check for highlights
previousDragPositionRef.current = tokenGroup.position();
const characters = layer.find(".character");
const characters = tokenGroup.getLayer()?.find(".character") || [];
attachmentCharactersRef.current = characters;
}
if (tokenState.category === "character") {
// Find all attachments and check whether they are close to the center of this token
const attachments = layer.find(".attachment");
for (let attachment of attachments) {
if (attachment === tokenGroup) {
continue;
}
const distance = Vector2.distance(
tokenGroup.position(),
attachment.position()
);
if (distance < attachmentThreshold) {
attachedTokensRef.current.push(attachment);
}
}
}
setDragging(true);
onTokenDragStart(
event,
@ -228,8 +198,8 @@ function Token({
}
function handleClick() {
if (selectable && draggable && tokenRef.current) {
onTokenMenuOpen(tokenState.id, tokenRef.current, true);
if (selectable && draggable && transformRootRef.current) {
onTokenMenuOpen(tokenState.id, transformRootRef.current, true);
}
}
@ -251,11 +221,11 @@ function Token({
}
// Check token click when locked and selectable
// We can't use onClick because that doesn't check pointer distance
if (tokenState.locked && selectable && tokenRef.current) {
if (tokenState.locked && selectable && transformRootRef.current) {
// If down and up time is small trigger a click
const delta = event.evt.timeStamp - tokenPointerDownTimeRef.current;
if (delta < 300) {
onTokenMenuOpen(tokenState.id, tokenRef.current, true);
onTokenMenuOpen(tokenState.id, transformRootRef.current, true);
}
}
}
@ -272,30 +242,7 @@ function Token({
}
}
const tokenRef = useRef<Konva.Group>(null);
const [isTransforming, setIsTransforming] = useState(false);
function handleTransformStart() {
setIsTransforming(true);
onTokenMenuClose();
}
function handleTransformEnd(event: Konva.KonvaEventObject<Event>) {
if (tokenRef.current) {
const sizeChange = event.target.scaleX();
const rotation = event.target.rotation();
onTokenStateChange({
[tokenState.id]: {
size: tokenState.size * sizeChange,
rotation: rotation,
},
});
tokenRef.current.scaleX(1);
tokenRef.current.scaleY(1);
onTokenMenuOpen(tokenState.id, tokenRef.current, false);
}
setIsTransforming(false);
}
const transformRootRef = useRef<Konva.Group>(null);
const minCellSize = Math.min(
gridCellPixelSize.width,
@ -317,6 +264,132 @@ function Token({
immediate: skipAnimation,
});
const getAttachedTokens = useCallback(() => {
const transformRoot = transformRootRef.current;
const tokenGroup = transformRoot?.parent;
const layer = transformRoot?.getLayer();
let attachedTokens: Konva.Node[] = [];
if (tokenGroup && layer) {
if (tokenState.category === "vehicle") {
const tokenIntersection = new Intersection(
getScaledOutline(tokenState, tokenWidth, tokenHeight),
{ x: tokenX - tokenWidth / 2, y: tokenY - tokenHeight / 2 },
{ x: tokenX, y: tokenY },
tokenState.rotation
);
// Find all other characters on the map and check whether they're
// intersecting the vehicle
const characters = layer.find(".character");
const attachments = layer.find(".attachment");
const tokens = [...characters, ...attachments];
for (let other of tokens) {
const id = other.id();
if (id in mapState.tokens) {
const position = {
x: mapState.tokens[id].x * mapWidth,
y: mapState.tokens[id].y * mapHeight,
};
if (tokenIntersection.intersects(position)) {
attachedTokens.push(other);
}
}
}
}
if (tokenState.category === "character") {
// Find all attachments and check whether they are close to the center of this token
const attachments = layer.find(".attachment");
for (let attachment of attachments) {
const id = attachment.id();
if (id in mapState.tokens) {
const position = {
x: mapState.tokens[id].x * mapWidth,
y: mapState.tokens[id].y * mapHeight,
};
const distance = Vector2.distance(tokenGroup.position(), position);
if (distance < attachmentThreshold) {
attachedTokens.push(attachment);
}
}
}
}
}
return attachedTokens;
}, [
attachmentThreshold,
tokenHeight,
tokenWidth,
tokenState,
tokenX,
tokenY,
mapState,
mapWidth,
mapHeight,
]);
// Override transform active to always show this transformer when using it
const [overrideTransformActive, setOverrideTransformActive] = useState(false);
function handleTransformStart(event: Konva.KonvaEventObject<Event>) {
setOverrideTransformActive(true);
onTokenTransformStart(event);
onTokenMenuClose();
}
function handleTransformEnd(event: Konva.KonvaEventObject<Event>) {
const transformer = event.currentTarget as Konva.Transformer;
const nodes = transformer.nodes();
const tokenChanges: Record<string, Partial<TokenState>> = {};
for (let node of nodes) {
const id = node.id();
if (id in mapState.tokens) {
const sizeChange = node.scaleX();
const rotation = node.rotation();
const xChange = node.x() / mapWidth;
const yChange = node.y() / mapHeight;
tokenChanges[id] = {
size: mapState.tokens[id].size * sizeChange,
rotation: rotation,
x: mapState.tokens[id].x + xChange,
y: mapState.tokens[id].y + yChange,
};
}
node.scaleX(1);
node.scaleY(1);
node.x(0);
node.y(0);
}
onTokenStateChange(tokenChanges);
if (transformRootRef.current) {
onTokenMenuOpen(tokenState.id, transformRootRef.current, false);
}
setOverrideTransformActive(false);
onTokenTransformEnd(event);
}
const transformerActive = useMemo(
() => (!tokenState.locked && selected) || overrideTransformActive,
[tokenState, selected, overrideTransformActive]
);
const transformerNodes = useMemo(
() => () => {
if (transformRootRef.current) {
// Find attached transform roots
const attached = getAttachedTokens().map((node) =>
(node as Konva.Group).findOne(".transform-root")
);
return [transformRootRef.current, ...attached];
} else {
return [];
}
},
[getAttachedTokens]
);
// When a token is hidden if you aren't the map owner hide it completely
if (map && !tokenState.visible && map.owner !== userId) {
return null;
@ -354,7 +427,9 @@ function Token({
id={tokenState.id}
>
<Group
ref={tokenRef}
ref={transformRootRef}
id={tokenState.id}
name="transform-root"
rotation={tokenState.rotation}
offsetX={tokenWidth / 2}
offsetY={tokenHeight / 2}
@ -378,7 +453,7 @@ function Token({
hitFunc={() => {}}
/>
</Group>
{!isTransforming ? (
{!transforming ? (
<Group offsetX={tokenWidth / 2} offsetY={tokenHeight / 2}>
{tokenState.statuses?.length > 0 ? (
<TokenStatus
@ -410,8 +485,8 @@ function Token({
) : null}
</animated.Group>
<Transformer
active={(!tokenState.locked && selected) || isTransforming}
nodeRef={tokenRef}
active={transformerActive}
nodes={transformerNodes}
onTransformEnd={handleTransformEnd}
onTransformStart={handleTransformStart}
gridScale={map.grid.measurement.scale}

View File

@ -16,9 +16,9 @@ import scaleDark from "../../images/ScaleDark.png";
import rotateDark from "../../images/RotateDark.png";
import { parseGridScale } from "../../helpers/grid";
type ResizerProps = {
type TransformerProps = {
active: boolean;
nodeRef: React.RefObject<Konva.Node>;
nodes: () => Konva.Node[];
onTransformStart?: (event: Konva.KonvaEventObject<Event>) => void;
onTransformEnd?: (event: Konva.KonvaEventObject<Event>) => void;
gridScale: string;
@ -26,11 +26,11 @@ type ResizerProps = {
function Transformer({
active,
nodeRef,
nodes,
onTransformStart,
onTransformEnd,
gridScale,
}: ResizerProps) {
}: TransformerProps) {
const setPreventMapInteraction = useSetPreventMapInteraction();
const gridCellPixelSize = useGridCellPixelSize();
@ -50,21 +50,25 @@ function Transformer({
const [anchorRotate, anchorRotateStatus] = useAnchorImage(96, rotateDark);
useEffect(() => {
const transformer = transformerRef.current;
if (
active &&
transformerRef.current &&
nodeRef.current &&
transformer &&
anchorScaleStatus === "loaded" &&
anchorRotateStatus === "loaded"
) {
// we need to attach transformer manually
transformerRef.current.nodes([nodeRef.current]);
const n = nodes();
// Slice the nodes to only attach a single node to allow useSingleNodeRotation to
// control the transformer rotation
transformer.setNodes(n.slice(0, 1));
// Update the private _nodes to allow transforming all the added nodes
// TODO: Look at subclassing Konva.Transformer and remove this hack
transformer._nodes = n;
const middleLeft =
transformerRef.current.findOne<Konva.Rect>(".middle-left");
const middleRight =
transformerRef.current.findOne<Konva.Rect>(".middle-right");
const rotater = transformerRef.current.findOne<Konva.Rect>(".rotater");
const middleLeft = transformer.findOne<Konva.Rect>(".middle-left");
const middleRight = transformer.findOne<Konva.Rect>(".middle-right");
const rotater = transformer.findOne<Konva.Rect>(".rotater");
middleLeft.fillPriority("pattern");
middleLeft.fillPatternImage(anchorScale);
@ -84,11 +88,11 @@ function Transformer({
rotater.fillPatternScaleX(0.5);
rotater.fillPatternScaleY(0.5);
transformerRef.current.getLayer()?.batchDraw();
transformer.getLayer()?.batchDraw();
}
}, [
active,
nodeRef,
nodes,
anchorScale,
anchorRotate,
anchorScaleStatus,
@ -97,8 +101,8 @@ function Transformer({
function updateGridCellAbsoluteSize() {
if (active) {
const node = nodeRef.current;
const stage = node?.getStage();
const transformer = transformerRef.current;
const stage = transformer?.getStage();
const mapImage = stage?.findOne("#mapImage");
if (!mapImage) {
return;
@ -123,13 +127,12 @@ function Transformer({
const transformTextRef = useRef<Konva.Group>();
function handleTransformStart(e: Konva.KonvaEventObject<Event>) {
const transformer = transformerRef.current;
const node = nodeRef.current;
if (transformer && node) {
if (transformer) {
movingAnchorRef.current = transformer._movingAnchorName;
setPreventMapInteraction(true);
const transformText = new Konva.Label();
const stageScale = node.getStage()?.scale() || { x: 1, y: 1 };
const stageScale = transformer.getStage()?.scale() || { x: 1, y: 1 };
transformText.scale(Vector2.divide({ x: 1, y: 1 }, stageScale));
const tag = new Konva.Tag();
@ -148,7 +151,7 @@ function Transformer({
transformText.add(tag);
transformText.add(text);
node.getLayer()?.add(transformText);
transformer.getLayer()?.add(transformText);
transformTextRef.current = transformText;
updateGridCellAbsoluteSize();
@ -159,10 +162,10 @@ function Transformer({
}
function updateTransformText() {
const node = nodeRef.current;
const movingAnchor = movingAnchorRef.current;
const transformText = transformTextRef.current;
const transformer = transformerRef.current;
const node = transformer?.nodes()[0];
if (node && transformText && transformer) {
const text = transformText.getChildren()[1] as Konva.Text;
if (movingAnchor === "rotater") {
@ -286,7 +289,6 @@ function Transformer({
anchorCornerRadius={24}
borderStrokeWidth={0}
anchorSize={48}
useSingleNodeRotation={true}
/>
</Portal>
);

View File

@ -117,9 +117,23 @@ function useMapTokens(
useKeyboard(handleKeyDown, handleKeyUp);
useBlur(handleBlur);
const [transformingTokensIds, setTransformingTokenIds] = useState<string[]>(
[]
);
function handleTokenTransformStart(event: Konva.KonvaEventObject<Event>) {
const transformer = event.currentTarget as Konva.Transformer;
const nodes = transformer.nodes();
setTransformingTokenIds(nodes.map((node) => node.id()));
}
function handleTokenTransformEnd() {
setTransformingTokenIds([]);
}
function tokenFromTokenState(tokenState: TokenState) {
return (
map && (
map &&
mapState && (
<Token
key={tokenState.id}
tokenState={tokenState}
@ -128,6 +142,9 @@ function useMapTokens(
onTokenMenuClose={handleTokenMenuClose}
onTokenDragStart={handleTokenDragStart}
onTokenDragEnd={handleTokenDragEnd}
onTokenTransformStart={handleTokenTransformStart}
onTokenTransformEnd={handleTokenTransformEnd}
transforming={transformingTokensIds.includes(tokenState.id)}
draggable={
selectedToolId === "move" &&
!(tokenState.id in disabledTokens) &&
@ -142,6 +159,7 @@ function useMapTokens(
tokenState.category !== "prop" && selectedToolId === "drawing"
}
map={map}
mapState={mapState}
selected={
!!tokenMenuOptions &&
isTokenMenuOpen &&

View File

@ -38,6 +38,9 @@ export type TokenDragEventHandler = (
tokenStateId: string,
attachedTokenStateIds: string[]
) => void;
export type TokenTransformEventHandler = (
event: Konva.KonvaEventObject<Event>
) => void;
export type NoteCreateEventHander = (notes: Note[]) => void;
export type NoteRemoveEventHander = (noteIds: string[]) => void;