Add attachment token category
This commit is contained in:
parent
6eb1f71bc2
commit
73c549102b
@ -33,6 +33,7 @@ import {
|
||||
TokenStateChangeEventHandler,
|
||||
} from "../../types/Events";
|
||||
import Transformer from "./Transformer";
|
||||
import TokenAttachment from "./TokenAttachment";
|
||||
|
||||
type MapTokenProps = {
|
||||
tokenState: TokenState;
|
||||
@ -76,14 +77,27 @@ function Token({
|
||||
|
||||
const snapPositionToGrid = useGridSnapping();
|
||||
|
||||
const intersectingTokensRef = useRef<Konva.Node[]>([]);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const previousDragPositionRef = useRef({ x: 0, y: 0 });
|
||||
|
||||
// Tokens that are attached to this token and should move when it moves
|
||||
const attachedTokensRef = useRef<Konva.Node[]>([]);
|
||||
// If this an attachment is it over a character
|
||||
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;
|
||||
|
||||
function handleDragStart(event: Konva.KonvaEventObject<DragEvent>) {
|
||||
const tokenGroup = event.target;
|
||||
const tokenGroup = event.target as Konva.Shape;
|
||||
const layer = tokenGroup.getLayer();
|
||||
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
previousDragPositionRef.current = tokenGroup.position();
|
||||
|
||||
if (tokenState.category === "vehicle") {
|
||||
previousDragPositionRef.current = tokenGroup.position();
|
||||
const tokenIntersection = new Intersection(
|
||||
getScaledOutline(tokenState, tokenWidth, tokenHeight),
|
||||
{ x: tokenX - tokenWidth / 2, y: tokenY - tokenHeight / 2 },
|
||||
@ -91,19 +105,44 @@ function Token({
|
||||
tokenState.rotation
|
||||
);
|
||||
|
||||
// Find all other tokens on the map
|
||||
const layer = tokenGroup.getLayer() as Konva.Layer;
|
||||
// Find all other characters on the map and check whether they're
|
||||
// intersecting the vehicle
|
||||
const tokens = layer.find(".character");
|
||||
for (let other of tokens) {
|
||||
if (other === tokenGroup) {
|
||||
continue;
|
||||
}
|
||||
if (tokenIntersection.intersects(other.position())) {
|
||||
intersectingTokensRef.current.push(other);
|
||||
attachedTokensRef.current.push(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
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, tokenState.id);
|
||||
}
|
||||
|
||||
@ -113,37 +152,54 @@ function Token({
|
||||
if (map.snapToGrid) {
|
||||
tokenGroup.position(snapPositionToGrid(tokenGroup.position()));
|
||||
}
|
||||
if (tokenState.category === "vehicle") {
|
||||
if (attachedTokensRef.current.length > 0) {
|
||||
const deltaPosition = Vector2.subtract(
|
||||
tokenGroup.position(),
|
||||
previousDragPositionRef.current
|
||||
);
|
||||
for (let other of intersectingTokensRef.current) {
|
||||
for (let other of attachedTokensRef.current) {
|
||||
other.position(Vector2.add(other.position(), deltaPosition));
|
||||
}
|
||||
previousDragPositionRef.current = tokenGroup.position();
|
||||
}
|
||||
// Check whether an attachment is over a character
|
||||
if (tokenState.category === "attachment") {
|
||||
const characters = attachmentCharactersRef.current;
|
||||
let overCharacter = false;
|
||||
for (let character of characters) {
|
||||
const distance = Vector2.distance(
|
||||
tokenGroup.position(),
|
||||
character.position()
|
||||
);
|
||||
if (distance < attachmentThreshold) {
|
||||
overCharacter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (attachmentOverCharacter !== overCharacter) {
|
||||
setAttachmentOverCharacter(overCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragEnd(event: Konva.KonvaEventObject<DragEvent>) {
|
||||
const tokenGroup = event.target;
|
||||
|
||||
const mountChanges: Record<string, Partial<TokenState>> = {};
|
||||
if (tokenState.category === "vehicle") {
|
||||
for (let other of intersectingTokensRef.current) {
|
||||
mountChanges[other.id()] = {
|
||||
const attachedTokenChanges: Record<string, Partial<TokenState>> = {};
|
||||
if (attachedTokensRef.current.length > 0) {
|
||||
for (let other of attachedTokensRef.current) {
|
||||
attachedTokenChanges[other.id()] = {
|
||||
x: other.x() / mapWidth,
|
||||
y: other.y() / mapHeight,
|
||||
lastModifiedBy: userId,
|
||||
lastModified: Date.now(),
|
||||
};
|
||||
}
|
||||
intersectingTokensRef.current = [];
|
||||
}
|
||||
|
||||
setPreventMapInteraction(false);
|
||||
onTokenStateChange({
|
||||
...mountChanges,
|
||||
...attachedTokenChanges,
|
||||
[tokenState.id]: {
|
||||
x: tokenGroup.x() / mapWidth,
|
||||
y: tokenGroup.y() / mapHeight,
|
||||
@ -151,6 +207,12 @@ function Token({
|
||||
lastModified: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
setDragging(false);
|
||||
attachmentCharactersRef.current = [];
|
||||
attachedTokensRef.current = [];
|
||||
setAttachmentOverCharacter(false);
|
||||
|
||||
onTokenDragEnd(event, tokenState.id);
|
||||
}
|
||||
|
||||
@ -290,6 +352,10 @@ function Token({
|
||||
<TokenOutline
|
||||
outline={getScaledOutline(tokenState, tokenWidth, tokenHeight)}
|
||||
hidden={!!tokenImage}
|
||||
// Disable hit detection for attachments
|
||||
hitFunc={
|
||||
tokenState.category === "attachment" ? () => {} : undefined
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
<KonvaImage
|
||||
@ -310,6 +376,18 @@ function Token({
|
||||
height={tokenHeight}
|
||||
/>
|
||||
) : null}
|
||||
{tokenState.category === "attachment" ? (
|
||||
<Group offsetX={-tokenWidth / 2} offsetY={-tokenHeight / 2}>
|
||||
<Group rotation={tokenState.rotation}>
|
||||
<TokenAttachment
|
||||
tokenHeight={tokenHeight}
|
||||
dragging={dragging}
|
||||
highlight={attachmentOverCharacter}
|
||||
radius={attachmentThreshold * 2}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
) : null}
|
||||
{tokenState.label ? (
|
||||
<TokenLabel
|
||||
tokenState={tokenState}
|
||||
|
52
src/components/konva/TokenAttachment.tsx
Normal file
52
src/components/konva/TokenAttachment.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { Circle, Group, Rect } from "react-konva";
|
||||
|
||||
type TokenAttachmentProps = {
|
||||
tokenHeight: number;
|
||||
dragging: boolean;
|
||||
highlight: boolean;
|
||||
radius: number;
|
||||
};
|
||||
|
||||
function TokenAttachment({
|
||||
tokenHeight,
|
||||
dragging,
|
||||
highlight,
|
||||
radius,
|
||||
}: TokenAttachmentProps) {
|
||||
return (
|
||||
<Group>
|
||||
{/* Make a bigger hidden rect for hit registration */}
|
||||
<Rect
|
||||
width={10}
|
||||
height={5}
|
||||
x={-5}
|
||||
y={tokenHeight / 2 - 2.5}
|
||||
cornerRadius={2.5}
|
||||
fill="transparent"
|
||||
/>
|
||||
<Rect
|
||||
width={5}
|
||||
height={2}
|
||||
x={-2.5}
|
||||
y={tokenHeight / 2 - 1}
|
||||
cornerRadius={2.5}
|
||||
fill="rgba(36, 39, 51, 0.5)"
|
||||
shadowColor="rgba(0,0,0,0.12)"
|
||||
shadowOffsetY={1}
|
||||
shadowBlur={2}
|
||||
hitFunc={() => {}}
|
||||
/>
|
||||
{dragging ? (
|
||||
<Circle
|
||||
radius={radius}
|
||||
stroke={
|
||||
highlight ? "hsl(260, 100%, 80%)" : "rgba(255, 255, 255, 0.85)"
|
||||
}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
) : null}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default TokenAttachment;
|
@ -1,3 +1,4 @@
|
||||
import Konva from "konva";
|
||||
import { Rect, Circle, Line } from "react-konva";
|
||||
|
||||
import colors from "../../helpers/colors";
|
||||
@ -6,9 +7,9 @@ import { Outline } from "../../types/Outline";
|
||||
type TokenOutlineProps = {
|
||||
outline: Outline;
|
||||
hidden: boolean;
|
||||
};
|
||||
} & Konva.ShapeConfig;
|
||||
|
||||
function TokenOutline({ outline, hidden }: TokenOutlineProps) {
|
||||
function TokenOutline({ outline, hidden, ...props }: TokenOutlineProps) {
|
||||
const sharedProps = {
|
||||
fill: colors.black,
|
||||
opacity: hidden ? 0 : 0.8,
|
||||
@ -21,6 +22,7 @@ function TokenOutline({ outline, hidden }: TokenOutlineProps) {
|
||||
x={outline.x}
|
||||
y={outline.y}
|
||||
{...sharedProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else if (outline.type === "circle") {
|
||||
@ -30,6 +32,7 @@ function TokenOutline({ outline, hidden }: TokenOutlineProps) {
|
||||
x={outline.x}
|
||||
y={outline.y}
|
||||
{...sharedProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@ -39,6 +42,7 @@ function TokenOutline({ outline, hidden }: TokenOutlineProps) {
|
||||
closed
|
||||
tension={outline.points.length < 200 ? 0 : 0.33}
|
||||
{...sharedProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -20,12 +20,15 @@ import { tokenSources } from "../../tokens";
|
||||
|
||||
import Grid from "../konva/Grid";
|
||||
import { Token } from "../../types/Token";
|
||||
import SettingsContext, { useSettings } from "../../contexts/SettingsContext";
|
||||
|
||||
type TokenPreviewProps = {
|
||||
token: Token;
|
||||
};
|
||||
|
||||
function TokenPreview({ token }: TokenPreviewProps) {
|
||||
const settings = useSettings();
|
||||
|
||||
const tokenURL = useDataURL(token, tokenSources);
|
||||
const [tokenSourceImage] = useImage(tokenURL || "");
|
||||
|
||||
@ -105,39 +108,41 @@ function TokenPreview({ token }: TokenPreviewProps) {
|
||||
scale={{ x: stageScale, y: stageScale }}
|
||||
ref={tokenStageRef}
|
||||
>
|
||||
<Layer ref={tokenLayerRef}>
|
||||
<Image
|
||||
image={tokenSourceImage}
|
||||
width={tokenWidth}
|
||||
height={tokenHeight}
|
||||
/>
|
||||
{showGridPreview && (
|
||||
<Group offsetY={gridHeight - tokenHeight}>
|
||||
<GridProvider
|
||||
grid={{
|
||||
size: { x: gridX, y: gridY },
|
||||
inset: {
|
||||
topLeft: { x: 0, y: 0 },
|
||||
bottomRight: { x: 1, y: 1 },
|
||||
},
|
||||
type: "square",
|
||||
measurement: { type: "chebyshev", scale: "5ft" },
|
||||
}}
|
||||
width={gridWidth}
|
||||
height={gridHeight}
|
||||
>
|
||||
<Grid />
|
||||
</GridProvider>
|
||||
<Rect
|
||||
width={gridWidth}
|
||||
height={gridHeight}
|
||||
fill="transparent"
|
||||
stroke="rgba(255, 255, 255, 0.75)"
|
||||
strokeWidth={borderWidth}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Layer>
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<Layer ref={tokenLayerRef}>
|
||||
<Image
|
||||
image={tokenSourceImage}
|
||||
width={tokenWidth}
|
||||
height={tokenHeight}
|
||||
/>
|
||||
{showGridPreview && (
|
||||
<Group offsetY={gridHeight - tokenHeight}>
|
||||
<GridProvider
|
||||
grid={{
|
||||
size: { x: gridX, y: gridY },
|
||||
inset: {
|
||||
topLeft: { x: 0, y: 0 },
|
||||
bottomRight: { x: 1, y: 1 },
|
||||
},
|
||||
type: "square",
|
||||
measurement: { type: "chebyshev", scale: "5ft" },
|
||||
}}
|
||||
width={gridWidth}
|
||||
height={gridHeight}
|
||||
>
|
||||
<Grid />
|
||||
</GridProvider>
|
||||
<Rect
|
||||
width={gridWidth}
|
||||
height={gridHeight}
|
||||
fill="transparent"
|
||||
stroke="rgba(255, 255, 255, 0.75)"
|
||||
strokeWidth={borderWidth}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Layer>
|
||||
</SettingsContext.Provider>
|
||||
</Stage>
|
||||
</ReactResizeDetector>
|
||||
<IconButton
|
||||
|
@ -12,6 +12,7 @@ const categorySettings: CategorySetting[] = [
|
||||
{ value: "character", label: "Character" },
|
||||
{ value: "prop", label: "Prop" },
|
||||
{ value: "vehicle", label: "Vehicle / Mount" },
|
||||
{ value: "attachment", label: "Attachment" },
|
||||
];
|
||||
|
||||
type TokenSettingsProps = {
|
||||
|
@ -147,12 +147,14 @@ export default useMapTokens;
|
||||
|
||||
function getMapTokenCategoryWeight(category: TokenCategory) {
|
||||
switch (category) {
|
||||
case "character":
|
||||
case "attachment":
|
||||
return 0;
|
||||
case "vehicle":
|
||||
case "character":
|
||||
return 1;
|
||||
case "prop":
|
||||
case "vehicle":
|
||||
return 2;
|
||||
case "prop":
|
||||
return 3;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@ -164,7 +166,7 @@ function sortMapTokenStates(
|
||||
b: TokenState,
|
||||
tokenDraggingOptions?: TokenDraggingOptions
|
||||
) {
|
||||
// If categories are different sort in order "prop", "vehicle", "character"
|
||||
// If categories are different sort in order "prop", "vehicle", "character", "attachment"
|
||||
if (b.category !== a.category) {
|
||||
const aWeight = getMapTokenCategoryWeight(a.category);
|
||||
const bWeight = getMapTokenCategoryWeight(b.category);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Konva from "konva";
|
||||
import { Outline } from "./Outline";
|
||||
|
||||
export type TokenCategory = "character" | "vehicle" | "prop";
|
||||
export type TokenCategory = "character" | "vehicle" | "prop" | "attachment";
|
||||
|
||||
export type BaseToken = {
|
||||
id: string;
|
||||
|
@ -16,7 +16,7 @@ export const TokenStateSchema = {
|
||||
],
|
||||
definitions: {
|
||||
TokenCategory: {
|
||||
enum: ["character", "prop", "vehicle"],
|
||||
enum: ["character", "prop", "vehicle", "attachment"],
|
||||
type: "string",
|
||||
},
|
||||
BaseTokenState: {
|
||||
|
Loading…
Reference in New Issue
Block a user