From 3ed6e630800a53f8b7bdccdeed7c5243c1cbd220 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 13 Aug 2021 17:45:15 +1000 Subject: [PATCH] Updated transformer to also transform attached tokens --- src/components/konva/Note.tsx | 2 +- src/components/konva/Token.tsx | 233 ++++++++++++++++++--------- src/components/konva/Transformer.tsx | 46 +++--- src/hooks/useMapTokens.tsx | 20 ++- src/types/Events.ts | 3 + 5 files changed, 201 insertions(+), 103 deletions(-) diff --git a/src/components/konva/Note.tsx b/src/components/konva/Note.tsx index 824234e..1f451f2 100644 --- a/src/components/konva/Note.tsx +++ b/src/components/konva/Note.tsx @@ -287,7 +287,7 @@ function Note({ (noteRef.current ? [noteRef.current] : [])} onTransformEnd={handleTransformEnd} onTransformStart={handleTransformStart} gridScale={map?.grid.measurement.scale || ""} diff --git a/src/components/konva/Token.tsx b/src/components/konva/Token.tsx index 507caaf..5062e4a 100644 --- a/src/components/konva/Token.tsx +++ b/src/components/konva/Token.tsx @@ -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([]); - const attachmentThreshold = Vector2.componentMin(gridCellPixelSize) / 4; + const attachmentThreshold = useMemo( + () => Vector2.componentMin(gridCellPixelSize) / 4, + [gridCellPixelSize] + ); function handleDragStart(event: Konva.KonvaEventObject) { 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(null); - - const [isTransforming, setIsTransforming] = useState(false); - function handleTransformStart() { - setIsTransforming(true); - onTokenMenuClose(); - } - - function handleTransformEnd(event: Konva.KonvaEventObject) { - 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(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) { + setOverrideTransformActive(true); + onTokenTransformStart(event); + onTokenMenuClose(); + } + + function handleTransformEnd(event: Konva.KonvaEventObject) { + const transformer = event.currentTarget as Konva.Transformer; + const nodes = transformer.nodes(); + const tokenChanges: Record> = {}; + 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} > {}} /> - {!isTransforming ? ( + {!transforming ? ( {tokenState.statuses?.length > 0 ? ( ; + nodes: () => Konva.Node[]; onTransformStart?: (event: Konva.KonvaEventObject) => void; onTransformEnd?: (event: Konva.KonvaEventObject) => 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(".middle-left"); - const middleRight = - transformerRef.current.findOne(".middle-right"); - const rotater = transformerRef.current.findOne(".rotater"); + const middleLeft = transformer.findOne(".middle-left"); + const middleRight = transformer.findOne(".middle-right"); + const rotater = transformer.findOne(".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(); function handleTransformStart(e: Konva.KonvaEventObject) { 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} /> ); diff --git a/src/hooks/useMapTokens.tsx b/src/hooks/useMapTokens.tsx index a41c2ab..232603f 100644 --- a/src/hooks/useMapTokens.tsx +++ b/src/hooks/useMapTokens.tsx @@ -117,9 +117,23 @@ function useMapTokens( useKeyboard(handleKeyDown, handleKeyUp); useBlur(handleBlur); + const [transformingTokensIds, setTransformingTokenIds] = useState( + [] + ); + function handleTokenTransformStart(event: Konva.KonvaEventObject) { + 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 && ( void; +export type TokenTransformEventHandler = ( + event: Konva.KonvaEventObject +) => void; export type NoteCreateEventHander = (notes: Note[]) => void; export type NoteRemoveEventHander = (noteIds: string[]) => void;