Fix transform attachments affecting transform anchor positions
This commit is contained in:
parent
37f0ab3b26
commit
4eca2dcfc7
@ -287,7 +287,7 @@ function Note({
|
|||||||
</animated.Group>
|
</animated.Group>
|
||||||
<Transformer
|
<Transformer
|
||||||
active={(!note.locked && selected) || isTransforming}
|
active={(!note.locked && selected) || isTransforming}
|
||||||
nodes={() => (noteRef.current ? [noteRef.current] : [])}
|
nodes={noteRef.current ? [noteRef.current] : []}
|
||||||
onTransformEnd={handleTransformEnd}
|
onTransformEnd={handleTransformEnd}
|
||||||
onTransformStart={handleTransformStart}
|
onTransformStart={handleTransformStart}
|
||||||
gridScale={map?.grid.measurement.scale || ""}
|
gridScale={map?.grid.measurement.scale || ""}
|
||||||
|
@ -31,7 +31,7 @@ import {
|
|||||||
TokenMenuCloseChangeEventHandler,
|
TokenMenuCloseChangeEventHandler,
|
||||||
TokenMenuOpenChangeEventHandler,
|
TokenMenuOpenChangeEventHandler,
|
||||||
TokenStateChangeEventHandler,
|
TokenStateChangeEventHandler,
|
||||||
TokenTransformEventHandler,
|
CustomTransformEventHandler,
|
||||||
} from "../../types/Events";
|
} from "../../types/Events";
|
||||||
import Transformer from "./Transformer";
|
import Transformer from "./Transformer";
|
||||||
import TokenAttachment from "./TokenAttachment";
|
import TokenAttachment from "./TokenAttachment";
|
||||||
@ -44,8 +44,8 @@ type MapTokenProps = {
|
|||||||
onTokenMenuClose: TokenMenuCloseChangeEventHandler;
|
onTokenMenuClose: TokenMenuCloseChangeEventHandler;
|
||||||
onTokenDragStart: TokenDragEventHandler;
|
onTokenDragStart: TokenDragEventHandler;
|
||||||
onTokenDragEnd: TokenDragEventHandler;
|
onTokenDragEnd: TokenDragEventHandler;
|
||||||
onTokenTransformStart: TokenTransformEventHandler;
|
onTokenTransformStart: CustomTransformEventHandler;
|
||||||
onTokenTransformEnd: TokenTransformEventHandler;
|
onTokenTransformEnd: CustomTransformEventHandler;
|
||||||
transforming: boolean;
|
transforming: boolean;
|
||||||
draggable: boolean;
|
draggable: boolean;
|
||||||
selectable: boolean;
|
selectable: boolean;
|
||||||
@ -332,15 +332,21 @@ function Token({
|
|||||||
// Override transform active to always show this transformer when using it
|
// Override transform active to always show this transformer when using it
|
||||||
const [overrideTransformActive, setOverrideTransformActive] = useState(false);
|
const [overrideTransformActive, setOverrideTransformActive] = useState(false);
|
||||||
|
|
||||||
function handleTransformStart(event: Konva.KonvaEventObject<Event>) {
|
function handleTransformStart(
|
||||||
|
event: Konva.KonvaEventObject<Event>,
|
||||||
|
attachments: Konva.Node[]
|
||||||
|
) {
|
||||||
setOverrideTransformActive(true);
|
setOverrideTransformActive(true);
|
||||||
onTokenTransformStart(event);
|
onTokenTransformStart(event, attachments);
|
||||||
onTokenMenuClose();
|
onTokenMenuClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTransformEnd(event: Konva.KonvaEventObject<Event>) {
|
function handleTransformEnd(
|
||||||
|
event: Konva.KonvaEventObject<Event>,
|
||||||
|
attachments: Konva.Node[]
|
||||||
|
) {
|
||||||
const transformer = event.currentTarget as Konva.Transformer;
|
const transformer = event.currentTarget as Konva.Transformer;
|
||||||
const nodes = transformer.nodes();
|
const nodes = [...transformer.nodes(), ...attachments];
|
||||||
const tokenChanges: Record<string, Partial<TokenState>> = {};
|
const tokenChanges: Record<string, Partial<TokenState>> = {};
|
||||||
for (let node of nodes) {
|
for (let node of nodes) {
|
||||||
const id = node.id();
|
const id = node.id();
|
||||||
@ -367,7 +373,7 @@ function Token({
|
|||||||
onTokenMenuOpen(tokenState.id, transformRootRef.current, false);
|
onTokenMenuOpen(tokenState.id, transformRootRef.current, false);
|
||||||
}
|
}
|
||||||
setOverrideTransformActive(false);
|
setOverrideTransformActive(false);
|
||||||
onTokenTransformEnd(event);
|
onTokenTransformEnd(event, attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformerActive = useMemo(
|
const transformerActive = useMemo(
|
||||||
@ -375,20 +381,16 @@ function Token({
|
|||||||
[tokenState, selected, overrideTransformActive]
|
[tokenState, selected, overrideTransformActive]
|
||||||
);
|
);
|
||||||
|
|
||||||
const transformerNodes = useMemo(
|
const transformerAttachments = useMemo(() => {
|
||||||
() => () => {
|
if (transformerActive) {
|
||||||
if (transformRootRef.current) {
|
// Find attached transform roots
|
||||||
// Find attached transform roots
|
return getAttachedTokens().map((node) =>
|
||||||
const attached = getAttachedTokens().map((node) =>
|
(node as Konva.Group).findOne(".transform-root")
|
||||||
(node as Konva.Group).findOne(".transform-root")
|
);
|
||||||
);
|
} else {
|
||||||
return [transformRootRef.current, ...attached];
|
return [];
|
||||||
} else {
|
}
|
||||||
return [];
|
}, [getAttachedTokens, transformerActive]);
|
||||||
}
|
|
||||||
},
|
|
||||||
[getAttachedTokens]
|
|
||||||
);
|
|
||||||
|
|
||||||
// When a token is hidden if you aren't the map owner hide it completely
|
// When a token is hidden if you aren't the map owner hide it completely
|
||||||
if (map && !tokenState.visible && map.owner !== userId) {
|
if (map && !tokenState.visible && map.owner !== userId) {
|
||||||
@ -486,7 +488,12 @@ function Token({
|
|||||||
</animated.Group>
|
</animated.Group>
|
||||||
<Transformer
|
<Transformer
|
||||||
active={transformerActive}
|
active={transformerActive}
|
||||||
nodes={transformerNodes}
|
nodes={
|
||||||
|
transformRootRef.current
|
||||||
|
? [transformRootRef.current as Konva.Node]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
attachments={transformerAttachments}
|
||||||
onTransformEnd={handleTransformEnd}
|
onTransformEnd={handleTransformEnd}
|
||||||
onTransformStart={handleTransformStart}
|
onTransformStart={handleTransformStart}
|
||||||
gridScale={map.grid.measurement.scale}
|
gridScale={map.grid.measurement.scale}
|
||||||
|
@ -1,35 +1,216 @@
|
|||||||
import Konva from "konva";
|
import Konva from "konva";
|
||||||
import { Transform } from "konva/lib/Util";
|
import { Transform } from "konva/lib/Util";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Transformer as KonvaTransformer } from "react-konva";
|
|
||||||
import { Portal } from "react-konva-utils";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useGridCellPixelSize,
|
useGridCellPixelSize,
|
||||||
useGridSnappingSensitivity,
|
useGridSnappingSensitivity,
|
||||||
} from "../../contexts/GridContext";
|
} from "../../contexts/GridContext";
|
||||||
import { useSetPreventMapInteraction } from "../../contexts/MapInteractionContext";
|
import { useSetPreventMapInteraction } from "../../contexts/MapInteractionContext";
|
||||||
|
import { useMapStage } from "../../contexts/MapStageContext";
|
||||||
|
|
||||||
import { roundTo } from "../../helpers/shared";
|
import { roundTo } from "../../helpers/shared";
|
||||||
import Vector2 from "../../helpers/Vector2";
|
import Vector2 from "../../helpers/Vector2";
|
||||||
|
import { parseGridScale } from "../../helpers/grid";
|
||||||
|
|
||||||
import scaleDark from "../../images/ScaleDark.png";
|
import scaleDark from "../../images/ScaleDark.png";
|
||||||
import rotateDark from "../../images/RotateDark.png";
|
import rotateDark from "../../images/RotateDark.png";
|
||||||
import { parseGridScale } from "../../helpers/grid";
|
|
||||||
|
import { CustomTransformEventHandler } from "../../types/Events";
|
||||||
|
|
||||||
type TransformerProps = {
|
type TransformerProps = {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
nodes: () => Konva.Node[];
|
nodes: Konva.Node[];
|
||||||
onTransformStart?: (event: Konva.KonvaEventObject<Event>) => void;
|
attachments: Konva.Node[];
|
||||||
onTransformEnd?: (event: Konva.KonvaEventObject<Event>) => void;
|
onTransformStart?: CustomTransformEventHandler;
|
||||||
|
onTransform?: CustomTransformEventHandler;
|
||||||
|
onTransformEnd?: CustomTransformEventHandler;
|
||||||
gridScale: string;
|
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({
|
function Transformer({
|
||||||
active,
|
active,
|
||||||
nodes,
|
nodes,
|
||||||
|
attachments,
|
||||||
onTransformStart,
|
onTransformStart,
|
||||||
|
onTransform,
|
||||||
onTransformEnd,
|
onTransformEnd,
|
||||||
gridScale,
|
gridScale,
|
||||||
|
portalSelector,
|
||||||
}: TransformerProps) {
|
}: TransformerProps) {
|
||||||
const setPreventMapInteraction = useSetPreventMapInteraction();
|
const setPreventMapInteraction = useSetPreventMapInteraction();
|
||||||
|
|
||||||
@ -42,13 +223,235 @@ function Transformer({
|
|||||||
|
|
||||||
const snappingSensitivity = useGridSnappingSensitivity();
|
const snappingSensitivity = useGridSnappingSensitivity();
|
||||||
// Clamp snapping to 0 to accound for -1 snapping override
|
// 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 [anchorScale, anchorScaleStatus] = useAnchorImage(96, scaleDark);
|
||||||
const [anchorRotate, anchorRotateStatus] = useAnchorImage(96, rotateDark);
|
const [anchorRotate, anchorRotateStatus] = useAnchorImage(96, rotateDark);
|
||||||
|
|
||||||
|
// Add nodes to transformer and setup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const transformer = transformerRef.current;
|
const transformer = transformerRef.current;
|
||||||
if (
|
if (
|
||||||
@ -57,14 +460,8 @@ function Transformer({
|
|||||||
anchorScaleStatus === "loaded" &&
|
anchorScaleStatus === "loaded" &&
|
||||||
anchorRotateStatus === "loaded"
|
anchorRotateStatus === "loaded"
|
||||||
) {
|
) {
|
||||||
// we need to attach transformer manually
|
transformer.setNodes(nodes);
|
||||||
const n = nodes();
|
transformer.attachments = attachments;
|
||||||
// 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 = transformer.findOne<Konva.Rect>(".middle-left");
|
const middleLeft = transformer.findOne<Konva.Rect>(".middle-left");
|
||||||
const middleRight = transformer.findOne<Konva.Rect>(".middle-right");
|
const middleRight = transformer.findOne<Konva.Rect>(".middle-right");
|
||||||
@ -93,207 +490,21 @@ function Transformer({
|
|||||||
}, [
|
}, [
|
||||||
active,
|
active,
|
||||||
nodes,
|
nodes,
|
||||||
|
attachments,
|
||||||
anchorScale,
|
anchorScale,
|
||||||
anchorRotate,
|
anchorRotate,
|
||||||
anchorScaleStatus,
|
anchorScaleStatus,
|
||||||
anchorRotateStatus,
|
anchorRotateStatus,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function updateGridCellAbsoluteSize() {
|
return null;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Transformer.defaultProps = {
|
||||||
|
portalSelector: "#portal",
|
||||||
|
attachments: [],
|
||||||
|
};
|
||||||
|
|
||||||
type AnchorImageStatus = "loading" | "loaded" | "failed";
|
type AnchorImageStatus = "loading" | "loaded" | "failed";
|
||||||
|
|
||||||
function useAnchorImage(
|
function useAnchorImage(
|
||||||
|
@ -120,10 +120,15 @@ function useMapTokens(
|
|||||||
const [transformingTokensIds, setTransformingTokenIds] = useState<string[]>(
|
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 transformer = event.currentTarget as Konva.Transformer;
|
||||||
const nodes = transformer.nodes();
|
const nodes = transformer.nodes();
|
||||||
setTransformingTokenIds(nodes.map((node) => node.id()));
|
setTransformingTokenIds(
|
||||||
|
[...nodes, ...attachments].map((node) => node.id())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTokenTransformEnd() {
|
function handleTokenTransformEnd() {
|
||||||
|
@ -38,9 +38,6 @@ export type TokenDragEventHandler = (
|
|||||||
tokenStateId: string,
|
tokenStateId: string,
|
||||||
attachedTokenStateIds: string[]
|
attachedTokenStateIds: string[]
|
||||||
) => void;
|
) => void;
|
||||||
export type TokenTransformEventHandler = (
|
|
||||||
event: Konva.KonvaEventObject<Event>
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
export type NoteCreateEventHander = (notes: Note[]) => void;
|
export type NoteCreateEventHander = (notes: Note[]) => void;
|
||||||
export type NoteRemoveEventHander = (noteIds: string[]) => void;
|
export type NoteRemoveEventHander = (noteIds: string[]) => void;
|
||||||
@ -79,3 +76,8 @@ export type SelectionItemsCreateEventHandler = (
|
|||||||
tokenStates: TokenState[],
|
tokenStates: TokenState[],
|
||||||
notes: Note[]
|
notes: Note[]
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
export type CustomTransformEventHandler = (
|
||||||
|
event: Konva.KonvaEventObject<Event>,
|
||||||
|
attachments: Konva.Node[]
|
||||||
|
) => void;
|
||||||
|
Loading…
Reference in New Issue
Block a user