Updated transformer to also transform attached tokens
This commit is contained in:
parent
54debeb61c
commit
3ed6e63080
@ -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 || ""}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 &&
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user