Fix transform attachments affecting transform anchor positions
This commit is contained in:
parent
37f0ab3b26
commit
4eca2dcfc7
@ -287,7 +287,7 @@ function Note({
|
||||
</animated.Group>
|
||||
<Transformer
|
||||
active={(!note.locked && selected) || isTransforming}
|
||||
nodes={() => (noteRef.current ? [noteRef.current] : [])}
|
||||
nodes={noteRef.current ? [noteRef.current] : []}
|
||||
onTransformEnd={handleTransformEnd}
|
||||
onTransformStart={handleTransformStart}
|
||||
gridScale={map?.grid.measurement.scale || ""}
|
||||
|
@ -31,7 +31,7 @@ import {
|
||||
TokenMenuCloseChangeEventHandler,
|
||||
TokenMenuOpenChangeEventHandler,
|
||||
TokenStateChangeEventHandler,
|
||||
TokenTransformEventHandler,
|
||||
CustomTransformEventHandler,
|
||||
} from "../../types/Events";
|
||||
import Transformer from "./Transformer";
|
||||
import TokenAttachment from "./TokenAttachment";
|
||||
@ -44,8 +44,8 @@ type MapTokenProps = {
|
||||
onTokenMenuClose: TokenMenuCloseChangeEventHandler;
|
||||
onTokenDragStart: TokenDragEventHandler;
|
||||
onTokenDragEnd: TokenDragEventHandler;
|
||||
onTokenTransformStart: TokenTransformEventHandler;
|
||||
onTokenTransformEnd: TokenTransformEventHandler;
|
||||
onTokenTransformStart: CustomTransformEventHandler;
|
||||
onTokenTransformEnd: CustomTransformEventHandler;
|
||||
transforming: boolean;
|
||||
draggable: boolean;
|
||||
selectable: boolean;
|
||||
@ -332,15 +332,21 @@ function Token({
|
||||
// Override transform active to always show this transformer when using it
|
||||
const [overrideTransformActive, setOverrideTransformActive] = useState(false);
|
||||
|
||||
function handleTransformStart(event: Konva.KonvaEventObject<Event>) {
|
||||
function handleTransformStart(
|
||||
event: Konva.KonvaEventObject<Event>,
|
||||
attachments: Konva.Node[]
|
||||
) {
|
||||
setOverrideTransformActive(true);
|
||||
onTokenTransformStart(event);
|
||||
onTokenTransformStart(event, attachments);
|
||||
onTokenMenuClose();
|
||||
}
|
||||
|
||||
function handleTransformEnd(event: Konva.KonvaEventObject<Event>) {
|
||||
function handleTransformEnd(
|
||||
event: Konva.KonvaEventObject<Event>,
|
||||
attachments: Konva.Node[]
|
||||
) {
|
||||
const transformer = event.currentTarget as Konva.Transformer;
|
||||
const nodes = transformer.nodes();
|
||||
const nodes = [...transformer.nodes(), ...attachments];
|
||||
const tokenChanges: Record<string, Partial<TokenState>> = {};
|
||||
for (let node of nodes) {
|
||||
const id = node.id();
|
||||
@ -367,7 +373,7 @@ function Token({
|
||||
onTokenMenuOpen(tokenState.id, transformRootRef.current, false);
|
||||
}
|
||||
setOverrideTransformActive(false);
|
||||
onTokenTransformEnd(event);
|
||||
onTokenTransformEnd(event, attachments);
|
||||
}
|
||||
|
||||
const transformerActive = useMemo(
|
||||
@ -375,20 +381,16 @@ function Token({
|
||||
[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]
|
||||
);
|
||||
const transformerAttachments = useMemo(() => {
|
||||
if (transformerActive) {
|
||||
// Find attached transform roots
|
||||
return getAttachedTokens().map((node) =>
|
||||
(node as Konva.Group).findOne(".transform-root")
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [getAttachedTokens, transformerActive]);
|
||||
|
||||
// When a token is hidden if you aren't the map owner hide it completely
|
||||
if (map && !tokenState.visible && map.owner !== userId) {
|
||||
@ -486,7 +488,12 @@ function Token({
|
||||
</animated.Group>
|
||||
<Transformer
|
||||
active={transformerActive}
|
||||
nodes={transformerNodes}
|
||||
nodes={
|
||||
transformRootRef.current
|
||||
? [transformRootRef.current as Konva.Node]
|
||||
: []
|
||||
}
|
||||
attachments={transformerAttachments}
|
||||
onTransformEnd={handleTransformEnd}
|
||||
onTransformStart={handleTransformStart}
|
||||
gridScale={map.grid.measurement.scale}
|
||||
|
@ -1,35 +1,216 @@
|
||||
import Konva from "konva";
|
||||
import { Transform } from "konva/lib/Util";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Transformer as KonvaTransformer } from "react-konva";
|
||||
import { Portal } from "react-konva-utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
useGridCellPixelSize,
|
||||
useGridSnappingSensitivity,
|
||||
} from "../../contexts/GridContext";
|
||||
import { useSetPreventMapInteraction } from "../../contexts/MapInteractionContext";
|
||||
import { useMapStage } from "../../contexts/MapStageContext";
|
||||
|
||||
import { roundTo } from "../../helpers/shared";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
import { parseGridScale } from "../../helpers/grid";
|
||||
|
||||
import scaleDark from "../../images/ScaleDark.png";
|
||||
import rotateDark from "../../images/RotateDark.png";
|
||||
import { parseGridScale } from "../../helpers/grid";
|
||||
|
||||
import { CustomTransformEventHandler } from "../../types/Events";
|
||||
|
||||
type TransformerProps = {
|
||||
active: boolean;
|
||||
nodes: () => Konva.Node[];
|
||||
onTransformStart?: (event: Konva.KonvaEventObject<Event>) => void;
|
||||
onTransformEnd?: (event: Konva.KonvaEventObject<Event>) => void;
|
||||
nodes: Konva.Node[];
|
||||
attachments: Konva.Node[];
|
||||
onTransformStart?: CustomTransformEventHandler;
|
||||
onTransform?: CustomTransformEventHandler;
|
||||
onTransformEnd?: CustomTransformEventHandler;
|
||||
gridScale: string;
|
||||
portalSelector: string;
|
||||
};
|
||||
|
||||
export class CustomTransformer extends Konva.Transformer {
|
||||
attachments: Konva.Node[] = [];
|
||||
|
||||
// Override fitNodesInto applying transform to attachments as well
|
||||
_fitNodesInto(newAttrs: any, evt?: any) {
|
||||
var oldAttrs = this._getNodeRect();
|
||||
|
||||
const minSize = 1;
|
||||
|
||||
if (
|
||||
Konva.Util._inRange(
|
||||
newAttrs.width,
|
||||
-this.padding() * 2 - minSize,
|
||||
minSize
|
||||
)
|
||||
) {
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
Konva.Util._inRange(
|
||||
newAttrs.height,
|
||||
-this.padding() * 2 - minSize,
|
||||
minSize
|
||||
)
|
||||
) {
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
|
||||
const allowNegativeScale = this.flipEnabled();
|
||||
var t = new Transform();
|
||||
t.rotate(this.rotation());
|
||||
if (
|
||||
this._movingAnchorName &&
|
||||
newAttrs.width < 0 &&
|
||||
this._movingAnchorName.indexOf("left") >= 0
|
||||
) {
|
||||
const offset = t.point({
|
||||
x: -this.padding() * 2,
|
||||
y: 0,
|
||||
});
|
||||
newAttrs.x += offset.x;
|
||||
newAttrs.y += offset.y;
|
||||
newAttrs.width += this.padding() * 2;
|
||||
this._movingAnchorName = this._movingAnchorName.replace("left", "right");
|
||||
this._anchorDragOffset.x -= offset.x;
|
||||
this._anchorDragOffset.y -= offset.y;
|
||||
if (!allowNegativeScale) {
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
this._movingAnchorName &&
|
||||
newAttrs.width < 0 &&
|
||||
this._movingAnchorName.indexOf("right") >= 0
|
||||
) {
|
||||
const offset = t.point({
|
||||
x: this.padding() * 2,
|
||||
y: 0,
|
||||
});
|
||||
this._movingAnchorName = this._movingAnchorName.replace("right", "left");
|
||||
this._anchorDragOffset.x -= offset.x;
|
||||
this._anchorDragOffset.y -= offset.y;
|
||||
newAttrs.width += this.padding() * 2;
|
||||
if (!allowNegativeScale) {
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (
|
||||
this._movingAnchorName &&
|
||||
newAttrs.height < 0 &&
|
||||
this._movingAnchorName.indexOf("top") >= 0
|
||||
) {
|
||||
const offset = t.point({
|
||||
x: 0,
|
||||
y: -this.padding() * 2,
|
||||
});
|
||||
newAttrs.x += offset.x;
|
||||
newAttrs.y += offset.y;
|
||||
this._movingAnchorName = this._movingAnchorName.replace("top", "bottom");
|
||||
this._anchorDragOffset.x -= offset.x;
|
||||
this._anchorDragOffset.y -= offset.y;
|
||||
newAttrs.height += this.padding() * 2;
|
||||
if (!allowNegativeScale) {
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
this._movingAnchorName &&
|
||||
newAttrs.height < 0 &&
|
||||
this._movingAnchorName.indexOf("bottom") >= 0
|
||||
) {
|
||||
const offset = t.point({
|
||||
x: 0,
|
||||
y: this.padding() * 2,
|
||||
});
|
||||
this._movingAnchorName = this._movingAnchorName.replace("bottom", "top");
|
||||
this._anchorDragOffset.x -= offset.x;
|
||||
this._anchorDragOffset.y -= offset.y;
|
||||
newAttrs.height += this.padding() * 2;
|
||||
if (!allowNegativeScale) {
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.boundBoxFunc()) {
|
||||
const bounded = this.boundBoxFunc()(oldAttrs, newAttrs);
|
||||
if (bounded) {
|
||||
newAttrs = bounded;
|
||||
} else {
|
||||
Konva.Util.warn(
|
||||
"boundBoxFunc returned falsy. You should return new bound rect from it!"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// base size value doesn't really matter
|
||||
// we just need to think about bounding boxes as transforms
|
||||
// but how?
|
||||
// the idea is that we have a transformed rectangle with the size of "baseSize"
|
||||
const baseSize = 10000000;
|
||||
const oldTr = new Transform();
|
||||
oldTr.translate(oldAttrs.x, oldAttrs.y);
|
||||
oldTr.rotate(oldAttrs.rotation);
|
||||
oldTr.scale(oldAttrs.width / baseSize, oldAttrs.height / baseSize);
|
||||
|
||||
const newTr = new Transform();
|
||||
newTr.translate(newAttrs.x, newAttrs.y);
|
||||
newTr.rotate(newAttrs.rotation);
|
||||
newTr.scale(newAttrs.width / baseSize, newAttrs.height / baseSize);
|
||||
|
||||
// now lets think we had [old transform] and n ow we have [new transform]
|
||||
// Now, the questions is: how can we transform "parent" to go from [old transform] into [new transform]
|
||||
// in equation it will be:
|
||||
// [delta transform] * [old transform] = [new transform]
|
||||
// that means that
|
||||
// [delta transform] = [new transform] * [old transform inverted]
|
||||
const delta = newTr.multiply(oldTr.invert());
|
||||
|
||||
[...this._nodes, ...this.attachments].forEach((node) => {
|
||||
// for each node we have the same [delta transform]
|
||||
// the equations is
|
||||
// [delta transform] * [parent transform] * [old local transform] = [parent transform] * [new local transform]
|
||||
// and we need to find [new local transform]
|
||||
// [new local] = [parent inverted] * [delta] * [parent] * [old local]
|
||||
const parentTransform = node.getParent().getAbsoluteTransform();
|
||||
const localTransform = node.getTransform().copy();
|
||||
// skip offset:
|
||||
localTransform.translate(node.offsetX(), node.offsetY());
|
||||
|
||||
const newLocalTransform = new Transform();
|
||||
newLocalTransform
|
||||
.multiply(parentTransform.copy().invert())
|
||||
.multiply(delta)
|
||||
.multiply(parentTransform)
|
||||
.multiply(localTransform);
|
||||
|
||||
const attrs = newLocalTransform.decompose();
|
||||
node.setAttrs(attrs);
|
||||
this._fire("transform", { evt: evt, target: node });
|
||||
node._fire("transform", { evt: evt, target: node });
|
||||
node.getLayer()?.batchDraw();
|
||||
});
|
||||
this.rotation(Konva.Util._getRotation(newAttrs.rotation));
|
||||
this._resetTransformCache();
|
||||
this.update();
|
||||
this.getLayer()?.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
function Transformer({
|
||||
active,
|
||||
nodes,
|
||||
attachments,
|
||||
onTransformStart,
|
||||
onTransform,
|
||||
onTransformEnd,
|
||||
gridScale,
|
||||
portalSelector,
|
||||
}: TransformerProps) {
|
||||
const setPreventMapInteraction = useSetPreventMapInteraction();
|
||||
|
||||
@ -42,13 +223,235 @@ function Transformer({
|
||||
|
||||
const snappingSensitivity = useGridSnappingSensitivity();
|
||||
// Clamp snapping to 0 to accound for -1 snapping override
|
||||
const gridSnappingSensitivity = Math.max(snappingSensitivity, 0);
|
||||
const gridSnappingSensitivity = useMemo(
|
||||
() => Math.max(snappingSensitivity, 0),
|
||||
[snappingSensitivity]
|
||||
);
|
||||
|
||||
const transformerRef = useRef<Konva.Transformer>(null);
|
||||
const mapStageRef = useMapStage();
|
||||
const transformerRef = useRef<CustomTransformer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let transformer = transformerRef.current;
|
||||
const stage = mapStageRef.current;
|
||||
if (active && stage && !transformer) {
|
||||
transformer = new CustomTransformer({
|
||||
centeredScaling: true,
|
||||
rotateAnchorOffset: 16,
|
||||
enabledAnchors: ["middle-left", "middle-right"],
|
||||
flipEnabled: false,
|
||||
ignoreStroke: true,
|
||||
borderStroke: "invisible",
|
||||
anchorStroke: "invisible",
|
||||
anchorCornerRadius: 24,
|
||||
borderStrokeWidth: 0,
|
||||
anchorSize: 48,
|
||||
});
|
||||
const portal = stage.findOne<Konva.Group>(portalSelector);
|
||||
if (portal) {
|
||||
portal.add(transformer);
|
||||
transformerRef.current = transformer;
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (stage && transformer) {
|
||||
transformer.destroy();
|
||||
transformerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [mapStageRef, portalSelector, active]);
|
||||
|
||||
useEffect(() => {
|
||||
transformerRef.current?.boundBoxFunc((oldBox, newBox) => {
|
||||
let snapBox = { ...newBox };
|
||||
const movingAnchor = movingAnchorRef.current;
|
||||
if (movingAnchor === "middle-left" || movingAnchor === "middle-right") {
|
||||
// Account for grid snapping
|
||||
const nearestCellWidth = roundTo(
|
||||
snapBox.width,
|
||||
gridCellAbsoluteSizeRef.current.x
|
||||
);
|
||||
const distanceToSnap = Math.abs(snapBox.width - nearestCellWidth);
|
||||
let snapping = false;
|
||||
if (
|
||||
distanceToSnap <
|
||||
gridCellAbsoluteSizeRef.current.x * gridSnappingSensitivity
|
||||
) {
|
||||
snapBox.width = nearestCellWidth;
|
||||
snapping = true;
|
||||
}
|
||||
|
||||
const deltaWidth = snapBox.width - oldBox.width;
|
||||
// Account for node ratio
|
||||
const inverseRatio =
|
||||
Math.round(oldBox.height) / Math.round(oldBox.width);
|
||||
const deltaHeight = inverseRatio * deltaWidth;
|
||||
|
||||
// Account for node rotation
|
||||
// Create a transform to unrotate the x,y position of the Box
|
||||
const rotator = new Transform();
|
||||
rotator.rotate(-snapBox.rotation);
|
||||
|
||||
// Unrotate and add the resize amount
|
||||
let rotatedMin = rotator.point({ x: snapBox.x, y: snapBox.y });
|
||||
rotatedMin.y = rotatedMin.y - deltaHeight / 2;
|
||||
// Snap x position if needed
|
||||
if (snapping) {
|
||||
const snapDelta = newBox.width - nearestCellWidth;
|
||||
rotatedMin.x = rotatedMin.x + snapDelta / 2;
|
||||
}
|
||||
|
||||
// Rotated back
|
||||
rotator.invert();
|
||||
rotatedMin = rotator.point(rotatedMin);
|
||||
|
||||
snapBox = {
|
||||
...snapBox,
|
||||
height: snapBox.height + deltaHeight,
|
||||
x: rotatedMin.x,
|
||||
y: rotatedMin.y,
|
||||
};
|
||||
}
|
||||
|
||||
if (snapBox.width < 5 || snapBox.height < 5) {
|
||||
return oldBox;
|
||||
}
|
||||
return snapBox;
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
transformerRef.current?.rotationSnaps(
|
||||
snappingSensitivity === -1
|
||||
? [] // Disabled rotation snapping if grid snapping disabled with shortcut
|
||||
: [...Array(24).keys()].map((n) => n * 15)
|
||||
);
|
||||
});
|
||||
|
||||
const movingAnchorRef = useRef<string>();
|
||||
const transformTextRef = useRef<Konva.Group>();
|
||||
|
||||
useEffect(() => {
|
||||
function updateGridCellAbsoluteSize() {
|
||||
if (active) {
|
||||
const transformer = transformerRef.current;
|
||||
const stage = transformer?.getStage();
|
||||
const mapImage = stage?.findOne("#mapImage");
|
||||
if (!mapImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use min side for hex grids
|
||||
const minSize = Vector2.componentMin(gridCellPixelSize);
|
||||
const size = new Vector2(minSize, minSize);
|
||||
|
||||
// Get grid cell size in screen coordinates
|
||||
const mapTransform = mapImage.getAbsoluteTransform();
|
||||
const absoluteSize = Vector2.subtract(
|
||||
mapTransform.point(size),
|
||||
mapTransform.point({ x: 0, y: 0 })
|
||||
);
|
||||
|
||||
gridCellAbsoluteSizeRef.current = absoluteSize;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTransformStart(e: Konva.KonvaEventObject<Event>) {
|
||||
const transformer = transformerRef.current;
|
||||
if (transformer) {
|
||||
movingAnchorRef.current = transformer._movingAnchorName;
|
||||
setPreventMapInteraction(true);
|
||||
|
||||
const transformText = new Konva.Label();
|
||||
const stageScale = transformer.getStage()?.scale() || { x: 1, y: 1 };
|
||||
transformText.scale(Vector2.divide({ x: 1, y: 1 }, stageScale));
|
||||
|
||||
const tag = new Konva.Tag();
|
||||
tag.fill("hsla(230, 25%, 15%, 0.8)");
|
||||
tag.cornerRadius(4);
|
||||
// @ts-ignore
|
||||
tag.pointerDirection("down");
|
||||
tag.pointerHeight(4);
|
||||
tag.pointerWidth(4);
|
||||
|
||||
const text = new Konva.Text();
|
||||
text.fontSize(16);
|
||||
text.padding(4);
|
||||
text.fill("white");
|
||||
|
||||
transformText.add(tag);
|
||||
transformText.add(text);
|
||||
|
||||
transformer.getLayer()?.add(transformText);
|
||||
transformTextRef.current = transformText;
|
||||
|
||||
updateGridCellAbsoluteSize();
|
||||
updateTransformText();
|
||||
|
||||
onTransformStart && onTransformStart(e, attachments);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransformText() {
|
||||
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") {
|
||||
text.text(`${node.rotation().toFixed(0)}°`);
|
||||
} else {
|
||||
const nodeRect = node.getClientRect({ skipShadow: true });
|
||||
const nodeScale = Vector2.divide(
|
||||
{ x: nodeRect.width, y: nodeRect.height },
|
||||
gridCellAbsoluteSizeRef.current
|
||||
);
|
||||
text.text(
|
||||
`${(nodeScale.x * scale.multiplier).toFixed(scale.digits)}${
|
||||
scale.unit
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const nodePosition = node.getStage()?.getPointerPosition();
|
||||
if (nodePosition) {
|
||||
transformText.absolutePosition({
|
||||
x: nodePosition.x,
|
||||
y: nodePosition.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTransform(e: Konva.KonvaEventObject<Event>) {
|
||||
updateTransformText();
|
||||
onTransform?.(e, attachments);
|
||||
}
|
||||
|
||||
function handleTransformEnd(e: Konva.KonvaEventObject<Event>) {
|
||||
setPreventMapInteraction(false);
|
||||
transformTextRef.current?.destroy();
|
||||
transformTextRef.current = undefined;
|
||||
onTransformEnd && onTransformEnd(e, attachments);
|
||||
}
|
||||
|
||||
transformerRef.current?.on("transformstart", handleTransformStart);
|
||||
transformerRef.current?.on("transform", handleTransform);
|
||||
transformerRef.current?.on("transformend", handleTransformEnd);
|
||||
|
||||
return () => {
|
||||
transformerRef.current?.off("transformstart", handleTransformStart);
|
||||
transformerRef.current?.off("transform", handleTransform);
|
||||
transformerRef.current?.off("transformend", handleTransformEnd);
|
||||
};
|
||||
});
|
||||
|
||||
const [anchorScale, anchorScaleStatus] = useAnchorImage(96, scaleDark);
|
||||
const [anchorRotate, anchorRotateStatus] = useAnchorImage(96, rotateDark);
|
||||
|
||||
// Add nodes to transformer and setup
|
||||
useEffect(() => {
|
||||
const transformer = transformerRef.current;
|
||||
if (
|
||||
@ -57,14 +460,8 @@ function Transformer({
|
||||
anchorScaleStatus === "loaded" &&
|
||||
anchorRotateStatus === "loaded"
|
||||
) {
|
||||
// we need to attach transformer manually
|
||||
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;
|
||||
transformer.setNodes(nodes);
|
||||
transformer.attachments = attachments;
|
||||
|
||||
const middleLeft = transformer.findOne<Konva.Rect>(".middle-left");
|
||||
const middleRight = transformer.findOne<Konva.Rect>(".middle-right");
|
||||
@ -93,207 +490,21 @@ function Transformer({
|
||||
}, [
|
||||
active,
|
||||
nodes,
|
||||
attachments,
|
||||
anchorScale,
|
||||
anchorRotate,
|
||||
anchorScaleStatus,
|
||||
anchorRotateStatus,
|
||||
]);
|
||||
|
||||
function updateGridCellAbsoluteSize() {
|
||||
if (active) {
|
||||
const transformer = transformerRef.current;
|
||||
const stage = transformer?.getStage();
|
||||
const mapImage = stage?.findOne("#mapImage");
|
||||
if (!mapImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use min side for hex grids
|
||||
const minSize = Vector2.componentMin(gridCellPixelSize);
|
||||
const size = new Vector2(minSize, minSize);
|
||||
|
||||
// Get grid cell size in screen coordinates
|
||||
const mapTransform = mapImage.getAbsoluteTransform();
|
||||
const absoluteSize = Vector2.subtract(
|
||||
mapTransform.point(size),
|
||||
mapTransform.point({ x: 0, y: 0 })
|
||||
);
|
||||
|
||||
gridCellAbsoluteSizeRef.current = absoluteSize;
|
||||
}
|
||||
}
|
||||
|
||||
const movingAnchorRef = useRef<string>();
|
||||
const transformTextRef = useRef<Konva.Group>();
|
||||
function handleTransformStart(e: Konva.KonvaEventObject<Event>) {
|
||||
const transformer = transformerRef.current;
|
||||
if (transformer) {
|
||||
movingAnchorRef.current = transformer._movingAnchorName;
|
||||
setPreventMapInteraction(true);
|
||||
|
||||
const transformText = new Konva.Label();
|
||||
const stageScale = transformer.getStage()?.scale() || { x: 1, y: 1 };
|
||||
transformText.scale(Vector2.divide({ x: 1, y: 1 }, stageScale));
|
||||
|
||||
const tag = new Konva.Tag();
|
||||
tag.fill("hsla(230, 25%, 15%, 0.8)");
|
||||
tag.cornerRadius(4);
|
||||
// @ts-ignore
|
||||
tag.pointerDirection("down");
|
||||
tag.pointerHeight(4);
|
||||
tag.pointerWidth(4);
|
||||
|
||||
const text = new Konva.Text();
|
||||
text.fontSize(16);
|
||||
text.padding(4);
|
||||
text.fill("white");
|
||||
|
||||
transformText.add(tag);
|
||||
transformText.add(text);
|
||||
|
||||
transformer.getLayer()?.add(transformText);
|
||||
transformTextRef.current = transformText;
|
||||
|
||||
updateGridCellAbsoluteSize();
|
||||
updateTransformText();
|
||||
|
||||
onTransformStart && onTransformStart(e);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransformText() {
|
||||
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") {
|
||||
text.text(`${node.rotation().toFixed(0)}°`);
|
||||
} else {
|
||||
const nodeRect = node.getClientRect({ skipShadow: true });
|
||||
const nodeScale = Vector2.divide(
|
||||
{ x: nodeRect.width, y: nodeRect.height },
|
||||
gridCellAbsoluteSizeRef.current
|
||||
);
|
||||
text.text(
|
||||
`${(nodeScale.x * scale.multiplier).toFixed(scale.digits)}${
|
||||
scale.unit
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const nodePosition = node.getStage()?.getPointerPosition();
|
||||
if (nodePosition) {
|
||||
transformText.absolutePosition({
|
||||
x: nodePosition.x,
|
||||
y: nodePosition.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTrasform() {
|
||||
updateTransformText();
|
||||
}
|
||||
|
||||
function handleTransformEnd(e: Konva.KonvaEventObject<Event>) {
|
||||
setPreventMapInteraction(false);
|
||||
transformTextRef.current?.destroy();
|
||||
transformTextRef.current = undefined;
|
||||
onTransformEnd && onTransformEnd(e);
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal selector="#portal" enabled>
|
||||
<KonvaTransformer
|
||||
ref={transformerRef}
|
||||
boundBoxFunc={(oldBox, newBox) => {
|
||||
let snapBox = { ...newBox };
|
||||
const movingAnchor = movingAnchorRef.current;
|
||||
if (
|
||||
movingAnchor === "middle-left" ||
|
||||
movingAnchor === "middle-right"
|
||||
) {
|
||||
// Account for grid snapping
|
||||
const nearestCellWidth = roundTo(
|
||||
snapBox.width,
|
||||
gridCellAbsoluteSizeRef.current.x
|
||||
);
|
||||
const distanceToSnap = Math.abs(snapBox.width - nearestCellWidth);
|
||||
let snapping = false;
|
||||
if (
|
||||
distanceToSnap <
|
||||
gridCellAbsoluteSizeRef.current.x * gridSnappingSensitivity
|
||||
) {
|
||||
snapBox.width = nearestCellWidth;
|
||||
snapping = true;
|
||||
}
|
||||
|
||||
const deltaWidth = snapBox.width - oldBox.width;
|
||||
// Account for node ratio
|
||||
const inverseRatio =
|
||||
Math.round(oldBox.height) / Math.round(oldBox.width);
|
||||
const deltaHeight = inverseRatio * deltaWidth;
|
||||
|
||||
// Account for node rotation
|
||||
// Create a transform to unrotate the x,y position of the Box
|
||||
const rotator = new Transform();
|
||||
rotator.rotate(-snapBox.rotation);
|
||||
|
||||
// Unrotate and add the resize amount
|
||||
let rotatedMin = rotator.point({ x: snapBox.x, y: snapBox.y });
|
||||
rotatedMin.y = rotatedMin.y - deltaHeight / 2;
|
||||
// Snap x position if needed
|
||||
if (snapping) {
|
||||
const snapDelta = newBox.width - nearestCellWidth;
|
||||
rotatedMin.x = rotatedMin.x + snapDelta / 2;
|
||||
}
|
||||
|
||||
// Rotated back
|
||||
rotator.invert();
|
||||
rotatedMin = rotator.point(rotatedMin);
|
||||
|
||||
snapBox = {
|
||||
...snapBox,
|
||||
height: snapBox.height + deltaHeight,
|
||||
x: rotatedMin.x,
|
||||
y: rotatedMin.y,
|
||||
};
|
||||
}
|
||||
|
||||
if (snapBox.width < 5 || snapBox.height < 5) {
|
||||
return oldBox;
|
||||
}
|
||||
return snapBox;
|
||||
}}
|
||||
onTransformStart={handleTransformStart}
|
||||
onTransform={handleTrasform}
|
||||
onTransformEnd={handleTransformEnd}
|
||||
centeredScaling={true}
|
||||
rotationSnaps={
|
||||
snappingSensitivity === -1
|
||||
? [] // Disabled rotation snapping if grid snapping disabled with shortcut
|
||||
: [...Array(24).keys()].map((n) => n * 15)
|
||||
}
|
||||
rotateAnchorOffset={16}
|
||||
enabledAnchors={["middle-left", "middle-right"]}
|
||||
flipEnabled={false}
|
||||
ignoreStroke={true}
|
||||
borderStroke="invisible"
|
||||
anchorStroke="invisible"
|
||||
anchorCornerRadius={24}
|
||||
borderStrokeWidth={0}
|
||||
anchorSize={48}
|
||||
/>
|
||||
</Portal>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
Transformer.defaultProps = {
|
||||
portalSelector: "#portal",
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
type AnchorImageStatus = "loading" | "loaded" | "failed";
|
||||
|
||||
function useAnchorImage(
|
||||
|
@ -120,10 +120,15 @@ function useMapTokens(
|
||||
const [transformingTokensIds, setTransformingTokenIds] = useState<string[]>(
|
||||
[]
|
||||
);
|
||||
function handleTokenTransformStart(event: Konva.KonvaEventObject<Event>) {
|
||||
function handleTokenTransformStart(
|
||||
event: Konva.KonvaEventObject<Event>,
|
||||
attachments: Konva.Node[]
|
||||
) {
|
||||
const transformer = event.currentTarget as Konva.Transformer;
|
||||
const nodes = transformer.nodes();
|
||||
setTransformingTokenIds(nodes.map((node) => node.id()));
|
||||
setTransformingTokenIds(
|
||||
[...nodes, ...attachments].map((node) => node.id())
|
||||
);
|
||||
}
|
||||
|
||||
function handleTokenTransformEnd() {
|
||||
|
@ -38,9 +38,6 @@ 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;
|
||||
@ -79,3 +76,8 @@ export type SelectionItemsCreateEventHandler = (
|
||||
tokenStates: TokenState[],
|
||||
notes: Note[]
|
||||
) => void;
|
||||
|
||||
export type CustomTransformEventHandler = (
|
||||
event: Konva.KonvaEventObject<Event>,
|
||||
attachments: Konva.Node[]
|
||||
) => void;
|
||||
|
Loading…
Reference in New Issue
Block a user